forked from Transparency/kgroad-frontend2
added next-intl, composed next-auth and next-intl in middleware, fixed some bugs, completed details page, added google auth
This commit is contained in:
1 parent
9cd7b2a67d
commit
6637ab4eae
172 files changed
+1772
-502
No files matched your search
@@ -0,0 +1,137 @@
|
||||
.profile-table {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 30px;
|
||||
&__wrapper {
|
||||
overflow: hidden;
|
||||
overflow-x: auto;
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
table {
|
||||
width: 100%;
|
||||
|
||||
thead {
|
||||
padding: 0 40px;
|
||||
width: 100%;
|
||||
height: 68px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
background-color: rgb(244, 244, 244);
|
||||
|
||||
tr {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: grid;
|
||||
grid-template-columns: 160px 271px 300px 164px 100px;
|
||||
justify-content: space-between;
|
||||
|
||||
td {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 100%;
|
||||
background-color: rgb(244, 244, 244);
|
||||
}
|
||||
|
||||
td,
|
||||
button {
|
||||
font-size: 16px;
|
||||
font-weight: 400;
|
||||
line-height: 18px;
|
||||
color: rgb(102, 112, 133);
|
||||
}
|
||||
|
||||
button {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
tbody {
|
||||
display: flex;
|
||||
|
||||
flex-direction: column;
|
||||
|
||||
tr {
|
||||
padding: 0 40px;
|
||||
height: 80px;
|
||||
display: grid;
|
||||
grid-template-columns: 160px 271px 300px 164px 100px;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
border-bottom: 1px solid rgb(241, 244, 249);
|
||||
|
||||
td {
|
||||
font-size: 18px;
|
||||
font-weight: 500;
|
||||
line-height: 20px;
|
||||
}
|
||||
|
||||
#my-report-date {
|
||||
color: rgb(102, 112, 133);
|
||||
}
|
||||
|
||||
#my-report-location {
|
||||
a {
|
||||
display: flex;
|
||||
font-size: 20px;
|
||||
color: rgb(50, 48, 58);
|
||||
}
|
||||
}
|
||||
|
||||
#my-report-status {
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
#my-report-reviews {
|
||||
color: rgb(72, 159, 225);
|
||||
}
|
||||
|
||||
#my-report-likes {
|
||||
color: rgb(74, 192, 63);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&__message {
|
||||
font-size: 18px;
|
||||
font-weight: 500;
|
||||
line-height: 22px;
|
||||
color: rgb(102, 102, 102);
|
||||
}
|
||||
|
||||
#my-reports-link {
|
||||
display: none;
|
||||
padding: 10px;
|
||||
border-radius: 8px;
|
||||
background-color: rgb(57, 152, 232);
|
||||
font-size: 16px;
|
||||
font-weight: 800;
|
||||
line-height: 19px;
|
||||
color: rgb(255, 255, 255);
|
||||
}
|
||||
}
|
||||
|
||||
.no-reports-in-profile {
|
||||
border: 1px solid rgb(197, 198, 197);
|
||||
border-radius: 10px;
|
||||
width: 100%;
|
||||
height: 150px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 550px) {
|
||||
.profile-table {
|
||||
a {
|
||||
display: flex;
|
||||
align-self: center;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,197 @@
|
||||
"use client";
|
||||
|
||||
import Image from "next/image";
|
||||
import "./ProfileTable.scss";
|
||||
import arrows from "@/shared/icons/arrows.svg";
|
||||
import {
|
||||
REPORT_STATUS,
|
||||
REPORT_STATUS_COLORS,
|
||||
} from "@/shared/variables/report-status";
|
||||
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 { Link, useRouter } from "@/shared/config/navigation";
|
||||
|
||||
interface IProfileTableProps {
|
||||
searchParams: { ["страница-обращений"]: string };
|
||||
}
|
||||
|
||||
const ProfileTable: React.FC<IProfileTableProps> = ({
|
||||
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() {
|
||||
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() {
|
||||
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
|
||||
);
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
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
|
||||
className={`profile-table__wrapper ${
|
||||
reports.results.length ? "" : "no-reports-in-profile"
|
||||
}`}
|
||||
>
|
||||
{reports.results.length ? (
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
{params.map((p) => (
|
||||
<td key={p.param}>
|
||||
{p.handleClick ? (
|
||||
<button onClick={p.handleClick}>
|
||||
{p.param}{" "}
|
||||
<Image src={arrows} alt="Arrows Icon" />
|
||||
</button>
|
||||
) : (
|
||||
p.param
|
||||
)}
|
||||
</td>
|
||||
))}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{reports.results.map((report) => (
|
||||
<tr key={report.id}>
|
||||
<td id="my-report-date">
|
||||
{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"
|
||||
style={{
|
||||
color: `${REPORT_STATUS_COLORS[report.status]}`,
|
||||
}}
|
||||
>
|
||||
{REPORT_STATUS[report.status]}
|
||||
</td>
|
||||
<td id="my-report-reviews">
|
||||
{report.count_reviews}
|
||||
</td>
|
||||
<td id="my-report-likes">{report.total_likes}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
) : (
|
||||
<p className="profile-table__message">
|
||||
Вы пока не оставили ни одного обращения.
|
||||
</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>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ProfileTable;
|
||||
@@ -0,0 +1,6 @@
|
||||
export const sliceDate = (date: string) => {
|
||||
return `${date.slice(8, 10)}.${date.slice(5, 7)}.${date.slice(
|
||||
0,
|
||||
4
|
||||
)}`;
|
||||
};
|
||||
@@ -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 });
|
||||
}
|
||||
},
|
||||
})
|
||||
);
|
||||
@@ -0,0 +1,121 @@
|
||||
@import "@/shared/ui/variables.scss";
|
||||
|
||||
.statistics-table {
|
||||
position: relative;
|
||||
|
||||
&__popup {
|
||||
width: 123px;
|
||||
position: absolute;
|
||||
top: 58%;
|
||||
left: 2%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background-color: #fff;
|
||||
border-radius: 8px;
|
||||
border: 1px solid rgb(255, 255, 255);
|
||||
box-shadow: 0px 4px 6px -2px rgba(16, 24, 40, 0.03),
|
||||
0px 12px 16px -4px rgba(16, 24, 40, 0.08);
|
||||
button {
|
||||
padding: 10px 14px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
line-height: 24px;
|
||||
color: $gray-900;
|
||||
}
|
||||
|
||||
button:hover {
|
||||
background-color: #f5f5f5;
|
||||
}
|
||||
|
||||
&-current {
|
||||
background-color: #f5f5f5;
|
||||
}
|
||||
}
|
||||
|
||||
&__wrapper {
|
||||
border: 1px solid rgb(213, 213, 213);
|
||||
border-radius: 12px;
|
||||
|
||||
overflow: hidden;
|
||||
overflow-x: auto;
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
table {
|
||||
width: 100%;
|
||||
thead {
|
||||
width: 100%;
|
||||
padding: 0 20px;
|
||||
height: 76px;
|
||||
display: flex;
|
||||
background-color: rgb(244, 244, 244);
|
||||
align-items: center;
|
||||
|
||||
tr {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
||||
display: grid;
|
||||
grid-template-columns: 100px 167px 186px 188px 263px 170px 151px;
|
||||
align-items: center;
|
||||
|
||||
td {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 100%;
|
||||
background-color: rgb(244, 244, 244);
|
||||
}
|
||||
|
||||
td,
|
||||
button {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
line-height: 18px;
|
||||
color: rgb(102, 112, 133);
|
||||
}
|
||||
|
||||
button {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
tbody {
|
||||
padding: 0 20px;
|
||||
tr {
|
||||
display: grid;
|
||||
grid-template-columns: 100px 167px 186px 188px 263px 170px 151px;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
height: 80px;
|
||||
border-bottom: 1px solid rgb(241, 244, 249);
|
||||
|
||||
td {
|
||||
font-size: 20px;
|
||||
font-weight: 500;
|
||||
line-height: 20px;
|
||||
color: rgb(102, 112, 133);
|
||||
}
|
||||
|
||||
#statistics-table-stat-name {
|
||||
color: black;
|
||||
}
|
||||
}
|
||||
|
||||
#statistics-table__no-data-warning {
|
||||
grid-template-columns: 1fr;
|
||||
justify-content: center;
|
||||
|
||||
td {
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,258 @@
|
||||
"use client";
|
||||
|
||||
import "./StatisticsTable.scss";
|
||||
import { useStatisticsStore } from "./statistics.store";
|
||||
import { useShallow } from "zustand/react/shallow";
|
||||
import SearchForm from "@/features/SearchForm/SearchForm";
|
||||
import chevron_down from "./icons/chevron-down.svg";
|
||||
import arrows from "@/shared/icons/arrows.svg";
|
||||
import Image from "next/image";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useDebounce } from "use-debounce";
|
||||
import { useRouter } from "@/shared/config/navigation";
|
||||
|
||||
interface IStatisticsTableProps {
|
||||
searchParams: {
|
||||
["поиск-населенного-пункта"]: string;
|
||||
};
|
||||
}
|
||||
|
||||
const StatisticsTable: React.FC<IStatisticsTableProps> = ({
|
||||
searchParams,
|
||||
}: IStatisticsTableProps) => {
|
||||
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: "Добавлено дорог",
|
||||
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) => {
|
||||
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
|
||||
className={
|
||||
location === "state"
|
||||
? "statistics-table__popup-current"
|
||||
: ""
|
||||
}
|
||||
onClick={() => getStatsByLocation("state")}
|
||||
>
|
||||
Область
|
||||
</button>
|
||||
<button
|
||||
className={
|
||||
location === "city"
|
||||
? "statistics-table__popup-current"
|
||||
: ""
|
||||
}
|
||||
onClick={() => getStatsByLocation("city")}
|
||||
>
|
||||
Город
|
||||
</button>
|
||||
<button
|
||||
className={
|
||||
location === "village"
|
||||
? "statistics-table__popup-current"
|
||||
: ""
|
||||
}
|
||||
onClick={() => getStatsByLocation("village")}
|
||||
>
|
||||
Деревня
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="statistics-table__wrapper">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<td>
|
||||
<button onClick={() => setOpenPopup((prev) => !prev)}>
|
||||
Город
|
||||
<Image src={chevron_down} alt=" Chevron Icon" />
|
||||
</button>
|
||||
</td>
|
||||
{params.map((param) => (
|
||||
<td key={param.param}>
|
||||
<button onClick={param.handleClick}>
|
||||
{param.param}
|
||||
<Image src={arrows} alt=" Arrows Icon" />
|
||||
</button>
|
||||
</td>
|
||||
))}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{statistics.length ? (
|
||||
statistics.map((stat) => (
|
||||
<tr key={stat.name}>
|
||||
<td id="statistics-table-stat-name">{stat.name}</td>
|
||||
<td>{stat.broken_road_1}</td>
|
||||
<td>{stat.local_defect_3}</td>
|
||||
<td>{stat.hotbed_of_accidents_2}</td>
|
||||
<td>{stat.local_defect_fixed_6}</td>
|
||||
<td>{stat.repair_plans_4}</td>
|
||||
<td>{stat.repaired_5}</td>
|
||||
</tr>
|
||||
))
|
||||
) : (
|
||||
<tr id="statistics-table__no-data-warning">
|
||||
<td>
|
||||
{error
|
||||
? error
|
||||
: "Oops, looks like there is no data"}
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default StatisticsTable;
|
||||
@@ -0,0 +1,7 @@
|
||||
<svg width="9.000000" height="6.000000" viewBox="0 0 9 6" fill="none" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<desc>
|
||||
Created with Pixso.
|
||||
</desc>
|
||||
<defs/>
|
||||
<path id="Vector" d="M8.25 0L0.75 0C0.609375 0 0.46875 0.0410156 0.347656 0.117188C0.226562 0.193359 0.132812 0.302734 0.0703125 0.431641C0.0117188 0.561523 -0.0117188 0.705078 0.0078125 0.84668C0.0234375 0.988281 0.0820312 1.12109 0.175781 1.23047L3.92578 5.73047C3.99609 5.81543 4.08203 5.88281 4.18359 5.92969C4.28125 5.97656 4.39062 6 4.5 6C4.60938 6 4.71875 5.97656 4.81641 5.92969C4.91797 5.88281 5.00781 5.81543 5.07812 5.73047L8.82812 1.23047C8.91797 1.12109 8.97656 0.988281 8.99609 0.84668C9.01172 0.705078 8.99219 0.561523 8.92969 0.431641C8.87109 0.302734 8.77344 0.193359 8.65234 0.117188C8.53125 0.0410156 8.39453 0 8.25 0ZM4.5 4.07812L2.35156 1.5L6.64844 1.5L4.5 4.07812Z" fill="#667085" fill-opacity="1.000000" fill-rule="nonzero"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 955 B |
@@ -0,0 +1,76 @@
|
||||
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,
|
||||
filter: { option: string; toggle: boolean },
|
||||
query: string
|
||||
) => void;
|
||||
}
|
||||
|
||||
export const useStatisticsStore = create<IStatisticsStore>((set) => ({
|
||||
isLoading: false,
|
||||
error: "",
|
||||
data: [],
|
||||
getStatistics: async (
|
||||
endpoint: string,
|
||||
filter: { option: string; toggle: boolean },
|
||||
query: string = ""
|
||||
) => {
|
||||
try {
|
||||
set({ isLoading: true });
|
||||
const response = await apiInstance.get<IStatistics[]>(
|
||||
`/report/${endpoint}/stats`
|
||||
);
|
||||
|
||||
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 });
|
||||
} else {
|
||||
set({ error: "an error ocured" });
|
||||
}
|
||||
} finally {
|
||||
set({ isLoading: false });
|
||||
}
|
||||
},
|
||||
}));
|
||||
@@ -0,0 +1,73 @@
|
||||
.volunteers-table {
|
||||
overflow: hidden;
|
||||
overflow-x: auto;
|
||||
&::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
border: 1px solid rgb(213, 213, 213);
|
||||
border-radius: 6px;
|
||||
|
||||
table {
|
||||
width: 100%;
|
||||
thead {
|
||||
width: 100%;
|
||||
padding: 0 20px;
|
||||
height: 76px;
|
||||
display: flex;
|
||||
background-color: rgb(244, 244, 244);
|
||||
align-items: center;
|
||||
|
||||
tr {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
||||
display: grid;
|
||||
grid-template-columns: 100px 320px 210px 213px 222px 92px;
|
||||
align-items: center;
|
||||
|
||||
td {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 100%;
|
||||
background-color: rgb(244, 244, 244);
|
||||
}
|
||||
|
||||
td,
|
||||
button {
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
line-height: 18px;
|
||||
color: rgb(102, 112, 133);
|
||||
}
|
||||
|
||||
button {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
tbody {
|
||||
padding: 0 20px;
|
||||
tr {
|
||||
display: grid;
|
||||
grid-template-columns: 100px 320px 210px 213px 222px 92px;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
height: 80px;
|
||||
border-bottom: 1px solid rgb(241, 244, 249);
|
||||
|
||||
td {
|
||||
font-size: 20px;
|
||||
font-weight: 500;
|
||||
line-height: 20px;
|
||||
}
|
||||
|
||||
#volunteers-user-cell {
|
||||
color: rgb(72, 159, 225);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,139 @@
|
||||
"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";
|
||||
|
||||
const VolunteersTable = () => {
|
||||
const { data, isLoading, error, getVolunteers } =
|
||||
useVolunteersStore();
|
||||
const [filter, setFilter] = useState({
|
||||
option: "report_count",
|
||||
toggle: true,
|
||||
});
|
||||
const params = [
|
||||
{ param: "№" },
|
||||
{ param: "Активист" },
|
||||
{
|
||||
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) => {
|
||||
return `${email.slice(0, 3)}*****@****.com`;
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
getVolunteers(filter);
|
||||
}, []);
|
||||
return (
|
||||
<div className="volunteers-table">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
{params.map((param) => (
|
||||
<td key={param.param}>
|
||||
{param.handleClick ? (
|
||||
<button onClick={param.handleClick}>
|
||||
{param.param}
|
||||
<Image src={arrows} alt="Arrows Icon" />
|
||||
</button>
|
||||
) : (
|
||||
<>{param.param}</>
|
||||
)}
|
||||
</td>
|
||||
))}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{data.map((user, index) => (
|
||||
<tr key={user.user_id}>
|
||||
<td>{index + 1}</td>
|
||||
<td id="volunteers-user-cell">
|
||||
{hideEmail(user.username)}
|
||||
</td>
|
||||
<td>{user.report_count}</td>
|
||||
<td>{user.likes_received_count}</td>
|
||||
<td>{user.likes_given_count}</td>
|
||||
<td>{user.average_rating}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default VolunteersTable;
|
||||
@@ -0,0 +1,71 @@
|
||||
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;
|
||||
|
||||
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 });
|
||||
}
|
||||
},
|
||||
}));
|
||||
Reference in new issue
Block a user