forked from Transparency/kgroad-frontend2
fixed bugs
This commit is contained in:
parent
d4c49a1061
commit
b0870824ba
@ -6,38 +6,16 @@
|
|||||||
h2 {
|
h2 {
|
||||||
width: fit-content;
|
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) {
|
@media screen and (max-width: 768px) {
|
||||||
.news {
|
.news {
|
||||||
gap: 30px;
|
gap: 30px;
|
||||||
|
|
||||||
&__list {
|
|
||||||
grid-template-columns: 1fr 1fr;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media screen and (max-width: 550px) {
|
@media screen and (max-width: 550px) {
|
||||||
.news {
|
.news {
|
||||||
gap: 20px;
|
gap: 20px;
|
||||||
|
|
||||||
&__list {
|
|
||||||
grid-template-columns: 1fr;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -2,41 +2,20 @@ import "./News.scss";
|
|||||||
import Typography from "@/shared/ui/components/Typography/Typography";
|
import Typography from "@/shared/ui/components/Typography/Typography";
|
||||||
import { apiInstance } from "@/shared/config/apiConfig";
|
import { apiInstance } from "@/shared/config/apiConfig";
|
||||||
import { INewsList } from "@/shared/types/news-type";
|
import { INewsList } from "@/shared/types/news-type";
|
||||||
import NewsCard from "@/entities/NewsCard/NewsCard";
|
import NewsList from "@/widgets/NewsList/NewsList";
|
||||||
|
|
||||||
const News = async () => {
|
const News = ({
|
||||||
const getNews = async () => {
|
searchParams,
|
||||||
try {
|
}: {
|
||||||
const response = await apiInstance.get<INewsList>("/news/");
|
searchParams: {
|
||||||
|
["страница-новостей"]: string;
|
||||||
return response.data;
|
|
||||||
} catch (error) {
|
|
||||||
console.log(error);
|
|
||||||
|
|
||||||
return {
|
|
||||||
results: [],
|
|
||||||
};
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
}) => {
|
||||||
const data = await getNews();
|
|
||||||
return (
|
return (
|
||||||
<div className="news page-padding">
|
<div className="news page-padding">
|
||||||
<Typography element="h2">Новости</Typography>
|
<Typography element="h2">Новости</Typography>
|
||||||
|
|
||||||
<ul className="news__list">
|
<NewsList searchParams={searchParams} />
|
||||||
{data.results.map((news) => (
|
|
||||||
<li key={news.id}>
|
|
||||||
<NewsCard
|
|
||||||
id={news.id}
|
|
||||||
title={news.title}
|
|
||||||
description={news.description}
|
|
||||||
image={news.image}
|
|
||||||
date={news.created_at}
|
|
||||||
/>
|
|
||||||
</li>
|
|
||||||
))}
|
|
||||||
</ul>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -3,13 +3,7 @@ import Header from "@/widgets/Header/Header";
|
|||||||
import StatisticsSection from "@/widgets/StatisticsSection/StatisticsSection";
|
import StatisticsSection from "@/widgets/StatisticsSection/StatisticsSection";
|
||||||
import RatingSection from "@/widgets/RatingSection/RatingSection";
|
import RatingSection from "@/widgets/RatingSection/RatingSection";
|
||||||
import NewsSection from "@/widgets/NewsSection/NewsSection";
|
import NewsSection from "@/widgets/NewsSection/NewsSection";
|
||||||
|
import MapSection from "@/widgets/MapSection/MapSection";
|
||||||
const DynamicMap = dynamic(
|
|
||||||
() => import("@/widgets/MapSection/MapSection"),
|
|
||||||
{
|
|
||||||
ssr: false,
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
const Home = async ({
|
const Home = async ({
|
||||||
searchParams,
|
searchParams,
|
||||||
@ -25,18 +19,8 @@ const Home = async ({
|
|||||||
<div className="home">
|
<div className="home">
|
||||||
<Header />
|
<Header />
|
||||||
<StatisticsSection />
|
<StatisticsSection />
|
||||||
<DynamicMap
|
{/* <MapSection searchParams={searchParams} /> */}
|
||||||
categories={searchParams["тип-дороги"]}
|
<RatingSection searchParams={searchParams} />
|
||||||
queryMap={searchParams["поиск-на-карте"]}
|
|
||||||
queryRating={searchParams["поиск-рейтинг"]}
|
|
||||||
page={searchParams["страница-рейтинга"]}
|
|
||||||
/>
|
|
||||||
<RatingSection
|
|
||||||
categories={searchParams["тип-дороги"]}
|
|
||||||
queryMap={searchParams["поиск-на-карте"]}
|
|
||||||
queryRating={searchParams["поиск-рейтинг"]}
|
|
||||||
page={searchParams["страница-рейтинга"]}
|
|
||||||
/>
|
|
||||||
<NewsSection />
|
<NewsSection />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@ -2,16 +2,43 @@ import Typography from "@/shared/ui/components/Typography/Typography";
|
|||||||
import "./Profile.scss";
|
import "./Profile.scss";
|
||||||
import ProfileNav from "@/widgets/ProfileNav/ProfileNav";
|
import ProfileNav from "@/widgets/ProfileNav/ProfileNav";
|
||||||
import AuthGuard from "./AuthGuard";
|
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,
|
children,
|
||||||
}: Readonly<{
|
}: Readonly<{
|
||||||
children: React.ReactNode;
|
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 (
|
return (
|
||||||
<div className="profile page-padding">
|
<div className="profile page-padding">
|
||||||
<Typography element="h2">Личный кабинет</Typography>
|
<Typography element="h2">Личный кабинет</Typography>
|
||||||
<ProfileNav />
|
<ProfileNav report_count={data?.report_count as number} />
|
||||||
|
|
||||||
<AuthGuard>{children}</AuthGuard>
|
<AuthGuard>{children}</AuthGuard>
|
||||||
</div>
|
</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 ProfileTable from "@/widgets/ProfileTable/ProfileTable";
|
||||||
import { getServerSession } from "next-auth";
|
|
||||||
import React from "react";
|
import React from "react";
|
||||||
|
|
||||||
const MyReports = async () => {
|
const MyReports = async ({
|
||||||
const session = await getServerSession(authConfig);
|
searchParams,
|
||||||
|
}: {
|
||||||
const getMyReports = async () => {
|
searchParams: { ["страница-обращений"]: string };
|
||||||
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();
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<ProfileTable reports={data} />
|
<ProfileTable searchParams={searchParams} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -5,28 +5,15 @@ import { IStatistics } from "@/shared/types/statistics-type";
|
|||||||
import { AxiosError } from "axios";
|
import { AxiosError } from "axios";
|
||||||
import StatisticsTable from "@/widgets/StatisticsTable/StatisticsTable";
|
import StatisticsTable from "@/widgets/StatisticsTable/StatisticsTable";
|
||||||
|
|
||||||
const Statistics = async () => {
|
const Statistics = ({
|
||||||
const getStatistics = async (): Promise<IStatistics[] | string> => {
|
searchParams,
|
||||||
try {
|
}: {
|
||||||
const response = await apiInstance.get<IStatistics[]>(
|
searchParams: { ["поиск-населенного-пункта"]: string };
|
||||||
"/report/city/stats/"
|
}) => {
|
||||||
);
|
|
||||||
|
|
||||||
return response.data;
|
|
||||||
} catch (error: unknown) {
|
|
||||||
if (error instanceof AxiosError) {
|
|
||||||
return error.message;
|
|
||||||
} else {
|
|
||||||
return "Произошла непредвиденная ошибк";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const data = await getStatistics();
|
|
||||||
return (
|
return (
|
||||||
<div className="statistics page-padding">
|
<div className="statistics page-padding">
|
||||||
<Typography element="h2">Статистика</Typography>
|
<Typography element="h2">Статистика</Typography>
|
||||||
<StatisticsTable firstData={data} />
|
<StatisticsTable searchParams={searchParams} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -5,30 +5,11 @@ import { AxiosError } from "axios";
|
|||||||
import { IUserRatings } from "@/shared/types/user-rating-type";
|
import { IUserRatings } from "@/shared/types/user-rating-type";
|
||||||
import VolunteersTable from "@/widgets/VolunteersTable/VolunteersTable";
|
import VolunteersTable from "@/widgets/VolunteersTable/VolunteersTable";
|
||||||
|
|
||||||
const Volunteers = async () => {
|
const Volunteers = () => {
|
||||||
const getVolunteers = async (): Promise<
|
|
||||||
IUserRatings[] | string
|
|
||||||
> => {
|
|
||||||
try {
|
|
||||||
const response = await apiInstance.get<IUserRatings[]>(
|
|
||||||
"/report/user_ratings/"
|
|
||||||
);
|
|
||||||
|
|
||||||
return response.data;
|
|
||||||
} catch (error: unknown) {
|
|
||||||
if (error instanceof AxiosError) {
|
|
||||||
return error.message;
|
|
||||||
} else {
|
|
||||||
return "Произошла непредвиденная ошибк";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const data = await getVolunteers();
|
|
||||||
return (
|
return (
|
||||||
<div className="volunteers page-padding">
|
<div className="volunteers page-padding">
|
||||||
<Typography element="h2">Волонтеры</Typography>
|
<Typography element="h2">Волонтеры</Typography>
|
||||||
<VolunteersTable firstData={data} />
|
<VolunteersTable />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -11,6 +11,7 @@ interface IPaginationProps {
|
|||||||
next: string | null;
|
next: string | null;
|
||||||
count: number;
|
count: number;
|
||||||
current_count: number;
|
current_count: number;
|
||||||
|
limit: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
const Pagination: React.FC<IPaginationProps> = ({
|
const Pagination: React.FC<IPaginationProps> = ({
|
||||||
@ -20,8 +21,9 @@ const Pagination: React.FC<IPaginationProps> = ({
|
|||||||
next,
|
next,
|
||||||
count,
|
count,
|
||||||
current_count,
|
current_count,
|
||||||
|
limit,
|
||||||
}: IPaginationProps) => {
|
}: IPaginationProps) => {
|
||||||
const pages_count = count % 8;
|
const pages_count = Math.ceil(count / limit);
|
||||||
const showPages = () => {
|
const showPages = () => {
|
||||||
const btns = [];
|
const btns = [];
|
||||||
|
|
||||||
|
@ -6,4 +6,5 @@ export interface IProfile {
|
|||||||
image: string;
|
image: string;
|
||||||
role: number;
|
role: number;
|
||||||
govern_status: null;
|
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_purple_icon from "./icons/geo-purple.svg";
|
||||||
import geo_red_icon from "./icons/geo-red.svg";
|
import geo_red_icon from "./icons/geo-red.svg";
|
||||||
import geo_yellow_icon from "./icons/geo-yellow.svg";
|
import geo_yellow_icon from "./icons/geo-yellow.svg";
|
||||||
|
import geo_white_icon from "./icons/geo-white.svg";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
DivIcon,
|
DivIcon,
|
||||||
Icon,
|
Icon,
|
||||||
@ -27,6 +29,7 @@ import Link from "next/link";
|
|||||||
import { Fragment, useEffect, useState } from "react";
|
import { Fragment, useEffect, useState } from "react";
|
||||||
import L from "leaflet";
|
import L from "leaflet";
|
||||||
import { ILocation } from "@/shared/types/location-type";
|
import { ILocation } from "@/shared/types/location-type";
|
||||||
|
import { useMapStore } from "../mapSectionStore";
|
||||||
|
|
||||||
interface IData {
|
interface IData {
|
||||||
id: number;
|
id: number;
|
||||||
@ -40,14 +43,13 @@ interface ILatLng {
|
|||||||
}
|
}
|
||||||
|
|
||||||
interface IHomeMapProps {
|
interface IHomeMapProps {
|
||||||
data: IData[];
|
reports: IData[];
|
||||||
latLng: ILatLng;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const HomeMap: React.FC<IHomeMapProps> = ({
|
const HomeMap: React.FC<IHomeMapProps> = ({
|
||||||
data,
|
reports,
|
||||||
latLng,
|
|
||||||
}: IHomeMapProps) => {
|
}: IHomeMapProps) => {
|
||||||
|
const { display_location, latLng } = useMapStore();
|
||||||
const [position, setPosition] = useState<ILatLng>({
|
const [position, setPosition] = useState<ILatLng>({
|
||||||
lat: 42.8746,
|
lat: 42.8746,
|
||||||
lng: 74.606,
|
lng: 74.606,
|
||||||
@ -70,6 +72,11 @@ const HomeMap: React.FC<IHomeMapProps> = ({
|
|||||||
6: createCustomIcon(geo_green_icon),
|
6: createCustomIcon(geo_green_icon),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const searchResultIcon = new Icon({
|
||||||
|
iconUrl: geo_white_icon.src,
|
||||||
|
iconSize: [100, 100],
|
||||||
|
});
|
||||||
|
|
||||||
const categoryToPolyline: Record<number, { color: string }> = {
|
const categoryToPolyline: Record<number, { color: string }> = {
|
||||||
1: { color: "rgba(230, 68, 82, 0.8)" },
|
1: { color: "rgba(230, 68, 82, 0.8)" },
|
||||||
2: { color: "rgba(198, 152, 224, 0.8)" },
|
2: { color: "rgba(198, 152, 224, 0.8)" },
|
||||||
@ -88,10 +95,23 @@ const HomeMap: React.FC<IHomeMapProps> = ({
|
|||||||
setPosition(latLng);
|
setPosition(latLng);
|
||||||
}, [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 (
|
return (
|
||||||
<MapContainer
|
<MapContainer
|
||||||
@ -105,7 +125,7 @@ const HomeMap: React.FC<IHomeMapProps> = ({
|
|||||||
url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
|
url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{data.map((report) =>
|
{reports.map((report) =>
|
||||||
report.location.length === 2 ? (
|
report.location.length === 2 ? (
|
||||||
<Polyline
|
<Polyline
|
||||||
key={report.id}
|
key={report.id}
|
||||||
@ -124,7 +144,7 @@ const HomeMap: React.FC<IHomeMapProps> = ({
|
|||||||
) : null
|
) : null
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{data?.map((report) =>
|
{reports.map((report) =>
|
||||||
report.location.map((marker) => (
|
report.location.map((marker) => (
|
||||||
<Marker
|
<Marker
|
||||||
key={marker.id}
|
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 "./MapSearch.scss";
|
||||||
import Image from "next/image";
|
import Image from "next/image";
|
||||||
import search from "./icons/search.svg";
|
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
|
interface IMapSearchProps
|
||||||
extends React.InputHTMLAttributes<HTMLInputElement> {
|
extends React.InputHTMLAttributes<HTMLInputElement> {
|
||||||
options: IDisplayMap[];
|
searchParams: {
|
||||||
setMapSearch: (string: string) => void;
|
["тип-дороги"]: string;
|
||||||
setLatLng: ({ lat, lng }: { lat: number; lng: number }) => void;
|
["поиск-на-карте"]: string;
|
||||||
|
["поиск-рейтинг"]: string;
|
||||||
|
["страница-рейтинга"]: string;
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const MapSearch: React.FC<IMapSearchProps> = ({
|
const MapSearch: React.FC<IMapSearchProps> = ({
|
||||||
name,
|
searchParams,
|
||||||
placeholder,
|
|
||||||
value,
|
|
||||||
onChange,
|
|
||||||
options,
|
|
||||||
setMapSearch,
|
|
||||||
setLatLng,
|
|
||||||
}: IMapSearchProps) => {
|
}: IMapSearchProps) => {
|
||||||
|
const router = useRouter();
|
||||||
|
const [searchMap, setSearchMap] = useState<string>(
|
||||||
|
searchParams["поиск-на-карте"] || ""
|
||||||
|
);
|
||||||
|
const [query] = useDebounce(searchMap, 500);
|
||||||
|
|
||||||
|
const {
|
||||||
|
setLatLng,
|
||||||
|
searchData: options,
|
||||||
|
getLocations,
|
||||||
|
setDisplayLocation,
|
||||||
|
} = useMapStore();
|
||||||
|
|
||||||
const handleSubmit = (
|
const handleSubmit = (
|
||||||
display_name: string,
|
display_name: string,
|
||||||
latLng: { lat: number; lng: number }
|
latLng: { lat: number; lng: number }
|
||||||
) => {
|
) => {
|
||||||
setMapSearch(display_name);
|
setSearchMap(display_name);
|
||||||
|
setDisplayLocation(display_name);
|
||||||
setLatLng(latLng);
|
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 (
|
return (
|
||||||
<div className="map-search">
|
<div className="map-search">
|
||||||
<form
|
<form
|
||||||
onSubmit={(e) => {
|
onSubmit={(e: React.MouseEvent<HTMLFormElement>) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
handleSubmit(options[0].display_name, {
|
handleSubmit(options[0].display_name, {
|
||||||
lat: +options[0].lat,
|
lat: +options[0].lat,
|
||||||
@ -42,10 +75,10 @@ const MapSearch: React.FC<IMapSearchProps> = ({
|
|||||||
<div className="map-search__input">
|
<div className="map-search__input">
|
||||||
<Image src={search} alt="Search Icon" />
|
<Image src={search} alt="Search Icon" />
|
||||||
<input
|
<input
|
||||||
onChange={onChange}
|
value={searchMap}
|
||||||
value={value}
|
placeholder="Введите город, село или регион"
|
||||||
placeholder={placeholder}
|
onChange={(e) => setSearchMap(e.target.value)}
|
||||||
name={name}
|
name="map-search"
|
||||||
type="text"
|
type="text"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@ -59,11 +92,16 @@ const MapSearch: React.FC<IMapSearchProps> = ({
|
|||||||
<li key={opt.place_id}>
|
<li key={opt.place_id}>
|
||||||
<button
|
<button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setMapSearch(opt.display_name);
|
setSearchMap(opt.display_name);
|
||||||
|
setDisplayLocation(opt.display_name);
|
||||||
setLatLng({
|
setLatLng({
|
||||||
lat: +opt.lat,
|
lat: +opt.lat,
|
||||||
lng: +opt.lon,
|
lng: +opt.lon,
|
||||||
});
|
});
|
||||||
|
window.scrollTo({
|
||||||
|
top: 1400,
|
||||||
|
behavior: "smooth",
|
||||||
|
});
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{opt.display_name}
|
{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 "./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 {
|
import {
|
||||||
ROAD_TYPES,
|
ROAD_TYPES,
|
||||||
ROAD_TYPES_COLORS,
|
ROAD_TYPES_COLORS,
|
||||||
} from "@/shared/variables/road-types";
|
} from "@/shared/variables/road-types";
|
||||||
import HomeMap from "./HomeMap/HomeMap";
|
|
||||||
import { useRouter } from "next/navigation";
|
const DynamicMap = dynamic(() => import("./HomeMap/HomeMap"), {
|
||||||
import { useMapStore } from "./mapSectionStore";
|
ssr: false,
|
||||||
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";
|
|
||||||
|
|
||||||
interface IMapSectionProps {
|
interface IMapSectionProps {
|
||||||
[key: string]: string;
|
searchParams: {
|
||||||
|
["тип-дороги"]: string;
|
||||||
|
["поиск-на-карте"]: string;
|
||||||
|
["поиск-рейтинг"]: string;
|
||||||
|
["страница-рейтинга"]: string;
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ILatLng {
|
const MapSection: React.FC<IMapSectionProps> = async ({
|
||||||
lat: number;
|
searchParams,
|
||||||
lng: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
const MapSection: React.FC<IMapSectionProps> = ({
|
|
||||||
categories = "1,2,3,4,5,6",
|
|
||||||
queryMap,
|
|
||||||
queryRating,
|
|
||||||
page = "1",
|
|
||||||
}: IMapSectionProps) => {
|
}: IMapSectionProps) => {
|
||||||
const [mapSearch, setMapSearch] = useState<string>(queryMap || "");
|
const getReports = async (categories: string) => {
|
||||||
const [latLng, setLatLng] = useState<ILatLng>({
|
const res = await apiInstance<IReport[]>(
|
||||||
lat: 42.8746,
|
`/report/?category=${categories}`
|
||||||
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,
|
|
||||||
}
|
|
||||||
);
|
);
|
||||||
|
|
||||||
getLocations(query);
|
return res.data;
|
||||||
}, [categories, query, queryRating]);
|
};
|
||||||
|
|
||||||
const setSearchParams = (category: string) => {
|
const data = await getReports(
|
||||||
const availableCategories = ["1", "2", "3", "4", "5", "6"];
|
searchParams["тип-дороги"] || "1,2,3,4,5,6"
|
||||||
|
);
|
||||||
|
|
||||||
if (!categories || !availableCategories.includes(category))
|
const setCategories = (category: string) => {
|
||||||
return categories;
|
if (searchParams["тип-дороги"] === undefined) {
|
||||||
|
const categories = ["1", "2", "3", "4", "5", "6"];
|
||||||
if (categories?.includes(category)) {
|
return categories.filter((cat) => cat !== category).join(",");
|
||||||
const updatedCategories = categories
|
|
||||||
?.replace(category + ",", "")
|
|
||||||
.replace("," + category, "")
|
|
||||||
.replace(category, "");
|
|
||||||
|
|
||||||
return updatedCategories;
|
|
||||||
} else {
|
|
||||||
const newValue = category + ",";
|
|
||||||
const updatedCategories = newValue + categories;
|
|
||||||
return updatedCategories;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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 (
|
return (
|
||||||
<section className="map-section">
|
<div className="map-section">
|
||||||
<div className="map-section__header">
|
<div className="map-section__header">
|
||||||
<Typography element="h3">Карта дорог</Typography>
|
<Typography element="h3">Карта дорог</Typography>
|
||||||
<Paragraph>
|
<Paragraph>
|
||||||
@ -99,48 +66,71 @@ const MapSection: React.FC<IMapSectionProps> = ({
|
|||||||
</Paragraph>
|
</Paragraph>
|
||||||
</div>
|
</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">
|
<div className="map-section__categories">
|
||||||
<ul className="map-section__categories_left">
|
<ul>
|
||||||
{[1, 2, 3].map((sw) => (
|
{[1, 2, 3].map((sw) => (
|
||||||
<li key={sw}>
|
<li key={sw}>
|
||||||
<Switch
|
<Switch
|
||||||
href={`/?тип-дороги=${setSearchParams(
|
defaultState={
|
||||||
sw.toString()
|
searchParams["тип-дороги"]
|
||||||
)}`}
|
? searchParams["тип-дороги"]?.includes(
|
||||||
|
sw.toString()
|
||||||
|
)
|
||||||
|
: true
|
||||||
|
}
|
||||||
color={ROAD_TYPES_COLORS[sw]}
|
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>
|
</li>
|
||||||
))}
|
))}
|
||||||
</ul>
|
</ul>
|
||||||
<ul className="map-section__categories_right">
|
<ul>
|
||||||
{[4, 5, 6].map((sw) => (
|
{[4, 5, 6].map((sw) => (
|
||||||
<li key={sw}>
|
<li key={sw}>
|
||||||
<Switch
|
<Switch
|
||||||
href={`/?тип-дороги=${setSearchParams(
|
defaultState={
|
||||||
sw.toString()
|
searchParams["тип-дороги"]
|
||||||
)}`}
|
? searchParams["тип-дороги"]?.includes(
|
||||||
|
sw.toString()
|
||||||
|
)
|
||||||
|
: true
|
||||||
|
}
|
||||||
color={ROAD_TYPES_COLORS[sw]}
|
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>
|
</li>
|
||||||
))}
|
))}
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<HomeMap data={data.results} latLng={latLng} />
|
<MapSearch searchParams={searchParams} />
|
||||||
</section>
|
|
||||||
|
<div className="map-section__categories"></div>
|
||||||
|
|
||||||
|
<DynamicMap reports={data} />
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -5,7 +5,7 @@ import "./Switch.scss";
|
|||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
|
|
||||||
interface ISwitchProps {
|
interface ISwitchProps {
|
||||||
defaultState?: boolean;
|
defaultState: boolean;
|
||||||
color?: string;
|
color?: string;
|
||||||
href: string;
|
href: string;
|
||||||
}
|
}
|
||||||
@ -25,7 +25,7 @@ const Switch: React.FC<ISwitchProps> = ({
|
|||||||
onClick={() => setToggleSwitch((prev) => !prev)}
|
onClick={() => setToggleSwitch((prev) => !prev)}
|
||||||
style={{
|
style={{
|
||||||
backgroundColor: !toggleSwitch
|
backgroundColor: !toggleSwitch
|
||||||
? "rgb(50, 48, 58)"
|
? "rgb(71, 85, 105)"
|
||||||
: color
|
: color
|
||||||
? color
|
? color
|
||||||
: "rgb(230, 68, 82)",
|
: "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 { IDisplayMap } from "@/shared/types/map-type";
|
||||||
import { IReport } from "@/shared/types/report-type";
|
|
||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
import { create } from "zustand";
|
import { create } from "zustand";
|
||||||
|
|
||||||
interface IFetchReports extends IList {
|
interface IMapStore {
|
||||||
results: IReport[];
|
setLatLng: (latLng: { lat: number; lng: number }) => void;
|
||||||
}
|
latLng: { lat: number; lng: number };
|
||||||
|
|
||||||
interface IMapStore extends IFetch {
|
|
||||||
data: IFetchReports;
|
|
||||||
searchData: IDisplayMap[];
|
|
||||||
getReports: (categories: string) => Promise<void>;
|
|
||||||
getLocations: (query: string) => void;
|
getLocations: (query: string) => void;
|
||||||
|
searchData: IDisplayMap[];
|
||||||
|
setDisplayLocation: (display_location: string) => void;
|
||||||
|
display_location: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const useMapStore = create<IMapStore>((set) => ({
|
export const useMapStore = create<IMapStore>((set) => ({
|
||||||
data: {
|
|
||||||
count: 0,
|
|
||||||
previous: null,
|
|
||||||
next: null,
|
|
||||||
results: [],
|
|
||||||
},
|
|
||||||
searchData: [],
|
searchData: [],
|
||||||
isLoading: false,
|
display_location: "",
|
||||||
error: "",
|
latLng: {
|
||||||
getReports: async (categories: string = "1,2,3,4,5,6") => {
|
lat: 42.8746,
|
||||||
try {
|
lng: 74.606,
|
||||||
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 });
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
getLocations: async (query: string = "") => {
|
getLocations: async (query: string = "") => {
|
||||||
const params: Record<string, any> = {
|
const params: Record<string, any> = {
|
||||||
@ -60,4 +36,10 @@ export const useMapStore = create<IMapStore>((set) => ({
|
|||||||
|
|
||||||
set({ searchData: inKG });
|
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 { apiInstance } from "@/shared/config/apiConfig";
|
||||||
import { IList } from "@/shared/types/list-type";
|
import { IList } from "@/shared/types/list-type";
|
||||||
import { INews } from "@/shared/types/news-type";
|
import { INews } from "@/shared/types/news-type";
|
||||||
import axios, { AxiosError } from "axios";
|
import { AxiosError } from "axios";
|
||||||
|
|
||||||
interface IFetchNews extends IList {
|
interface IFetchNews extends IList {
|
||||||
results: INews[];
|
results: INews[];
|
||||||
|
@ -5,7 +5,13 @@ import LogoutButton from "@/features/LogoutButton/LogoutButton";
|
|||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { usePathname } from "next/navigation";
|
import { usePathname } from "next/navigation";
|
||||||
|
|
||||||
const ProfileNav = () => {
|
interface IProfileNavProps {
|
||||||
|
report_count: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ProfileNav: React.FC<IProfileNavProps> = ({
|
||||||
|
report_count,
|
||||||
|
}: IProfileNavProps) => {
|
||||||
const pathname = usePathname();
|
const pathname = usePathname();
|
||||||
return (
|
return (
|
||||||
<nav className="profile-nav">
|
<nav className="profile-nav">
|
||||||
@ -30,7 +36,7 @@ const ProfileNav = () => {
|
|||||||
>
|
>
|
||||||
Мои обращения
|
Мои обращения
|
||||||
</Link>
|
</Link>
|
||||||
<span>3</span>
|
<span>{report_count}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{pathname === "/profile/personal" ? (
|
{pathname === "/profile/personal" ? (
|
||||||
|
@ -76,8 +76,11 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
#my-report-location {
|
#my-report-location {
|
||||||
font-size: 20px;
|
a {
|
||||||
color: rgb(50, 48, 58);
|
display: flex;
|
||||||
|
font-size: 20px;
|
||||||
|
color: rgb(50, 48, 58);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#my-report-status {
|
#my-report-status {
|
||||||
@ -102,7 +105,7 @@
|
|||||||
color: rgb(102, 102, 102);
|
color: rgb(102, 102, 102);
|
||||||
}
|
}
|
||||||
|
|
||||||
a {
|
#my-reports-link {
|
||||||
display: none;
|
display: none;
|
||||||
padding: 10px;
|
padding: 10px;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
|
@ -1,33 +1,122 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
import Image from "next/image";
|
import Image from "next/image";
|
||||||
import "./ProfileTable.scss";
|
import "./ProfileTable.scss";
|
||||||
import arrows from "@/shared/icons/arrows.svg";
|
import arrows from "@/shared/icons/arrows.svg";
|
||||||
import { IMyReportsList } from "@/shared/types/my-reports";
|
|
||||||
import {
|
import {
|
||||||
REPORT_STATUS,
|
REPORT_STATUS,
|
||||||
REPORT_STATUS_COLORS,
|
REPORT_STATUS_COLORS,
|
||||||
} from "@/shared/variables/report-status";
|
} from "@/shared/variables/report-status";
|
||||||
import Link from "next/link";
|
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 {
|
interface IProfileTableProps {
|
||||||
reports: IMyReportsList;
|
searchParams: { ["страница-обращений"]: string };
|
||||||
}
|
}
|
||||||
|
|
||||||
const ProfileTable: React.FC<IProfileTableProps> = ({
|
const ProfileTable: React.FC<IProfileTableProps> = ({
|
||||||
reports,
|
searchParams,
|
||||||
}: IProfileTableProps) => {
|
}: 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 = [
|
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: "Статус" },
|
{ 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(
|
useEffect(() => {
|
||||||
0,
|
if (session.status === "loading") return;
|
||||||
4
|
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 (
|
return (
|
||||||
<div className="profile-table">
|
<div className="profile-table">
|
||||||
<div
|
<div
|
||||||
@ -42,7 +131,7 @@ const ProfileTable: React.FC<IProfileTableProps> = ({
|
|||||||
{params.map((p) => (
|
{params.map((p) => (
|
||||||
<td key={p.param}>
|
<td key={p.param}>
|
||||||
{p.handleClick ? (
|
{p.handleClick ? (
|
||||||
<button>
|
<button onClick={p.handleClick}>
|
||||||
{p.param}{" "}
|
{p.param}{" "}
|
||||||
<Image src={arrows} alt="Arrows Icon" />
|
<Image src={arrows} alt="Arrows Icon" />
|
||||||
</button>
|
</button>
|
||||||
@ -60,7 +149,13 @@ const ProfileTable: React.FC<IProfileTableProps> = ({
|
|||||||
{sliceDate(report.created_at)}
|
{sliceDate(report.created_at)}
|
||||||
</td>
|
</td>
|
||||||
<td id="my-report-location">
|
<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>
|
||||||
<td
|
<td
|
||||||
id="my-report-status"
|
id="my-report-status"
|
||||||
@ -84,6 +179,15 @@ const ProfileTable: React.FC<IProfileTableProps> = ({
|
|||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
</div>
|
</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 id="my-reports-link" href="/create-report">
|
||||||
Написать обращение
|
Написать обращение
|
||||||
</Link>
|
</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";
|
} from "./helpers";
|
||||||
|
|
||||||
interface IRatingSectionProps {
|
interface IRatingSectionProps {
|
||||||
[key: string]: string;
|
searchParams: {
|
||||||
|
["тип-дороги"]: string;
|
||||||
|
["поиск-на-карте"]: string;
|
||||||
|
["поиск-рейтинг"]: string;
|
||||||
|
["страница-рейтинга"]: string;
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const RatingSection: React.FC<IRatingSectionProps> = ({
|
const RatingSection: React.FC<IRatingSectionProps> = ({
|
||||||
categories = "1,2,3,4,5,6",
|
searchParams,
|
||||||
queryMap,
|
|
||||||
queryRating,
|
|
||||||
page = "1",
|
|
||||||
}: IRatingSectionProps) => {
|
}: IRatingSectionProps) => {
|
||||||
const [ratingSearch, setRatingSearch] = useState<string>(
|
const [ratingSearch, setRatingSearch] = useState<string>(
|
||||||
queryRating || ""
|
searchParams["поиск-рейтинг"] || ""
|
||||||
|
);
|
||||||
|
const [activePage, setActivePage] = useState<number>(
|
||||||
|
+searchParams["страница-рейтинга"] || 1
|
||||||
);
|
);
|
||||||
const [activePage, setActivePage] = useState<number>(+page);
|
|
||||||
const [filter, setFilter] = useState({
|
const [filter, setFilter] = useState({
|
||||||
option: "date",
|
option: "date",
|
||||||
toggle: false,
|
toggle: false,
|
||||||
@ -50,7 +54,7 @@ const RatingSection: React.FC<IRatingSectionProps> = ({
|
|||||||
);
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
getReports(ratingSearch, +page, filter);
|
getReports(ratingSearch, activePage, filter);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const handleSubmit: React.FormEventHandler<
|
const handleSubmit: React.FormEventHandler<
|
||||||
@ -61,11 +65,13 @@ const RatingSection: React.FC<IRatingSectionProps> = ({
|
|||||||
setRatingSearch(formData.get("rating-search") as string);
|
setRatingSearch(formData.get("rating-search") as string);
|
||||||
|
|
||||||
router.push(
|
router.push(
|
||||||
`/?тип-дороги=${categories}${
|
`/?тип-дороги=${searchParams["тип-дороги"] || "1,2,3,4,5,6"}${
|
||||||
queryMap ? `&поиск-на-карте=${queryMap}` : ""
|
searchParams["поиск-на-карте"]
|
||||||
|
? `&поиск-на-карте=${searchParams["поиск-на-карте"] || ""}`
|
||||||
|
: ""
|
||||||
}${
|
}${
|
||||||
ratingSearch ? `&поиск-рейтинг=${ratingSearch}` : ""
|
ratingSearch ? `&поиск-рейтинг=${ratingSearch || ""}` : ""
|
||||||
}&страница-рейтинга=${page}`,
|
}&страница-рейтинга=${searchParams["страница-рейтинга"]}`,
|
||||||
{
|
{
|
||||||
scroll: false,
|
scroll: false,
|
||||||
}
|
}
|
||||||
@ -73,16 +79,21 @@ const RatingSection: React.FC<IRatingSectionProps> = ({
|
|||||||
|
|
||||||
getReports(ratingSearch, activePage, filter);
|
getReports(ratingSearch, activePage, filter);
|
||||||
|
|
||||||
if (reports.results.length < 8 && page !== "1") {
|
if (
|
||||||
|
reports.results.length < 8 &&
|
||||||
|
searchParams["страница-рейтинга"] !== "1"
|
||||||
|
) {
|
||||||
setActivePage(1);
|
setActivePage(1);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
router.push(
|
router.push(
|
||||||
`/?тип-дороги=${categories}${
|
`/?тип-дороги=${searchParams["тип-дороги"] || "1,2,3,4,5,6"}${
|
||||||
queryMap ? `&поиск-на-карте=${queryMap}` : ""
|
searchParams["поиск-на-карте"]
|
||||||
}${ratingSearch ? `&поиск-рейтинг=${ratingSearch}` : ""}${
|
? `&поиск-на-карте=${searchParams["поиск-на-карте"] || ""}`
|
||||||
|
: ""
|
||||||
|
}${ratingSearch ? `&поиск-рейтинг=${ratingSearch || ""}` : ""}${
|
||||||
activePage === 1 ? "" : `&страница-рейтинг=${activePage}`
|
activePage === 1 ? "" : `&страница-рейтинг=${activePage}`
|
||||||
}`,
|
}`,
|
||||||
{
|
{
|
||||||
@ -216,6 +227,7 @@ const RatingSection: React.FC<IRatingSectionProps> = ({
|
|||||||
next={reports.next}
|
next={reports.next}
|
||||||
prev={reports.previous}
|
prev={reports.previous}
|
||||||
current_count={reports.results.length}
|
current_count={reports.results.length}
|
||||||
|
limit={8}
|
||||||
/>
|
/>
|
||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
|
@ -35,7 +35,9 @@ export const useRatingStore = create<IRatingStore>((set) => ({
|
|||||||
set({ isLoading: true });
|
set({ isLoading: true });
|
||||||
|
|
||||||
const data = (
|
const data = (
|
||||||
await apiInstance.get<IFetchReports>(`/report/?page=${page}`)
|
await apiInstance.get<IFetchReports>(
|
||||||
|
`/report/?page=${page}&page_size=${8}`
|
||||||
|
)
|
||||||
).data;
|
).data;
|
||||||
|
|
||||||
const searched = data.results.filter((rating) => {
|
const searched = data.results.filter((rating) => {
|
||||||
|
@ -44,6 +44,14 @@
|
|||||||
line-height: 22px;
|
line-height: 22px;
|
||||||
color: rgb(197, 198, 197);
|
color: rgb(197, 198, 197);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
p {
|
||||||
|
color: rgb(240, 68, 56);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__error {
|
||||||
|
color: rgb(240, 68, 56);
|
||||||
}
|
}
|
||||||
|
|
||||||
&__map {
|
&__map {
|
||||||
@ -102,6 +110,10 @@
|
|||||||
color: rgb(102, 102, 102);
|
color: rgb(102, 102, 102);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&-error {
|
||||||
|
color: rgb(240, 68, 56) !important;
|
||||||
|
}
|
||||||
|
|
||||||
input[type="file"] {
|
input[type="file"] {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
@ -6,7 +6,6 @@ import Image from "next/image";
|
|||||||
import {
|
import {
|
||||||
MapContainer,
|
MapContainer,
|
||||||
Marker,
|
Marker,
|
||||||
Popup,
|
|
||||||
TileLayer,
|
TileLayer,
|
||||||
useMapEvents,
|
useMapEvents,
|
||||||
} from "react-leaflet";
|
} from "react-leaflet";
|
||||||
@ -16,9 +15,12 @@ import pin_image from "./icons/pin-image.svg";
|
|||||||
import { ChangeEventHandler, useState } from "react";
|
import { ChangeEventHandler, useState } from "react";
|
||||||
import { Icon } from "leaflet";
|
import { Icon } from "leaflet";
|
||||||
import pin_icon from "./icons/pin_icon.svg";
|
import pin_icon from "./icons/pin_icon.svg";
|
||||||
import axios from "axios";
|
import axios, { AxiosError } from "axios";
|
||||||
import { apiInstance } from "@/shared/config/apiConfig";
|
import { apiInstance } from "@/shared/config/apiConfig";
|
||||||
import { useSession } from "next-auth/react";
|
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 {
|
interface ILatLng {
|
||||||
lat: number;
|
lat: number;
|
||||||
@ -27,6 +29,7 @@ interface ILatLng {
|
|||||||
|
|
||||||
const ReportForm = () => {
|
const ReportForm = () => {
|
||||||
const session = useSession();
|
const session = useSession();
|
||||||
|
const router = useRouter();
|
||||||
const [latLng, setLatLng] = useState<ILatLng[]>([]);
|
const [latLng, setLatLng] = useState<ILatLng[]>([]);
|
||||||
const [displayLatLng, setDisplayLatLng] = useState<string[]>([]);
|
const [displayLatLng, setDisplayLatLng] = useState<string[]>([]);
|
||||||
const [images, setImages] = useState<File[]>([]);
|
const [images, setImages] = useState<File[]>([]);
|
||||||
@ -34,6 +37,12 @@ const ReportForm = () => {
|
|||||||
lat: 42.8746,
|
lat: 42.8746,
|
||||||
lng: 74.606,
|
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({
|
const customIcon = new Icon({
|
||||||
iconUrl: pin_icon.src,
|
iconUrl: pin_icon.src,
|
||||||
@ -55,13 +64,45 @@ const ReportForm = () => {
|
|||||||
HTMLFormElement
|
HTMLFormElement
|
||||||
> = async (e) => {
|
> = async (e) => {
|
||||||
e.preventDefault();
|
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 Authorization = `Bearer ${session.data?.access_token}`;
|
||||||
const config = {
|
const config = {
|
||||||
headers: {
|
headers: {
|
||||||
Authorization,
|
Authorization,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
const formData = new FormData(e.currentTarget);
|
|
||||||
|
|
||||||
images.forEach((image) => {
|
images.forEach((image) => {
|
||||||
formData.append("image", image);
|
formData.append("image", image);
|
||||||
@ -70,13 +111,27 @@ const ReportForm = () => {
|
|||||||
formData.append("longitude1", latLng[0].lng.toString());
|
formData.append("longitude1", latLng[0].lng.toString());
|
||||||
formData.append("latitude2", latLng[1].lat.toString());
|
formData.append("latitude2", latLng[1].lat.toString());
|
||||||
formData.append("longitude2", latLng[1].lng.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) => {
|
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) => (
|
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="Отметьте точки на карте"
|
placeholder="Отметьте точки на карте"
|
||||||
type="text"
|
type="text"
|
||||||
/>
|
/>
|
||||||
|
{locationWarning ? <p>{locationWarning}</p> : null}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="report-form__map">
|
<div className="report-form__map">
|
||||||
@ -146,6 +226,7 @@ const ReportForm = () => {
|
|||||||
<div className="report-form__input">
|
<div className="report-form__input">
|
||||||
<label>Добавьте описание проблемы</label>
|
<label>Добавьте описание проблемы</label>
|
||||||
<textarea name="description" placeholder="Введите описание" />
|
<textarea name="description" placeholder="Введите описание" />
|
||||||
|
{descriptionWarning ? <p>{descriptionWarning}</p> : null}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="report-form__add-images">
|
<div className="report-form__add-images">
|
||||||
@ -162,6 +243,12 @@ const ReportForm = () => {
|
|||||||
Прикрепить файл
|
Прикрепить файл
|
||||||
<span>(до 5 МБ)</span>
|
<span>(до 5 МБ)</span>
|
||||||
</label>
|
</label>
|
||||||
|
{imageWarning ? (
|
||||||
|
<p className="report-form__add-images-error">
|
||||||
|
{imageWarning}
|
||||||
|
</p>
|
||||||
|
) : null}
|
||||||
|
|
||||||
<input
|
<input
|
||||||
onChange={handleImages}
|
onChange={handleImages}
|
||||||
multiple
|
multiple
|
||||||
@ -184,10 +271,11 @@ const ReportForm = () => {
|
|||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
<button type="submit">
|
<button disabled={loader} type="submit">
|
||||||
Отправить на модерацию
|
{loader ? <Loader /> : "Отправить на модерацию"}
|
||||||
<Image src={arrow_right} alt="Arrow Right Icon" />
|
<Image src={arrow_right} alt="Arrow Right Icon" />
|
||||||
</button>
|
</button>
|
||||||
|
{error ? <p className="report-form__error">{error}</p> : null}
|
||||||
</form>
|
</form>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -104,7 +104,7 @@ const ReviewSection: React.FC<IReviewsSectionProps> = ({
|
|||||||
/>
|
/>
|
||||||
<div className="review__header">
|
<div className="review__header">
|
||||||
<h5 className="review__author-name">
|
<h5 className="review__author-name">
|
||||||
{review.author.first_name}
|
{review.author.first_name}{" "}
|
||||||
{review.author.last_name}
|
{review.author.last_name}
|
||||||
</h5>
|
</h5>
|
||||||
<div className="review__date">
|
<div className="review__date">
|
||||||
|
@ -24,6 +24,14 @@
|
|||||||
line-height: 24px;
|
line-height: 24px;
|
||||||
color: $gray-900;
|
color: $gray-900;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
button:hover {
|
||||||
|
background-color: #f5f5f5;
|
||||||
|
}
|
||||||
|
|
||||||
|
&-current {
|
||||||
|
background-color: #f5f5f5;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&__wrapper {
|
&__wrapper {
|
||||||
@ -98,6 +106,15 @@
|
|||||||
color: black;
|
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 SearchForm from "@/features/SearchForm/SearchForm";
|
||||||
import chevron_down from "./icons/chevron-down.svg";
|
import chevron_down from "./icons/chevron-down.svg";
|
||||||
import arrows from "@/shared/icons/arrows.svg";
|
import arrows from "@/shared/icons/arrows.svg";
|
||||||
|
|
||||||
import Image from "next/image";
|
import Image from "next/image";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
|
import { useDebounce } from "use-debounce";
|
||||||
|
import { useRouter } from "next/navigation";
|
||||||
|
|
||||||
interface IStatisticsTableProps {
|
interface IStatisticsTableProps {
|
||||||
firstData: IStatistics[] | string;
|
searchParams: {
|
||||||
|
["поиск-населенного-пункта"]: string;
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const StatisticsTable: React.FC<IStatisticsTableProps> = ({
|
const StatisticsTable: React.FC<IStatisticsTableProps> = ({
|
||||||
firstData,
|
searchParams,
|
||||||
}: IStatisticsTableProps) => {
|
}: IStatisticsTableProps) => {
|
||||||
const { data, error, isLoading, getStatistics } =
|
const router = useRouter();
|
||||||
useStatisticsStore(useShallow((state) => state));
|
const [location, setLocation] = useState<string>("city");
|
||||||
|
const [queryStatistics, setQueryStatistics] = useState<string>(
|
||||||
const [statistics, setStatistics] = useState<IStatistics[]>(
|
searchParams["поиск-населенного-пункта"] || ""
|
||||||
typeof firstData === "string" ? [] : firstData
|
|
||||||
);
|
);
|
||||||
|
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 [openPopup, setOpenPopup] = useState<boolean>(false);
|
||||||
|
|
||||||
const handleSubmit = () => {};
|
const handleSubmit = () => {};
|
||||||
|
|
||||||
const params = [
|
const params = [
|
||||||
{ param: "Добавлено дорог", handleSubmit() {} },
|
{
|
||||||
{ param: "Локальных дефектов", handleSubmit() {} },
|
param: "Добавлено дорог",
|
||||||
{ param: "Очагов аварийности", handleSubmit() {} },
|
async handleClick() {
|
||||||
{ param: "Локальных дефектов исправлено", handleSubmit() {} },
|
if (filter.option !== "broken_road_1") {
|
||||||
{ param: "В планах ремонта", handleSubmit() {} },
|
return setFilter({
|
||||||
{ param: "Отремонтировано", handleSubmit() {} },
|
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) => {
|
const getStatsByLocation = async (location: string) => {
|
||||||
try {
|
setLocation(location);
|
||||||
const data: IStatistics[] | undefined = await getStatistics(
|
getStatistics(location, filter, query);
|
||||||
location
|
console.error("Error fetching statistics:", error);
|
||||||
);
|
|
||||||
|
|
||||||
if (data === undefined)
|
|
||||||
throw new Error("Ошибка на стороне сервера");
|
|
||||||
setStatistics(data);
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Error fetching statistics:", error);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
getStatistics(location, filter, query);
|
||||||
|
|
||||||
|
router.push(`/statistics?поиск-населенного-пункта=${query}`);
|
||||||
|
}, [query]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="statistics-table">
|
<div className="statistics-table">
|
||||||
<SearchForm
|
<SearchForm
|
||||||
|
onChange={(e) => setQueryStatistics(e.target.value)}
|
||||||
|
value={queryStatistics}
|
||||||
|
placeholder="Введите населенный пункт"
|
||||||
style={{ width: "100%" }}
|
style={{ width: "100%" }}
|
||||||
handleSubmit={handleSubmit}
|
handleSubmit={handleSubmit}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{openPopup && (
|
{openPopup && (
|
||||||
<div className="statistics-table__popup">
|
<div className="statistics-table__popup">
|
||||||
<button onClick={() => getStatsByLocation("state")}>
|
<button
|
||||||
|
className={
|
||||||
|
location === "state"
|
||||||
|
? "statistics-table__popup-current"
|
||||||
|
: ""
|
||||||
|
}
|
||||||
|
onClick={() => getStatsByLocation("state")}
|
||||||
|
>
|
||||||
Область
|
Область
|
||||||
</button>
|
</button>
|
||||||
<button onClick={() => getStatsByLocation("city")}>
|
<button
|
||||||
|
className={
|
||||||
|
location === "city"
|
||||||
|
? "statistics-table__popup-current"
|
||||||
|
: ""
|
||||||
|
}
|
||||||
|
onClick={() => getStatsByLocation("city")}
|
||||||
|
>
|
||||||
Город
|
Город
|
||||||
</button>
|
</button>
|
||||||
<button onClick={() => getStatsByLocation("village")}>
|
<button
|
||||||
|
className={
|
||||||
|
location === "village"
|
||||||
|
? "statistics-table__popup-current"
|
||||||
|
: ""
|
||||||
|
}
|
||||||
|
onClick={() => getStatsByLocation("village")}
|
||||||
|
>
|
||||||
Деревня
|
Деревня
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@ -84,7 +219,7 @@ const StatisticsTable: React.FC<IStatisticsTableProps> = ({
|
|||||||
</td>
|
</td>
|
||||||
{params.map((param) => (
|
{params.map((param) => (
|
||||||
<td key={param.param}>
|
<td key={param.param}>
|
||||||
<button>
|
<button onClick={param.handleClick}>
|
||||||
{param.param}
|
{param.param}
|
||||||
<Image src={arrows} alt=" Arrows Icon" />
|
<Image src={arrows} alt=" Arrows Icon" />
|
||||||
</button>
|
</button>
|
||||||
@ -93,17 +228,27 @@ const StatisticsTable: React.FC<IStatisticsTableProps> = ({
|
|||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{statistics.map((stat) => (
|
{statistics.length ? (
|
||||||
<tr key={stat.name}>
|
statistics.map((stat) => (
|
||||||
<td id="statistics-table-stat-name">{stat.name}</td>
|
<tr key={stat.name}>
|
||||||
<td>{stat.broken_road_1}</td>
|
<td id="statistics-table-stat-name">{stat.name}</td>
|
||||||
<td>{stat.local_defect_3}</td>
|
<td>{stat.broken_road_1}</td>
|
||||||
<td>{stat.hotbed_of_accidents_2}</td>
|
<td>{stat.local_defect_3}</td>
|
||||||
<td>{stat.local_defect_fixed_6}</td>
|
<td>{stat.hotbed_of_accidents_2}</td>
|
||||||
<td>{stat.repair_plans_4}</td>
|
<td>{stat.local_defect_fixed_6}</td>
|
||||||
<td>{stat.repaired_5}</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>
|
</tr>
|
||||||
))}
|
)}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,29 +1,68 @@
|
|||||||
import { apiInstance } from "@/shared/config/apiConfig";
|
import { apiInstance } from "@/shared/config/apiConfig";
|
||||||
import { IFetch } from "@/shared/types/fetch-type";
|
import { IFetch } from "@/shared/types/fetch-type";
|
||||||
|
import { IList } from "@/shared/types/list-type";
|
||||||
import { IStatistics } from "@/shared/types/statistics-type";
|
import { IStatistics } from "@/shared/types/statistics-type";
|
||||||
import { AxiosError } from "axios";
|
import { AxiosError } from "axios";
|
||||||
import { create } from "zustand";
|
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 {
|
interface IStatisticsStore extends IFetch {
|
||||||
|
data: IStatistics[];
|
||||||
getStatistics: (
|
getStatistics: (
|
||||||
endpoint: string
|
endpoint: string,
|
||||||
) => Promise<IStatistics[] | undefined>;
|
filter: { option: string; toggle: boolean },
|
||||||
|
query: string
|
||||||
|
) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const useStatisticsStore = create<IStatisticsStore>((set) => ({
|
export const useStatisticsStore = create<IStatisticsStore>((set) => ({
|
||||||
isLoading: false,
|
isLoading: false,
|
||||||
error: "",
|
error: "",
|
||||||
|
data: [],
|
||||||
getStatistics: async (
|
getStatistics: async (
|
||||||
endpoint: string
|
endpoint: string,
|
||||||
): Promise<IStatistics[] | undefined> => {
|
filter: { option: string; toggle: boolean },
|
||||||
|
query: string = ""
|
||||||
|
) => {
|
||||||
try {
|
try {
|
||||||
set({ isLoading: true });
|
set({ isLoading: true });
|
||||||
const response = await apiInstance.get<IStatistics[]>(
|
const response = await apiInstance.get<IStatistics[]>(
|
||||||
`/report/${endpoint}/stats`
|
`/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) {
|
} catch (error: unknown) {
|
||||||
if (error instanceof AxiosError) {
|
if (error instanceof AxiosError) {
|
||||||
set({ error: error.message });
|
set({ error: error.message });
|
||||||
|
@ -1,23 +1,112 @@
|
|||||||
import { IUserRatings } from "@/shared/types/user-rating-type";
|
"use client";
|
||||||
|
|
||||||
import "./VolunteersTable.scss";
|
import "./VolunteersTable.scss";
|
||||||
import Image from "next/image";
|
import Image from "next/image";
|
||||||
import arrows from "@/shared/icons/arrows.svg";
|
import arrows from "@/shared/icons/arrows.svg";
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import { useVolunteersStore } from "./volunteers.store";
|
||||||
|
|
||||||
interface IVolunteersTableProps {
|
const VolunteersTable = () => {
|
||||||
firstData: IUserRatings[] | string;
|
const { data, isLoading, error, getVolunteers } =
|
||||||
}
|
useVolunteersStore();
|
||||||
|
const [filter, setFilter] = useState({
|
||||||
const VolunteersTable: React.FC<IVolunteersTableProps> = ({
|
option: "report_count",
|
||||||
firstData,
|
toggle: false,
|
||||||
}: IVolunteersTableProps) => {
|
});
|
||||||
const params = [
|
const params = [
|
||||||
{ param: "№" },
|
{ param: "№" },
|
||||||
{ param: "Активист" },
|
{ param: "Активист" },
|
||||||
{ param: "Добавлено дорог", handleClick() {} },
|
{
|
||||||
{ param: "Получено голосов", handleClick() {} },
|
param: "Добавлено дорог",
|
||||||
{ param: "Оставлено голосов", handleClick() {} },
|
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 (
|
return (
|
||||||
<div className="volunteers-table">
|
<div className="volunteers-table">
|
||||||
<table>
|
<table>
|
||||||
@ -26,7 +115,7 @@ const VolunteersTable: React.FC<IVolunteersTableProps> = ({
|
|||||||
{params.map((param) => (
|
{params.map((param) => (
|
||||||
<td key={param.param}>
|
<td key={param.param}>
|
||||||
{param.handleClick ? (
|
{param.handleClick ? (
|
||||||
<button>
|
<button onClick={param.handleClick}>
|
||||||
{param.param}
|
{param.param}
|
||||||
<Image src={arrows} alt="Arrows Icon" />
|
<Image src={arrows} alt="Arrows Icon" />
|
||||||
</button>
|
</button>
|
||||||
@ -38,20 +127,18 @@ const VolunteersTable: React.FC<IVolunteersTableProps> = ({
|
|||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{typeof firstData === "string" ? (
|
{data.map((user, index) => (
|
||||||
<h3>{firstData}</h3>
|
<tr key={user.user_id}>
|
||||||
) : (
|
<td>{index + 1}</td>
|
||||||
firstData.map((user, index) => (
|
<td id="volunteers-user-cell">
|
||||||
<tr key={user.user_id}>
|
{hideEmail(user.username)}
|
||||||
<td>{index + 1}</td>
|
</td>
|
||||||
<td id="volunteers-user-cell">{user.username}</td>
|
<td>{user.report_count}</td>
|
||||||
<td>{user.report_count}</td>
|
<td>{user.likes_received_count}</td>
|
||||||
<td>{user.likes_received_count}</td>
|
<td>{user.likes_given_count}</td>
|
||||||
<td>{user.likes_given_count}</td>
|
<td>{user.average_rating}</td>
|
||||||
<td>{user.average_rating}</td>
|
</tr>
|
||||||
</tr>
|
))}
|
||||||
))
|
|
||||||
)}
|
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</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