forked from Transparency/kgroad-frontend2
made news, stats, volunteers, auth
This commit is contained in:
parent
45a9d698cd
commit
f6ceb252b2
86
src/App/about-us/AboutUs.scss
Normal file
86
src/App/about-us/AboutUs.scss
Normal file
@ -0,0 +1,86 @@
|
|||||||
|
.about-us {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
|
h2 {
|
||||||
|
margin-bottom: 40px;
|
||||||
|
width: fit-content;
|
||||||
|
}
|
||||||
|
|
||||||
|
img {
|
||||||
|
align-self: center;
|
||||||
|
margin-bottom: 65px;
|
||||||
|
border-radius: 12px;
|
||||||
|
max-width: 1072px;
|
||||||
|
width: 100%;
|
||||||
|
height: 598px;
|
||||||
|
object-fit: cover;
|
||||||
|
}
|
||||||
|
|
||||||
|
h3 {
|
||||||
|
margin-bottom: 10px;
|
||||||
|
font-size: 24px;
|
||||||
|
font-weight: 500;
|
||||||
|
line-height: 29px;
|
||||||
|
color: rgb(62, 50, 50);
|
||||||
|
}
|
||||||
|
|
||||||
|
&__descriptions {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 20px;
|
||||||
|
|
||||||
|
p {
|
||||||
|
font-size: 20px;
|
||||||
|
font-weight: 400;
|
||||||
|
line-height: 34px;
|
||||||
|
color: rgb(62, 50, 50);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (max-width: 768px) {
|
||||||
|
.about-us {
|
||||||
|
h2 {
|
||||||
|
margin-bottom: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
img {
|
||||||
|
margin-bottom: 30px;
|
||||||
|
height: 392px;
|
||||||
|
}
|
||||||
|
|
||||||
|
h3 {
|
||||||
|
font-size: 20px;
|
||||||
|
line-height: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__descriptions {
|
||||||
|
gap: 16px;
|
||||||
|
|
||||||
|
p {
|
||||||
|
font-size: 18px;
|
||||||
|
line-height: 34px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (max-width: 550px) {
|
||||||
|
.about-us {
|
||||||
|
h2 {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
img {
|
||||||
|
height: 231px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__descriptions {
|
||||||
|
p {
|
||||||
|
font-size: 16px;
|
||||||
|
line-height: 140%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
12
src/App/about-us/assets/header.svg
Normal file
12
src/App/about-us/assets/header.svg
Normal file
File diff suppressed because one or more lines are too long
After Width: | Height: | Size: 332 KiB |
@ -1,7 +1,63 @@
|
|||||||
import "./styles.scss";
|
import Typography from "@/shared/ui/components/Typography/Typography";
|
||||||
|
import "./AboutUs.scss";
|
||||||
|
import Image from "next/image";
|
||||||
|
import header from "./assets/header.svg";
|
||||||
|
|
||||||
const AboutUs = () => {
|
const AboutUs = () => {
|
||||||
return <div>AboutUs</div>;
|
return (
|
||||||
|
<div className="about-us page-padding">
|
||||||
|
<Typography element="h2">О нас</Typography>
|
||||||
|
|
||||||
|
<Image src={header} alt="Header Image" />
|
||||||
|
|
||||||
|
<h3>Don’t wait. The purpose of our lives is to be happy!</h3>
|
||||||
|
|
||||||
|
<div className="about-us__descriptions">
|
||||||
|
<p>
|
||||||
|
arrival, your senses will be rewarded with the pleasant
|
||||||
|
scent of lemongrass oil used to clean the natural wood found
|
||||||
|
throughout the room, creating a relaxing atmosphere within
|
||||||
|
the space. A wonderful serenity has taken possession of my
|
||||||
|
entire soul, like these sweet mornings of spring which I
|
||||||
|
enjoy with my whole heart. I am alone, and feel the charm of
|
||||||
|
existence in this spot, which was created for the bliss of
|
||||||
|
souls like mine. I am so happy, my dear friend, so absorbed
|
||||||
|
in the exquisite.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
When you are ready to indulge your sense of excitement,
|
||||||
|
check out the range of water- sports opportunities at the
|
||||||
|
resort’s on-site water-sports center. Want to leave your
|
||||||
|
stress on the water? The resort has kayaks, paddleboards, or
|
||||||
|
the low-key pedal boats. Snorkeling equipment is available
|
||||||
|
as well, so you can experience the ever-changing undersea
|
||||||
|
environment. Not only do visitors to a bed and breakfast get
|
||||||
|
a unique perspective on the place they are visiting, they
|
||||||
|
have options for special packages not available in other
|
||||||
|
hotel settings.{" "}
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
Bed and breakfasts can partner easily with local businesses
|
||||||
|
for a smoothly organized and highly personalized vacation
|
||||||
|
experience. The Fife and Drum Inn offers options such as the
|
||||||
|
Historic Triangle Package that includes three nights at the
|
||||||
|
Inn, breakfasts, and admissions to historic Williamsburg,
|
||||||
|
Jamestown, and Yorktown. Bed and breakfasts also lend
|
||||||
|
themselves to romance.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
Part of the charm of a bed and breakfast is the uniqueness;
|
||||||
|
art, décor, and food are integrated to create a complete
|
||||||
|
experience. For example, the Fife and Drum retains the
|
||||||
|
colonial feel of the area in all its guest rooms. Special
|
||||||
|
features include antique furnishings, elegant four poster
|
||||||
|
beds in some guest rooms, as well folk art and artifacts
|
||||||
|
from the restoration period of the historic area available
|
||||||
|
for guests to enjoy.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default AboutUs;
|
export default AboutUs;
|
||||||
|
43
src/App/news/News.scss
Normal file
43
src/App/news/News.scss
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
.news {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 40px;
|
||||||
|
|
||||||
|
h2 {
|
||||||
|
width: fit-content;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__list {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr 1fr 1fr;
|
||||||
|
gap: 30px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (max-width: 1024px) {
|
||||||
|
.news {
|
||||||
|
&__list {
|
||||||
|
grid-template-columns: 1fr 1fr 1fr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (max-width: 768px) {
|
||||||
|
.news {
|
||||||
|
gap: 30px;
|
||||||
|
|
||||||
|
&__list {
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (max-width: 550px) {
|
||||||
|
.news {
|
||||||
|
gap: 20px;
|
||||||
|
|
||||||
|
&__list {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
104
src/App/news/[id]/NewsDetails.scss
Normal file
104
src/App/news/[id]/NewsDetails.scss
Normal file
@ -0,0 +1,104 @@
|
|||||||
|
.news-details {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 40px;
|
||||||
|
|
||||||
|
&__img {
|
||||||
|
align-self: center;
|
||||||
|
#news-img {
|
||||||
|
margin-bottom: 30px;
|
||||||
|
width: 100%;
|
||||||
|
max-width: 1072px;
|
||||||
|
height: 598px;
|
||||||
|
border-radius: 12px;
|
||||||
|
object-fit: cover;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__date-and-reviews {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 80px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__date,
|
||||||
|
&__reviews {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
|
||||||
|
p {
|
||||||
|
font-size: 15px;
|
||||||
|
font-weight: 500;
|
||||||
|
line-height: 20px;
|
||||||
|
color: rgba(62, 50, 50, 0.75);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__text {
|
||||||
|
width: 100%;
|
||||||
|
max-width: 1072px;
|
||||||
|
align-self: center;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 10px;
|
||||||
|
color: rgb(62, 50, 50);
|
||||||
|
|
||||||
|
h3 {
|
||||||
|
font-size: 24px;
|
||||||
|
font-weight: 500;
|
||||||
|
line-height: 29px;
|
||||||
|
}
|
||||||
|
|
||||||
|
p {
|
||||||
|
font-size: 20px;
|
||||||
|
font-weight: 400;
|
||||||
|
line-height: 34px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (max-width: 768px) {
|
||||||
|
.news-details {
|
||||||
|
gap: 30px;
|
||||||
|
|
||||||
|
&__img {
|
||||||
|
#news-img {
|
||||||
|
margin-bottom: 25px;
|
||||||
|
height: 392px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__text {
|
||||||
|
h3 {
|
||||||
|
font-size: 20px;
|
||||||
|
line-height: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
p {
|
||||||
|
font-size: 18px;
|
||||||
|
line-height: 34px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (max-width: 550px) {
|
||||||
|
.news-details {
|
||||||
|
gap: 20px;
|
||||||
|
|
||||||
|
&__img {
|
||||||
|
#news-img {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
height: 231px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__text {
|
||||||
|
p {
|
||||||
|
font-size: 16px;
|
||||||
|
line-height: 140%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
17
src/App/news/[id]/icons/calendar.svg
Normal file
17
src/App/news/[id]/icons/calendar.svg
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
<svg width="16.000000" height="16.000000" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||||
|
<desc>
|
||||||
|
Created with Pixso.
|
||||||
|
</desc>
|
||||||
|
<defs>
|
||||||
|
<clipPath id="clip2340_50999">
|
||||||
|
<rect id="calendar" width="16.000000" height="16.000000" fill="white" fill-opacity="0"/>
|
||||||
|
</clipPath>
|
||||||
|
</defs>
|
||||||
|
<rect id="calendar" width="16.000000" height="16.000000" fill="#FFFFFF" fill-opacity="0"/>
|
||||||
|
<g clip-path="url(#clip2340_50999)">
|
||||||
|
<path id="Vector" d="M12.666 2.66699C13.4023 2.66699 14 3.26367 14 4L14 13.334C14 14.0703 13.4023 14.667 12.666 14.667L3.33398 14.667C2.59766 14.667 2 14.0703 2 13.334L2 4C2 3.26367 2.59766 2.66699 3.33398 2.66699L12.666 2.66699Z" stroke="#6E6565" stroke-opacity="1.000000" stroke-width="1.000000" stroke-linejoin="round"/>
|
||||||
|
<path id="Vector" d="M10.666 1.33301L10.666 4" stroke="#6E6565" stroke-opacity="1.000000" stroke-width="1.000000" stroke-linejoin="round" stroke-linecap="round"/>
|
||||||
|
<path id="Vector" d="M5.33398 1.33301L5.33398 4" stroke="#6E6565" stroke-opacity="1.000000" stroke-width="1.000000" stroke-linejoin="round" stroke-linecap="round"/>
|
||||||
|
<path id="Vector" d="M2 6.66699L14 6.66699" stroke="#6E6565" stroke-opacity="1.000000" stroke-width="1.000000" stroke-linejoin="round" stroke-linecap="round"/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 1.3 KiB |
14
src/App/news/[id]/icons/message.svg
Normal file
14
src/App/news/[id]/icons/message.svg
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
<svg width="16.000000" height="16.000000" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||||
|
<desc>
|
||||||
|
Created with Pixso.
|
||||||
|
</desc>
|
||||||
|
<defs>
|
||||||
|
<clipPath id="clip2340_51004">
|
||||||
|
<rect id="message-circle" width="16.000000" height="16.000000" fill="white" fill-opacity="0"/>
|
||||||
|
</clipPath>
|
||||||
|
</defs>
|
||||||
|
<rect id="message-circle" width="16.000000" height="16.000000" fill="#FFFFFF" fill-opacity="0"/>
|
||||||
|
<g clip-path="url(#clip2340_51004)">
|
||||||
|
<path id="Vector" d="M13.4004 10.2002C12.9297 11.1416 12.207 11.9326 11.3125 12.4863C10.416 13.04 9.38477 13.333 8.33398 13.333C7.45312 13.3359 6.58594 13.1299 5.80078 12.7334L2 14L3.26758 10.2002C2.86914 9.41504 2.66406 8.54688 2.66602 7.66699C2.66797 6.61426 2.96094 5.58301 3.51367 4.68848C4.06641 3.79395 4.85938 3.07031 5.80078 2.59961C6.58594 2.20312 7.45312 1.99805 8.33398 2L8.66602 2C10.0566 2.07715 11.3691 2.66309 12.3535 3.64746C13.3359 4.63086 13.9238 5.94336 14 7.33301L14 7.66699C14.002 8.54688 13.7969 9.41504 13.4004 10.2002Z" stroke="#6E6565" stroke-opacity="1.000000" stroke-width="1.000000" stroke-linejoin="round"/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 1.1 KiB |
75
src/App/news/[id]/page.tsx
Normal file
75
src/App/news/[id]/page.tsx
Normal file
@ -0,0 +1,75 @@
|
|||||||
|
import Typography from "@/shared/ui/components/Typography/Typography";
|
||||||
|
import "./NewsDetails.scss";
|
||||||
|
import { apiInstance } from "@/shared/config/apiConfig";
|
||||||
|
import { INews } from "@/shared/types/news-type";
|
||||||
|
import Image from "next/image";
|
||||||
|
import message from "./icons/message.svg";
|
||||||
|
import calendar from "./icons/calendar.svg";
|
||||||
|
import ReviewSection from "@/widgets/ReviewSection/ReviewSection";
|
||||||
|
|
||||||
|
const NewsDetails = async ({
|
||||||
|
params,
|
||||||
|
}: {
|
||||||
|
params: { id: string };
|
||||||
|
}) => {
|
||||||
|
const getNewsById = async () => {
|
||||||
|
const response = await apiInstance.get<INews>(
|
||||||
|
`/news/${params.id}/`
|
||||||
|
);
|
||||||
|
|
||||||
|
return response.data;
|
||||||
|
};
|
||||||
|
|
||||||
|
const data = await getNewsById();
|
||||||
|
|
||||||
|
const months: Record<string, string> = {
|
||||||
|
"01": "Январь",
|
||||||
|
"02": "Февраль",
|
||||||
|
"03": "Март",
|
||||||
|
"04": "Апрель",
|
||||||
|
"05": "Май",
|
||||||
|
"06": "Июнь",
|
||||||
|
"07": "Июль",
|
||||||
|
"08": "Август",
|
||||||
|
"09": "Сентябрь",
|
||||||
|
"10": "Октябрь",
|
||||||
|
"11": "Ноябрь",
|
||||||
|
"12": "Декабрь",
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="news-details page-padding">
|
||||||
|
<Typography element="h2">{data.title}</Typography>
|
||||||
|
|
||||||
|
<div className="news-details__img">
|
||||||
|
<img id="news-img" src={data.image} alt="News Image" />
|
||||||
|
<div className="news-details__date-and-reviews">
|
||||||
|
<div className="news-details__date">
|
||||||
|
<Image src={calendar} alt="Calendar Icon" />
|
||||||
|
<p>
|
||||||
|
{months[data.created_at.slice(5, 7)]}{" "}
|
||||||
|
{data.created_at.slice(5, 7).slice(0, 1) === "0"
|
||||||
|
? data.created_at.slice(6, 7)
|
||||||
|
: data.created_at.slice(5, 7)}
|
||||||
|
, {data.created_at.slice(0, 4)}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="news-details__reviews">
|
||||||
|
<Image src={message} alt="Message Icon" />
|
||||||
|
<p>Комментарии: {data.count_reviews}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="news-details__text">
|
||||||
|
<h3>{data.title}</h3>
|
||||||
|
<p>{data.description}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<ReviewSection endpoint="news" id={data.id} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default NewsDetails;
|
36
src/App/news/page.tsx
Normal file
36
src/App/news/page.tsx
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
import Typography from "@/shared/ui/components/Typography/Typography";
|
||||||
|
import "./News.scss";
|
||||||
|
import { apiInstance } from "@/shared/config/apiConfig";
|
||||||
|
import { INewsList } from "@/shared/types/news-type";
|
||||||
|
import NewsCard from "@/entities/NewsCard/NewsCard";
|
||||||
|
|
||||||
|
const News = async () => {
|
||||||
|
const getNews = async () => {
|
||||||
|
const response = await apiInstance.get<INewsList>("/news/");
|
||||||
|
|
||||||
|
return response.data;
|
||||||
|
};
|
||||||
|
|
||||||
|
const data = await getNews();
|
||||||
|
return (
|
||||||
|
<div className="news page-padding">
|
||||||
|
<Typography element="h2">Новости</Typography>
|
||||||
|
|
||||||
|
<ul className="news__list">
|
||||||
|
{data.results.map((news) => (
|
||||||
|
<li key={news.id}>
|
||||||
|
<NewsCard
|
||||||
|
id={news.id}
|
||||||
|
title={news.title}
|
||||||
|
description={news.description}
|
||||||
|
image={news.image}
|
||||||
|
date={news.created_at}
|
||||||
|
/>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default News;
|
21
src/App/statistics/Statistics.scss
Normal file
21
src/App/statistics/Statistics.scss
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
.statistics {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 40px;
|
||||||
|
|
||||||
|
h2 {
|
||||||
|
width: fit-content;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (max-width: 768px) {
|
||||||
|
.statistics {
|
||||||
|
gap: 30px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (max-width: 550px) {
|
||||||
|
.statistics {
|
||||||
|
gap: 20px;
|
||||||
|
}
|
||||||
|
}
|
34
src/App/statistics/page.tsx
Normal file
34
src/App/statistics/page.tsx
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
import Typography from "@/shared/ui/components/Typography/Typography";
|
||||||
|
import "./Statistics.scss";
|
||||||
|
import { apiInstance } from "@/shared/config/apiConfig";
|
||||||
|
import { IStatistics } from "@/shared/types/statistics-type";
|
||||||
|
import { AxiosError } from "axios";
|
||||||
|
import StatisticsTable from "@/widgets/StatisticsTable/StatisticsTable";
|
||||||
|
|
||||||
|
const Statistics = async () => {
|
||||||
|
const getStatistics = async (): Promise<IStatistics[] | string> => {
|
||||||
|
try {
|
||||||
|
const response = await apiInstance.get<IStatistics[]>(
|
||||||
|
"/report/city/stats/"
|
||||||
|
);
|
||||||
|
|
||||||
|
return response.data;
|
||||||
|
} catch (error: unknown) {
|
||||||
|
if (error instanceof AxiosError) {
|
||||||
|
return error.message;
|
||||||
|
} else {
|
||||||
|
return "Произошла непредвиденная ошибк";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const data = await getStatistics();
|
||||||
|
return (
|
||||||
|
<div className="statistics page-padding">
|
||||||
|
<Typography element="h2">Статистика</Typography>
|
||||||
|
<StatisticsTable firstData={data} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Statistics;
|
21
src/App/volunteers/Volunteers.scss
Normal file
21
src/App/volunteers/Volunteers.scss
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
.volunteers {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 40px;
|
||||||
|
|
||||||
|
h2 {
|
||||||
|
width: fit-content;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (max-width: 768px) {
|
||||||
|
.volunteers {
|
||||||
|
gap: 30px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (max-width: 550px) {
|
||||||
|
.volunteers {
|
||||||
|
gap: 20px;
|
||||||
|
}
|
||||||
|
}
|
36
src/App/volunteers/page.tsx
Normal file
36
src/App/volunteers/page.tsx
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
import Typography from "@/shared/ui/components/Typography/Typography";
|
||||||
|
import "./Volunteers.scss";
|
||||||
|
import { apiInstance } from "@/shared/config/apiConfig";
|
||||||
|
import { AxiosError } from "axios";
|
||||||
|
import { IUserRatings } from "@/shared/types/user-rating-type";
|
||||||
|
import VolunteersTable from "@/widgets/VolunteersTable/VolunteersTable";
|
||||||
|
|
||||||
|
const Volunteers = async () => {
|
||||||
|
const getVolunteers = async (): Promise<
|
||||||
|
IUserRatings[] | string
|
||||||
|
> => {
|
||||||
|
try {
|
||||||
|
const response = await apiInstance.get<IUserRatings[]>(
|
||||||
|
"/report/user_ratings/"
|
||||||
|
);
|
||||||
|
|
||||||
|
return response.data;
|
||||||
|
} catch (error: unknown) {
|
||||||
|
if (error instanceof AxiosError) {
|
||||||
|
return error.message;
|
||||||
|
} else {
|
||||||
|
return "Произошла непредвиденная ошибк";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const data = await getVolunteers();
|
||||||
|
return (
|
||||||
|
<div className="volunteers page-padding">
|
||||||
|
<Typography element="h2">Волонтеры</Typography>
|
||||||
|
<VolunteersTable firstData={data} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Volunteers;
|
@ -1,5 +1,5 @@
|
|||||||
export interface IFetch {
|
export interface IFetch {
|
||||||
error: string;
|
error: string;
|
||||||
data: any;
|
data?: any;
|
||||||
isLoading: boolean;
|
isLoading: boolean;
|
||||||
}
|
}
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
import { IList } from "./list-type";
|
||||||
|
|
||||||
export interface INews {
|
export interface INews {
|
||||||
id: number;
|
id: number;
|
||||||
image: string;
|
image: string;
|
||||||
@ -8,3 +10,7 @@ export interface INews {
|
|||||||
updated_at: string;
|
updated_at: string;
|
||||||
count_reviews: number;
|
count_reviews: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface INewsList extends IList {
|
||||||
|
results: INews[];
|
||||||
|
}
|
||||||
|
18
src/Shared/types/review-type.ts
Normal file
18
src/Shared/types/review-type.ts
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
import { IList } from "./list-type";
|
||||||
|
|
||||||
|
export interface IReview {
|
||||||
|
id: number;
|
||||||
|
author: {
|
||||||
|
id: number;
|
||||||
|
first_name: string;
|
||||||
|
last_name: string;
|
||||||
|
image: string;
|
||||||
|
govern_status: null;
|
||||||
|
};
|
||||||
|
review: string;
|
||||||
|
created_at: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IReviewList extends IList {
|
||||||
|
results: IReview[];
|
||||||
|
}
|
9
src/Shared/types/statistics-type.ts
Normal file
9
src/Shared/types/statistics-type.ts
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
export interface IStatistics {
|
||||||
|
name: string;
|
||||||
|
broken_road_1: number;
|
||||||
|
hotbed_of_accidents_2: number;
|
||||||
|
local_defect_3: number;
|
||||||
|
repair_plans_4: number;
|
||||||
|
repaired_5: number;
|
||||||
|
local_defect_fixed_6: number;
|
||||||
|
}
|
8
src/Shared/types/user-rating-type.ts
Normal file
8
src/Shared/types/user-rating-type.ts
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
export interface IUserRatings {
|
||||||
|
user_id: number;
|
||||||
|
username: string;
|
||||||
|
report_count: number;
|
||||||
|
likes_given_count: number;
|
||||||
|
likes_received_count: number;
|
||||||
|
average_rating: number;
|
||||||
|
}
|
@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
.typography-h2,
|
.typography-h2,
|
||||||
.typography-h3 {
|
.typography-h3 {
|
||||||
|
width: fit-content;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
color: $black;
|
color: $black;
|
||||||
font-size: 42px;
|
font-size: 42px;
|
||||||
|
170
src/Widgets/ReviewSection/ReviewSection.scss
Normal file
170
src/Widgets/ReviewSection/ReviewSection.scss
Normal file
@ -0,0 +1,170 @@
|
|||||||
|
.review-section {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
|
h3 {
|
||||||
|
margin-bottom: 50px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
font-size: 24px;
|
||||||
|
font-weight: 500;
|
||||||
|
line-height: 29px;
|
||||||
|
color: rgb(62, 50, 50);
|
||||||
|
|
||||||
|
span {
|
||||||
|
width: 4px;
|
||||||
|
height: 10px;
|
||||||
|
border-radius: 12px;
|
||||||
|
background: rgb(57, 152, 232);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
form {
|
||||||
|
margin-bottom: 70px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 16px;
|
||||||
|
textarea {
|
||||||
|
height: 258px;
|
||||||
|
width: 100%;
|
||||||
|
padding: 26px 18px;
|
||||||
|
border: 1px solid rgb(197, 198, 197);
|
||||||
|
border-radius: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
align-self: flex-end;
|
||||||
|
padding: 10px 16px;
|
||||||
|
border-radius: 12px;
|
||||||
|
background: rgb(57, 152, 232);
|
||||||
|
color: white;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 500;
|
||||||
|
line-height: 20px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__list {
|
||||||
|
ul {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.review {
|
||||||
|
padding: 20px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 15px;
|
||||||
|
border: 1px solid rgb(197, 198, 197);
|
||||||
|
border-radius: 12px;
|
||||||
|
|
||||||
|
&__author {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
|
||||||
|
#author-img {
|
||||||
|
width: 60px;
|
||||||
|
height: 60px;
|
||||||
|
border-radius: 50%;
|
||||||
|
object-fit: cover;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
&__header {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__author-name {
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 800;
|
||||||
|
line-height: 19px;
|
||||||
|
color: rgb(62, 50, 50);
|
||||||
|
}
|
||||||
|
|
||||||
|
&__date {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
p {
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 500;
|
||||||
|
line-height: 20px;
|
||||||
|
|
||||||
|
color: rgba(62, 50, 50, 0.75);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__description {
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 400;
|
||||||
|
line-height: 20px;
|
||||||
|
color: rgba(0, 0, 0, 0.75);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (max-width: 768px) {
|
||||||
|
.review-section {
|
||||||
|
h3 {
|
||||||
|
margin-bottom: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
form {
|
||||||
|
margin-bottom: 56px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__list {
|
||||||
|
ul {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 25px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.review {
|
||||||
|
padding: 15px 20px;
|
||||||
|
|
||||||
|
&__description {
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (max-width: 550px) {
|
||||||
|
.review-section {
|
||||||
|
h3 {
|
||||||
|
margin-bottom: 10px;
|
||||||
|
font-size: 20px;
|
||||||
|
line-height: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
form {
|
||||||
|
margin-bottom: 50px;
|
||||||
|
gap: 12px;
|
||||||
|
|
||||||
|
textarea {
|
||||||
|
height: 130px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__list {
|
||||||
|
ul {
|
||||||
|
gap: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.review {
|
||||||
|
padding: 15px;
|
||||||
|
|
||||||
|
&__description {
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
132
src/Widgets/ReviewSection/ReviewSection.tsx
Normal file
132
src/Widgets/ReviewSection/ReviewSection.tsx
Normal file
@ -0,0 +1,132 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import "./ReviewSection.scss";
|
||||||
|
import { apiInstance } from "@/shared/config/apiConfig";
|
||||||
|
import { IReviewList } from "@/shared/types/review-type";
|
||||||
|
import { useSession } from "next-auth/react";
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import calendar from "./icons/calendar.svg";
|
||||||
|
import Image from "next/image";
|
||||||
|
|
||||||
|
interface IReviewsSectionProps {
|
||||||
|
endpoint: string;
|
||||||
|
id: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ReviewSection: React.FC<IReviewsSectionProps> = ({
|
||||||
|
endpoint,
|
||||||
|
id,
|
||||||
|
}: IReviewsSectionProps) => {
|
||||||
|
const [reviews, setReviews] = useState<IReviewList>();
|
||||||
|
const session = useSession();
|
||||||
|
const handleSubmit: React.MouseEventHandler<
|
||||||
|
HTMLFormElement
|
||||||
|
> = async (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
const Authorization = `Bearer ${session.data?.access_token}`;
|
||||||
|
|
||||||
|
const formData = new FormData(e.currentTarget);
|
||||||
|
|
||||||
|
const config = {
|
||||||
|
headers: {
|
||||||
|
Authorization,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!formData.get("review")) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
formData.append("news", id.toString());
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await apiInstance.post(
|
||||||
|
`/${endpoint}/${id}/reviews/`,
|
||||||
|
formData,
|
||||||
|
config
|
||||||
|
);
|
||||||
|
getReviews();
|
||||||
|
} catch (error) {
|
||||||
|
console.log(error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const getReviews = async () => {
|
||||||
|
const response = await apiInstance.get<IReviewList>(
|
||||||
|
`/${endpoint}/${id}/reviews/`
|
||||||
|
);
|
||||||
|
|
||||||
|
setReviews(response.data);
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
getReviews();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const months: Record<string, string> = {
|
||||||
|
"01": "Январь",
|
||||||
|
"02": "Февраль",
|
||||||
|
"03": "Март",
|
||||||
|
"04": "Апрель",
|
||||||
|
"05": "Май",
|
||||||
|
"06": "Июнь",
|
||||||
|
"07": "Июль",
|
||||||
|
"08": "Август",
|
||||||
|
"09": "Сентябрь",
|
||||||
|
"10": "Октябрь",
|
||||||
|
"11": "Ноябрь",
|
||||||
|
"12": "Декабрь",
|
||||||
|
};
|
||||||
|
return (
|
||||||
|
<section className="review-section">
|
||||||
|
<h3>
|
||||||
|
<span id="blue-point" /> Написать комментарий
|
||||||
|
</h3>
|
||||||
|
|
||||||
|
<form onSubmit={handleSubmit}>
|
||||||
|
<textarea name="review" />
|
||||||
|
<button type="submit">Отправить</button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<div className="review-section__list">
|
||||||
|
<h3>
|
||||||
|
<span id="blue-point" /> Комментарии
|
||||||
|
</h3>
|
||||||
|
|
||||||
|
<ul>
|
||||||
|
{reviews?.results.map((review) => (
|
||||||
|
<li key={review.id} className="review">
|
||||||
|
<div className="review__author">
|
||||||
|
<img
|
||||||
|
id="author-img"
|
||||||
|
src={review.author.image}
|
||||||
|
alt="Author Image"
|
||||||
|
/>
|
||||||
|
<div className="review__header">
|
||||||
|
<h5 className="review__author-name">
|
||||||
|
{review.author.first_name}
|
||||||
|
{review.author.last_name}
|
||||||
|
</h5>
|
||||||
|
<div className="review__date">
|
||||||
|
<Image src={calendar} alt="Calendar Icon" />
|
||||||
|
<p>
|
||||||
|
{months[review.created_at.slice(5, 7)]}{" "}
|
||||||
|
{review.created_at.slice(5, 7).slice(0, 1) ===
|
||||||
|
"0"
|
||||||
|
? review.created_at.slice(6, 7)
|
||||||
|
: review.created_at.slice(5, 7)}
|
||||||
|
, {review.created_at.slice(0, 4)}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<p className="review__description">{review.review}</p>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ReviewSection;
|
17
src/Widgets/ReviewSection/icons/calendar.svg
Normal file
17
src/Widgets/ReviewSection/icons/calendar.svg
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
<svg width="16.000000" height="16.000000" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||||
|
<desc>
|
||||||
|
Created with Pixso.
|
||||||
|
</desc>
|
||||||
|
<defs>
|
||||||
|
<clipPath id="clip2340_50999">
|
||||||
|
<rect id="calendar" width="16.000000" height="16.000000" fill="white" fill-opacity="0"/>
|
||||||
|
</clipPath>
|
||||||
|
</defs>
|
||||||
|
<rect id="calendar" width="16.000000" height="16.000000" fill="#FFFFFF" fill-opacity="0"/>
|
||||||
|
<g clip-path="url(#clip2340_50999)">
|
||||||
|
<path id="Vector" d="M12.666 2.66699C13.4023 2.66699 14 3.26367 14 4L14 13.334C14 14.0703 13.4023 14.667 12.666 14.667L3.33398 14.667C2.59766 14.667 2 14.0703 2 13.334L2 4C2 3.26367 2.59766 2.66699 3.33398 2.66699L12.666 2.66699Z" stroke="#6E6565" stroke-opacity="1.000000" stroke-width="1.000000" stroke-linejoin="round"/>
|
||||||
|
<path id="Vector" d="M10.666 1.33301L10.666 4" stroke="#6E6565" stroke-opacity="1.000000" stroke-width="1.000000" stroke-linejoin="round" stroke-linecap="round"/>
|
||||||
|
<path id="Vector" d="M5.33398 1.33301L5.33398 4" stroke="#6E6565" stroke-opacity="1.000000" stroke-width="1.000000" stroke-linejoin="round" stroke-linecap="round"/>
|
||||||
|
<path id="Vector" d="M2 6.66699L14 6.66699" stroke="#6E6565" stroke-opacity="1.000000" stroke-width="1.000000" stroke-linejoin="round" stroke-linecap="round"/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 1.3 KiB |
104
src/Widgets/StatisticsTable/StatisticsTable.scss
Normal file
104
src/Widgets/StatisticsTable/StatisticsTable.scss
Normal file
@ -0,0 +1,104 @@
|
|||||||
|
@import "@/shared/ui/variables.scss";
|
||||||
|
|
||||||
|
.statistics-table {
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
&__popup {
|
||||||
|
width: 123px;
|
||||||
|
position: absolute;
|
||||||
|
top: 58%;
|
||||||
|
left: 2%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
background-color: #fff;
|
||||||
|
border-radius: 8px;
|
||||||
|
border: 1px solid rgb(255, 255, 255);
|
||||||
|
box-shadow: 0px 4px 6px -2px rgba(16, 24, 40, 0.03),
|
||||||
|
0px 12px 16px -4px rgba(16, 24, 40, 0.08);
|
||||||
|
button {
|
||||||
|
padding: 10px 14px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 500;
|
||||||
|
line-height: 24px;
|
||||||
|
color: $gray-900;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__wrapper {
|
||||||
|
border: 1px solid rgb(213, 213, 213);
|
||||||
|
border-radius: 12px;
|
||||||
|
|
||||||
|
overflow: hidden;
|
||||||
|
overflow-x: auto;
|
||||||
|
|
||||||
|
&::-webkit-scrollbar {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
table {
|
||||||
|
width: 100%;
|
||||||
|
thead {
|
||||||
|
width: 100%;
|
||||||
|
padding: 0 20px;
|
||||||
|
height: 76px;
|
||||||
|
display: flex;
|
||||||
|
background-color: rgb(244, 244, 244);
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
tr {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 100px 167px 186px 188px 263px 170px 151px;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
td {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
height: 100%;
|
||||||
|
background-color: rgb(244, 244, 244);
|
||||||
|
}
|
||||||
|
|
||||||
|
td,
|
||||||
|
button {
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 500;
|
||||||
|
line-height: 18px;
|
||||||
|
color: rgb(102, 112, 133);
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
tbody {
|
||||||
|
padding: 0 20px;
|
||||||
|
tr {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 100px 167px 186px 188px 263px 170px 151px;
|
||||||
|
align-items: center;
|
||||||
|
width: 100%;
|
||||||
|
height: 80px;
|
||||||
|
border-bottom: 1px solid rgb(241, 244, 249);
|
||||||
|
|
||||||
|
td {
|
||||||
|
font-size: 20px;
|
||||||
|
font-weight: 500;
|
||||||
|
line-height: 20px;
|
||||||
|
color: rgb(102, 112, 133);
|
||||||
|
}
|
||||||
|
|
||||||
|
#statistics-table-stat-name {
|
||||||
|
color: black;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
114
src/Widgets/StatisticsTable/StatisticsTable.tsx
Normal file
114
src/Widgets/StatisticsTable/StatisticsTable.tsx
Normal file
@ -0,0 +1,114 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { IStatistics } from "@/shared/types/statistics-type";
|
||||||
|
import "./StatisticsTable.scss";
|
||||||
|
import { useStatisticsStore } from "./statistics.store";
|
||||||
|
import { useShallow } from "zustand/react/shallow";
|
||||||
|
import SearchForm from "@/features/SearchForm/SearchForm";
|
||||||
|
import chevron_down from "./icons/chevron-down.svg";
|
||||||
|
import arrows from "@/shared/icons/arrows.svg";
|
||||||
|
|
||||||
|
import Image from "next/image";
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
|
||||||
|
interface IStatisticsTableProps {
|
||||||
|
firstData: IStatistics[] | string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const StatisticsTable: React.FC<IStatisticsTableProps> = ({
|
||||||
|
firstData,
|
||||||
|
}: IStatisticsTableProps) => {
|
||||||
|
const { data, error, isLoading, getStatistics } =
|
||||||
|
useStatisticsStore(useShallow((state) => state));
|
||||||
|
|
||||||
|
const [statistics, setStatistics] = useState<IStatistics[]>(
|
||||||
|
typeof firstData === "string" ? [] : firstData
|
||||||
|
);
|
||||||
|
const [openPopup, setOpenPopup] = useState<boolean>(false);
|
||||||
|
|
||||||
|
const handleSubmit = () => {};
|
||||||
|
|
||||||
|
const params = [
|
||||||
|
{ param: "Добавлено дорог", handleSubmit() {} },
|
||||||
|
{ param: "Локальных дефектов", handleSubmit() {} },
|
||||||
|
{ param: "Очагов аварийности", handleSubmit() {} },
|
||||||
|
{ param: "Локальных дефектов исправлено", handleSubmit() {} },
|
||||||
|
{ param: "В планах ремонта", handleSubmit() {} },
|
||||||
|
{ param: "Отремонтировано", handleSubmit() {} },
|
||||||
|
];
|
||||||
|
|
||||||
|
const getStatsByLocation = async (location: string) => {
|
||||||
|
try {
|
||||||
|
const data: IStatistics[] | undefined = await getStatistics(
|
||||||
|
location
|
||||||
|
);
|
||||||
|
|
||||||
|
if (data === undefined)
|
||||||
|
throw new Error("Ошибка на стороне сервера");
|
||||||
|
setStatistics(data);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error fetching statistics:", error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="statistics-table">
|
||||||
|
<SearchForm
|
||||||
|
style={{ width: "100%" }}
|
||||||
|
handleSubmit={handleSubmit}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{openPopup && (
|
||||||
|
<div className="statistics-table__popup">
|
||||||
|
<button onClick={() => getStatsByLocation("state")}>
|
||||||
|
Область
|
||||||
|
</button>
|
||||||
|
<button onClick={() => getStatsByLocation("city")}>
|
||||||
|
Город
|
||||||
|
</button>
|
||||||
|
<button onClick={() => getStatsByLocation("village")}>
|
||||||
|
Деревня
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="statistics-table__wrapper">
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<button onClick={() => setOpenPopup((prev) => !prev)}>
|
||||||
|
Город
|
||||||
|
<Image src={chevron_down} alt=" Chevron Icon" />
|
||||||
|
</button>
|
||||||
|
</td>
|
||||||
|
{params.map((param) => (
|
||||||
|
<td key={param.param}>
|
||||||
|
<button>
|
||||||
|
{param.param}
|
||||||
|
<Image src={arrows} alt=" Arrows Icon" />
|
||||||
|
</button>
|
||||||
|
</td>
|
||||||
|
))}
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{statistics.map((stat) => (
|
||||||
|
<tr key={stat.name}>
|
||||||
|
<td id="statistics-table-stat-name">{stat.name}</td>
|
||||||
|
<td>{stat.broken_road_1}</td>
|
||||||
|
<td>{stat.local_defect_3}</td>
|
||||||
|
<td>{stat.hotbed_of_accidents_2}</td>
|
||||||
|
<td>{stat.local_defect_fixed_6}</td>
|
||||||
|
<td>{stat.repair_plans_4}</td>
|
||||||
|
<td>{stat.repaired_5}</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default StatisticsTable;
|
7
src/Widgets/StatisticsTable/icons/chevron-down.svg
Normal file
7
src/Widgets/StatisticsTable/icons/chevron-down.svg
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
<svg width="9.000000" height="6.000000" viewBox="0 0 9 6" fill="none" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||||
|
<desc>
|
||||||
|
Created with Pixso.
|
||||||
|
</desc>
|
||||||
|
<defs/>
|
||||||
|
<path id="Vector" d="M8.25 0L0.75 0C0.609375 0 0.46875 0.0410156 0.347656 0.117188C0.226562 0.193359 0.132812 0.302734 0.0703125 0.431641C0.0117188 0.561523 -0.0117188 0.705078 0.0078125 0.84668C0.0234375 0.988281 0.0820312 1.12109 0.175781 1.23047L3.92578 5.73047C3.99609 5.81543 4.08203 5.88281 4.18359 5.92969C4.28125 5.97656 4.39062 6 4.5 6C4.60938 6 4.71875 5.97656 4.81641 5.92969C4.91797 5.88281 5.00781 5.81543 5.07812 5.73047L8.82812 1.23047C8.91797 1.12109 8.97656 0.988281 8.99609 0.84668C9.01172 0.705078 8.99219 0.561523 8.92969 0.431641C8.87109 0.302734 8.77344 0.193359 8.65234 0.117188C8.53125 0.0410156 8.39453 0 8.25 0ZM4.5 4.07812L2.35156 1.5L6.64844 1.5L4.5 4.07812Z" fill="#667085" fill-opacity="1.000000" fill-rule="nonzero"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 955 B |
37
src/Widgets/StatisticsTable/statistics.store.ts
Normal file
37
src/Widgets/StatisticsTable/statistics.store.ts
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
import { apiInstance } from "@/shared/config/apiConfig";
|
||||||
|
import { IFetch } from "@/shared/types/fetch-type";
|
||||||
|
import { IStatistics } from "@/shared/types/statistics-type";
|
||||||
|
import { AxiosError } from "axios";
|
||||||
|
import { create } from "zustand";
|
||||||
|
|
||||||
|
interface IStatisticsStore extends IFetch {
|
||||||
|
getStatistics: (
|
||||||
|
endpoint: string
|
||||||
|
) => Promise<IStatistics[] | undefined>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useStatisticsStore = create<IStatisticsStore>((set) => ({
|
||||||
|
isLoading: false,
|
||||||
|
error: "",
|
||||||
|
|
||||||
|
getStatistics: async (
|
||||||
|
endpoint: string
|
||||||
|
): Promise<IStatistics[] | undefined> => {
|
||||||
|
try {
|
||||||
|
set({ isLoading: true });
|
||||||
|
const response = await apiInstance.get<IStatistics[]>(
|
||||||
|
`/report/${endpoint}/stats`
|
||||||
|
);
|
||||||
|
|
||||||
|
return response.data;
|
||||||
|
} catch (error: unknown) {
|
||||||
|
if (error instanceof AxiosError) {
|
||||||
|
set({ error: error.message });
|
||||||
|
} else {
|
||||||
|
set({ error: "an error ocured" });
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
set({ isLoading: false });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}));
|
73
src/Widgets/VolunteersTable/VolunteersTable.scss
Normal file
73
src/Widgets/VolunteersTable/VolunteersTable.scss
Normal file
@ -0,0 +1,73 @@
|
|||||||
|
.volunteers-table {
|
||||||
|
overflow: hidden;
|
||||||
|
overflow-x: auto;
|
||||||
|
&::-webkit-scrollbar {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
border: 1px solid rgb(213, 213, 213);
|
||||||
|
border-radius: 6px;
|
||||||
|
|
||||||
|
table {
|
||||||
|
width: 100%;
|
||||||
|
thead {
|
||||||
|
width: 100%;
|
||||||
|
padding: 0 20px;
|
||||||
|
height: 76px;
|
||||||
|
display: flex;
|
||||||
|
background-color: rgb(244, 244, 244);
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
tr {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 100px 320px 210px 213px 222px 92px;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
td {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
height: 100%;
|
||||||
|
background-color: rgb(244, 244, 244);
|
||||||
|
}
|
||||||
|
|
||||||
|
td,
|
||||||
|
button {
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 500;
|
||||||
|
line-height: 18px;
|
||||||
|
color: rgb(102, 112, 133);
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
tbody {
|
||||||
|
padding: 0 20px;
|
||||||
|
tr {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 100px 320px 210px 213px 222px 92px;
|
||||||
|
align-items: center;
|
||||||
|
width: 100%;
|
||||||
|
height: 80px;
|
||||||
|
border-bottom: 1px solid rgb(241, 244, 249);
|
||||||
|
|
||||||
|
td {
|
||||||
|
font-size: 20px;
|
||||||
|
font-weight: 500;
|
||||||
|
line-height: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#volunteers-user-cell {
|
||||||
|
color: rgb(72, 159, 225);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
61
src/Widgets/VolunteersTable/VolunteersTable.tsx
Normal file
61
src/Widgets/VolunteersTable/VolunteersTable.tsx
Normal file
@ -0,0 +1,61 @@
|
|||||||
|
import { IUserRatings } from "@/shared/types/user-rating-type";
|
||||||
|
import "./VolunteersTable.scss";
|
||||||
|
import Image from "next/image";
|
||||||
|
import arrows from "@/shared/icons/arrows.svg";
|
||||||
|
|
||||||
|
interface IVolunteersTableProps {
|
||||||
|
firstData: IUserRatings[] | string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const VolunteersTable: React.FC<IVolunteersTableProps> = ({
|
||||||
|
firstData,
|
||||||
|
}: IVolunteersTableProps) => {
|
||||||
|
const params = [
|
||||||
|
{ param: "№" },
|
||||||
|
{ param: "Активист" },
|
||||||
|
{ param: "Добавлено дорог", handleClick() {} },
|
||||||
|
{ param: "Получено голосов", handleClick() {} },
|
||||||
|
{ param: "Оставлено голосов", handleClick() {} },
|
||||||
|
{ param: "Рейтинг", handleClick() {} },
|
||||||
|
];
|
||||||
|
return (
|
||||||
|
<div className="volunteers-table">
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
{params.map((param) => (
|
||||||
|
<td key={param.param}>
|
||||||
|
{param.handleClick ? (
|
||||||
|
<button>
|
||||||
|
{param.param}
|
||||||
|
<Image src={arrows} alt="Arrows Icon" />
|
||||||
|
</button>
|
||||||
|
) : (
|
||||||
|
<>{param.param}</>
|
||||||
|
)}
|
||||||
|
</td>
|
||||||
|
))}
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{typeof firstData === "string" ? (
|
||||||
|
<h3>{firstData}</h3>
|
||||||
|
) : (
|
||||||
|
firstData.map((user, index) => (
|
||||||
|
<tr key={user.user_id}>
|
||||||
|
<td>{index + 1}</td>
|
||||||
|
<td id="volunteers-user-cell">{user.username}</td>
|
||||||
|
<td>{user.report_count}</td>
|
||||||
|
<td>{user.likes_received_count}</td>
|
||||||
|
<td>{user.likes_given_count}</td>
|
||||||
|
<td>{user.average_rating}</td>
|
||||||
|
</tr>
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default VolunteersTable;
|
@ -15,9 +15,14 @@ const SearchForm: React.FC<ISearchFormProps> = ({
|
|||||||
placeholder,
|
placeholder,
|
||||||
value,
|
value,
|
||||||
onChange,
|
onChange,
|
||||||
|
style,
|
||||||
}: ISearchFormProps) => {
|
}: ISearchFormProps) => {
|
||||||
return (
|
return (
|
||||||
<form className="search-form" onSubmit={handleSubmit}>
|
<form
|
||||||
|
style={style}
|
||||||
|
className="search-form"
|
||||||
|
onSubmit={handleSubmit}
|
||||||
|
>
|
||||||
<div className="search-form__input">
|
<div className="search-form__input">
|
||||||
<Image src={search} alt="Search Icon" />
|
<Image src={search} alt="Search Icon" />
|
||||||
<input
|
<input
|
||||||
|
Loading…
Reference in New Issue
Block a user