release004

This commit is contained in:
Vladislav Khorev committed 2024-03-22 08:39:52 +00:00
commit 80a3f658b9
204 files changed
+3509 -1228

No files matched your search

File renamed without changes.
File renamed without changes.
@@ -7,7 +7,7 @@
width: fit-content;
}
img {
&__image {
align-self: center;
margin-bottom: 65px;
border-radius: 12px;
@@ -36,6 +36,22 @@
line-height: 34px;
color: rgb(62, 50, 50);
}
ul {
display: flex;
flex-direction: column;
gap: 20px;
li {
font-size: 20px;
font-weight: 400;
line-height: 34px;
color: rgb(62, 50, 50);
}
}
}
&__author {
margin-top: 30px;
}
}
+109
View File
@@ -0,0 +1,109 @@
import Typography from "@/shared/ui/components/Typography/Typography";
import "./AboutUs.scss";
import Image from "next/image";
import header from "./assets/header.svg";
import { Metadata } from "next";
import BreadCrumbs from "@/features/BreadCrumbs/BreadCrumbs";
import { apiInstance } from "@/shared/config/apiConfig";
import { IMetatag } from "@/shared/types/metatag-type";
export async function generateMetadata(): Promise<Metadata> {
const data = await apiInstance
.get<IMetatag[]>("/metatags/")
.then((res) => res.data)
.catch((e) => console.log(e));
if (!data)
return {
title: "KG ROAD | О нас",
description:
"Transparency International - Кыргызстан - филиал международной организации Transparency International в Кыргызской Республике.",
openGraph: {
title: "KG ROAD | О нас",
description:
"Transparency International - Кыргызстан - филиал международной организации Transparency International в Кыргызской Республике.",
},
};
const metadata = data.filter((tag) => tag.page === "about-us")[0];
if (!metadata) {
return {
title: "KG ROAD | О нас",
description:
"Transparency International - Кыргызстан - филиал международной организации Transparency International в Кыргызской Республике.",
openGraph: {
title: "KG ROAD | О нас",
description:
"Transparency International - Кыргызстан - филиал международной организации Transparency International в Кыргызской Республике.",
},
};
}
return {
title: `KG ROAD | ${metadata.title}`,
description: metadata.description,
keywords: metadata.keywords.split(","),
openGraph: {
title: `KG ROAD | ${metadata.title}`,
description: metadata.description,
type: "website",
},
};
}
const AboutUs = () => {
return (
<div className="about-us page-padding">
<BreadCrumbs homeRequired />
<Typography element="h2">О нас</Typography>
<Image
className="about-us__image"
src={header}
alt="Header Image"
/>
<div className="about-us__descriptions">
<h3>
Transparency International-Кыргызстан - филиал международной
организации Transparency International в Кыргызской
Республике.
</h3>
<h3>Миссия ТИ-Кыргызстан</h3>
<p>
Продвижение эффективной общественной политики и надлежащего
управления в целях предотвращения коррупции и усиления
демократии в стране.{" "}
</p>
<h3>Цели и приоритеты ТИ-Кыргызстан:</h3>
<ul>
<li>
- антикоррупционное просвещение населения, повышение
общественного осознания значимости и важности борьбы с
коррупцией в Кыргызстане;
</li>
<li>
- организация изучения практики и теории борьбы с
коррупцией и участия в ней структур гражданского общества
в Кыргызстане и других странах;
</li>
<li>
- содействие гражданам и организациям в реализации их
конституционных прав и свобод;
</li>
<li>
- преимущественная ориентация на международный опыт
уменьшения коррупции, освоение его технологий, ресурсов, а
также включение структур гражданского общества в
международный диалог борьбы с коррупцией.
</li>
</ul>
</div>
<p className="about-us__author">
Photo By Mzximvs VdB from Brussels, belgium - Road to
Issyk-Kul (south shore), CC BY-SA 2.0
</p>
</div>
);
};
export default AboutUs;
@@ -2,15 +2,15 @@ import Typography from "@/shared/ui/components/Typography/Typography";
import "./CreateReport.scss";
import dynamic from "next/dynamic";
import { Metadata } from "next";
import BreadCrumbs from "@/features/BreadCrumbs/BreadCrumbs";
export const metadata: Metadata = {
title: "KG ROAD | Написать обращение",
description:
"Написать обращение KG ROAD",
description: "Написать обращение KG ROAD",
};
const DynamicForm = dynamic(
() => import("@/widgets/ReportForm/ReportForm"),
() => import("@/widgets/forms/ReportForm/ReportForm"),
{
ssr: false,
}
@@ -19,6 +19,7 @@ const DynamicForm = dynamic(
const CreateReport = () => {
return (
<div className="create-report page-padding">
<BreadCrumbs homeRequired />
<Typography element="h2">Написать обращение</Typography>
<DynamicForm />
+33
View File
@@ -0,0 +1,33 @@
import "../globals.scss";
import "./App.scss";
import "@/shared/fonts/fonts.scss";
import Navbar from "@/widgets/Navbar/Navbar";
import Footer from "@/widgets/Footer/Footer";
import { NextIntlClientProvider, useMessages } from "next-intl";
import { Providers } from "./Providers";
export default function LocaleLayout({
children,
params,
}: Readonly<{
children: React.ReactNode;
params: { locale: string };
}>) {
const messages = useMessages();
return (
<html lang={params.locale}>
<body>
<NextIntlClientProvider
locale={params.locale}
messages={messages}
>
<Providers>
<Navbar />
<div className="app">{children}</div>
<Footer />
</Providers>
</NextIntlClientProvider>
</body>
</html>
);
}
@@ -1,21 +1,25 @@
.news {
display: flex;
flex-direction: column;
gap: 40px;
h2 {
width: fit-content;
margin-bottom: 40px;
}
}
@media screen and (max-width: 768px) {
.news {
gap: 30px;
h2 {
margin-bottom: 30px;
}
}
}
@media screen and (max-width: 550px) {
.news {
gap: 20px;
h2 {
margin-bottom: 20px;
}
}
}
+9
View File
@@ -0,0 +1,9 @@
"use client";
import NotFound from "@/widgets/NotFound/NotFound";
const error = () => {
return <NotFound />;
};
export default error;
@@ -6,11 +6,33 @@ import Image from "next/image";
import message from "./icons/message.svg";
import calendar from "./icons/calendar.svg";
import ReviewSection from "@/widgets/ReviewSection/ReviewSection";
import BreadCrumbs from "@/features/BreadCrumbs/BreadCrumbs";
import { Metadata } from "next";
export async function generateMetadata({
params,
}: {
params: { id: string };
}): Promise<Metadata> {
const response = await apiInstance.get<INews>(
`/news/${params.id}/`
);
return {
title: response.data.title,
description: response.data.description,
openGraph: {
images: [response.data.image],
type: "article",
publishedTime: response.data.created_at,
},
};
}
const NewsDetails = async ({
params,
}: {
params: { id: string };
params: { id: string; новость: string };
}) => {
const getNewsById = async () => {
const response = await apiInstance.get<INews>(
@@ -39,6 +61,7 @@ const NewsDetails = async ({
return (
<div className="news-details page-padding">
<BreadCrumbs />
<Typography element="h2">{data.title}</Typography>
<div className="news-details__img">
+59
View File
@@ -0,0 +1,59 @@
import BreadCrumbs from "@/features/BreadCrumbs/BreadCrumbs";
import "./News.scss";
import Typography from "@/shared/ui/components/Typography/Typography";
import NewsList from "@/widgets/NewsList/NewsList";
import { Metadata } from "next";
import { apiInstance } from "@/shared/config/apiConfig";
import { IMetatag } from "@/shared/types/metatag-type";
export async function generateMetadata(): Promise<Metadata> {
const data = await apiInstance
.get<IMetatag[]>("/metatags/")
.then((res) => res.data)
.catch((e) => console.log(e));
if (!data)
return {
title: "KG ROAD | Новости",
description: "Страница новостей KG ROAD",
};
const metadata = data.filter((tag) => tag.page === "news")[0];
if (!metadata) {
return {
title: "KG ROAD | Новости",
description: "Страница новостей KG ROAD",
};
}
return {
title: `KG ROAD | ${metadata.title}`,
description: metadata.description,
keywords: metadata.keywords.split(","),
openGraph: {
title: `KG ROAD | ${metadata.title}`,
description: metadata.description,
type: "website",
},
};
}
const News = ({
searchParams,
}: {
searchParams: {
["страница-новостей"]: string;
};
}) => {
return (
<div className="news page-padding">
<BreadCrumbs homeRequired />
<Typography element="h2">Новости</Typography>
<NewsList searchParams={searchParams} />
</div>
);
};
export default News;
+9
View File
@@ -0,0 +1,9 @@
"use client";
import NotFound from "@/widgets/NotFound/NotFound";
import "../globals.scss";
import "@/shared/fonts/fonts.scss";
export default function NotFoundPage() {
return <NotFound />;
}
+78
View File
@@ -0,0 +1,78 @@
import Header from "@/widgets/home/Header/Header";
import StatisticsSection from "@/widgets/home/StatisticsSection/StatisticsSection";
import RatingSection from "@/widgets/home/RatingSection/RatingSection";
import NewsSection from "@/widgets/home/NewsSection/NewsSection";
import MapSection from "@/widgets/home/MapSection/MapSection";
import { Metadata } from "next";
import { apiInstance } from "@/shared/config/apiConfig";
import { IMetatag } from "@/shared/types/metatag-type";
export async function generateMetadata(): Promise<Metadata> {
const data = await apiInstance
.get<IMetatag[]>("/metatags/")
.then((res) => res.data)
.catch((e) => console.log(e));
if (!data)
return {
title: "KG ROAD | Главная",
description:
"Главная страница KG ROAD | Сделаем дороги безопасными!",
openGraph: {
title: "KG ROAD | Главная",
description:
"Главная страница KG ROAD | Сделаем дороги безопасными!",
type: "website",
},
};
const metadata = data.filter((tag) => tag.page === "home")[0];
if (!metadata) {
return {
title: "KG ROAD | Главная",
description:
"Главная страница KG ROAD | Сделаем дороги безопасными!",
openGraph: {
title: "KG ROAD | Главная",
description:
"Главная страница KG ROAD | Сделаем дороги безопасными!",
type: "website",
},
};
}
return {
title: `KG ROAD | ${metadata.title}`,
description: metadata.description,
keywords: metadata.keywords.split(","),
openGraph: {
title: `KG ROAD | ${metadata.title}`,
description: metadata.description,
type: "website",
},
};
}
const Home = async ({
searchParams,
}: {
searchParams: {
["тип-дороги"]: string;
["поиск-на-карте"]: string;
["поиск-рейтинг"]: string;
["страница-рейтинга"]: string;
};
}) => {
return (
<div className="home">
<Header />
<StatisticsSection />
<MapSection searchParams={searchParams} />
<RatingSection searchParams={searchParams} />
<NewsSection />
</div>
);
};
export default Home;
@@ -14,13 +14,9 @@ const AuthGuard = ({ children }: { children: React.ReactNode }) => {
};
await apiInstance.post("/token/verify/", data);
} catch (error: unknown) {
if (error instanceof AxiosError) {
if (error.response?.data.code === "token_not_valid") {
signOut({
callbackUrl: "/",
});
}
}
signOut({
callbackUrl: "/",
});
}
};
File renamed without changes.
@@ -6,13 +6,12 @@ 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";
import { Metadata } from "next";
import BreadCrumbs from "@/features/BreadCrumbs/BreadCrumbs";
export const metadata: Metadata = {
title: "KG ROAD | Профиль",
description:
"Страница профиля KG ROAD",
description: "Страница профиля KG ROAD",
};
const Profile = async ({
@@ -44,6 +43,7 @@ const Profile = async ({
return (
<div className="profile page-padding">
<BreadCrumbs />
<Typography element="h2">Личный кабинет</Typography>
<ProfileNav report_count={data?.report_count as number} />
@@ -1,4 +1,4 @@
import ProfileTable from "@/widgets/ProfileTable/ProfileTable";
import ProfileTable from "@/widgets/tables/ProfileTable/ProfileTable";
import React from "react";
const MyReports = async ({
+13
View File
@@ -0,0 +1,13 @@
"use client";
import { useRouter } from "@/shared/config/navigation";
import { useEffect } from "react";
const Profile = () => {
const router = useRouter();
useEffect(() => {
router.push("/profile/personal");
}, []);
return <></>;
};
export default Profile;
@@ -4,7 +4,7 @@ import ProfileAvatar from "@/features/ProfileAvatar/ProfileAvatar";
import { apiInstance } from "@/shared/config/apiConfig";
import { authConfig } from "@/shared/config/authConfig";
import { IProfile } from "@/shared/types/profile-type";
import ProfileForm from "@/widgets/ProfileForm/ProfileForm";
import ProfileForm from "@/widgets/forms/ProfileForm/ProfileForm";
import { AxiosError } from "axios";
import { getServerSession } from "next-auth";
import React from "react";
@@ -0,0 +1,37 @@
.report-details {
display: flex;
flex-direction: column;
gap: 75px;
&__container {
display: grid;
grid-template-columns: 1.05fr 1fr;
gap: 76px;
}
&__map {
grid-row: 2 / 3;
grid-column: 1 / 3;
}
}
@media screen and (max-width: 1024px) {
.report-details {
&__container {
grid-template-columns: 1fr;
gap: 45px;
}
&__map {
grid-row: 2 / 3;
grid-column: 1 / 2;
}
}
}
@media screen and (max-width: 550px) {
.report-details {
&__container {
gap: 40px;
}
}
}
+9
View File
@@ -0,0 +1,9 @@
"use client";
import NotFound from "@/widgets/NotFound/NotFound";
const error = () => {
return <NotFound />;
};
export default error;
+86
View File
@@ -0,0 +1,86 @@
import "./ReportDetails.scss";
import { IReport } from "@/shared/types/report-type";
import ReviewSection from "@/widgets/ReviewSection/ReviewSection";
import { Metadata } from "next";
import ReportInformation from "@/widgets/report-details/ReportInformation/ReportInformation";
import ReportImages from "@/widgets/report-details/ReportImages/ReportImages";
import dynamic from "next/dynamic";
import BreadCrumbs from "@/features/BreadCrumbs/BreadCrumbs";
import NotFound from "@/widgets/NotFound/NotFound";
import { apiInstance } from "@/shared/config/apiConfig";
const DynamicMap = dynamic(
() => import("@/widgets/report-details/ReportMap/ReportMap"),
{
ssr: false,
}
);
export async function generateMetadata({
params,
}: {
params: { id: string };
}): Promise<Metadata> {
const response = await apiInstance.get<IReport>(
`/report/${params.id}/`
);
return {
title: `KG ROAD | ${response.data.location[0].address}`,
description: response.data.description,
openGraph: {
title: `KG ROAD | ${response.data.location[0].address}`,
description: response.data.description,
images: [response.data.image[0].image],
type: "article",
},
};
}
const ReportDetails = async ({
params,
}: {
params: { id: string };
}) => {
const getReportDetails = async () => {
try {
const res = await fetch(
`${process.env.NEXT_PUBLIC_BASE_API}/report/${params.id}/`,
{ cache: "no-store" }
);
return res.json();
} catch (error) {
console.log(error);
}
};
const report: IReport = await getReportDetails();
return (
<div className="report-details page-padding">
<BreadCrumbs homeRequired />
<div className="report-details__container">
<ReportInformation
id={report.id}
location={report.location[0]}
date={report.created_at}
description={report.description}
category={report.category}
total_likes={report.total_likes}
author={report.author}
/>
<div className="report-details__map">
<DynamicMap
location={report.location}
category={report.category}
/>
</div>
<ReportImages images={report.image} />
</div>
<ReviewSection endpoint="report" id={+params.id} />
</div>
);
};
export default ReportDetails;
@@ -0,0 +1,16 @@
import BreadCrumbs from "@/features/BreadCrumbs/BreadCrumbs";
import "@/shared/ui/auth-classes.scss";
import ForgotPasswordForm from "@/widgets/forms/ForgotPasswordForm/ForgotPasswordForm";
const ForgotPassword = () => {
return (
<div className="page-padding">
<BreadCrumbs />
<div className="auth-page">
<ForgotPasswordForm />
</div>
</div>
);
};
export default ForgotPassword;
@@ -1,9 +1,9 @@
import "@/shared/ui/auth-classes.scss";
import Image from "next/image";
import sign_in_icon from "./icons/sign-in_icon.svg";
import SignInForm from "@/widgets/SignInForm/SignInForm";
import Link from "next/link";
import SignInForm from "@/widgets/forms/SignInForm/SignInForm";
import { Metadata } from "next";
import { Link } from "@/shared/config/navigation";
export const metadata: Metadata = {
title: "KG ROAD | Вход",
@@ -1,7 +1,7 @@
import "@/shared/ui/auth-classes.scss";
import Image from "next/image";
import key from "./icons/key.svg";
import ResetCodeForm from "@/widgets/ResetCodeForm/ResetCodeForm";
import ResetCodeForm from "@/widgets/forms/ResetCodeForm/ResetCodeForm";
const ResetCode = () => {
return (
@@ -0,0 +1,38 @@
import "@/shared/ui/auth-classes.scss";
import Image from "next/image";
import mail from "./icons/mail.svg";
import ConfirmEmailForm from "@/widgets/forms/ConfirmEmailForm/ConfirmEmailForm";
import BreadCrumbs from "@/features/BreadCrumbs/BreadCrumbs";
const ConfirmEmail = ({
searchParams,
}: {
searchParams: {
email: string;
};
}) => {
return (
<div className="page-padding">
<BreadCrumbs />
<div className="auth-page">
<div className="auth-wrapper">
<div className="auth-icon2">
<Image
src={mail}
alt="Mail icon"
width={56}
height={56}
/>
</div>
<div className="auth-header">
<h2>Проверьте свою почту</h2>
<p>Мы отправили код на почту {searchParams.email}</p>
</div>
<ConfirmEmailForm email={searchParams.email} />
</div>
</div>
</div>
);
};
export default ConfirmEmail;
File renamed without changes.
@@ -1,9 +1,9 @@
import "@/shared/ui/auth-classes.scss";
import Image from "next/image";
import flag from "./icons/flag.svg";
import Link from "next/link";
import SignUpForm from "@/widgets/SignUpForm/SignUpForm";
import SignUpForm from "@/widgets/forms/SignUpForm/SignUpForm";
import { Metadata } from "next";
import { Link } from "@/shared/config/navigation";
export const metadata: Metadata = {
title: "KG ROAD | Регистрация",
@@ -1,21 +1,25 @@
.statistics {
display: flex;
flex-direction: column;
gap: 40px;
h2 {
width: fit-content;
margin-bottom: 40px;
}
}
@media screen and (max-width: 768px) {
.statistics {
gap: 30px;
h2 {
margin-bottom: 30px;
}
}
}
@media screen and (max-width: 550px) {
.statistics {
gap: 20px;
h2 {
margin-bottom: 20px;
}
}
}
+56
View File
@@ -0,0 +1,56 @@
import Typography from "@/shared/ui/components/Typography/Typography";
import "./Statistics.scss";
import StatisticsTable from "@/widgets/tables/StatisticsTable/StatisticsTable";
import { Metadata } from "next";
import BreadCrumbs from "@/features/BreadCrumbs/BreadCrumbs";
import { IMetatag } from "@/shared/types/metatag-type";
import { apiInstance } from "@/shared/config/apiConfig";
export async function generateMetadata(): Promise<Metadata> {
const data = await apiInstance
.get<IMetatag[]>("/metatags/")
.then((res) => res.data)
.catch((e) => console.log(e));
if (!data)
return {
title: "KG ROAD | Статистика",
description: `Статистика по населенным пунктам Кыргызстана`,
keywords: ["Бишкек", "Чуй", "Кыргызстан", "Дороги"],
};
const metadata = data.filter((tag) => tag.page === "statistics")[0];
if (!metadata) {
return {
title: "KG ROAD | Статистика",
description: `Статистика по населенным пунктам Кыргызстана`,
};
}
return {
title: `KG ROAD | ${metadata.title}`,
description: metadata.description,
keywords: metadata.keywords.split(","),
openGraph: {
title: `KG ROAD | ${metadata.title}`,
description: metadata.description,
type: "website",
},
};
}
const Statistics = ({
searchParams,
}: {
searchParams: { ["поиск-населенного-пункта"]: string };
}) => {
return (
<div className="statistics page-padding">
<BreadCrumbs homeRequired />
<Typography element="h2">Статистика</Typography>
<StatisticsTable searchParams={searchParams} />
</div>
);
};
export default Statistics;
@@ -1,21 +1,25 @@
.volunteers {
display: flex;
flex-direction: column;
gap: 40px;
h2 {
width: fit-content;
margin-bottom: 40px;
}
}
@media screen and (max-width: 768px) {
.volunteers {
gap: 30px;
h2 {
margin-bottom: 30px;
}
}
}
@media screen and (max-width: 550px) {
.volunteers {
gap: 20px;
h2 {
margin-bottom: 20px;
}
}
}
+54
View File
@@ -0,0 +1,54 @@
import Typography from "@/shared/ui/components/Typography/Typography";
import "./Volunteers.scss";
import VolunteersTable from "@/widgets/tables/VolunteersTable/VolunteersTable";
import { Metadata } from "next";
import BreadCrumbs from "@/features/BreadCrumbs/BreadCrumbs";
import { apiInstance } from "@/shared/config/apiConfig";
import { IMetatag } from "@/shared/types/metatag-type";
export async function generateMetadata(): Promise<Metadata> {
const data = await apiInstance
.get<IMetatag[]>("/metatags/")
.then((res) => res.data)
.catch((e) => console.log(e));
if (!data)
return {
title: "KG ROAD | Волонтеры",
description:
"Страница лучших волонтеров Кыргызской Республики!",
};
const metadata = data.filter((tag) => tag.page === "volunteers")[0];
if (!metadata) {
return {
title: "KG ROAD | Волонтеры",
description:
"Страница лучших волонтеров Кыргызской Республики!",
};
}
return {
title: `KG ROAD | ${metadata.title}`,
description: metadata.description,
keywords: metadata.keywords.split(","),
openGraph: {
title: `KG ROAD | ${metadata.title}`,
description: metadata.description,
type: "website",
},
};
}
const Volunteers = () => {
return (
<div className="volunteers page-padding">
<BreadCrumbs homeRequired />
<Typography element="h2">Волонтеры</Typography>
<VolunteersTable />
</div>
);
};
export default Volunteers;
-36
View File
@@ -1,36 +0,0 @@
import Typography from "@/shared/ui/components/Typography/Typography";
import "./AboutUs.scss";
import Image from "next/image";
import header from "./assets/header.svg";
import { Metadata } from "next";
export const metadata: Metadata = {
title: "KG ROAD | О нас",
description:
'Страница "О Нас" KG ROAD',
};
const AboutUs = () => {
return (
<div className="about-us page-padding">
<Typography element="h2">О нас</Typography>
<Image src={header} alt="Header Image" />
<div className="about-us__descriptions">
<p>Transparency International-Кыргызстан - филиал международной организации Transparency International в Кыргызской Республике.</p>
<h4>Миссия ТИ-Кыргызстан</h4>
<p>Продвижение эффективной общественной политики и надлежащего управления в целях предотвращения коррупции и усиления демократии в стране. </p>
<h4>Цели и приоритеты ТИ-Кыргызстан:</h4>
<ul>
<li> - антикоррупционное просвещение населения, повышение общественного осознания значимости и важности борьбы с коррупцией в Кыргызстане;</li>
<li> - организация изучения практики и теории борьбы с коррупцией и участия в ней структур гражданского общества в Кыргызстане и других странах;</li>
<li> - содействие гражданам и организациям в реализации их конституционных прав и свобод;</li>
<li> - преимущественная ориентация на международный опыт уменьшения коррупции, освоение его технологий, ресурсов, а также включение структур гражданского общества в международный диалог борьбы с коррупцией.</li>
</ul>
</div>
<p>Photo By Mzximvs VdB from Brussels, belgium - Road to Issyk-Kul (south shore), CC BY-SA 2.0</p>
</div>
);
};
export default AboutUs;
+1 -4
View File
@@ -4,10 +4,7 @@
box-sizing: border-box;
padding: 0;
margin: 0;
font-family: system-ui, -apple-system, BlinkMacSystemFont,
"Segoe UI", Roboto, Oxygen, Ubuntu, Cantarell, "Open Sans",
"Helvetica Neue", sans-serif;
// font-family: "Tilda Sans";
font-family: "Tilda Sans";
}
button,
+10 -9
View File
@@ -1,13 +1,10 @@
import type { Metadata } from "next";
import "./globals.scss";
import "./App.scss";
// import "@/shared/fonts/fonts.scss";
import { Providers } from "./Providers";
import Navbar from "@/widgets/Navbar/Navbar";
import Footer from "@/widgets/Footer/Footer";
import { GoogleAnalytics } from '@next/third-parties/google'
import { ReactNode } from "react";
type Props = {
children: ReactNode;
};
/*
<<<<<<< HEAD
export default function RootLayout({
children,
}: Readonly<{
@@ -28,4 +25,8 @@ export default function RootLayout({
{<GoogleAnalytics gaId={process.env.NEXT_PUBLIC_GOOGLE_ANALYTICS || ""} />}
</html>
);
=======*/
export default function RootLayout({ children }: Props) {
return children;
//>>>>>>> ali
}
-30
View File
@@ -1,30 +0,0 @@
import "./News.scss";
import Typography from "@/shared/ui/components/Typography/Typography";
import { apiInstance } from "@/shared/config/apiConfig";
import { INewsList } from "@/shared/types/news-type";
import NewsList from "@/widgets/NewsList/NewsList";
import { Metadata } from "next";
export const metadata: Metadata = {
title: "KG ROAD | Новости",
description:
"Страница новостей KG ROAD",
};
const News = ({
searchParams,
}: {
searchParams: {
["страница-новостей"]: string;
};
}) => {
return (
<div className="news page-padding">
<Typography element="h2">Новости</Typography>
<NewsList searchParams={searchParams} />
</div>
);
};
export default News;
+15
View File
@@ -0,0 +1,15 @@
"use client";
import NotFound from "@/widgets/NotFound/NotFound";
import "./globals.scss";
import "@/shared/fonts/fonts.scss";
export default function NotFoundPage() {
return (
<html lang="en">
<body>
<NotFound />
</body>
</html>
);
}
-35
View File
@@ -1,35 +0,0 @@
import Header from "@/widgets/Header/Header";
import StatisticsSection from "@/widgets/StatisticsSection/StatisticsSection";
import RatingSection from "@/widgets/RatingSection/RatingSection";
import NewsSection from "@/widgets/NewsSection/NewsSection";
import MapSection from "@/widgets/MapSection/MapSection";
import { Metadata } from "next";
export const metadata: Metadata = {
title: "KG ROAD | Главная",
description:
"Главная страница KG ROAD",
};
const Home = async ({
searchParams,
}: {
searchParams: {
["тип-дороги"]: string;
["поиск-на-карте"]: string;
["поиск-рейтинг"]: string;
["страница-рейтинга"]: string;
};
}) => {
return (
<div className="home">
<Header />
<StatisticsSection />
<MapSection searchParams={searchParams} />
<RatingSection searchParams={searchParams} />
<NewsSection />
</div>
);
};
export default Home;
-7
View File
@@ -1,7 +0,0 @@
import React from "react";
const page = () => {
return <div>page</div>;
};
export default page;
-138
View File
@@ -1,138 +0,0 @@
import "./ReportDetails.scss";
import Image from "next/image";
import RoadType from "@/entities/RoadType/RoadType";
import ReportLike from "@/features/ReportLike/ReportLike";
import { apiInstance } from "@/shared/config/apiConfig";
import { IReport } from "@/shared/types/report-type";
import {
ROAD_TYPES,
ROAD_TYPES_COLORS,
} from "@/shared/variables/road-types";
import calendar from "./icons/calendar.svg";
import map_pin from "./icons/map-pin.svg";
import def_image from "./icons/def_image.svg";
import ReviewSection from "@/widgets/ReviewSection/ReviewSection";
import { Metadata } from "next";
export const metadata: Metadata = {
title: "KG ROAD | Обращение",
description:
"Страница обращения KG ROAD",
};
const ReportDetails = async ({
params,
}: {
params: { id: string };
}) => {
const getReportDetails = async () => {
const res = await fetch(
`${process.env.NEXT_PUBLIC_BASE_API}/report/${params.id}/`,
{ cache: "no-store" }
);
return res.json();
};
const report: IReport = await getReportDetails();
const months: Record<string, string> = {
"01": "Январь",
"02": "Февраль",
"03": "Март",
"04": "Апрель",
"05": "Май",
"06": "Июнь",
"07": "Июль",
"08": "Август",
"09": "Сентябрь",
"10": "Октябрь",
"11": "Ноябрь",
"12": "Декабрь",
};
const showImages = () => {
const images = [];
for (let i = 0; i < 5; i++) {
if (report.image[i]) {
const image = (
<img
className={`report-images__exist report-images__item${
i + 1
}`}
key={i}
src={report.image[i].image}
alt="Report Image"
/>
);
images.push(image);
} else {
const defImage = (
<div
className={`report-images__default report-images__item${
i + 1
}`}
key={i}
>
<Image src={def_image} alt="Default Image" />
</div>
);
images.push(defImage);
}
}
return images;
};
return (
<div className="report-details page-padding">
<div className="report-details__container">
<div className="report-information">
<RoadType color={ROAD_TYPES_COLORS[report.category]}>
{ROAD_TYPES[report.category]}
</RoadType>
<h2>{report.location[0].address}</h2>
<div className="report-information__date-and-like">
<div className="report-information__date">
<Image src={calendar} alt="Calendar Icon" />
<p>
{months[report.created_at.slice(5, 7)]}{" "}
{report.created_at.slice(5, 7).slice(0, 1) === "0"
? report.created_at.slice(6, 7)
: report.created_at.slice(5, 7)}
, {report.created_at.slice(0, 4)}
</p>
</div>
<ReportLike
count={report.total_likes}
report_id={report.id}
/>
</div>
<p className="report-information__description">
{report.description}
</p>
<p className="report-information__author">
Автор обращения:{" "}
<span>
{report.author.first_name}{" "}
{report.author.last_name.slice(0, 1)}.
</span>
</p>
<button className="report-information__show-map">
<Image src={map_pin} alt="Map Pin Icon" />
Показать на карте
</button>
</div>
<div className="report-images">
{showImages().map((image) => image)}
</div>
</div>
<ReviewSection endpoint="report" id={+params.id} />
</div>
);
};
export default ReportDetails;
-12
View File
@@ -1,12 +0,0 @@
import "@/shared/ui/auth-classes.scss";
import ForgotPasswordForm from "@/widgets/ForgotPasswordForm/ForgotPasswordForm";
const ForgotPassword = () => {
return (
<div className="auth-page">
<ForgotPasswordForm />
</div>
);
};
export default ForgotPassword;
-29
View File
@@ -1,29 +0,0 @@
import "@/shared/ui/auth-classes.scss";
import Image from "next/image";
import mail from "./icons/mail.svg";
import ConfirmEmailForm from "@/widgets/ConfirmEmailForm/ConfirmEmailForm";
const ConfirmEmail = ({
searchParams,
}: {
searchParams: {
email: string;
};
}) => {
return (
<div className="auth-page">
<div className="auth-wrapper">
<div className="auth-icon2">
<Image src={mail} alt="Mail icon" width={56} height={56} />
</div>
<div className="auth-header">
<h2>Проверьте свою почту</h2>
<p>Мы отправили код на почту {searchParams.email}</p>
</div>
<ConfirmEmailForm email={searchParams.email} />
</div>
</div>
);
};
export default ConfirmEmail;
-28
View File
@@ -1,28 +0,0 @@
import Typography from "@/shared/ui/components/Typography/Typography";
import "./Statistics.scss";
import { apiInstance } from "@/shared/config/apiConfig";
import { IStatistics } from "@/shared/types/statistics-type";
import { AxiosError } from "axios";
import StatisticsTable from "@/widgets/StatisticsTable/StatisticsTable";
import { Metadata } from "next";
export const metadata: Metadata = {
title: "KG ROAD | Статистика",
description:
"Страница статистики KG ROAD",
};
const Statistics = ({
searchParams,
}: {
searchParams: { ["поиск-населенного-пункта"]: string };
}) => {
return (
<div className="statistics page-padding">
<Typography element="h2">Статистика</Typography>
<StatisticsTable searchParams={searchParams} />
</div>
);
};
export default Statistics;
-21
View File
@@ -1,21 +0,0 @@
import Typography from "@/shared/ui/components/Typography/Typography";
import "./Volunteers.scss";
import VolunteersTable from "@/widgets/VolunteersTable/VolunteersTable";
import { Metadata } from "next";
export const metadata: Metadata = {
title: "KG ROAD | Волонтеры",
description:
"Страница волонтеров KG ROAD",
};
const Volunteers = () => {
return (
<div className="volunteers page-padding">
<Typography element="h2">Волонтеры</Typography>
<VolunteersTable />
</div>
);
};
export default Volunteers;
+10 -2
View File
@@ -3,6 +3,7 @@
import Image, { StaticImageData } from "next/image";
import "./NewsCard.scss";
import Link from "next/link";
import { useTranslations } from "next-intl";
interface INewsCard {
id: number;
@@ -19,6 +20,7 @@ const NewsCard: React.FC<INewsCard> = ({
description,
date,
}: INewsCard) => {
const t = useTranslations("general");
const sliceTitle = (title: string) => {
if (title.length > 35) {
return `${title.slice(0, 35)}...`;
@@ -56,8 +58,14 @@ const NewsCard: React.FC<INewsCard> = ({
<p>{sliceDescription(description)}</p>
</div>
<Link href={`/news/${id}`} className="news-card__more-btn">
Подробнее
<Link
href={{
pathname: `/news/${id}`,
query: { новость: title },
}}
className="news-card__more-btn"
>
{t("details")}
</Link>
</div>
);
+29
View File
@@ -0,0 +1,29 @@
.breadcrumbs {
margin-bottom: 40px;
display: flex;
align-items: center;
gap: 8px;
span,
a {
display: flex;
align-items: center;
font-size: 14px;
font-weight: 400;
line-height: 150%;
}
a {
gap: 8px;
color: rgb(72, 159, 225);
img {
width: 16px;
height: 16px;
}
}
span {
color: rgb(50, 48, 58);
}
}
+67
View File
@@ -0,0 +1,67 @@
"use client";
import { Link, usePathname } from "@/shared/config/navigation";
import "./BreadCrumbs.scss";
import { useSearchParams } from "next/navigation";
import Image from "next/image";
import chevron from "./icons/chevron-right.svg";
interface IBreadcrumbsProps {
homeRequired?: boolean;
}
const BreadCrumbs: React.FC<IBreadcrumbsProps> = ({
homeRequired,
}: IBreadcrumbsProps) => {
const pathname = usePathname();
const query = useSearchParams().get("новость");
const routes = pathname.split("/").filter((route) => route !== "");
const tRoutes: Record<string, string> = {
"about-us": "О нас",
"create-report": "Написать обращение",
news: "Новости",
profile: "Профиль",
"my-reports": "Мои обращения",
personal: "Личные данные",
report: "Обращение",
"sign-in": "Войти в аккаунт",
"forgot-password": "Восстановление пароля",
"reset-code": "Сброс пароля",
"sign-up": "Регистрация",
"confirm-email": "Потверждение почты",
statistics: "Статистика",
volunteers: "Волонтеры",
};
return (
<div className="breadcrumbs">
{homeRequired && (
<Link href="/">
Главная <Image src={chevron} alt="Chevron Right Icon" />
</Link>
)}
{routes.map((route, i, array) => {
if (routes.length === 1 && routes[0] === "profile")
return null;
if (parseInt(route)) {
if (routes[0] === "news") {
return <span key={query}>{query}</span>;
}
}
if (i === array.length - 1) {
return <span key={route}>{tRoutes[route]}</span>;
} else {
return route === "report" ? (
<span key={route}>{tRoutes[route]}</span>
) : (
<Link key={route} href={`/${route}`}>
{tRoutes[route]}
<Image src={chevron} alt="Chevron Right Icon" />
</Link>
);
}
})}
</div>
);
};
export default BreadCrumbs;
@@ -0,0 +1,14 @@
<svg width="16.000000" height="16.000000" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<desc>
Created with Pixso.
</desc>
<defs>
<clipPath id="clip7_52716">
<rect id="chevron-right" width="16.000000" height="16.000000" fill="white" fill-opacity="0"/>
</clipPath>
</defs>
<rect id="chevron-right" width="16.000000" height="16.000000" fill="#FFFFFF" fill-opacity="0"/>
<g clip-path="url(#clip7_52716)">
<path id="Vector" d="M6 4L10 8L6 12" stroke="#667085" stroke-opacity="1.000000" stroke-width="1.000000" stroke-linejoin="round" stroke-linecap="round"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 651 B

+17 -1
View File
@@ -1,10 +1,26 @@
"use client";
import Image from "next/image";
import "./GoogleButton.scss";
import google from "./icons/google.svg";
import { useSearchParams } from "next/navigation";
import { signIn } from "next-auth/react";
const GoogleButton = () => {
const searchParams = useSearchParams();
const callbackUrl =
searchParams.get("callbackUrl") || "/profile/personal";
const googleLogin = async () => {
await signIn("google", { callbackUrl });
};
return (
<button className="google-btn">
<button
type="button"
className="google-btn"
onClick={googleLogin}
>
<Image src={google} alt="Google Icon" />
Войти через Google
</button>
+123 -5
View File
@@ -11,11 +11,7 @@
object-fit: cover;
}
input[type="file"] {
display: none;
}
label {
&__change-btn {
position: absolute;
bottom: 0;
right: 0;
@@ -34,4 +30,126 @@
height: 22px;
}
}
&__modal {
width: 100%;
height: 100%;
position: fixed;
top: 0;
left: 0;
display: flex;
align-items: center;
justify-content: center;
background-color: rgba(0, 0, 0, 0.5);
}
&__wrapper {
position: relative;
max-width: 400px;
padding: 24px;
display: flex;
flex-direction: column;
align-items: center;
background-color: white;
border-radius: 15px;
box-shadow: 0px 16px 24px 0px rgba(0, 0, 0, 0.15);
}
&__text {
div {
display: flex;
align-items: center;
justify-content: space-between;
h4 {
font-size: 18px;
font-weight: 500;
line-height: 28px;
color: rgb(51, 65, 85);
}
button {
width: 24px;
height: 24px;
display: flex;
align-items: center;
justify-content: center;
border: 1px solid rgb(226, 232, 240);
border-radius: 50%;
}
}
p {
width: 90%;
font-size: 14px;
font-weight: 400;
line-height: 20px;
color: rgb(100, 116, 139);
}
}
&__user-img {
margin: 24px 0;
width: 130px;
height: 130px;
border-radius: 50%;
object-fit: cover;
}
input[type="file"] {
display: none;
}
&__btns {
display: flex;
align-items: center;
gap: 8px;
button,
label {
min-width: 110px;
padding: 8px 16px;
margin-top: 24px;
display: flex;
align-items: center;
justify-content: center;
font-size: 14px;
font-weight: 500;
line-height: 20px;
border-radius: 8px;
cursor: pointer;
}
}
&__blue-btn {
color: white;
background-color: rgb(72, 159, 225);
}
&__gray-btn {
color: rgb(51, 65, 85);
border: 1px solid rgb(226, 232, 240);
background: rgb(255, 255, 255);
}
&__message {
position: absolute;
bottom: 32px;
padding: 8px;
width: 302px;
display: flex;
align-items: center;
justify-content: space-between;
border-radius: 3px;
background-color: rgb(0, 0, 0);
font-size: 14px;
font-weight: 500;
line-height: 20px;
color: rgb(255, 255, 255);
button {
font-size: 0;
}
}
}
+220 -28
View File
@@ -3,9 +3,14 @@
import Image from "next/image";
import "./ProfileAvatar.scss";
import pen from "./icons/pen.svg";
import { apiInstance } from "@/shared/config/apiConfig";
import { useSession } from "next-auth/react";
import close from "./icons/close.svg";
import close_white from "./icons/close-white.svg";
import { authInstanse } from "@/shared/config/apiConfig";
import { useRouter } from "next/navigation";
import { useSession } from "next-auth/react";
import { useState } from "react";
import { AxiosError } from "axios";
import Loader from "@/shared/ui/components/Loader/Loader";
interface IProfileAvatarProps {
img: string;
@@ -14,35 +19,109 @@ interface IProfileAvatarProps {
const ProfileAvatar: React.FC<IProfileAvatarProps> = ({
img,
}: IProfileAvatarProps) => {
const [modal, setModal] = useState<boolean>(false);
const [display_image, setDisplayImage] = useState<File | string>(
img
);
const [isDeleting, setIsDeleting] = useState<boolean>(false);
const [message, setMessage] = useState<boolean>(false);
const def =
"https://api.kgroaduat.fishrungames.com/media/user_photo/default.webp";
const [success, setSuccess] = useState<string>("");
const [loader, setLoader] = useState<boolean>(false);
const [error, setError] = useState<string>("");
const session = useSession();
const router = useRouter();
const changeImage: React.ChangeEventHandler<
HTMLInputElement
> = async (e) => {
const Authorization = `Bearer ${session.data?.access_token}`;
const config = {
headers: {
Authorization,
},
};
const formData = new FormData();
if (e.target.files) {
const image = Array.from(e.target.files);
formData.append("image", image[0]);
}
try {
const res = await apiInstance.patch(
"/users/update_image/",
formData,
config
);
router.refresh();
} catch (error) {
console.log(error);
const handleChange: React.ChangeEventHandler<HTMLInputElement> = (
e
) => {
if (e.target.files) {
setDisplayImage(e.target.files[0]);
}
};
const changeImage = async () => {
const formData = new FormData();
if (session.status === "unauthenticated") return;
if (
typeof display_image === typeof "string" ||
display_image === img
)
return;
formData.append("image", display_image);
try {
setLoader(true);
const res = await authInstanse(
session.data?.access_token as string
).patch("/users/update_image/", formData);
setError("");
setSuccess("Фото профиля обновлено");
setMessage(true);
router.refresh();
setTimeout(() => {
setMessage(false);
}, 3000);
} catch (error: unknown) {
if (error instanceof AxiosError) {
setSuccess("");
setError(error.message);
setMessage(true);
setTimeout(() => {
setMessage(false);
}, 3000);
}
} finally {
setLoader(false);
}
};
const returnDefaultImage = async () => {
if (session.status === "unauthenticated") return;
try {
setLoader(true);
const res = await authInstanse(
session.data?.access_token as string
).patch("/users/delete_image/", {});
setError("");
setSuccess("Фото профиля удалено");
setMessage(true);
setDisplayImage(def);
router.refresh();
setTimeout(() => {
setMessage(false);
}, 3000);
} catch (error: unknown) {
if (error instanceof AxiosError) {
setSuccess("");
setError(error.message);
setMessage(true);
setTimeout(() => {
setMessage(false);
}, 3000);
}
} finally {
setLoader(false);
}
};
const imageIsString =
typeof display_image === "string"
? display_image
: typeof display_image === "undefined"
? ""
: URL.createObjectURL(display_image as File);
return (
<div className="profile-avatar">
<img
@@ -50,10 +129,123 @@ const ProfileAvatar: React.FC<IProfileAvatarProps> = ({
src={img}
alt="User Image"
/>
<label htmlFor="profile-image">
<button
onClick={() => setModal(true)}
className="profile-avatar__change-btn"
>
<Image src={pen} alt="Pen Icon" />
</label>
<input onChange={changeImage} id="profile-image" type="file" />
</button>
{modal && (
<div
onClick={() => setModal(false)}
className="profile-avatar__modal"
>
<div
onClick={(e) => e.stopPropagation()}
className="profile-avatar__wrapper"
>
<div className="profile-avatar__text">
<div>
<h4>Фото профиля</h4>
<button onClick={() => setModal(false)}>
<Image src={close} alt="Close Icon" />
</button>
</div>
<p>
По фото профиля другие люди смогут вас узнавать, а вам
будет проще определять, в какой аккаунт вы вошли.
</p>
</div>
<img
className="profile-avatar__user-img"
src={imageIsString}
alt="User image"
/>
<div className="profile-avatar__btns">
{img === def && display_image === def ? (
<>
<label
className="profile-avatar__blue-btn"
htmlFor="change-image"
>
Добавить фото профиля
</label>
<input
onChange={handleChange}
id="change-image"
type="file"
/>
</>
) : isDeleting ? (
<>
<button
onClick={() => setIsDeleting(false)}
className="profile-avatar__gray-btn"
>
Отмена
</button>
<button
onClick={returnDefaultImage}
disabled={loader}
className="profile-avatar__blue-btn"
>
{loader ? <Loader /> : "Удалить"}
</button>
</>
) : img === display_image || success !== "" ? (
<>
<button
onClick={() => setIsDeleting(true)}
className="profile-avatar__gray-btn"
>
Удалить
</button>
<label
className="profile-avatar__blue-btn"
htmlFor="change-image"
>
Сменить
</label>
<input
onChange={handleChange}
id="change-image"
type="file"
/>
</>
) : (
<>
<button
onClick={() => setDisplayImage(img)}
className="profile-avatar__gray-btn"
>
Назад
</button>
<button
disabled={loader}
onClick={changeImage}
className="profile-avatar__blue-btn"
>
{loader ? <Loader /> : "Сохранить"}
</button>
</>
)}
</div>
{message && (
<div className="profile-avatar__message">
<p>
{success} {error}
</p>
<button onClick={() => setMessage(false)}>
<Image src={close_white} alt="Close Icon White" />
</button>
</div>
)}
</div>
</div>
)}
</div>
);
};
@@ -0,0 +1,14 @@
<svg width="20.000000" height="20.000000" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<desc>
Created with Pixso.
</desc>
<defs>
<clipPath id="clip2704_56355">
<rect id="icon" width="20.000000" height="20.000000" fill="white" fill-opacity="0"/>
</clipPath>
</defs>
<g clip-path="url(#clip2704_56355)">
<path id="Vector" d="M14.375 5.625L5.625 14.375" stroke="#FFFFFF" stroke-opacity="1.000000" stroke-width="1.500000" stroke-linejoin="round" stroke-linecap="round"/>
<path id="Vector" d="M5.625 5.625L14.375 14.375" stroke="#FFFFFF" stroke-opacity="1.000000" stroke-width="1.500000" stroke-linejoin="round" stroke-linecap="round"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 730 B

@@ -0,0 +1,14 @@
<svg width="12.000000" height="12.000000" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<desc>
Created with Pixso.
</desc>
<defs>
<clipPath id="clip4_25648">
<rect id="close" width="12.000000" height="12.000000" fill="white" fill-opacity="0"/>
</clipPath>
</defs>
<g clip-path="url(#clip4_25648)">
<path id="Vector" d="M8.625 3.375L3.375 8.625" stroke="#334155" stroke-opacity="1.000000" stroke-width="1.500000" stroke-linejoin="round" stroke-linecap="round"/>
<path id="Vector" d="M3.375 3.375L8.625 8.625" stroke="#334155" stroke-opacity="1.000000" stroke-width="1.500000" stroke-linejoin="round" stroke-linecap="round"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 721 B

+7 -2
View File
@@ -4,9 +4,9 @@ import "./ReportLike.scss";
import Image from "next/image";
import like from "./icons/like.svg";
import { apiInstance } from "@/shared/config/apiConfig";
import { useRouter } from "next/navigation";
import { useSession } from "next-auth/react";
import { signIn, useSession } from "next-auth/react";
import { useEffect } from "react";
import { usePathname, useRouter } from "@/shared/config/navigation";
interface IReportLikeProps {
count: number;
@@ -20,6 +20,7 @@ const ReportLike: React.FC<IReportLikeProps> = ({
const session = useSession();
const router = useRouter();
const pathname = usePathname();
const Authorization = `Bearer ${session?.data?.access_token}`;
const config = {
@@ -29,6 +30,10 @@ const ReportLike: React.FC<IReportLikeProps> = ({
};
const checkLike = async () => {
if (session.status === "unauthenticated") {
signIn(undefined, { callbackUrl: pathname });
}
const res = await apiInstance.get<{ detail: string }>(
`/report/${report_id}/like/check/`,
config
+3 -1
View File
@@ -3,6 +3,7 @@
import "./SearchForm.scss";
import Image from "next/image";
import search from "./icons/search.svg";
import { useTranslations } from "next-intl";
interface ISearchFormProps
extends React.InputHTMLAttributes<HTMLInputElement> {
@@ -17,6 +18,7 @@ const SearchForm: React.FC<ISearchFormProps> = ({
onChange,
style,
}: ISearchFormProps) => {
const t = useTranslations("general");
return (
<form
style={style}
@@ -33,7 +35,7 @@ const SearchForm: React.FC<ISearchFormProps> = ({
type="text"
/>
</div>
<button type="submit">Поиск</button>
<button type="submit">{t("search")}</button>
</form>
);
};
@@ -0,0 +1,20 @@
"use client";
import Image from "next/image";
import map_pin from "./icons/map-pin.svg";
import { useReportStore } from "@/widgets/report-details/reportStore";
const ShowMapButton = () => {
const { setShowMap, showMap } = useReportStore();
return (
<button
onClick={() => setShowMap(!showMap)}
className="report-information__show-map"
>
<Image src={map_pin} alt="Map Pin Icon" />
Показать на карте
</button>
);
};
export default ShowMapButton;
+11
View File
@@ -0,0 +1,11 @@
import { notFound } from "next/navigation";
import { getRequestConfig } from "next-intl/server";
import { locales } from "@/shared/config/navigation";
export default getRequestConfig(async ({ locale }) => {
if (!locales.includes(locale as any)) notFound();
return {
messages: (await import(`../messages/${locale}.json`)).default,
};
});
+48 -6
View File
@@ -1,9 +1,51 @@
export { default } from "next-auth/middleware";
import { withAuth } from "next-auth/middleware";
import createIntlMiddleware from "next-intl/middleware";
import { NextRequest } from "next/server";
import { locales, localePrefix } from "./shared/config/navigation";
const privatePages = [
"/profile",
"/profile/personal",
"/profile/my-reports",
"/create-report",
];
const intlMiddleware = createIntlMiddleware({
locales,
localePrefix,
defaultLocale: "ru",
});
const authMiddleware = withAuth(
function onSuccess(req) {
return intlMiddleware(req);
},
{
callbacks: {
authorized: ({ token }) => token != null,
},
pages: {
signIn: "/sign-in",
},
}
);
export default function middleware(req: NextRequest) {
const publicPathnameRegex = RegExp(
`^(/(${locales.join("|")}))?(${privatePages
.flatMap((p) => (p === "/" ? ["", "/"] : p))
.join("|")})/?$`,
"i"
);
const isPublicPage = publicPathnameRegex.test(req.nextUrl.pathname);
if (!isPublicPage) {
return intlMiddleware(req);
} else {
return (authMiddleware as any)(req);
}
}
export const config = {
matcher: [
"/profile/personal",
"/profile/my-reports",
"/create-report",
],
matcher: ["/((?!api|_next|.*\\..*).*)"],
};
+10
View File
@@ -5,3 +5,13 @@ const API_URL = process.env["NEXT_PUBLIC_BASE_API"];
export const apiInstance = axios.create({
baseURL: API_URL,
});
export const authInstanse = (access_token: string) => {
return axios.create({
baseURL: API_URL,
headers: {
Authorization: `Bearer ${access_token}`,
},
method: "",
});
};
+88 -27
View File
@@ -2,24 +2,32 @@ import axios from "axios";
import { AuthOptions } from "next-auth";
import { JWT } from "next-auth/jwt";
import CredentialsProvider from "next-auth/providers/credentials";
interface IToken {
access: string;
}
import { apiInstance } from "./apiConfig";
import { IRefresh, ITokens } from "../types/token-type";
import GoogleProvider from "next-auth/providers/google";
const refreshToken = async (token: JWT): Promise<JWT> => {
const UTC = new Date();
const data = {
refresh: token.refresh_token,
};
const response = await axios.post<IToken>(
"https://api.kgroad.org/api/v1/token/refresh/",
const response = await apiInstance.post<IRefresh>(
"/users/refresh/",
data
);
const expirationTime = new Date(UTC.getTime() + 14 * 60000);
expirationTime.setTime(
expirationTime.getTime() +
expirationTime.getTimezoneOffset() * 60 * 1000 * -1
);
return {
...token,
access_token: response.data.access,
expires_in: expirationTime,
};
};
@@ -27,7 +35,6 @@ export const authConfig: AuthOptions = {
providers: [
CredentialsProvider({
name: "Credentials",
credentials: {
email: {
label: "Email",
@@ -36,35 +43,47 @@ export const authConfig: AuthOptions = {
},
password: { label: "Password", type: "password" },
},
async authorize(credentials, req) {
async authorize(credentials, req): Promise<any> {
if (!credentials?.email || !credentials?.password)
return null;
const { email, password } = credentials as any;
const { email, password } = credentials;
const data = {
email,
password,
};
const res = await fetch(
"https://api.kgroad.org/api/v1/users/login/",
{
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
email,
password,
}),
}
const res = await apiInstance.post<ITokens>(
"/users/login/",
data
);
if (res.status.toString()[0] === "4") {
return null;
if ([200, 201].includes(res.status)) {
const currentTime = new Date();
const expirationTime = new Date(
currentTime.getTime() + 14 * 60000
);
expirationTime.setTime(
expirationTime.getTime() +
expirationTime.getTimezoneOffset() * 60 * 1000 * -1
);
const user = {
refresh_token: res.data.refresh_token,
access_token: res.data.access_token,
expires_in: expirationTime,
};
return user;
}
const user = await res.json();
return user;
return null;
},
}),
GoogleProvider({
clientId: process.env.CLIENT_ID as string,
clientSecret: process.env.CLIENT_SECRET as string,
}),
],
pages: {
signIn: "/sign-in",
@@ -73,15 +92,57 @@ export const authConfig: AuthOptions = {
strategy: "jwt",
},
callbacks: {
async signIn({ account, user }) {
if (account?.provider === "google") {
const data = {
auth_token: account?.id_token,
};
const res = await apiInstance.post<ITokens>(
"/users/google/",
data
);
if (![200, 201].includes(res.status)) {
return false;
}
const currentTime = new Date();
const expirationTime = new Date(
currentTime.getTime() + 15 * 60000
);
user.access_token = res.data.access_token;
user.refresh_token = res.data.refresh_token;
user.expires_in = expirationTime;
}
return true;
},
async jwt({ token, user }) {
if (user) return { ...token, ...user };
return refreshToken(token);
const UTC = new Date();
const currentTime = new Date(UTC.getTime());
currentTime.setTime(
currentTime.getTime() +
currentTime.getTimezoneOffset() * 60 * 1000 * -1
);
const isValid =
new Date(currentTime).getTime() <=
new Date(token.expires_in).getTime();
if (isValid) return token;
return await refreshToken(token);
},
async session({ token, session }) {
session.access_token = token.access_token;
session.refresh_token = token.refresh_token;
session.expires_in = token.expires_in;
return session;
},
+7
View File
@@ -0,0 +1,7 @@
import { createSharedPathnamesNavigation } from "next-intl/navigation";
export const locales = ["en", "ru", "kg"] as const;
export const localePrefix = "always"; // Default
export const { Link, redirect, usePathname, useRouter } =
createSharedPathnamesNavigation({ locales, localePrefix });
+1 -1
View File
@@ -3,5 +3,5 @@
@import "./TildaSans-Medium/TildaSans-Medium.css";
@import "./TildaSans-Semibold/TildaSans-Semibold.css";
@import "./TildaSans-Bold/TildaSans-Bold.css";
@import "./TildaSans-Extrabold/TildaSans-Extrabold.css";
@import "./TildaSans-ExtraBold/TildaSans-ExtraBold.css";
@import "./TildaSans-Black/TildaSans-Black.css";
+1 -1
View File
@@ -1,5 +1,5 @@
export interface IList {
count: number | null;
count: number;
previous: string | null;
next: string | null;
}
+1 -1
View File
@@ -1,5 +1,5 @@
export interface ILocation {
id: 4;
id: number;
latitude: string;
longitude: string;
address: string;
+8
View File
@@ -0,0 +1,8 @@
export interface IMetatag {
id: number;
title: string;
description: string;
keywords: string;
og_image?: string;
page: string;
}
+14
View File
@@ -15,3 +15,17 @@ export interface IReport {
total_likes: number;
count_reviews: number;
}
export interface IRatingReport {
created_at: string;
category: null;
author: {
id: number;
first_name: string;
last_name: string;
govern_status: null;
};
total_likes: number;
count_reviews: number;
address: string;
}
+4
View File
@@ -12,3 +12,7 @@ export interface IReview {
review: string;
created_at: string;
}
export interface IReviewList extends IList {
results: IReview[];
}
+5
View File
@@ -3,3 +3,8 @@ export interface ITokens {
access_token: string;
expires_in: string;
}
export interface IRefresh {
access: string;
expires_at: string;
}
+2 -3
View File
@@ -29,8 +29,8 @@
}
.auth-icon2 {
width: 60px;
height: 60px;
min-width: 60px;
min-height: 60px;
display: flex;
align-items: center;
justify-content: center;
@@ -90,7 +90,6 @@
@media screen and (max-width: 550px) {
.auth-page {
padding: 48px 16px;
min-height: 600px;
}
.auth-wrapper {
+11 -2
View File
@@ -1,7 +1,16 @@
import "./Loader.scss";
const Loader = () => {
return <span className="loader"></span>;
interface ILoader {
color?: string;
}
const Loader: React.FC<ILoader> = ({ color }: ILoader) => {
return (
<span
className="loader"
style={{ borderTop: `3px solid ${color}` }}
></span>
);
};
export default Loader;
+23 -7
View File
@@ -1,7 +1,23 @@
export const LINKS = [
{ id: 1, pagename: "Главная", pathname: "/" },
{ id: 2, pagename: "О нас", pathname: "/about-us" },
{ id: 3, pagename: "Статистика", pathname: "/statistics" },
{ id: 4, pagename: "Новости", pathname: "/news" },
{ id: 5, pagename: "Волонтеры", pathname: "/volunteers" },
];
import { useTranslations } from "next-intl";
export const LINKS = () => {
const t = useTranslations("navigation");
const LINKS = [
{ id: 1, pagename: t("home"), pathname: "/" },
{ id: 2, pagename: t("about_us"), pathname: "/about-us" },
{ id: 3, pagename: t("statistics"), pathname: "/statistics" },
{ id: 4, pagename: t("news"), pathname: "/news" },
{ id: 5, pagename: t("volunteers"), pathname: "/volunteers" },
];
return LINKS;
};
// export const LINKS = [
// { id: 1, pagename: "Главная", pathname: "/" },
// { id: 2, pagename: "О нас", pathname: "/about-us" },
// { id: 3, pagename: "Статистика", pathname: "/statistics" },
// { id: 4, pagename: "Новости", pathname: "/news" },
// { id: 5, pagename: "Волонтеры", pathname: "/volunteers" },
// ];
+14
View File
@@ -0,0 +1,14 @@
export const MONTHS: Record<string, string> = {
"01": "Январь",
"02": "Февраль",
"03": "Март",
"04": "Апрель",
"05": "Май",
"06": "Июнь",
"07": "Июль",
"08": "Август",
"09": "Сентябрь",
"10": "Октябрь",
"11": "Ноябрь",
"12": "Декабрь",
};
+1 -17
View File
@@ -1,20 +1,4 @@
export const ROAD_TYPES_STATS: Record<number, string> = {
1: "Разбитых дорог",
2: "Очагов аварийности",
3: "Локальных дефектов",
4: "В планах ремонта",
5: "Отремонтировано",
6: "Локальных дефектов исправлено",
};
export const ROAD_TYPES: Record<number, string> = {
1: "Разбитая дорога",
2: "Очаг аварийности",
3: "Локальный дефект",
4: "В планах ремонта",
5: "Отремонтировано",
6: "Локальный дефект исправлен",
};
import { getTranslations } from "next-intl/server";
export const ROAD_TYPES_COLORS: Record<number, string> = {
1: "rgb(230, 68, 82)",
+13
View File
@@ -4,6 +4,19 @@
grid-template-columns: 1fr 1fr 1fr 1fr;
gap: 30px;
background-color: rgb(15, 23, 42);
&__logo {
display: flex;
flex-direction: column;
gap: 24px;
p {
color: white;
font-family: "Inter", sans-serif;
font-size: 16px;
}
}
a,
li,
h4 {
+19 -13
View File
@@ -1,6 +1,5 @@
import "./Footer.scss";
import logo from "../../shared/assets/logo.svg";
import Link from "next/link";
import Image from "next/image";
import { LINKS } from "@/shared/variables/links";
import youtube from "./icons/youtube.svg";
@@ -8,18 +7,27 @@ import facebook from "./icons/facebook.svg";
import instagram from "./icons/instagram.svg";
import app_store_btn from "./icons/app-store-btn.svg";
import play_market_btn from "./icons/play-market-btn.svg";
import { Link } from "@/shared/config/navigation";
import { useTranslations } from "next-intl";
import NetKgTracker from "@/widgets/NetKgTracker/NetKgTracker";
const Footer = () => {
const t = useTranslations("general");
const tDisclaimer = useTranslations("disclaimer");
const tRights = useTranslations("rights");
return (
<footer className="footer">
<Link href="/">
<Image src={logo} alt="Logo" />
</Link>
<div className="footer__logo">
<Link href="/">
<Image src={logo} alt="Logo" />
</Link>
<p>© {tRights("text")}</p>
<p>{tDisclaimer("text")}</p>
</div>
<div className="footer__links">
<h4>Навигация</h4>
<h4>{t("navigation")}</h4>
<ul>
{LINKS.map((link) => (
{LINKS().map((link) => (
<li key={link.id}>
<Link href={link.pathname} key={link.id}>
{link.pagename}
@@ -31,11 +39,11 @@ const Footer = () => {
</div>
<div className="footer__contacts">
<h4>Контакты</h4>
<h4>{t("contacts")}</h4>
<ul>
<li>admin@kgroad.org</li>
<li>+9960312394038</li>
{/*
<li>
{[youtube, facebook, instagram].map((net, i) => (
<Link key={i} href="#">
@@ -43,13 +51,12 @@ const Footer = () => {
</Link>
))}
</li>
*/}
<li>Photo By ThomasG, CC BY-SA 3.0</li>
</ul>
<p className="text-white">Photo By ThomasG, CC BY-SA 3.0</p>
</div>
{/*
<div className="footer__apps">
<h4>Скачивай наше приложение</h4>
<h4>{t("download_our_app")}</h4>
<div className="footer__apps-btns">
{[app_store_btn, play_market_btn].map((app, i) => (
<Link key={i} href="#">
@@ -58,7 +65,6 @@ const Footer = () => {
))}
</div>
</div>
*/}
</footer>
);
};
+7 -3
View File
@@ -1,7 +1,8 @@
import Link from "next/link";
import "./NavAuth.scss";
import { usePathname } from "next/navigation";
import { useSession } from "next-auth/react";
import { Link } from "@/shared/config/navigation";
import { useTranslations } from "next-intl";
interface INavAuthProps {
responsible?: boolean;
@@ -12,8 +13,11 @@ const NavAuth: React.FC<INavAuthProps> = ({
responsible,
setOpenMenu,
}: INavAuthProps) => {
const t = useTranslations("navigation");
const session = useSession();
const auth = session.status === "authenticated" ? true : false;
const pathname = usePathname();
return (
<>
@@ -27,7 +31,7 @@ const NavAuth: React.FC<INavAuthProps> = ({
: "lg"
}`}
>
Профиль
{t("profile")}
</Link>
) : (
<Link
@@ -39,7 +43,7 @@ const NavAuth: React.FC<INavAuthProps> = ({
: "lg"
}`}
>
Войти
{t("login")}
</Link>
)}
</>
+44 -9
View File
@@ -1,20 +1,52 @@
"use client";
import "./NavLanguage.scss";
import Image from "next/image";
import { useState } from "react";
import { useEffect, useRef, useState, useTransition } from "react";
import globus from "./icons/globus.svg";
import chevron from "./icons/chevron-down.svg";
import check from "./icons/check.svg";
import { useParams, useRouter } from "next/navigation";
import { LANGUAGES } from "./variables";
import { usePathname } from "@/shared/config/navigation";
const NavLanguage = () => {
const [language, setLanguage] = useState<string>("ru");
const [openMenu, setOpenMenu] = useState<boolean>(false);
const LANGUAGES = [
{ id: 1, language: "Русский", index: "ru" },
{ id: 2, language: "Кыргызча", index: "kg" },
{ id: 3, language: "English", index: "en" },
];
const menuRef: React.RefObject<HTMLDivElement> = useRef(null);
const router = useRouter();
const pathname = usePathname();
const { locale } = useParams();
const [_, startTransition] = useTransition();
const [language, setLanguage] = useState<string>(locale as string);
useEffect(() => {
const params = window.location.search || "";
startTransition(() => {
router.replace(`/${language}${pathname}${params}`, {
scroll: false,
});
});
}, [language]);
useEffect(() => {
function handleClickOutside(event: any) {
if (
menuRef.current &&
!menuRef.current.contains(event.target)
) {
setOpenMenu(false);
}
}
document.addEventListener("mousedown", handleClickOutside);
return () => {
document.removeEventListener("mousedown", handleClickOutside);
};
}, [menuRef]);
return (
<div className="nav-language">
<div ref={menuRef} className="nav-language">
<button
onClick={() => setOpenMenu((prev) => !prev)}
className="nav-language__btn"
@@ -29,7 +61,10 @@ const NavLanguage = () => {
className={`nav-language__option${
language === lang.index ? "_active" : ""
}`}
onClick={() => setLanguage(lang.index)}
onClick={() => {
setLanguage(lang.index);
setOpenMenu(false);
}}
key={lang.id}
>
{lang.language}
@@ -0,0 +1,11 @@
interface ILangs {
id: number;
language: string;
index: string;
}
export const LANGUAGES: ILangs[] = [
{ id: 1, language: "Русский", index: "ru" },
{ id: 2, language: "Кыргызча", index: "kg" },
{ id: 3, language: "English", index: "en" },
];
+5 -5
View File
@@ -1,8 +1,7 @@
import { LINKS } from "@/shared/variables/links";
import "./NavMenu.scss";
import Link from "next/link";
import { usePathname } from "next/navigation";
import NavAuth from "../NavAuth/NavAuth";
import { Link, usePathname } from "@/shared/config/navigation";
interface INavMenuProps {
setOpenMenu: (boolean: boolean) => void;
@@ -12,15 +11,16 @@ const NavMenu: React.FC<INavMenuProps> = ({
setOpenMenu,
}: INavMenuProps) => {
const pathname = usePathname();
return (
<nav className="nav-menu">
{LINKS.map((link) => (
{LINKS().map((link) => (
<Link
onClick={() => setOpenMenu(false)}
className={`nav-menu__link${
pathname === link.pathname ? "_active" : ""
pathname.slice(4) === link.pathname ? "_active" : ""
}`}
href={link.pathname}
href={`${pathname.slice(1, 3)}/${link.pathname}`}
key={link.id}
>
{link.pagename}
+23 -1
View File
@@ -11,6 +11,18 @@
align-items: center;
background-color: #fff;
&__logo {
display: flex;
align-items: center;
gap: 10px;
&_last {
min-width: 78px;
max-width: 78px;
height: 100%;
}
}
&__links {
height: 40%;
display: flex;
@@ -46,6 +58,16 @@
}
}
@media screen and (max-width: 1220px) {
.navbar {
padding: 0 60px;
&__links {
gap: 40px;
}
}
}
@media screen and (max-width: 1024px) {
.navbar {
padding: 0 30px;
@@ -60,7 +82,7 @@
}
}
@media screen and (max-width: 768px) {
@media screen and (max-width: 900px) {
.navbar {
height: 72px;
+25 -8
View File
@@ -1,31 +1,48 @@
"use client";
import "./Navbar.scss";
import Image from "next/image";
import Link from "next/link";
import { usePathname } from "next/navigation";
import Image, { StaticImageData } from "next/image";
import { Link } from "@/shared/config/navigation";
import { usePathname } from "@/shared/config/navigation";
import logo from "@/shared/assets/logo.svg";
import { LINKS } from "@/shared/variables/links";
import NavLanguage from "./NavLanguage/NavLanguage";
import NavAuth from "./NavAuth/NavAuth";
import menu from "./icons/menu.svg";
import cross from "./icons/cross.svg";
import NavMenu from "./NavMenu/NavMenu";
import { useState } from "react";
import founded_ru from "./assets/founded-ru.png";
import founded_en from "./assets/founded-en.png";
import founded_kg from "./assets/founded-kg.png";
import { useParams } from "next/navigation";
const Navbar = () => {
const pathname = usePathname();
const [openMenu, setOpenMenu] = useState<boolean>(false);
const { locale } = useParams();
const FOUNDED: Record<string, StaticImageData> = {
ru: founded_ru,
kg: founded_kg,
en: founded_en,
};
return (
<section className="navbar">
<Link href="/">
<Image src={logo} alt="Logo" />
</Link>
<div className="navbar__logo">
<Link href="/">
<Image src={logo} alt="Logo" />
</Link>
<Image
className="navbar__logo_last"
src={FOUNDED[locale as string]}
alt="Founded by EU Image"
/>
</div>
<nav className="navbar__links">
{LINKS.map((link) => (
{LINKS().map((link) => (
<Link
className={`navbar__link${
pathname === link.pathname ? "_active" : ""
Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 74 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

+51
View File
@@ -0,0 +1,51 @@
.not-found {
padding: 16px;
margin-bottom: 200px;
display: flex;
flex-direction: column;
align-items: center;
img {
width: 100%;
height: 497px;
aspect-ratio: 1 / 1;
}
div {
display: flex;
flex-direction: column;
align-items: center;
gap: 16px;
text-align: center;
h2 {
color: rgb(11, 31, 51);
font-size: 20px;
font-weight: 800;
line-height: 24px;
}
p {
color: rgb(131, 130, 130);
font-size: 20px;
font-weight: 400;
line-height: 24px;
}
}
a {
max-width: 320px;
width: 100%;
height: 55px;
margin-top: 62px;
display: flex;
align-items: center;
justify-content: center;
color: rgb(255, 255, 255);
font-size: 20px;
font-weight: 800;
line-height: 24px;
border-radius: 13px;
background-color: rgb(57, 152, 232);
}
}
+21
View File
@@ -0,0 +1,21 @@
import Link from "next/link";
import not_found from "./assets/not-found.svg";
import "./NotFound.scss";
const NotFound = () => {
return (
<div className="not-found">
<img src={not_found.src} alt="Not Found Image" />
<div>
<h2>Страница не найдена (404)</h2>
<p>
Неправильно набран адрес или такой страницы не существует.
</p>
</div>
<Link href="/">На главную</Link>
</div>
);
};
export default NotFound;
+16
View File
@@ -0,0 +1,16 @@
<svg width="560" height="497" viewBox="0 0 560 497" fill="none" xmlns="http://www.w3.org/2000/svg">
<circle cx="294.5" cy="261.5" r="175.5" fill="#F0F5FE"/>
<circle cx="130" cy="143" r="11" fill="#F0F5FE"/>
<circle cx="414.5" cy="99.5" r="20.5" fill="#F0F5FE"/>
<circle cx="133.5" cy="374.5" r="19.5" fill="#F0F5FE"/>
<circle cx="482.5" cy="355.5" r="19.5" fill="#F0F5FE"/>
<circle cx="444.5" cy="377.5" r="9.5" fill="#F0F5FE"/>
<path d="M270.308 296.62L268.543 297.188L269.958 298.242L282.135 307.321L282.142 307.327L282.15 307.333C289.224 312.512 291.962 316.136 291.5 318.444L291.496 318.462L291.494 318.48C291.189 320.741 288.071 323.024 281.218 325.226C276.575 326.719 272.166 326.931 267.861 326.137C263.431 325.319 258.568 323.336 253.307 319.895C251.495 318.711 250.274 317.819 249.57 317.198L249.527 317.16L249.481 317.124L238.595 308.611L236.965 307.336L234.857 308.014C207.222 316.895 186.182 323.575 171.733 328.056L171.72 328.06L171.708 328.064C168.182 329.197 163.583 329.328 157.571 328.279C151.698 327.254 146.234 325.164 141.179 321.858C138.205 319.913 135.658 317.64 133.564 315.023C120.573 298.391 108.813 278.479 98.2963 255.277C96.0874 250.353 95.6916 246.72 96.8513 244.267L96.8536 244.262L96.8558 244.257C97.9905 241.751 102.074 239.23 109.718 236.773C113.66 235.506 117.053 235.188 119.95 235.512L120.005 235.518L120.059 235.523C123.249 235.813 125.855 236.635 128.053 238.073C130.558 239.711 132.283 241.652 133.154 243.94L133.161 243.96L133.17 243.979C144.494 269.62 155.587 289.754 166.462 304.349L167.779 306.117L170.498 305.243L214.526 291.093L216.2 290.555L214.955 289.532C201.712 278.649 190.082 267.989 180.059 257.551C176.547 253.829 175.268 250.725 175.939 248.172L175.943 248.154L175.947 248.136C176.453 245.557 179.359 243.318 185.119 241.466C189.749 239.978 193.749 239.554 197.169 239.894C200.494 240.224 203.822 241.333 207.18 243.53C209.305 244.919 210.607 245.956 211.249 246.676L211.286 246.719L211.33 246.761C222.166 257.202 233.971 267.902 246.742 278.861L248.334 280.228L250.55 279.516L260.842 276.208C265.669 274.657 269.84 274.2 273.404 274.54C276.863 274.871 280.293 276.006 283.727 278.252C289.486 282.018 291.206 284.911 290.016 287.15C288.736 289.559 284.38 292.098 276.312 294.69L270.308 296.62Z" fill="#CAE1FB" stroke="#CAE1FB" stroke-width="4"/>
<path d="M469.021 231.365L467.255 231.932L468.67 232.987L476.197 238.599L476.205 238.605L476.213 238.611C480.563 241.796 482.045 243.888 481.796 245.13L481.793 245.148L481.79 245.166C481.626 246.383 479.925 247.712 475.765 249.049C473.05 249.921 470.607 250.009 468.307 249.584C465.88 249.136 463.058 248.018 459.877 245.938C458.768 245.213 458.055 244.689 457.661 244.342L457.619 244.304L457.572 244.268L450.843 239.005L449.213 237.73L447.105 238.408C430.022 243.898 417.019 248.026 408.092 250.795L408.079 250.799L408.066 250.803C406.142 251.421 403.602 251.515 400.107 250.905C396.75 250.32 393.568 249.118 390.56 247.151C388.801 246 387.279 244.646 386.019 243.072C378.015 232.824 370.76 220.543 364.267 206.218C362.914 203.203 362.703 201.039 363.376 199.616L363.378 199.612L363.38 199.607C364.019 198.195 366.363 196.702 371.023 195.204C373.364 194.451 375.236 194.309 376.693 194.472L376.748 194.478L376.802 194.483C378.479 194.636 379.818 195.055 380.992 195.823C382.409 196.75 383.41 197.863 383.921 199.206L383.929 199.225L383.937 199.245C390.942 215.105 397.812 227.579 404.561 236.638L405.878 238.405L408.597 237.531L435.815 228.784L437.489 228.246L436.244 227.222C428.077 220.511 420.907 213.94 414.731 207.507C412.619 205.269 411.898 203.453 412.282 201.989L412.287 201.972L412.29 201.954C412.574 200.507 414.204 199.208 417.635 198.105C420.41 197.214 422.666 197.006 424.455 197.184C426.148 197.352 427.957 197.92 429.908 199.196C431.2 200.041 431.936 200.637 432.275 201.018L432.313 201.06L432.356 201.102C439.065 207.566 446.372 214.189 454.276 220.972L455.868 222.339L458.084 221.627L464.447 219.582C467.345 218.65 469.71 218.423 471.59 218.603C473.366 218.772 475.233 219.357 477.228 220.661C480.684 222.922 481.532 224.535 480.902 225.72C480.182 227.076 477.67 228.585 472.732 230.172L469.021 231.365Z" fill="#CAE1FB" stroke="#CAE1FB" stroke-width="4"/>
<path d="M337.384 222.018C353.059 229.83 365.498 240.199 372.969 250.555C380.501 260.998 382.678 270.934 379.013 278.288C375.347 285.643 366.103 289.887 353.231 290.16C340.464 290.43 324.696 286.741 309.02 278.929C293.344 271.116 280.906 260.747 273.435 250.391C265.903 239.949 263.726 230.013 267.391 222.658C271.057 215.304 280.301 211.06 293.173 210.787C305.94 210.517 321.708 214.205 337.384 222.018Z" fill="#DDEBFF" stroke="#CAE1FB" stroke-width="4"/>
<path d="M261.097 188.743L293 268.5L298.5 272.5L307 277.5L314.5 280.5L278.494 187.279C278.196 186.508 277.455 186 276.628 186L262.954 186C261.539 186 260.572 187.429 261.097 188.743Z" fill="#DDEBFF" stroke="#CAE1FB" stroke-width="4"/>
<path d="M296.897 171.111L344.285 289.463C344.418 289.795 344.718 290.031 345.072 290.082L347.93 290.49C347.976 290.497 348.024 290.5 348.071 290.5L355 290.5L358.923 290.009C358.974 290.003 359.025 289.993 359.074 289.979L362.5 289L365.084 287.893C365.59 287.676 365.826 287.09 365.61 286.582L315.864 169.519C315.7 169.133 315.313 168.891 314.895 168.911L297.777 169.741C297.09 169.774 296.641 170.473 296.897 171.111Z" fill="#DDEBFF" stroke="#CAE1FB" stroke-width="4"/>
<path d="M286.168 201.956L313.171 193.293C314.673 192.811 316.282 193.638 316.764 195.141C317.246 196.643 316.419 198.252 314.916 198.734L287.914 207.398L286.168 201.956Z" fill="#CAE1FB" stroke="#CAE1FB" stroke-width="4"/>
<path d="M297.429 230.854L323.957 220.829C325.433 220.271 327.082 221.016 327.64 222.492C328.198 223.968 327.453 225.617 325.977 226.175L299.45 236.2L297.429 230.854Z" fill="#CAE1FB" stroke="#CAE1FB" stroke-width="4"/>
<path d="M309.081 258.52L332.472 248.859C333.931 248.256 335.602 248.95 336.204 250.409C336.807 251.868 336.112 253.538 334.654 254.141L311.262 263.802L309.081 258.52Z" fill="#CAE1FB" stroke="#CAE1FB" stroke-width="4"/>
</svg>

After

Width:  |  Height:  |  Size: 5.9 KiB

+1 -2
View File
@@ -2,8 +2,7 @@
import "./ProfileNav.scss";
import LogoutButton from "@/features/LogoutButton/LogoutButton";
import Link from "next/link";
import { usePathname } from "next/navigation";
import { Link, usePathname } from "@/shared/config/navigation";
interface IProfileNavProps {
report_count: number;
@@ -1,106 +0,0 @@
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 });
}
},
})
);
@@ -1,100 +0,0 @@
import { apiInstance } from "@/shared/config/apiConfig";
import { IFetch } from "@/shared/types/fetch-type";
import { IList } from "@/shared/types/list-type";
import { IReport } from "@/shared/types/report-type";
import { create } from "zustand";
interface IFetchReports extends IList {
results: IReport[];
}
interface IRatingStore extends IFetch {
data: IFetchReports;
getReports: (
categories: string,
page: number,
filter: { option: string; toggle: boolean }
) => Promise<void>;
}
export const useRatingStore = create<IRatingStore>((set) => ({
data: {
count: 0,
previous: null,
next: null,
results: [],
},
isLoading: false,
error: "",
getReports: async (
query: string = "",
page: number = 1,
filter: { option: string; toggle: boolean }
) => {
try {
set({ isLoading: true });
const data = (
await apiInstance.get<IFetchReports>(
`/report/?page=${page}&page_size=${8}`
)
).data;
const searched = data.results.filter((rating) => {
return rating.location.some((location) => {
return location.address
.toLowerCase()
.includes(query.toLowerCase());
});
});
data.results = [...searched];
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 === "reviews" && filter.toggle === false) {
data.results = data.results.sort(
(a, b) => a.count_reviews - b.count_reviews
);
} else if (
filter.option === "reviews" &&
filter.toggle === true
) {
data.results = data.results.sort(
(a, b) => b.count_reviews - a.count_reviews
);
}
if (filter.option === "rating" && filter.toggle === false) {
data.results = data.results.sort(
(a, b) => a.total_likes - b.total_likes
);
} else if (
filter.option === "rating" &&
filter.toggle === true
) {
data.results = data.results.sort(
(a, b) => b.total_likes - a.total_likes
);
}
set({ data: data });
} catch (error: any) {
set({ error: error.message });
} finally {
set({ isLoading: false });
}
},
}));
Loaded 100 of 204 files, more files were not shown because too many files have changed in this diff. Show more