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
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
@@ -10,7 +10,7 @@ export const metadata: Metadata = {
|
||||
};
|
||||
|
||||
const DynamicForm = dynamic(
|
||||
() => import("@/widgets/ReportForm/ReportForm"),
|
||||
() => import("@/widgets/forms/ReportForm/ReportForm"),
|
||||
{
|
||||
ssr: false,
|
||||
}
|
||||
File renamed without changes.
@@ -0,0 +1,34 @@
|
||||
import type { Metadata } from "next";
|
||||
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>
|
||||
);
|
||||
}
|
||||
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
@@ -1,7 +1,5 @@
|
||||
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";
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
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 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";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
@@ -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,7 +6,6 @@ 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";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
@@ -1,4 +1,4 @@
|
||||
import ProfileTable from "@/widgets/ProfileTable/ProfileTable";
|
||||
import ProfileTable from "@/widgets/tables/ProfileTable/ProfileTable";
|
||||
import React from "react";
|
||||
|
||||
const MyReports = async ({
|
||||
@@ -1,5 +1,3 @@
|
||||
import React from "react";
|
||||
|
||||
const page = () => {
|
||||
return <div>page</div>;
|
||||
};
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
File renamed without changes.
@@ -0,0 +1,66 @@
|
||||
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";
|
||||
|
||||
const DynamicMap = dynamic(
|
||||
() => import("@/widgets/report-details/ReportMap/ReportMap"),
|
||||
{
|
||||
ssr: false,
|
||||
}
|
||||
);
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "KG ROAD | Обращение",
|
||||
description:
|
||||
"Страница обращения Kyrgyzstan Transperency International",
|
||||
};
|
||||
|
||||
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();
|
||||
|
||||
return (
|
||||
<div className="report-details page-padding">
|
||||
<BreadCrumbs />
|
||||
<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;
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
import "@/shared/ui/auth-classes.scss";
|
||||
import ForgotPasswordForm from "@/widgets/ForgotPasswordForm/ForgotPasswordForm";
|
||||
import ForgotPasswordForm from "@/widgets/forms/ForgotPasswordForm/ForgotPasswordForm";
|
||||
|
||||
const ForgotPassword = () => {
|
||||
return (
|
||||
File renamed without changes.
@@ -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 | Вход",
|
||||
File renamed without changes.
@@ -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 (
|
||||
File renamed without changes.
+1
-1
@@ -1,7 +1,7 @@
|
||||
import "@/shared/ui/auth-classes.scss";
|
||||
import Image from "next/image";
|
||||
import mail from "./icons/mail.svg";
|
||||
import ConfirmEmailForm from "@/widgets/ConfirmEmailForm/ConfirmEmailForm";
|
||||
import ConfirmEmailForm from "@/widgets/forms/ConfirmEmailForm/ConfirmEmailForm";
|
||||
|
||||
const ConfirmEmail = ({
|
||||
searchParams,
|
||||
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 | Регистрация",
|
||||
File renamed without changes.
@@ -1,9 +1,6 @@
|
||||
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 StatisticsTable from "@/widgets/tables/StatisticsTable/StatisticsTable";
|
||||
import { Metadata } from "next";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
File renamed without changes.
@@ -1,6 +1,6 @@
|
||||
import Typography from "@/shared/ui/components/Typography/Typography";
|
||||
import "./Volunteers.scss";
|
||||
import VolunteersTable from "@/widgets/VolunteersTable/VolunteersTable";
|
||||
import VolunteersTable from "@/widgets/tables/VolunteersTable/VolunteersTable";
|
||||
import { Metadata } from "next";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
+7
-23
@@ -1,25 +1,9 @@
|
||||
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 { ReactNode } from "react";
|
||||
|
||||
export default function RootLayout({
|
||||
children,
|
||||
}: Readonly<{
|
||||
children: React.ReactNode;
|
||||
}>) {
|
||||
return (
|
||||
<html lang="en">
|
||||
<body>
|
||||
<Providers>
|
||||
<Navbar />
|
||||
<div className="app">{children}</div>
|
||||
<Footer />
|
||||
</Providers>
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
type Props = {
|
||||
children: ReactNode;
|
||||
};
|
||||
|
||||
export default function RootLayout({ children }: Props) {
|
||||
return children;
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
"use client";
|
||||
|
||||
import Error from "next/error";
|
||||
|
||||
export default function NotFound() {
|
||||
return (
|
||||
<html lang="en">
|
||||
<body>
|
||||
<Error statusCode={404} />
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
@@ -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:
|
||||
"Страница обращения Kyrgyzstan Transperency International",
|
||||
};
|
||||
|
||||
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;
|
||||
@@ -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)}...`;
|
||||
@@ -57,7 +59,7 @@ const NewsCard: React.FC<INewsCard> = ({
|
||||
</div>
|
||||
|
||||
<Link href={`/news/${id}`} className="news-card__more-btn">
|
||||
Подробнее
|
||||
{t("details")}
|
||||
</Link>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
.breadcrumbs {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
"use client";
|
||||
|
||||
import "./BreadCrumbs.scss";
|
||||
|
||||
const BreadCrumbs = () => {
|
||||
return <div className="breadcrumbs"></div>;
|
||||
};
|
||||
|
||||
export default BreadCrumbs;
|
||||
@@ -1,10 +1,27 @@
|
||||
"use client";
|
||||
|
||||
import Image from "next/image";
|
||||
import "./GoogleButton.scss";
|
||||
import google from "./icons/google.svg";
|
||||
import { useSearchParams } from "next/navigation";
|
||||
import { signIn, useSession } from "next-auth/react";
|
||||
|
||||
const GoogleButton = () => {
|
||||
const session = useSession();
|
||||
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>
|
||||
|
||||
@@ -3,9 +3,9 @@
|
||||
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 { authInstanse } from "@/shared/config/apiConfig";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useSession } from "next-auth/react";
|
||||
|
||||
interface IProfileAvatarProps {
|
||||
img: string;
|
||||
@@ -19,24 +19,18 @@ const ProfileAvatar: React.FC<IProfileAvatarProps> = ({
|
||||
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]);
|
||||
}
|
||||
|
||||
if (session.status === "unauthenticated") return;
|
||||
|
||||
try {
|
||||
const res = await apiInstance.patch(
|
||||
"/users/update_image/",
|
||||
formData,
|
||||
config
|
||||
);
|
||||
const res = await authInstanse(
|
||||
session.data?.access_token as string
|
||||
).patch("/users/update_image/", formData);
|
||||
router.refresh();
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
|
||||
@@ -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,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>
|
||||
);
|
||||
};
|
||||
|
||||
File renamed without changes.
@@ -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;
|
||||
File renamed without changes.
+11
@@ -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
@@ -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|.*\\..*).*)"],
|
||||
};
|
||||
@@ -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: "",
|
||||
});
|
||||
};
|
||||
@@ -2,24 +2,45 @@ import axios from "axios";
|
||||
import { AuthOptions } from "next-auth";
|
||||
import { JWT } from "next-auth/jwt";
|
||||
import CredentialsProvider from "next-auth/providers/credentials";
|
||||
import { apiInstance } from "./apiConfig";
|
||||
import { IRefresh, ITokens } from "../types/token-type";
|
||||
import GoogleProvider from "next-auth/providers/google";
|
||||
|
||||
interface IToken {
|
||||
access: string;
|
||||
}
|
||||
const verifyToken = async (access_token: string) => {
|
||||
const res = await apiInstance.post("/token/verify/", {
|
||||
token: access_token,
|
||||
});
|
||||
|
||||
if ([200, 201].includes(res.status)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
const refreshToken = async (token: JWT): Promise<JWT> => {
|
||||
const data = {
|
||||
refresh: token.refresh_token,
|
||||
};
|
||||
// const date = new Date().toLocaleTimeString();
|
||||
// const expire = new Date(token.expires_in).toLocaleTimeString();
|
||||
|
||||
const response = await axios.post<IToken>(
|
||||
"https://api.kgroaduat.fishrungames.com/api/v1/token/refresh/",
|
||||
const verify = await verifyToken(token.access_token);
|
||||
|
||||
if (verify)
|
||||
return {
|
||||
...token,
|
||||
};
|
||||
|
||||
const response = await apiInstance.post<IRefresh>(
|
||||
"/users/refresh/",
|
||||
data
|
||||
);
|
||||
|
||||
return {
|
||||
...token,
|
||||
access_token: response.data.access,
|
||||
expires_in: response.data.expires_at,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -27,7 +48,6 @@ export const authConfig: AuthOptions = {
|
||||
providers: [
|
||||
CredentialsProvider({
|
||||
name: "Credentials",
|
||||
|
||||
credentials: {
|
||||
email: {
|
||||
label: "Email",
|
||||
@@ -36,35 +56,30 @@ export const authConfig: AuthOptions = {
|
||||
},
|
||||
password: { label: "Password", type: "password" },
|
||||
},
|
||||
|
||||
async authorize(credentials, req) {
|
||||
if (!credentials?.email || !credentials?.password)
|
||||
return null;
|
||||
|
||||
const { email, password } = credentials as any;
|
||||
const data = {
|
||||
email,
|
||||
password,
|
||||
};
|
||||
|
||||
const res = await fetch(
|
||||
"https://api.kgroaduat.fishrungames.com/api/v1/users/login/",
|
||||
{
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
email,
|
||||
password,
|
||||
}),
|
||||
}
|
||||
);
|
||||
const res = await apiInstance.post("/users/login/", data);
|
||||
|
||||
if (res.status.toString()[0] === "4") {
|
||||
if (![200, 201].includes(res.status)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const user = await res.json();
|
||||
const user = res.data;
|
||||
return user;
|
||||
},
|
||||
}),
|
||||
GoogleProvider({
|
||||
clientId: process.env.CLIENT_ID as string,
|
||||
clientSecret: process.env.CLIENT_SECRET as string,
|
||||
}),
|
||||
],
|
||||
pages: {
|
||||
signIn: "/sign-in",
|
||||
@@ -73,9 +88,39 @@ export const authConfig: AuthOptions = {
|
||||
strategy: "jwt",
|
||||
},
|
||||
callbacks: {
|
||||
async jwt({ token, user }) {
|
||||
async signIn({ account, profile, user }) {
|
||||
if (!profile?.email) {
|
||||
throw new Error("No Profile");
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
user.access_token = res.data.access_token;
|
||||
user.refresh_token = res.data.refresh_token;
|
||||
user.expires_in = res.data.expires_in;
|
||||
}
|
||||
|
||||
return true;
|
||||
},
|
||||
async jwt({ token, user, account }) {
|
||||
if (user) return { ...token, ...user };
|
||||
|
||||
if (token.exp) {
|
||||
return token;
|
||||
}
|
||||
|
||||
return refreshToken(token);
|
||||
},
|
||||
|
||||
|
||||
@@ -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,5 +1,5 @@
|
||||
export interface ILocation {
|
||||
id: 4;
|
||||
id: number;
|
||||
latitude: string;
|
||||
longitude: string;
|
||||
address: string;
|
||||
|
||||
@@ -3,3 +3,8 @@ export interface ITokens {
|
||||
access_token: string;
|
||||
expires_in: string;
|
||||
}
|
||||
|
||||
export interface IRefresh {
|
||||
access: string;
|
||||
expires_at: string;
|
||||
}
|
||||
@@ -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" },
|
||||
// ];
|
||||
@@ -0,0 +1,14 @@
|
||||
export const MONTHS: Record<string, string> = {
|
||||
"01": "Январь",
|
||||
"02": "Февраль",
|
||||
"03": "Март",
|
||||
"04": "Апрель",
|
||||
"05": "Май",
|
||||
"06": "Июнь",
|
||||
"07": "Июль",
|
||||
"08": "Август",
|
||||
"09": "Сентябрь",
|
||||
"10": "Октябрь",
|
||||
"11": "Ноябрь",
|
||||
"12": "Декабрь",
|
||||
};
|
||||
@@ -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)",
|
||||
|
||||
@@ -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,17 +7,20 @@ 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";
|
||||
|
||||
const Footer = () => {
|
||||
const t = useTranslations("general");
|
||||
return (
|
||||
<footer className="footer">
|
||||
<Link href="/">
|
||||
<Image src={logo} alt="Logo" />
|
||||
</Link>
|
||||
<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}
|
||||
@@ -29,7 +31,7 @@ const Footer = () => {
|
||||
</div>
|
||||
|
||||
<div className="footer__contacts">
|
||||
<h4>Контакты</h4>
|
||||
<h4>{t("contacts")}</h4>
|
||||
<ul>
|
||||
<li>namename@gmail.com</li>
|
||||
<li>+09646895467</li>
|
||||
@@ -44,7 +46,7 @@ const Footer = () => {
|
||||
</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="#">
|
||||
|
||||
@@ -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,6 +13,7 @@ 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();
|
||||
@@ -27,7 +29,7 @@ const NavAuth: React.FC<INavAuthProps> = ({
|
||||
: "lg"
|
||||
}`}
|
||||
>
|
||||
Профиль
|
||||
{t("profile")}
|
||||
</Link>
|
||||
) : (
|
||||
<Link
|
||||
@@ -39,7 +41,7 @@ const NavAuth: React.FC<INavAuthProps> = ({
|
||||
: "lg"
|
||||
}`}
|
||||
>
|
||||
Войти
|
||||
{t("login")}
|
||||
</Link>
|
||||
)}
|
||||
</>
|
||||
|
||||
@@ -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" },
|
||||
];
|
||||
@@ -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}
|
||||
|
||||
@@ -2,15 +2,14 @@
|
||||
|
||||
import "./Navbar.scss";
|
||||
import Image from "next/image";
|
||||
import Link from "next/link";
|
||||
import { usePathname } from "next/navigation";
|
||||
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";
|
||||
|
||||
@@ -25,7 +24,7 @@ const Navbar = () => {
|
||||
</Link>
|
||||
|
||||
<nav className="navbar__links">
|
||||
{LINKS.map((link) => (
|
||||
{LINKS().map((link) => (
|
||||
<Link
|
||||
className={`navbar__link${
|
||||
pathname === link.pathname ? "_active" : ""
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -20,6 +20,32 @@
|
||||
}
|
||||
}
|
||||
|
||||
&__auth-warning,
|
||||
&__warning {
|
||||
height: 150px;
|
||||
margin-bottom: 70px;
|
||||
text-align: center;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 4px;
|
||||
|
||||
font-size: 18px;
|
||||
font-weight: 500;
|
||||
line-height: 22px;
|
||||
|
||||
border: 1px solid #c5c6c5;
|
||||
border-radius: 10px;
|
||||
|
||||
button {
|
||||
text-decoration: underline;
|
||||
color: #0077b6;
|
||||
font-size: 18px;
|
||||
font-weight: 500;
|
||||
line-height: 22px;
|
||||
}
|
||||
}
|
||||
|
||||
form {
|
||||
margin-bottom: 70px;
|
||||
display: flex;
|
||||
|
||||
@@ -3,10 +3,15 @@
|
||||
import "./ReviewSection.scss";
|
||||
import { apiInstance } from "@/shared/config/apiConfig";
|
||||
import { IReviewList } from "@/shared/types/review-type";
|
||||
import { useSession } from "next-auth/react";
|
||||
import { signIn, useSession } from "next-auth/react";
|
||||
import { useEffect, useState } from "react";
|
||||
import calendar from "./icons/calendar.svg";
|
||||
import Image from "next/image";
|
||||
import {
|
||||
Link,
|
||||
usePathname,
|
||||
useRouter,
|
||||
} from "@/shared/config/navigation";
|
||||
|
||||
interface IReviewsSectionProps {
|
||||
endpoint: string;
|
||||
@@ -18,6 +23,7 @@ const ReviewSection: React.FC<IReviewsSectionProps> = ({
|
||||
id,
|
||||
}: IReviewsSectionProps) => {
|
||||
const [reviews, setReviews] = useState<IReviewList>();
|
||||
const pathname = usePathname();
|
||||
const session = useSession();
|
||||
const handleSubmit: React.MouseEventHandler<
|
||||
HTMLFormElement
|
||||
@@ -33,6 +39,10 @@ const ReviewSection: React.FC<IReviewsSectionProps> = ({
|
||||
},
|
||||
};
|
||||
|
||||
if (session.status === "unauthenticated") {
|
||||
signIn(undefined, { callbackUrl: pathname });
|
||||
}
|
||||
|
||||
if (!formData.get("review")) {
|
||||
return;
|
||||
}
|
||||
@@ -53,7 +63,7 @@ const ReviewSection: React.FC<IReviewsSectionProps> = ({
|
||||
|
||||
const getReviews = async () => {
|
||||
const response = await apiInstance.get<IReviewList>(
|
||||
`/${endpoint}/${id}/reviews/`
|
||||
`/${endpoint}/${id}/reviews/?page_size=8`
|
||||
);
|
||||
|
||||
setReviews(response.data);
|
||||
@@ -83,47 +93,66 @@ const ReviewSection: React.FC<IReviewsSectionProps> = ({
|
||||
<span id="blue-point" /> Написать комментарий
|
||||
</h3>
|
||||
|
||||
<form onSubmit={handleSubmit}>
|
||||
<textarea name="review" />
|
||||
<button type="submit">Отправить</button>
|
||||
</form>
|
||||
{session.status === "authenticated" ? (
|
||||
<form onSubmit={handleSubmit}>
|
||||
<textarea name="review" />
|
||||
<button type="submit">Отправить</button>
|
||||
</form>
|
||||
) : (
|
||||
<p className="review-section__auth-warning">
|
||||
Перед тем как оставить комментарий, пожалуйста
|
||||
<button
|
||||
onClick={() =>
|
||||
signIn(undefined, { callbackUrl: pathname })
|
||||
}
|
||||
>
|
||||
войдите в аккаунт
|
||||
</button>
|
||||
</p>
|
||||
)}
|
||||
|
||||
<div className="review-section__list">
|
||||
<h3>
|
||||
<span id="blue-point" /> Комментарии
|
||||
</h3>
|
||||
|
||||
<ul>
|
||||
{reviews?.results.map((review) => (
|
||||
<li key={review.id} className="review">
|
||||
<div className="review__author">
|
||||
<img
|
||||
id="author-img"
|
||||
src={review.author.image}
|
||||
alt="Author Image"
|
||||
/>
|
||||
<div className="review__header">
|
||||
<h5 className="review__author-name">
|
||||
{review.author.first_name}{" "}
|
||||
{review.author.last_name}
|
||||
</h5>
|
||||
<div className="review__date">
|
||||
<Image src={calendar} alt="Calendar Icon" />
|
||||
<p>
|
||||
{months[review.created_at.slice(5, 7)]}{" "}
|
||||
{review.created_at.slice(5, 7).slice(0, 1) ===
|
||||
"0"
|
||||
? review.created_at.slice(6, 7)
|
||||
: review.created_at.slice(5, 7)}
|
||||
, {review.created_at.slice(0, 4)}
|
||||
</p>
|
||||
{reviews?.results.length !== 0 ? (
|
||||
<ul>
|
||||
{reviews?.results.map((review) => (
|
||||
<li key={review.id} className="review">
|
||||
<div className="review__author">
|
||||
<img
|
||||
id="author-img"
|
||||
src={review.author.image}
|
||||
alt="Author Image"
|
||||
/>
|
||||
<div className="review__header">
|
||||
<h5 className="review__author-name">
|
||||
{review.author.first_name}{" "}
|
||||
{review.author.last_name}
|
||||
</h5>
|
||||
<div className="review__date">
|
||||
<Image src={calendar} alt="Calendar Icon" />
|
||||
<p>
|
||||
{months[review.created_at.slice(5, 7)]}{" "}
|
||||
{review.created_at.slice(5, 7).slice(0, 1) ===
|
||||
"0"
|
||||
? review.created_at.slice(6, 7)
|
||||
: review.created_at.slice(5, 7)}
|
||||
, {review.created_at.slice(0, 4)}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<p className="review__description">{review.review}</p>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
<p className="review__description">{review.review}</p>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
) : (
|
||||
<p className="review-section__warning">
|
||||
Оставьте комментарий первым :)
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
|
||||
File renamed without changes.
+2
-2
@@ -1,10 +1,10 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import "./ConfirmEmailForm.scss";
|
||||
import { useEffect, useState } from "react";
|
||||
import { apiInstance } from "@/shared/config/apiConfig";
|
||||
import { useRouter } from "next/navigation";
|
||||
import Loader from "@/shared/ui/components/Loader/Loader";
|
||||
import { useRouter } from "@/shared/config/navigation";
|
||||
|
||||
interface IConfirmEmailFormProps {
|
||||
email: string;
|
||||
File renamed without changes.
File renamed without changes.
File renamed without changes.
+1
-1
@@ -5,8 +5,8 @@ import "./confirm-code.scss";
|
||||
import { useState } from "react";
|
||||
import { apiInstance } from "@/shared/config/apiConfig";
|
||||
import Loader from "@/shared/ui/components/Loader/Loader";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { ITokens } from "@/shared/types/token-type";
|
||||
import { useRouter } from "@/shared/config/navigation";
|
||||
|
||||
interface IConfirmCodeProps {
|
||||
setChangeForm: (boolean: boolean) => void;
|
||||
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
+2
-4
@@ -3,16 +3,14 @@
|
||||
import "./ProfileForm.scss";
|
||||
import Image from "next/image";
|
||||
import pen from "./icons/pen.svg";
|
||||
import eye_off from "./icons/eye-off.svg";
|
||||
import eye_on from "./icons/eye-on.svg";
|
||||
import { useState } from "react";
|
||||
import { apiInstance } from "@/shared/config/apiConfig";
|
||||
import { apiInstance, authInstanse } from "@/shared/config/apiConfig";
|
||||
import { AxiosError } from "axios";
|
||||
import Loader from "@/shared/ui/components/Loader/Loader";
|
||||
import { useSession } from "next-auth/react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import LogoutButton from "@/features/LogoutButton/LogoutButton";
|
||||
import ChangePassword from "./ChangePassword/ChangePassword";
|
||||
import { useRouter } from "@/shared/config/navigation";
|
||||
|
||||
interface IProfileFormProps {
|
||||
id: number;
|
||||
File renamed without changes.
Whitespace-only changes.
File renamed without changes.
File renamed without changes.
@@ -19,8 +19,8 @@ import axios, { AxiosError } from "axios";
|
||||
import { apiInstance } from "@/shared/config/apiConfig";
|
||||
import { useSession } from "next-auth/react";
|
||||
import { IDisplayMap } from "@/shared/types/map-type";
|
||||
import { useRouter } from "next/navigation";
|
||||
import Loader from "@/shared/ui/components/Loader/Loader";
|
||||
import { useRouter } from "@/shared/config/navigation";
|
||||
|
||||
interface ILatLng {
|
||||
lat: number;
|
||||
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
+1
-1
@@ -5,8 +5,8 @@ import "./ResetCodeForm.scss";
|
||||
import { useState } from "react";
|
||||
import { apiInstance } from "@/shared/config/apiConfig";
|
||||
import { ITokens } from "@/shared/types/token-type";
|
||||
import { useRouter } from "next/navigation";
|
||||
import Loader from "@/shared/ui/components/Loader/Loader";
|
||||
import { useRouter } from "@/shared/config/navigation";
|
||||
|
||||
const ResetCodeForm = () => {
|
||||
const [passwordWarning, setPasswordWarning] = useState<string>("");
|
||||
File renamed without changes.
Loaded 100 of 172 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user