made news, stats, volunteers, auth

This commit is contained in:
Alibek 2024-02-09 19:42:22 +06:00
parent 45a9d698cd
commit f6ceb252b2
31 changed files with 1321 additions and 4 deletions

View 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%;
}
}
}
}

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 332 KiB

View File

@ -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 = () => {
return <div>AboutUs</div>;
return (
<div className="about-us page-padding">
<Typography element="h2">О нас</Typography>
<Image src={header} alt="Header Image" />
<h3>Dont 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
resorts 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;

43
src/App/news/News.scss Normal file
View 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;
}
}
}

View 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%;
}
}
}
}

View 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

View 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

View 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
View 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;

View 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;
}
}

View 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;

View 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;
}
}

View 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;

View File

@ -1,5 +1,5 @@
export interface IFetch {
error: string;
data: any;
data?: any;
isLoading: boolean;
}

View File

@ -1,3 +1,5 @@
import { IList } from "./list-type";
export interface INews {
id: number;
image: string;
@ -8,3 +10,7 @@ export interface INews {
updated_at: string;
count_reviews: number;
}
export interface INewsList extends IList {
results: INews[];
}

View 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[];
}

View 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;
}

View 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;
}

View File

@ -2,6 +2,7 @@
.typography-h2,
.typography-h3 {
width: fit-content;
text-align: center;
color: $black;
font-size: 42px;

View 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;
}
}
}
}
}

View 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;

View 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

View 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;
}
}
}
}
}
}

View 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;

View 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

View 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 });
}
},
}));

View 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);
}
}
}
}
}

View 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;

View File

@ -15,9 +15,14 @@ const SearchForm: React.FC<ISearchFormProps> = ({
placeholder,
value,
onChange,
style,
}: ISearchFormProps) => {
return (
<form className="search-form" onSubmit={handleSubmit}>
<form
style={style}
className="search-form"
onSubmit={handleSubmit}
>
<div className="search-form__input">
<Image src={search} alt="Search Icon" />
<input