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:
Alibek committed 2024-02-29 14:09:37 +06:00
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.
@@ -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.
+34
View File
@@ -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.
@@ -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;
}
}
}
+66
View File
@@ -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,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 (
@@ -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 (
@@ -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 | Регистрация",
@@ -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 = {
@@ -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
View File
@@ -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;
}
+13
View File
@@ -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>
);
}
-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:
"Страница обращения 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 -1
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)}...`;
@@ -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;
}
+9
View File
@@ -0,0 +1,9 @@
"use client";
import "./BreadCrumbs.scss";
const BreadCrumbs = () => {
return <div className="breadcrumbs"></div>;
};
export default BreadCrumbs;
+18 -1
View File
@@ -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>
+7 -13
View File
@@ -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);
+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: "",
});
};
+68 -23
View File
@@ -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);
},
+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
@@ -1,5 +1,5 @@
export interface ILocation {
id: 4;
id: number;
latitude: string;
longitude: string;
address: string;
+5
View File
@@ -3,3 +3,8 @@ export interface ITokens {
access_token: string;
expires_in: string;
}
export interface IRefresh {
access: string;
expires_at: string;
}
+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)",
+7 -5
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,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="#">
+5 -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,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>
)}
</>
+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}
+3 -4
View File
@@ -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" : ""
+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;
@@ -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;
+64 -35
View File
@@ -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>
);
@@ -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;
@@ -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;
@@ -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;
Whitespace-only 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;
@@ -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>("");
Loaded 100 of 172 files, more files were not shown because too many files have changed in this diff. Show more