forked from Transparency/kgroad-frontend2
fixed bugs
This commit is contained in:
parent
d4c49a1061
commit
b0870824ba
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
@ -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>
|
||||
);
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
@ -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 = [];
|
||||
|
||||
|
@ -6,4 +6,5 @@ export interface IProfile {
|
||||
image: string;
|
||||
role: number;
|
||||
govern_status: null;
|
||||
report_count: number;
|
||||
}
|
||||
|
@ -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}
|
||||
|
17
src/widgets/MapSection/HomeMap/icons/geo-white.svg
Normal file
17
src/widgets/MapSection/HomeMap/icons/geo-white.svg
Normal 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 |
@ -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}
|
||||
|
7
src/widgets/MapSection/MapSearch/action.ts
Normal file
7
src/widgets/MapSection/MapSearch/action.ts
Normal file
@ -0,0 +1,7 @@
|
||||
"use server";
|
||||
|
||||
export const handleSearch = async (formData: FormData) => {
|
||||
"use server";
|
||||
|
||||
console.log(formData.get("map-search"));
|
||||
};
|
@ -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 = async (categories: string) => {
|
||||
const res = await apiInstance<IReport[]>(
|
||||
`/report/?category=${categories}`
|
||||
);
|
||||
|
||||
const getReports = useMapStore(
|
||||
useShallow((state) => state.getReports)
|
||||
);
|
||||
const getLocations = useMapStore(
|
||||
useShallow((state) => state.getLocations)
|
||||
);
|
||||
const router = useRouter();
|
||||
return res.data;
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
getReports(categories);
|
||||
}, [categories]);
|
||||
const data = await getReports(
|
||||
searchParams["тип-дороги"] || "1,2,3,4,5,6"
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
router.push(
|
||||
`/?тип-дороги=${categories}${
|
||||
mapSearch ? `&поиск-на-карте=${mapSearch}` : ""
|
||||
}${
|
||||
queryRating ? `&поиск-рейтинг=${queryRating}` : ""
|
||||
}&страница-рейтинга=${page}`,
|
||||
{
|
||||
scroll: false,
|
||||
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 !== ","
|
||||
);
|
||||
|
||||
getLocations(query);
|
||||
}, [categories, query, queryRating]);
|
||||
if (categories.includes(category))
|
||||
return categories.filter((cat) => cat !== category).join(",");
|
||||
|
||||
const setSearchParams = (category: string) => {
|
||||
const availableCategories = ["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;
|
||||
}
|
||||
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(
|
||||
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(
|
||||
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>
|
||||
);
|
||||
};
|
||||
|
||||
|
@ -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)",
|
||||
|
@ -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 });
|
||||
},
|
||||
}));
|
||||
|
38
src/widgets/NewsList/NewsList.scss
Normal file
38
src/widgets/NewsList/NewsList.scss
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
54
src/widgets/NewsList/NewsList.tsx
Normal file
54
src/widgets/NewsList/NewsList.tsx
Normal 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;
|
38
src/widgets/NewsList/newsStore.ts
Normal file
38
src/widgets/NewsList/newsStore.ts
Normal 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 });
|
||||
}
|
||||
},
|
||||
}));
|
@ -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[];
|
||||
|
@ -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" ? (
|
||||
|
@ -76,9 +76,12 @@
|
||||
}
|
||||
|
||||
#my-report-location {
|
||||
a {
|
||||
display: flex;
|
||||
font-size: 20px;
|
||||
color: rgb(50, 48, 58);
|
||||
}
|
||||
}
|
||||
|
||||
#my-report-status {
|
||||
font-size: 20px;
|
||||
@ -102,7 +105,7 @@
|
||||
color: rgb(102, 102, 102);
|
||||
}
|
||||
|
||||
a {
|
||||
#my-reports-link {
|
||||
display: none;
|
||||
padding: 10px;
|
||||
border-radius: 8px;
|
||||
|
@ -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.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>
|
||||
|
6
src/widgets/ProfileTable/helpers.ts
Normal file
6
src/widgets/ProfileTable/helpers.ts
Normal file
@ -0,0 +1,6 @@
|
||||
export const sliceDate = (date: string) => {
|
||||
return `${date.slice(8, 10)}.${date.slice(5, 7)}.${date.slice(
|
||||
0,
|
||||
4
|
||||
)}`;
|
||||
};
|
106
src/widgets/ProfileTable/profile-reports.store.ts
Normal file
106
src/widgets/ProfileTable/profile-reports.store.ts
Normal 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 });
|
||||
}
|
||||
},
|
||||
})
|
||||
);
|
@ -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>
|
||||
);
|
||||
|
@ -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) => {
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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());
|
||||
|
||||
try {
|
||||
setLoader(true);
|
||||
const res = await apiInstance.post(
|
||||
"/report/create/",
|
||||
formData,
|
||||
config
|
||||
);
|
||||
|
||||
console.log(res.status);
|
||||
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>
|
||||
);
|
||||
};
|
||||
|
@ -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">
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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) {
|
||||
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,7 +228,8 @@ const StatisticsTable: React.FC<IStatisticsTableProps> = ({
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{statistics.map((stat) => (
|
||||
{statistics.length ? (
|
||||
statistics.map((stat) => (
|
||||
<tr key={stat.name}>
|
||||
<td id="statistics-table-stat-name">{stat.name}</td>
|
||||
<td>{stat.broken_road_1}</td>
|
||||
@ -103,7 +239,16 @@ const StatisticsTable: React.FC<IStatisticsTableProps> = ({
|
||||
<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>
|
||||
|
@ -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 });
|
||||
|
@ -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) => (
|
||||
{data.map((user, index) => (
|
||||
<tr key={user.user_id}>
|
||||
<td>{index + 1}</td>
|
||||
<td id="volunteers-user-cell">{user.username}</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>
|
||||
|
73
src/widgets/VolunteersTable/volunteers.store.ts
Normal file
73
src/widgets/VolunteersTable/volunteers.store.ts
Normal 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 });
|
||||
}
|
||||
},
|
||||
}));
|
Loading…
Reference in New Issue
Block a user