fixed bugs

This commit is contained in:
Alibek 2024-02-19 15:14:19 +06:00
parent d4c49a1061
commit b0870824ba
35 changed files with 1225 additions and 420 deletions

View File

@ -6,38 +6,16 @@
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

@ -2,41 +2,20 @@ import "./News.scss";
import Typography from "@/shared/ui/components/Typography/Typography";
import { apiInstance } from "@/shared/config/apiConfig";
import { INewsList } from "@/shared/types/news-type";
import NewsCard from "@/entities/NewsCard/NewsCard";
import NewsList from "@/widgets/NewsList/NewsList";
const News = async () => {
const getNews = async () => {
try {
const response = await apiInstance.get<INewsList>("/news/");
return response.data;
} catch (error) {
console.log(error);
return {
results: [],
};
}
const News = ({
searchParams,
}: {
searchParams: {
["страница-новостей"]: string;
};
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>
<NewsList searchParams={searchParams} />
</div>
);
};

View File

@ -3,13 +3,7 @@ import Header from "@/widgets/Header/Header";
import StatisticsSection from "@/widgets/StatisticsSection/StatisticsSection";
import RatingSection from "@/widgets/RatingSection/RatingSection";
import NewsSection from "@/widgets/NewsSection/NewsSection";
const DynamicMap = dynamic(
() => import("@/widgets/MapSection/MapSection"),
{
ssr: false,
}
);
import MapSection from "@/widgets/MapSection/MapSection";
const Home = async ({
searchParams,
@ -25,18 +19,8 @@ const Home = async ({
<div className="home">
<Header />
<StatisticsSection />
<DynamicMap
categories={searchParams["тип-дороги"]}
queryMap={searchParams["поиск-на-карте"]}
queryRating={searchParams["поиск-рейтинг"]}
page={searchParams["страница-рейтинга"]}
/>
<RatingSection
categories={searchParams["тип-дороги"]}
queryMap={searchParams["поиск-на-карте"]}
queryRating={searchParams["поиск-рейтинг"]}
page={searchParams["страница-рейтинга"]}
/>
{/* <MapSection searchParams={searchParams} /> */}
<RatingSection searchParams={searchParams} />
<NewsSection />
</div>
);

View File

@ -2,16 +2,43 @@ import Typography from "@/shared/ui/components/Typography/Typography";
import "./Profile.scss";
import ProfileNav from "@/widgets/ProfileNav/ProfileNav";
import AuthGuard from "./AuthGuard";
import { AxiosError } from "axios";
import { apiInstance } from "@/shared/config/apiConfig";
import { getServerSession } from "next-auth";
import { authConfig } from "@/shared/config/authConfig";
import { IProfile } from "@/shared/types/profile-type";
const Profile = ({
const Profile = async ({
children,
}: Readonly<{
children: React.ReactNode;
}>) => {
const session = await getServerSession(authConfig);
const getProfile = async () => {
const Authorization = `Bearer ${session?.access_token}`;
const config = {
headers: {
Authorization,
},
};
try {
const response = await apiInstance.get<{
report_count: number;
}>("/users/profile/", config);
return response.data;
} catch (error: unknown) {
if (error instanceof AxiosError) console.log(error.message);
}
};
const data = await getProfile();
return (
<div className="profile page-padding">
<Typography element="h2">Личный кабинет</Typography>
<ProfileNav />
<ProfileNav report_count={data?.report_count as number} />
<AuthGuard>{children}</AuthGuard>
</div>

View File

@ -1,32 +1,14 @@
import { apiInstance } from "@/shared/config/apiConfig";
import { authConfig } from "@/shared/config/authConfig";
import { IMyReportsList } from "@/shared/types/my-reports";
import ProfileTable from "@/widgets/ProfileTable/ProfileTable";
import { getServerSession } from "next-auth";
import React from "react";
const MyReports = async () => {
const session = await getServerSession(authConfig);
const getMyReports = async () => {
const Authorization = `Bearer ${session?.access_token}`;
const config = {
headers: {
Authorization,
},
};
const res = await apiInstance.get<IMyReportsList>(
"/users/reports/",
config
);
return res.data;
};
const data: IMyReportsList = await getMyReports();
const MyReports = async ({
searchParams,
}: {
searchParams: { ["страница-обращений"]: string };
}) => {
return (
<div>
<ProfileTable reports={data} />
<ProfileTable searchParams={searchParams} />
</div>
);
};

View File

@ -5,28 +5,15 @@ 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();
const Statistics = ({
searchParams,
}: {
searchParams: { ["поиск-населенного-пункта"]: string };
}) => {
return (
<div className="statistics page-padding">
<Typography element="h2">Статистика</Typography>
<StatisticsTable firstData={data} />
<StatisticsTable searchParams={searchParams} />
</div>
);
};

View File

@ -5,30 +5,11 @@ 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();
const Volunteers = () => {
return (
<div className="volunteers page-padding">
<Typography element="h2">Волонтеры</Typography>
<VolunteersTable firstData={data} />
<VolunteersTable />
</div>
);
};

View File

@ -11,6 +11,7 @@ interface IPaginationProps {
next: string | null;
count: number;
current_count: number;
limit: number;
}
const Pagination: React.FC<IPaginationProps> = ({
@ -20,8 +21,9 @@ const Pagination: React.FC<IPaginationProps> = ({
next,
count,
current_count,
limit,
}: IPaginationProps) => {
const pages_count = count % 8;
const pages_count = Math.ceil(count / limit);
const showPages = () => {
const btns = [];

View File

@ -6,4 +6,5 @@ export interface IProfile {
image: string;
role: number;
govern_status: null;
report_count: number;
}

View File

@ -16,6 +16,8 @@ import geo_pink_icon from "./icons/geo-pink.svg";
import geo_purple_icon from "./icons/geo-purple.svg";
import geo_red_icon from "./icons/geo-red.svg";
import geo_yellow_icon from "./icons/geo-yellow.svg";
import geo_white_icon from "./icons/geo-white.svg";
import {
DivIcon,
Icon,
@ -27,6 +29,7 @@ import Link from "next/link";
import { Fragment, useEffect, useState } from "react";
import L from "leaflet";
import { ILocation } from "@/shared/types/location-type";
import { useMapStore } from "../mapSectionStore";
interface IData {
id: number;
@ -40,14 +43,13 @@ interface ILatLng {
}
interface IHomeMapProps {
data: IData[];
latLng: ILatLng;
reports: IData[];
}
const HomeMap: React.FC<IHomeMapProps> = ({
data,
latLng,
reports,
}: IHomeMapProps) => {
const { display_location, latLng } = useMapStore();
const [position, setPosition] = useState<ILatLng>({
lat: 42.8746,
lng: 74.606,
@ -70,6 +72,11 @@ const HomeMap: React.FC<IHomeMapProps> = ({
6: createCustomIcon(geo_green_icon),
};
const searchResultIcon = new Icon({
iconUrl: geo_white_icon.src,
iconSize: [100, 100],
});
const categoryToPolyline: Record<number, { color: string }> = {
1: { color: "rgba(230, 68, 82, 0.8)" },
2: { color: "rgba(198, 152, 224, 0.8)" },
@ -88,10 +95,23 @@ const HomeMap: React.FC<IHomeMapProps> = ({
setPosition(latLng);
}, [latLng]);
return null;
};
const defPosition = {
lat: 42.8746,
lng: 74.606,
};
const defPos = [42.8746, 74.606];
if (
latLng.lat === defPosition.lat &&
latLng.lng === defPosition.lng
)
return null;
return (
<Marker position={latLng} icon={searchResultIcon}>
<Popup>{display_location}</Popup>
</Marker>
);
};
return (
<MapContainer
@ -105,7 +125,7 @@ const HomeMap: React.FC<IHomeMapProps> = ({
url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
/>
{data.map((report) =>
{reports.map((report) =>
report.location.length === 2 ? (
<Polyline
key={report.id}
@ -124,7 +144,7 @@ const HomeMap: React.FC<IHomeMapProps> = ({
) : null
)}
{data?.map((report) =>
{reports.map((report) =>
report.location.map((marker) => (
<Marker
key={marker.id}

View File

@ -0,0 +1,17 @@
<svg width="118" height="130" viewBox="0 0 118 130" fill="none" xmlns="http://www.w3.org/2000/svg">
<g filter="url(#filter0_d_122_42709)">
<path d="M59 20C48.5233 20 40 28.7481 40 39.501C40 52.8456 57.0031 68.5901 57.7271 69.4175C58.407 70.1948 59.5942 70.1935 60.2729 69.4175C60.9969 68.5901 78 52.8456 78 39.501C77.9998 28.7481 69.4766 20 59 20Z" fill="#3998E8"/>
</g>
<ellipse cx="58.7872" cy="39" rx="9.88775" ry="10" fill="white"/>
<defs>
<filter id="filter0_d_122_42709" x="0" y="0" width="118" height="130" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
<feOffset dy="20"/>
<feGaussianBlur stdDeviation="20"/>
<feColorMatrix type="matrix" values="0 0 0 0 0.314315 0 0 0 0 0.376772 0 0 0 0 0.406356 0 0 0 0.5 0"/>
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_122_42709"/>
<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow_122_42709" result="shape"/>
</filter>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@ -3,35 +3,68 @@
import "./MapSearch.scss";
import Image from "next/image";
import search from "./icons/search.svg";
import { IDisplayMap } from "@/shared/types/map-type";
import { useMapStore } from "../mapSectionStore";
import { useEffect, useState } from "react";
import { useDebounce } from "use-debounce";
import { useRouter } from "next/navigation";
interface IMapSearchProps
extends React.InputHTMLAttributes<HTMLInputElement> {
options: IDisplayMap[];
setMapSearch: (string: string) => void;
setLatLng: ({ lat, lng }: { lat: number; lng: number }) => void;
searchParams: {
["тип-дороги"]: string;
["поиск-на-карте"]: string;
["поиск-рейтинг"]: string;
["страница-рейтинга"]: string;
};
}
const MapSearch: React.FC<IMapSearchProps> = ({
name,
placeholder,
value,
onChange,
options,
setMapSearch,
setLatLng,
searchParams,
}: IMapSearchProps) => {
const router = useRouter();
const [searchMap, setSearchMap] = useState<string>(
searchParams["поиск-на-карте"] || ""
);
const [query] = useDebounce(searchMap, 500);
const {
setLatLng,
searchData: options,
getLocations,
setDisplayLocation,
} = useMapStore();
const handleSubmit = (
display_name: string,
latLng: { lat: number; lng: number }
) => {
setMapSearch(display_name);
setSearchMap(display_name);
setDisplayLocation(display_name);
setLatLng(latLng);
window.scrollTo({
top: 1400,
behavior: "smooth",
});
};
useEffect(() => {
getLocations(searchMap);
router.push(
`?тип-дороги=${
searchParams["тип-дороги"] || "1,2,3,4,5,6"
}&поиск-на-карте=${searchMap}&поиск-рейтинг=${
searchParams["поиск-рейтинг"] || ""
}&страница-рейтинга=${searchParams["страница-рейтинга"] || ""}`,
{ scroll: false }
);
}, [query]);
return (
<div className="map-search">
<form
onSubmit={(e) => {
onSubmit={(e: React.MouseEvent<HTMLFormElement>) => {
e.preventDefault();
handleSubmit(options[0].display_name, {
lat: +options[0].lat,
@ -42,10 +75,10 @@ const MapSearch: React.FC<IMapSearchProps> = ({
<div className="map-search__input">
<Image src={search} alt="Search Icon" />
<input
onChange={onChange}
value={value}
placeholder={placeholder}
name={name}
value={searchMap}
placeholder="Введите город, село или регион"
onChange={(e) => setSearchMap(e.target.value)}
name="map-search"
type="text"
/>
</div>
@ -59,11 +92,16 @@ const MapSearch: React.FC<IMapSearchProps> = ({
<li key={opt.place_id}>
<button
onClick={() => {
setMapSearch(opt.display_name);
setSearchMap(opt.display_name);
setDisplayLocation(opt.display_name);
setLatLng({
lat: +opt.lat,
lng: +opt.lon,
});
window.scrollTo({
top: 1400,
behavior: "smooth",
});
}}
>
{opt.display_name}

View File

@ -0,0 +1,7 @@
"use server";
export const handleSearch = async (formData: FormData) => {
"use server";
console.log(formData.get("map-search"));
};

View File

@ -1,96 +1,63 @@
"use client";
import Typography from "@/shared/ui/components/Typography/Typography";
import "./MapSection.scss";
import { useEffect, useState } from "react";
import Paragraph from "@/shared/ui/components/Paragraph/Paragraph";
import MapSearch from "./MapSearch/MapSearch";
import dynamic from "next/dynamic";
import { apiInstance } from "@/shared/config/apiConfig";
import { IReport } from "@/shared/types/report-type";
import Switch from "./Switch/Switch";
import {
ROAD_TYPES,
ROAD_TYPES_COLORS,
} from "@/shared/variables/road-types";
import HomeMap from "./HomeMap/HomeMap";
import { useRouter } from "next/navigation";
import { useMapStore } from "./mapSectionStore";
import Switch from "./Switch/Switch";
import { useShallow } from "zustand/react/shallow";
import Typography from "@/shared/ui/components/Typography/Typography";
import Paragraph from "@/shared/ui/components/Paragraph/Paragraph";
import { useDebounce } from "use-debounce";
import MapSearch from "./MapSearch/MapSearch";
const DynamicMap = dynamic(() => import("./HomeMap/HomeMap"), {
ssr: false,
});
interface IMapSectionProps {
[key: string]: string;
searchParams: {
["тип-дороги"]: string;
["поиск-на-карте"]: string;
["поиск-рейтинг"]: string;
["страница-рейтинга"]: string;
};
}
interface ILatLng {
lat: number;
lng: number;
}
const MapSection: React.FC<IMapSectionProps> = ({
categories = "1,2,3,4,5,6",
queryMap,
queryRating,
page = "1",
const MapSection: React.FC<IMapSectionProps> = async ({
searchParams,
}: IMapSectionProps) => {
const [mapSearch, setMapSearch] = useState<string>(queryMap || "");
const [latLng, setLatLng] = useState<ILatLng>({
lat: 42.8746,
lng: 74.606,
});
const [query] = useDebounce(mapSearch, 500);
const data = useMapStore(useShallow((state) => state.data));
const searchedData = useMapStore(
useShallow((state) => state.searchData)
);
const getReports = useMapStore(
useShallow((state) => state.getReports)
);
const getLocations = useMapStore(
useShallow((state) => state.getLocations)
);
const router = useRouter();
useEffect(() => {
getReports(categories);
}, [categories]);
useEffect(() => {
router.push(
`/?тип-дороги=${categories}${
mapSearch ? `&поиск-на-карте=${mapSearch}` : ""
}${
queryRating ? `&поиск-рейтинг=${queryRating}` : ""
}&страница-рейтинга=${page}`,
{
scroll: false,
}
const getReports = async (categories: string) => {
const res = await apiInstance<IReport[]>(
`/report/?category=${categories}`
);
getLocations(query);
}, [categories, query, queryRating]);
return res.data;
};
const setSearchParams = (category: string) => {
const availableCategories = ["1", "2", "3", "4", "5", "6"];
const data = await getReports(
searchParams["тип-дороги"] || "1,2,3,4,5,6"
);
if (!categories || !availableCategories.includes(category))
return categories;
if (categories?.includes(category)) {
const updatedCategories = categories
?.replace(category + ",", "")
.replace("," + category, "")
.replace(category, "");
return updatedCategories;
} else {
const newValue = category + ",";
const updatedCategories = newValue + categories;
return updatedCategories;
const setCategories = (category: string) => {
if (searchParams["тип-дороги"] === undefined) {
const categories = ["1", "2", "3", "4", "5", "6"];
return categories.filter((cat) => cat !== category).join(",");
}
const categories = Array.from(searchParams["тип-дороги"]).filter(
(part) => part !== ","
);
if (categories.includes(category))
return categories.filter((cat) => cat !== category).join(",");
categories.push(category);
return categories.join(",");
};
return (
<section className="map-section">
<div className="map-section">
<div className="map-section__header">
<Typography element="h3">Карта дорог</Typography>
<Paragraph>
@ -99,48 +66,71 @@ const MapSection: React.FC<IMapSectionProps> = ({
</Paragraph>
</div>
<MapSearch
setLatLng={setLatLng}
setMapSearch={setMapSearch}
options={searchedData}
onChange={(e) => setMapSearch(e.target.value)}
name="map-search"
value={mapSearch}
placeholder="Введите город, село или регион"
/>
<div className="map-section__categories">
<ul className="map-section__categories_left">
<ul>
{[1, 2, 3].map((sw) => (
<li key={sw}>
<Switch
href={`/?тип-дороги=${setSearchParams(
sw.toString()
)}`}
defaultState={
searchParams["тип-дороги"]
? searchParams["тип-дороги"]?.includes(
sw.toString()
)
: true
}
color={ROAD_TYPES_COLORS[sw]}
defaultState={categories.includes(sw.toString())}
href={`?${new URLSearchParams({
["тип-дороги"]: setCategories(
sw.toString()
) as string,
["поиск-на-карте"]:
searchParams["поиск-на-карте"] || "",
["поиск-рейтинг"]:
searchParams["поиск-рейтинг"] || "",
["страница-рейтинга"]:
searchParams["страница-рейтинга"] || "",
})}`}
/>
<label>{ROAD_TYPES[sw]}</label>
<p>{ROAD_TYPES[sw]}</p>
</li>
))}
</ul>
<ul className="map-section__categories_right">
<ul>
{[4, 5, 6].map((sw) => (
<li key={sw}>
<Switch
href={`/?тип-дороги=${setSearchParams(
sw.toString()
)}`}
defaultState={
searchParams["тип-дороги"]
? searchParams["тип-дороги"]?.includes(
sw.toString()
)
: true
}
color={ROAD_TYPES_COLORS[sw]}
defaultState={categories.includes(sw.toString())}
href={`?${new URLSearchParams({
["тип-дороги"]: setCategories(
sw.toString()
) as string,
["поиск-на-карте"]:
searchParams["поиск-на-карте"] || "",
["поиск-рейтинг"]:
searchParams["поиск-рейтинг"] || "",
["страница-рейтинга"]:
searchParams["страница-рейтинга"] || "",
})}`}
/>
<label>{ROAD_TYPES[sw]}</label>
<p>{ROAD_TYPES[sw]}</p>
</li>
))}
</ul>
</div>
<HomeMap data={data.results} latLng={latLng} />
</section>
<MapSearch searchParams={searchParams} />
<div className="map-section__categories"></div>
<DynamicMap reports={data} />
</div>
);
};

View File

@ -5,7 +5,7 @@ import "./Switch.scss";
import Link from "next/link";
interface ISwitchProps {
defaultState?: boolean;
defaultState: boolean;
color?: string;
href: string;
}
@ -25,7 +25,7 @@ const Switch: React.FC<ISwitchProps> = ({
onClick={() => setToggleSwitch((prev) => !prev)}
style={{
backgroundColor: !toggleSwitch
? "rgb(50, 48, 58)"
? "rgb(71, 85, 105)"
: color
? color
: "rgb(230, 68, 82)",

View File

@ -1,46 +1,22 @@
import { apiInstance } from "@/shared/config/apiConfig";
import { IFetch } from "@/shared/types/fetch-type";
import { IList } from "@/shared/types/list-type";
import { IDisplayMap } from "@/shared/types/map-type";
import { IReport } from "@/shared/types/report-type";
import axios from "axios";
import { create } from "zustand";
interface IFetchReports extends IList {
results: IReport[];
}
interface IMapStore extends IFetch {
data: IFetchReports;
searchData: IDisplayMap[];
getReports: (categories: string) => Promise<void>;
interface IMapStore {
setLatLng: (latLng: { lat: number; lng: number }) => void;
latLng: { lat: number; lng: number };
getLocations: (query: string) => void;
searchData: IDisplayMap[];
setDisplayLocation: (display_location: string) => void;
display_location: string;
}
export const useMapStore = create<IMapStore>((set) => ({
data: {
count: 0,
previous: null,
next: null,
results: [],
},
searchData: [],
isLoading: false,
error: "",
getReports: async (categories: string = "1,2,3,4,5,6") => {
try {
set({ isLoading: true });
const response = await apiInstance.get<IFetchReports>(
`/report/?category=${categories}`
);
set({ data: response.data });
} catch (error: any) {
set({ error: error.message });
} finally {
set({ isLoading: false });
}
display_location: "",
latLng: {
lat: 42.8746,
lng: 74.606,
},
getLocations: async (query: string = "") => {
const params: Record<string, any> = {
@ -60,4 +36,10 @@ export const useMapStore = create<IMapStore>((set) => ({
set({ searchData: inKG });
},
setLatLng: (latLng: { lat: number; lng: number }) => {
set({ latLng: latLng });
},
setDisplayLocation: (display_location: string) => {
set({ display_location: display_location });
},
}));

View File

@ -0,0 +1,38 @@
.news-list {
display: flex;
flex-direction: column;
gap: 20px;
ul {
display: grid;
grid-template-columns: 1fr 1fr 1fr 1fr;
gap: 30px;
}
}
@media screen and (max-width: 1024px) {
.news-list {
ul {
grid-template-columns: 1fr 1fr 1fr;
}
}
}
@media screen and (max-width: 768px) {
.news-list {
gap: 30px;
ul {
grid-template-columns: 1fr 1fr;
}
}
}
@media screen and (max-width: 550px) {
.news-list {
gap: 20px;
ul {
grid-template-columns: 1fr;
}
}
}

View File

@ -0,0 +1,54 @@
"use client";
import "./NewsList.scss";
import NewsCard from "@/entities/NewsCard/NewsCard";
import { useNewsStore } from "./newsStore";
import { useEffect, useState } from "react";
import Pagination from "@/features/Pagination/Pagination";
interface INewsListProps {
searchParams: {
["страница-новостей"]: string;
};
}
const NewsList: React.FC<INewsListProps> = ({
searchParams,
}: INewsListProps) => {
const [activePage, setActivePage] = useState<number>(
+searchParams["страница-новостей"] || 1
);
const { data: news, getNews, isLoading, error } = useNewsStore();
useEffect(() => {
getNews(activePage);
}, []);
return (
<div className="news-list">
<ul>
{news.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>
<Pagination
activePage={activePage}
setActivePage={setActivePage}
prev={news.previous}
next={news.next}
count={news.count as number}
current_count={news.results.length}
limit={20}
/>
</div>
);
};
export default NewsList;

View File

@ -0,0 +1,38 @@
import { apiInstance } from "@/shared/config/apiConfig";
import { IFetch } from "@/shared/types/fetch-type";
import { INewsList } from "@/shared/types/news-type";
import { AxiosError } from "axios";
import { create } from "zustand";
interface useNewsStore extends IFetch {
data: INewsList;
getNews: (page: number) => void;
}
export const useNewsStore = create<useNewsStore>((set) => ({
data: {
count: 0,
previous: null,
next: null,
results: [],
},
error: "",
isLoading: false,
getNews: async (page: number) => {
try {
set({ isLoading: true });
const res = await apiInstance.get(`/news/?page${page}`);
set({ data: res.data });
} catch (error: unknown) {
if (error instanceof AxiosError) {
set({ error: error.message });
} else {
set({ error: "An error ocured" });
}
} finally {
set({ isLoading: false });
}
},
}));

View File

@ -1,7 +1,7 @@
import { apiInstance } from "@/shared/config/apiConfig";
import { IList } from "@/shared/types/list-type";
import { INews } from "@/shared/types/news-type";
import axios, { AxiosError } from "axios";
import { AxiosError } from "axios";
interface IFetchNews extends IList {
results: INews[];

View File

@ -5,7 +5,13 @@ import LogoutButton from "@/features/LogoutButton/LogoutButton";
import Link from "next/link";
import { usePathname } from "next/navigation";
const ProfileNav = () => {
interface IProfileNavProps {
report_count: number;
}
const ProfileNav: React.FC<IProfileNavProps> = ({
report_count,
}: IProfileNavProps) => {
const pathname = usePathname();
return (
<nav className="profile-nav">
@ -30,7 +36,7 @@ const ProfileNav = () => {
>
Мои обращения
</Link>
<span>3</span>
<span>{report_count}</span>
</div>
{pathname === "/profile/personal" ? (

View File

@ -76,8 +76,11 @@
}
#my-report-location {
font-size: 20px;
color: rgb(50, 48, 58);
a {
display: flex;
font-size: 20px;
color: rgb(50, 48, 58);
}
}
#my-report-status {
@ -102,7 +105,7 @@
color: rgb(102, 102, 102);
}
a {
#my-reports-link {
display: none;
padding: 10px;
border-radius: 8px;

View File

@ -1,33 +1,122 @@
"use client";
import Image from "next/image";
import "./ProfileTable.scss";
import arrows from "@/shared/icons/arrows.svg";
import { IMyReportsList } from "@/shared/types/my-reports";
import {
REPORT_STATUS,
REPORT_STATUS_COLORS,
} from "@/shared/variables/report-status";
import Link from "next/link";
import Pagination from "@/features/Pagination/Pagination";
import { useEffect, useState } from "react";
import { useSession } from "next-auth/react";
import { sliceDate } from "./helpers";
import { useProfileReportsStore } from "./profile-reports.store";
import { useRouter } from "next/navigation";
interface IProfileTableProps {
reports: IMyReportsList;
searchParams: { ["страница-обращений"]: string };
}
const ProfileTable: React.FC<IProfileTableProps> = ({
reports,
searchParams,
}: IProfileTableProps) => {
const {
getMyReports,
data: reports,
isLoading,
error,
} = useProfileReportsStore();
const session = useSession();
const router = useRouter();
const [filter, setFilter] = useState({
option: "date",
toggle: true,
});
const [activePage, setActivePage] = useState<number>(
+searchParams["страница-обращений"] || 1
);
const params = [
{ param: "Дата", handleClick() {} },
{
param: "Дата",
handleClick() {
if (filter.option !== "date") {
return setFilter({ option: "date", toggle: false });
}
setFilter((prev) => {
return { option: "date", toggle: !prev.toggle };
});
getMyReports(
filter,
activePage,
session.data?.access_token as string
);
},
},
{ param: "Адрес" },
{ param: "Статус" },
{ param: "Комментарии", handleClick() {} },
{ param: "Рейтинг", handleClick() {} },
{
param: "Комментарии",
handleClick() {
if (filter.option !== "count_reviews") {
return setFilter({
option: "count_reviews",
toggle: false,
});
}
setFilter((prev) => {
return { option: "count_reviews", toggle: !prev.toggle };
});
getMyReports(
filter,
activePage,
session.data?.access_token as string
);
},
},
{
param: "Рейтинг",
handleClick() {
if (filter.option !== "total_likes") {
return setFilter({ option: "total_likes", toggle: false });
}
setFilter((prev) => {
return { option: "total_likes", toggle: !prev.toggle };
});
getMyReports(
filter,
activePage,
session.data?.access_token as string
);
},
},
];
const sliceDate = (date: string) => {
return `${date.slice(8, 10)}.${date.slice(5, 7)}.${date.slice(
0,
4
)}`;
};
useEffect(() => {
if (session.status === "loading") return;
getMyReports(
filter,
activePage,
session.data?.access_token as string
);
}, [session]);
useEffect(() => {
router.push(
`/profile/my-reports?страница-обращений=${activePage}`,
{ scroll: false }
);
getMyReports(
filter,
activePage,
session.data?.access_token as string
);
}, [activePage]);
return (
<div className="profile-table">
<div
@ -42,7 +131,7 @@ const ProfileTable: React.FC<IProfileTableProps> = ({
{params.map((p) => (
<td key={p.param}>
{p.handleClick ? (
<button>
<button onClick={p.handleClick}>
{p.param}{" "}
<Image src={arrows} alt="Arrows Icon" />
</button>
@ -60,7 +149,13 @@ const ProfileTable: React.FC<IProfileTableProps> = ({
{sliceDate(report.created_at)}
</td>
<td id="my-report-location">
{report.location[0].address}
{report.status === 2 ? (
<Link href={`/report/${report.id}`}>
{report.location[0].address}
</Link>
) : (
report.location[0].address
)}
</td>
<td
id="my-report-status"
@ -84,6 +179,15 @@ const ProfileTable: React.FC<IProfileTableProps> = ({
</p>
)}
</div>
<Pagination
activePage={activePage}
setActivePage={setActivePage}
count={reports.count as number}
prev={reports.previous}
next={reports.next}
current_count={reports.results.length}
limit={8}
/>
<Link id="my-reports-link" href="/create-report">
Написать обращение
</Link>

View File

@ -0,0 +1,6 @@
export const sliceDate = (date: string) => {
return `${date.slice(8, 10)}.${date.slice(5, 7)}.${date.slice(
0,
4
)}`;
};

View File

@ -0,0 +1,106 @@
import { apiInstance } from "@/shared/config/apiConfig";
import { IFetch } from "@/shared/types/fetch-type";
import {
IMyReports,
IMyReportsList,
} from "@/shared/types/my-reports";
import { AxiosError } from "axios";
import { create } from "zustand";
const filterCategories: Record<string, string> = {
count_reviews: "count_reviews",
total_likes: "total_likes",
};
interface IProfileReportsStore extends IFetch {
data: IMyReportsList;
getMyReports: (
filter: { option: string; toggle: boolean },
page: number,
access_token: string
) => void;
}
export const useProfileReportsStore = create<IProfileReportsStore>(
(set) => ({
isLoading: false,
error: "",
data: {
count: 0,
previous: null,
next: null,
results: [],
},
getMyReports: async (
filter: {
option: string;
toggle: boolean;
},
page: number,
access_token: string
) => {
try {
const Authorization = `Bearer ${access_token}`;
const config = {
headers: {
Authorization,
},
};
set({ isLoading: true });
const res = await apiInstance.get<IMyReportsList>(
`/users/reports/?page=${page}`,
config
);
let data = res.data;
if (filter.option === "date" && filter.toggle === false) {
data.results = data.results.sort((a, b) => {
const dateA = new Date(a.created_at) as unknown as number;
const dateB = new Date(b.created_at) as unknown as number;
return dateA - dateB;
});
} else if (
filter.option === "date" &&
filter.toggle === true
) {
data.results = data.results.sort((a, b) => {
const dateA = new Date(a.created_at) as unknown as number;
const dateB = new Date(b.created_at) as unknown as number;
return dateB - dateA;
});
}
if (
filter.option === filterCategories[filter.option] &&
filter.toggle === false
) {
data.results = data.results.sort((a, b) => {
const optionKey = filter.option as keyof IMyReports;
return (Number(a[optionKey]) -
Number(b[optionKey])) as number;
});
} else if (
filter.option === "rating" &&
filter.toggle === true
) {
data.results = data.results.sort((a, b) => {
const optionKey = filter.option as keyof IMyReports;
return (Number(a[optionKey]) -
Number(b[optionKey])) as number;
});
}
set({ data: data });
} catch (error: unknown) {
if (error instanceof AxiosError) {
set({ error: error.message });
} else {
set({ error: "an error ocured" });
}
} finally {
set({ isLoading: false });
}
},
})
);

View File

@ -25,19 +25,23 @@ import {
} from "./helpers";
interface IRatingSectionProps {
[key: string]: string;
searchParams: {
["тип-дороги"]: string;
["поиск-на-карте"]: string;
["поиск-рейтинг"]: string;
["страница-рейтинга"]: string;
};
}
const RatingSection: React.FC<IRatingSectionProps> = ({
categories = "1,2,3,4,5,6",
queryMap,
queryRating,
page = "1",
searchParams,
}: IRatingSectionProps) => {
const [ratingSearch, setRatingSearch] = useState<string>(
queryRating || ""
searchParams["поиск-рейтинг"] || ""
);
const [activePage, setActivePage] = useState<number>(
+searchParams["страница-рейтинга"] || 1
);
const [activePage, setActivePage] = useState<number>(+page);
const [filter, setFilter] = useState({
option: "date",
toggle: false,
@ -50,7 +54,7 @@ const RatingSection: React.FC<IRatingSectionProps> = ({
);
useEffect(() => {
getReports(ratingSearch, +page, filter);
getReports(ratingSearch, activePage, filter);
}, []);
const handleSubmit: React.FormEventHandler<
@ -61,11 +65,13 @@ const RatingSection: React.FC<IRatingSectionProps> = ({
setRatingSearch(formData.get("rating-search") as string);
router.push(
`/?тип-дороги=${categories}${
queryMap ? `&поиск-на-карте=${queryMap}` : ""
`/?тип-дороги=${searchParams["тип-дороги"] || "1,2,3,4,5,6"}${
searchParams["поиск-на-карте"]
? `&поиск-на-карте=${searchParams["поиск-на-карте"] || ""}`
: ""
}${
ratingSearch ? `&поиск-рейтинг=${ratingSearch}` : ""
}&страница-рейтинга=${page}`,
ratingSearch ? `&поиск-рейтинг=${ratingSearch || ""}` : ""
}&страница-рейтинга=${searchParams["страница-рейтинга"]}`,
{
scroll: false,
}
@ -73,16 +79,21 @@ const RatingSection: React.FC<IRatingSectionProps> = ({
getReports(ratingSearch, activePage, filter);
if (reports.results.length < 8 && page !== "1") {
if (
reports.results.length < 8 &&
searchParams["страница-рейтинга"] !== "1"
) {
setActivePage(1);
}
};
useEffect(() => {
router.push(
`/?тип-дороги=${categories}${
queryMap ? `&поиск-на-карте=${queryMap}` : ""
}${ratingSearch ? `&поиск-рейтинг=${ratingSearch}` : ""}${
`/?тип-дороги=${searchParams["тип-дороги"] || "1,2,3,4,5,6"}${
searchParams["поиск-на-карте"]
? `&поиск-на-карте=${searchParams["поиск-на-карте"] || ""}`
: ""
}${ratingSearch ? `&поиск-рейтинг=${ratingSearch || ""}` : ""}${
activePage === 1 ? "" : `&страница-рейтинг=${activePage}`
}`,
{
@ -216,6 +227,7 @@ const RatingSection: React.FC<IRatingSectionProps> = ({
next={reports.next}
prev={reports.previous}
current_count={reports.results.length}
limit={8}
/>
</section>
);

View File

@ -35,7 +35,9 @@ export const useRatingStore = create<IRatingStore>((set) => ({
set({ isLoading: true });
const data = (
await apiInstance.get<IFetchReports>(`/report/?page=${page}`)
await apiInstance.get<IFetchReports>(
`/report/?page=${page}&page_size=${8}`
)
).data;
const searched = data.results.filter((rating) => {

View File

@ -44,6 +44,14 @@
line-height: 22px;
color: rgb(197, 198, 197);
}
p {
color: rgb(240, 68, 56);
}
}
&__error {
color: rgb(240, 68, 56);
}
&__map {
@ -102,6 +110,10 @@
color: rgb(102, 102, 102);
}
&-error {
color: rgb(240, 68, 56) !important;
}
input[type="file"] {
display: none;
}

View File

@ -6,7 +6,6 @@ import Image from "next/image";
import {
MapContainer,
Marker,
Popup,
TileLayer,
useMapEvents,
} from "react-leaflet";
@ -16,9 +15,12 @@ import pin_image from "./icons/pin-image.svg";
import { ChangeEventHandler, useState } from "react";
import { Icon } from "leaflet";
import pin_icon from "./icons/pin_icon.svg";
import axios from "axios";
import axios, { AxiosError } from "axios";
import { apiInstance } from "@/shared/config/apiConfig";
import { useSession } from "next-auth/react";
import { IDisplayMap } from "@/shared/types/map-type";
import { useRouter } from "next/navigation";
import Loader from "@/shared/ui/components/Loader/Loader";
interface ILatLng {
lat: number;
@ -27,6 +29,7 @@ interface ILatLng {
const ReportForm = () => {
const session = useSession();
const router = useRouter();
const [latLng, setLatLng] = useState<ILatLng[]>([]);
const [displayLatLng, setDisplayLatLng] = useState<string[]>([]);
const [images, setImages] = useState<File[]>([]);
@ -34,6 +37,12 @@ const ReportForm = () => {
lat: 42.8746,
lng: 74.606,
};
const [loader, setLoader] = useState<boolean>(false);
const [locationWarning, setLocationWarning] = useState<string>("");
const [descriptionWarning, setDescriptionWarning] =
useState<string>("");
const [imageWarning, setImageWarning] = useState<string>("");
const [error, setError] = useState<string>("");
const customIcon = new Icon({
iconUrl: pin_icon.src,
@ -55,13 +64,45 @@ const ReportForm = () => {
HTMLFormElement
> = async (e) => {
e.preventDefault();
const formData = new FormData(e.currentTarget);
if (displayLatLng.length === 0) {
setDescriptionWarning("");
setImageWarning("");
setError("");
setLocationWarning("Выберите хотя бы одну точку!");
return;
}
if (!formData.get("description")) {
setLocationWarning("");
setImageWarning("");
setError("");
setDescriptionWarning("Пожалуйста опишите свое обращение");
return;
}
if (images.length === 0) {
setLocationWarning("");
setDescriptionWarning("");
setError("");
setImageWarning("Загрузите по меньшей мере одну фотографию");
return;
}
setLocationWarning("");
setDescriptionWarning("");
setError("");
setImageWarning("");
const Authorization = `Bearer ${session.data?.access_token}`;
const config = {
headers: {
Authorization,
},
};
const formData = new FormData(e.currentTarget);
images.forEach((image) => {
formData.append("image", image);
@ -70,13 +111,27 @@ const ReportForm = () => {
formData.append("longitude1", latLng[0].lng.toString());
formData.append("latitude2", latLng[1].lat.toString());
formData.append("longitude2", latLng[1].lng.toString());
const res = await apiInstance.post(
"/report/create/",
formData,
config
);
console.log(res.status);
try {
setLoader(true);
const res = await apiInstance.post(
"/report/create/",
formData,
config
);
if ([200, 201].includes(res.status)) {
router.push("/");
}
} catch (error: unknown) {
if (error instanceof AxiosError) {
setError(error.message);
} else {
setError("Произошла непредвиденная ошибка");
}
} finally {
setLoader(false);
}
};
const MapPins = (e: any) => {
@ -100,8 +155,32 @@ const ReportForm = () => {
},
});
const removeMarker = (latLng: { lat: number; lng: number }) => {
setLatLng((prev) => prev.filter((l) => l !== latLng));
axios
.get<IDisplayMap>(
`https://nominatim.openstreetmap.org/reverse?lat=${latLng.lat}&lon=${latLng.lng}&format=json`
)
.then((res) =>
setDisplayLatLng((prev) =>
prev.filter((loc) => loc !== res.data.display_name)
)
)
.catch((error) => console.log(error));
};
return latLng.map((l) => (
<Marker key={l.lat} icon={customIcon} position={l} />
<Marker
eventHandlers={{
click: () => {
removeMarker(l);
},
}}
key={l.lat}
icon={customIcon}
position={l}
/>
));
};
@ -115,6 +194,7 @@ const ReportForm = () => {
placeholder="Отметьте точки на карте"
type="text"
/>
{locationWarning ? <p>{locationWarning}</p> : null}
</div>
<div className="report-form__map">
@ -146,6 +226,7 @@ const ReportForm = () => {
<div className="report-form__input">
<label>Добавьте описание проблемы</label>
<textarea name="description" placeholder="Введите описание" />
{descriptionWarning ? <p>{descriptionWarning}</p> : null}
</div>
<div className="report-form__add-images">
@ -162,6 +243,12 @@ const ReportForm = () => {
Прикрепить файл
<span>(до 5 МБ)</span>
</label>
{imageWarning ? (
<p className="report-form__add-images-error">
{imageWarning}
</p>
) : null}
<input
onChange={handleImages}
multiple
@ -184,10 +271,11 @@ const ReportForm = () => {
</div>
) : null}
</div>
<button type="submit">
Отправить на модерацию
<button disabled={loader} type="submit">
{loader ? <Loader /> : "Отправить на модерацию"}
<Image src={arrow_right} alt="Arrow Right Icon" />
</button>
{error ? <p className="report-form__error">{error}</p> : null}
</form>
);
};

View File

@ -104,7 +104,7 @@ const ReviewSection: React.FC<IReviewsSectionProps> = ({
/>
<div className="review__header">
<h5 className="review__author-name">
{review.author.first_name}
{review.author.first_name}{" "}
{review.author.last_name}
</h5>
<div className="review__date">

View File

@ -24,6 +24,14 @@
line-height: 24px;
color: $gray-900;
}
button:hover {
background-color: #f5f5f5;
}
&-current {
background-color: #f5f5f5;
}
}
&__wrapper {
@ -98,6 +106,15 @@
color: black;
}
}
#statistics-table__no-data-warning {
grid-template-columns: 1fr;
justify-content: center;
td {
text-align: center;
}
}
}
}
}

View File

@ -7,66 +7,201 @@ 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";
import { useDebounce } from "use-debounce";
import { useRouter } from "next/navigation";
interface IStatisticsTableProps {
firstData: IStatistics[] | string;
searchParams: {
["поиск-населенного-пункта"]: string;
};
}
const StatisticsTable: React.FC<IStatisticsTableProps> = ({
firstData,
searchParams,
}: IStatisticsTableProps) => {
const { data, error, isLoading, getStatistics } =
useStatisticsStore(useShallow((state) => state));
const [statistics, setStatistics] = useState<IStatistics[]>(
typeof firstData === "string" ? [] : firstData
const router = useRouter();
const [location, setLocation] = useState<string>("city");
const [queryStatistics, setQueryStatistics] = useState<string>(
searchParams["поиск-населенного-пункта"] || ""
);
const [query] = useDebounce(queryStatistics, 500);
const {
error,
isLoading,
getStatistics,
data: statistics,
} = useStatisticsStore(useShallow((state) => state));
const [filter, setFilter] = useState({
option: "broken_road",
toggle: false,
});
const [openPopup, setOpenPopup] = useState<boolean>(false);
const handleSubmit = () => {};
const params = [
{ param: "Добавлено дорог", handleSubmit() {} },
{ param: "Локальных дефектов", handleSubmit() {} },
{ param: "Очагов аварийности", handleSubmit() {} },
{ param: "Локальных дефектов исправлено", handleSubmit() {} },
{ param: "В планах ремонта", handleSubmit() {} },
{ param: "Отремонтировано", handleSubmit() {} },
{
param: "Добавлено дорог",
async handleClick() {
if (filter.option !== "broken_road_1") {
return setFilter({
option: "broken_road_1",
toggle: false,
});
}
setFilter((prev) => {
return { option: "broken_road_1", toggle: !prev.toggle };
});
getStatistics(location, filter, query);
},
},
{
param: "Локальных дефектов",
async handleClick() {
if (filter.option !== "local_defect_3") {
return setFilter({
option: "local_defect_3",
toggle: false,
});
}
setFilter((prev) => {
return { option: "local_defect_3", toggle: !prev.toggle };
});
getStatistics(location, filter, query);
},
},
{
param: "Очагов аварийности",
async handleClick() {
if (filter.option !== "hotbed_of_accidents_2") {
return setFilter({
option: "hotbed_of_accidents_2",
toggle: false,
});
}
setFilter((prev) => {
return {
option: "hotbed_of_accidents_2",
toggle: !prev.toggle,
};
});
getStatistics(location, filter, query);
},
},
{
param: "Локальных дефектов исправлено",
async handleClick() {
if (filter.option !== "local_defect_fixed_6") {
return setFilter({
option: "local_defect_fixed_6",
toggle: false,
});
}
setFilter((prev) => {
return {
option: "local_defect_fixed_6",
toggle: !prev.toggle,
};
});
getStatistics(location, filter, query);
},
},
{
param: "В планах ремонта",
async handleClick() {
if (filter.option !== "repair_plans_4") {
return setFilter({
option: "repair_plans_4",
toggle: false,
});
}
setFilter((prev) => {
return { option: "repair_plans_4", toggle: !prev.toggle };
});
getStatistics(location, filter, query);
},
},
{
param: "Отремонтировано",
async handleClick() {
if (filter.option !== "repaired_5") {
return setFilter({ option: "repaired_5", toggle: false });
}
setFilter((prev) => {
return { option: "repaired_5", toggle: !prev.toggle };
});
getStatistics(location, filter, query);
},
},
];
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);
}
setLocation(location);
getStatistics(location, filter, query);
console.error("Error fetching statistics:", error);
};
useEffect(() => {
getStatistics(location, filter, query);
router.push(`/statistics?поиск-населенного-пункта=${query}`);
}, [query]);
return (
<div className="statistics-table">
<SearchForm
onChange={(e) => setQueryStatistics(e.target.value)}
value={queryStatistics}
placeholder="Введите населенный пункт"
style={{ width: "100%" }}
handleSubmit={handleSubmit}
/>
{openPopup && (
<div className="statistics-table__popup">
<button onClick={() => getStatsByLocation("state")}>
<button
className={
location === "state"
? "statistics-table__popup-current"
: ""
}
onClick={() => getStatsByLocation("state")}
>
Область
</button>
<button onClick={() => getStatsByLocation("city")}>
<button
className={
location === "city"
? "statistics-table__popup-current"
: ""
}
onClick={() => getStatsByLocation("city")}
>
Город
</button>
<button onClick={() => getStatsByLocation("village")}>
<button
className={
location === "village"
? "statistics-table__popup-current"
: ""
}
onClick={() => getStatsByLocation("village")}
>
Деревня
</button>
</div>
@ -84,7 +219,7 @@ const StatisticsTable: React.FC<IStatisticsTableProps> = ({
</td>
{params.map((param) => (
<td key={param.param}>
<button>
<button onClick={param.handleClick}>
{param.param}
<Image src={arrows} alt=" Arrows Icon" />
</button>
@ -93,17 +228,27 @@ const StatisticsTable: React.FC<IStatisticsTableProps> = ({
</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>
{statistics.length ? (
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>
))
) : (
<tr id="statistics-table__no-data-warning">
<td>
{error
? error
: "Oops, looks like there is no data"}
</td>
</tr>
))}
)}
</tbody>
</table>
</div>

View File

@ -1,29 +1,68 @@
import { apiInstance } from "@/shared/config/apiConfig";
import { IFetch } from "@/shared/types/fetch-type";
import { IList } from "@/shared/types/list-type";
import { IStatistics } from "@/shared/types/statistics-type";
import { AxiosError } from "axios";
import { create } from "zustand";
const filterCategories: Record<string, string> = {
broken_road_1: "broken_road_1",
hotbed_of_accidents_2: "hotbed_of_accidents_2",
local_defect_3: "local_defect_3",
repair_plans_4: "repair_plans_4",
repaired_5: "repaired_5",
local_defect_fixed_6: "local_defect_fixed_6",
};
interface IStatisticsStore extends IFetch {
data: IStatistics[];
getStatistics: (
endpoint: string
) => Promise<IStatistics[] | undefined>;
endpoint: string,
filter: { option: string; toggle: boolean },
query: string
) => void;
}
export const useStatisticsStore = create<IStatisticsStore>((set) => ({
isLoading: false,
error: "",
data: [],
getStatistics: async (
endpoint: string
): Promise<IStatistics[] | undefined> => {
endpoint: string,
filter: { option: string; toggle: boolean },
query: string = ""
) => {
try {
set({ isLoading: true });
const response = await apiInstance.get<IStatistics[]>(
`/report/${endpoint}/stats`
);
return response.data;
let data = response.data.filter((loc) =>
loc.name.toLowerCase().includes(query.toLowerCase())
);
if (
filter.option === filterCategories[filter.option] &&
filter.toggle === false
) {
data = data.sort((a, b) => {
const optionKey = filter.option as keyof IStatistics;
return (Number(a[optionKey]) -
Number(b[optionKey])) as number;
});
} else if (
filter.option === "rating" &&
filter.toggle === true
) {
data = data.sort((a, b) => {
const optionKey = filter.option as keyof IStatistics;
return (Number(a[optionKey]) -
Number(b[optionKey])) as number;
});
}
set({ data: data });
} catch (error: unknown) {
if (error instanceof AxiosError) {
set({ error: error.message });

View File

@ -1,23 +1,112 @@
import { IUserRatings } from "@/shared/types/user-rating-type";
"use client";
import "./VolunteersTable.scss";
import Image from "next/image";
import arrows from "@/shared/icons/arrows.svg";
import { useEffect, useState } from "react";
import { useVolunteersStore } from "./volunteers.store";
interface IVolunteersTableProps {
firstData: IUserRatings[] | string;
}
const VolunteersTable: React.FC<IVolunteersTableProps> = ({
firstData,
}: IVolunteersTableProps) => {
const VolunteersTable = () => {
const { data, isLoading, error, getVolunteers } =
useVolunteersStore();
const [filter, setFilter] = useState({
option: "report_count",
toggle: false,
});
const params = [
{ param: "№" },
{ param: "Активист" },
{ param: "Добавлено дорог", handleClick() {} },
{ param: "Получено голосов", handleClick() {} },
{ param: "Оставлено голосов", handleClick() {} },
{ param: "Рейтинг", handleClick() {} },
{
param: "Добавлено дорог",
handleClick() {
if (filter.option !== "report_count") {
return setFilter({
option: "report_count",
toggle: false,
});
}
setFilter((prev) => {
return { option: "report_count", toggle: !prev.toggle };
});
getVolunteers(filter);
},
},
{
param: "Получено голосов",
handleClick() {
if (filter.option !== "likes_given_count") {
return setFilter({
option: "likes_given_count",
toggle: false,
});
}
setFilter((prev) => {
return {
option: "likes_given_count",
toggle: !prev.toggle,
};
});
getVolunteers(filter);
},
},
{
param: "Оставлено голосов",
handleClick() {
if (filter.option !== "likes_received_count") {
return setFilter({
option: "likes_received_count",
toggle: false,
});
}
setFilter((prev) => {
return {
option: "likes_received_count",
toggle: !prev.toggle,
};
});
getVolunteers(filter);
},
},
{
param: "Рейтинг",
handleClick() {
if (filter.option !== "average_rating") {
return setFilter({
option: "average_rating",
toggle: false,
});
}
setFilter((prev) => {
return { option: "average_rating", toggle: !prev.toggle };
});
getVolunteers(filter);
},
},
];
const hideEmail = (email: string) => {
const atIndex = email.indexOf("@");
if (atIndex !== -1) {
const prefix = email.slice(0, 3);
const domain = email.slice(atIndex);
return `${prefix}******${domain}`;
} else {
console.error("Invalid email format");
return email;
}
};
useEffect(() => {
getVolunteers(filter);
}, []);
return (
<div className="volunteers-table">
<table>
@ -26,7 +115,7 @@ const VolunteersTable: React.FC<IVolunteersTableProps> = ({
{params.map((param) => (
<td key={param.param}>
{param.handleClick ? (
<button>
<button onClick={param.handleClick}>
{param.param}
<Image src={arrows} alt="Arrows Icon" />
</button>
@ -38,20 +127,18 @@ const VolunteersTable: React.FC<IVolunteersTableProps> = ({
</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>
))
)}
{data.map((user, index) => (
<tr key={user.user_id}>
<td>{index + 1}</td>
<td id="volunteers-user-cell">
{hideEmail(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>

View File

@ -0,0 +1,73 @@
import { apiInstance } from "@/shared/config/apiConfig";
import { IFetch } from "@/shared/types/fetch-type";
import { IList } from "@/shared/types/list-type";
import { IStatistics } from "@/shared/types/statistics-type";
import { IUserRatings } from "@/shared/types/user-rating-type";
import { AxiosError } from "axios";
import { create } from "zustand";
const filterCategories: Record<string, string> = {
report_count: "report_count",
likes_given_count: "likes_given_count",
likes_received_count: "likes_received_count",
average_rating: "average_rating",
};
interface IVolunteersStore extends IFetch {
data: IUserRatings[];
getVolunteers: (filter: {
option: string;
toggle: boolean;
}) => void;
}
export const useVolunteersStore = create<IVolunteersStore>((set) => ({
isLoading: false,
error: "",
data: [],
getVolunteers: async (filter: {
option: string;
toggle: boolean;
}) => {
try {
set({ isLoading: true });
const response = await apiInstance.get<IUserRatings[]>(
`/report/user_ratings/`
);
let data = response.data;
console.log(filter);
if (
filter.option === filterCategories[filter.option] &&
filter.toggle === false
) {
data = data.sort((a, b) => {
const optionKey = filter.option as keyof IUserRatings;
return (Number(a[optionKey]) -
Number(b[optionKey])) as number;
});
} else if (
filter.option === "rating" &&
filter.toggle === true
) {
data = data.sort((a, b) => {
const optionKey = filter.option as keyof IUserRatings;
return (Number(a[optionKey]) -
Number(b[optionKey])) as number;
});
}
set({ data: data });
} catch (error: unknown) {
if (error instanceof AxiosError) {
set({ error: error.message });
} else {
set({ error: "an error ocured" });
}
} finally {
set({ isLoading: false });
}
},
}));