made all pages
@ -1,5 +1,20 @@
|
||||
.create-report {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 40px;
|
||||
h2 {
|
||||
text-align: start;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 768px) {
|
||||
.create-report {
|
||||
gap: 30px;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 550px) {
|
||||
.create-report {
|
||||
gap: 20px;
|
||||
}
|
||||
}
|
||||
|
@ -1,12 +1,20 @@
|
||||
import Typography from "@/shared/ui/components/Typography/Typography";
|
||||
import "./CreateReport.scss";
|
||||
import dynamic from "next/dynamic";
|
||||
|
||||
const DynamicForm = dynamic(
|
||||
() => import("@/widgets/ReportForm/ReportForm"),
|
||||
{
|
||||
ssr: false,
|
||||
}
|
||||
);
|
||||
|
||||
const CreateReport = () => {
|
||||
return (
|
||||
<div className="create-report page-padding">
|
||||
<Typography element="h2">Написать обращение</Typography>
|
||||
|
||||
<div className="create-report__wrapper"></div>
|
||||
<DynamicForm />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
35
src/App/profile/AuthGuard.tsx
Normal file
@ -0,0 +1,35 @@
|
||||
"use client";
|
||||
|
||||
import { apiInstance } from "@/shared/config/apiConfig";
|
||||
import { AxiosError } from "axios";
|
||||
import { signOut, useSession } from "next-auth/react";
|
||||
import { useEffect } from "react";
|
||||
|
||||
const AuthGuard = ({ children }: { children: React.ReactNode }) => {
|
||||
const session = useSession();
|
||||
const verifyToken = async () => {
|
||||
try {
|
||||
const data = {
|
||||
token: session.data?.access_token,
|
||||
};
|
||||
await apiInstance.post("/token/verify/", data);
|
||||
} catch (error: unknown) {
|
||||
if (error instanceof AxiosError) {
|
||||
if (error.response?.data.code === "token_not_valid") {
|
||||
signOut({
|
||||
callbackUrl: "/",
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (session.status === "loading") return;
|
||||
|
||||
verifyToken();
|
||||
}, [session.status]);
|
||||
return <>{children}</>;
|
||||
};
|
||||
|
||||
export default AuthGuard;
|
8
src/App/profile/Profile.scss
Normal file
@ -0,0 +1,8 @@
|
||||
.profile {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
h2 {
|
||||
margin-bottom: 50px;
|
||||
}
|
||||
}
|
21
src/App/profile/layout.tsx
Normal file
@ -0,0 +1,21 @@
|
||||
import Typography from "@/shared/ui/components/Typography/Typography";
|
||||
import "./Profile.scss";
|
||||
import ProfileNav from "@/widgets/ProfileNav/ProfileNav";
|
||||
import AuthGuard from "./AuthGuard";
|
||||
|
||||
const Profile = ({
|
||||
children,
|
||||
}: Readonly<{
|
||||
children: React.ReactNode;
|
||||
}>) => {
|
||||
return (
|
||||
<div className="profile page-padding">
|
||||
<Typography element="h2">Личный кабинет</Typography>
|
||||
<ProfileNav />
|
||||
|
||||
<AuthGuard>{children}</AuthGuard>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Profile;
|
34
src/App/profile/my-reports/page.tsx
Normal file
@ -0,0 +1,34 @@
|
||||
import { apiInstance } from "@/shared/config/apiConfig";
|
||||
import { authConfig } from "@/shared/config/authConfig";
|
||||
import { IMyReportsList } from "@/shared/types/my-reports";
|
||||
import ProfileTable from "@/widgets/ProfileTable/ProfileTable";
|
||||
import { getServerSession } from "next-auth";
|
||||
import React from "react";
|
||||
|
||||
const MyReports = async () => {
|
||||
const session = await getServerSession(authConfig);
|
||||
|
||||
const getMyReports = async () => {
|
||||
const Authorization = `Bearer ${session?.access_token}`;
|
||||
const config = {
|
||||
headers: {
|
||||
Authorization,
|
||||
},
|
||||
};
|
||||
const res = await apiInstance.get<IMyReportsList>(
|
||||
"/users/reports/",
|
||||
config
|
||||
);
|
||||
|
||||
return res.data;
|
||||
};
|
||||
|
||||
const data: IMyReportsList = await getMyReports();
|
||||
return (
|
||||
<div>
|
||||
<ProfileTable reports={data} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default MyReports;
|
@ -1,19 +1,7 @@
|
||||
"use client";
|
||||
import React from "react";
|
||||
|
||||
import { signOut } from "next-auth/react";
|
||||
import Link from "next/link";
|
||||
|
||||
const Profile = () => {
|
||||
return (
|
||||
<div>
|
||||
<Link
|
||||
href="#"
|
||||
onClick={() => signOut({ callbackUrl: "/sign-in" })}
|
||||
>
|
||||
Выйти в окно
|
||||
</Link>
|
||||
</div>
|
||||
);
|
||||
const page = () => {
|
||||
return <div>page</div>;
|
||||
};
|
||||
|
||||
export default Profile;
|
||||
export default page;
|
||||
|
48
src/App/profile/personal/page.tsx
Normal file
@ -0,0 +1,48 @@
|
||||
"use server";
|
||||
|
||||
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 { AxiosError } from "axios";
|
||||
import { getServerSession } from "next-auth";
|
||||
import React from "react";
|
||||
|
||||
const Personal = async () => {
|
||||
const session = await getServerSession(authConfig);
|
||||
const getProfile = async () => {
|
||||
const Authorization = `Bearer ${session?.access_token}`;
|
||||
const config = {
|
||||
headers: {
|
||||
Authorization,
|
||||
},
|
||||
};
|
||||
try {
|
||||
const response = await apiInstance.get<IProfile>(
|
||||
"/users/profile/",
|
||||
config
|
||||
);
|
||||
|
||||
return response.data;
|
||||
} catch (error: unknown) {
|
||||
if (error instanceof AxiosError) console.log(error.message);
|
||||
}
|
||||
};
|
||||
|
||||
const data = await getProfile();
|
||||
|
||||
return (
|
||||
<div className="personal">
|
||||
<ProfileAvatar img={data?.image as string} />
|
||||
<ProfileForm
|
||||
id={data?.id as number}
|
||||
first_name={data?.first_name as string}
|
||||
last_name={data?.last_name as string}
|
||||
email={data?.email as string}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Personal;
|
180
src/App/report/[id]/ReportDetails.scss
Normal file
@ -0,0 +1,180 @@
|
||||
.report-details {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 75px;
|
||||
&__container {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 70px;
|
||||
}
|
||||
}
|
||||
|
||||
.report-information {
|
||||
max-width: 646px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 24px;
|
||||
|
||||
h2 {
|
||||
margin-bottom: 4px;
|
||||
text-align: start;
|
||||
font-size: 42px;
|
||||
font-weight: 500;
|
||||
line-height: 50px;
|
||||
color: rgb(0, 0, 0);
|
||||
}
|
||||
|
||||
&__date-and-like {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 80px;
|
||||
}
|
||||
|
||||
&__date {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
|
||||
p {
|
||||
font-size: 15px;
|
||||
font-weight: 500;
|
||||
line-height: 20px;
|
||||
color: rgba(62, 50, 50, 0.75);
|
||||
}
|
||||
}
|
||||
|
||||
&__description {
|
||||
margin-bottom: 4px;
|
||||
font-size: 20px;
|
||||
font-weight: 400;
|
||||
line-height: 34px;
|
||||
color: rgb(62, 50, 50);
|
||||
}
|
||||
|
||||
&__author {
|
||||
margin-bottom: 40px;
|
||||
font-size: 20px;
|
||||
font-weight: 400;
|
||||
line-height: 34px;
|
||||
color: rgb(102, 102, 102);
|
||||
|
||||
span {
|
||||
color: rgb(72, 159, 225);
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
|
||||
&__show-map {
|
||||
width: fit-content;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
font-size: 20px;
|
||||
font-weight: 400;
|
||||
line-height: 34px;
|
||||
text-decoration: underline;
|
||||
color: rgb(102, 102, 102);
|
||||
}
|
||||
}
|
||||
|
||||
.report-images {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr 1fr 1fr;
|
||||
gap: 20px;
|
||||
|
||||
&__exist {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
&__default {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 10px;
|
||||
background-color: rgb(209, 217, 226);
|
||||
}
|
||||
|
||||
&__item1 {
|
||||
grid-column: 1 / 5;
|
||||
height: 441px;
|
||||
}
|
||||
|
||||
&__item2,
|
||||
&__item3,
|
||||
&__item4,
|
||||
&__item5 {
|
||||
height: 102px;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 1024px) {
|
||||
.report-details {
|
||||
&__container {
|
||||
grid-template-columns: 1fr;
|
||||
gap: 45px;
|
||||
}
|
||||
}
|
||||
|
||||
.report-information {
|
||||
max-width: 100%;
|
||||
|
||||
h2 {
|
||||
font-size: 36px;
|
||||
line-height: 43px;
|
||||
}
|
||||
|
||||
&__description {
|
||||
font-size: 18px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 550px) {
|
||||
.report-details {
|
||||
&__container {
|
||||
gap: 40px;
|
||||
}
|
||||
}
|
||||
|
||||
.report-information {
|
||||
h2 {
|
||||
font-size: 24px;
|
||||
line-height: 29px;
|
||||
}
|
||||
|
||||
&__description {
|
||||
font-size: 16px;
|
||||
line-height: 140%;
|
||||
}
|
||||
|
||||
&__author {
|
||||
margin-bottom: 0px;
|
||||
font-size: 16px;
|
||||
line-height: 34px;
|
||||
}
|
||||
|
||||
&__show-map {
|
||||
font-size: 16px;
|
||||
line-height: 34px;
|
||||
}
|
||||
}
|
||||
|
||||
.report-images {
|
||||
grid-template-columns: 1fr 1fr;
|
||||
|
||||
&__item1 {
|
||||
grid-column: 1 / 2;
|
||||
}
|
||||
|
||||
&__item1,
|
||||
&__item2,
|
||||
&__item3,
|
||||
&__item4,
|
||||
&__item5 {
|
||||
height: 162px;
|
||||
}
|
||||
}
|
||||
}
|
17
src/App/report/[id]/icons/calendar.svg
Normal file
@ -0,0 +1,17 @@
|
||||
<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="clip2340_50999">
|
||||
<rect id="calendar" width="16.000000" height="16.000000" fill="white" fill-opacity="0"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
<rect id="calendar" width="16.000000" height="16.000000" fill="#FFFFFF" fill-opacity="0"/>
|
||||
<g clip-path="url(#clip2340_50999)">
|
||||
<path id="Vector" d="M12.666 2.66699C13.4023 2.66699 14 3.26367 14 4L14 13.334C14 14.0703 13.4023 14.667 12.666 14.667L3.33398 14.667C2.59766 14.667 2 14.0703 2 13.334L2 4C2 3.26367 2.59766 2.66699 3.33398 2.66699L12.666 2.66699Z" stroke="#6E6565" stroke-opacity="1.000000" stroke-width="1.000000" stroke-linejoin="round"/>
|
||||
<path id="Vector" d="M10.666 1.33301L10.666 4" stroke="#6E6565" stroke-opacity="1.000000" stroke-width="1.000000" stroke-linejoin="round" stroke-linecap="round"/>
|
||||
<path id="Vector" d="M5.33398 1.33301L5.33398 4" stroke="#6E6565" stroke-opacity="1.000000" stroke-width="1.000000" stroke-linejoin="round" stroke-linecap="round"/>
|
||||
<path id="Vector" d="M2 6.66699L14 6.66699" stroke="#6E6565" stroke-opacity="1.000000" stroke-width="1.000000" stroke-linejoin="round" stroke-linecap="round"/>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 1.3 KiB |
8
src/App/report/[id]/icons/def_image.svg
Normal file
@ -0,0 +1,8 @@
|
||||
<svg width="38.337891" height="38.336914" viewBox="0 0 38.3379 38.3369" fill="none" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<desc>
|
||||
Created with Pixso.
|
||||
</desc>
|
||||
<defs/>
|
||||
<path id="Icon" d="M15.7637 0.834961L22.5762 0.834961C24.8477 0.834961 26.6543 0.834961 28.1113 0.954102C29.6035 1.07617 30.877 1.33105 32.043 1.9248C33.9238 2.88379 35.4531 4.41309 36.4121 6.29492C37.0059 7.46094 37.2617 8.7334 37.3828 10.2256C37.502 11.6836 37.502 13.4902 37.502 15.7617L37.502 22.5752C37.502 24.8467 37.502 26.6533 37.3828 28.1104C37.2617 29.6035 37.0059 30.876 36.4121 32.042C35.4531 33.9229 33.9238 35.4531 32.043 36.4121C30.877 37.0059 29.6035 37.2607 28.1113 37.3828C26.6543 37.502 24.8477 37.502 22.5762 37.502L15.7617 37.502C13.4902 37.502 11.6836 37.502 10.2266 37.3828C8.73438 37.2607 7.46094 37.0059 6.29688 36.4121C4.41406 35.4531 2.88477 33.9229 1.92578 32.042C1.33203 30.876 1.07617 29.6035 0.955078 28.1104C0.835938 26.6533 0.835938 24.8467 0.835938 22.5742L0.835938 15.7617C0.835938 13.4902 0.835938 11.6836 0.955078 10.2256C1.07617 8.7334 1.33203 7.46094 1.92578 6.29492C2.88477 4.41309 4.41406 2.88379 6.29688 1.9248C7.46094 1.33105 8.73438 1.07617 10.2266 0.954102C11.6836 0.834961 13.4922 0.834961 15.7637 0.834961ZM10.498 4.27637C9.21094 4.38184 8.42578 4.58008 7.80859 4.89453C6.55469 5.53418 5.53516 6.55371 4.89648 7.80859C4.58203 8.42578 4.38281 9.21094 4.27734 10.4971C4.16992 11.8037 4.16992 13.4736 4.16992 15.835L4.16992 17.502L6.8125 14.8584C8.11328 13.5566 10.2246 13.5566 11.5254 14.8584L21.9141 25.2461C22.2383 25.5713 22.7656 25.5713 23.0918 25.2461L26.8125 21.5254C28.1133 20.2236 30.2246 20.2236 31.5254 21.5254L34.168 24.166C34.1699 23.6523 34.1699 23.0986 34.1699 22.502L34.1699 15.835C34.1699 13.4736 34.168 11.8037 34.0605 10.4971C33.957 9.21094 33.7578 8.42578 33.4434 7.80859C32.8027 6.55371 31.7832 5.53418 30.5293 4.89453C29.9121 4.58008 29.127 4.38184 27.8398 4.27637C26.5352 4.16992 24.8633 4.16797 22.502 4.16797L15.8359 4.16797C13.4746 4.16797 11.8047 4.16992 10.498 4.27637ZM23.3359 7.50195C21.0352 7.50195 19.1699 9.36719 19.1699 11.668C19.1699 13.9697 21.0352 15.835 23.3359 15.835C25.6367 15.835 27.502 13.9697 27.502 11.668C27.502 9.36719 25.6367 7.50195 23.3359 7.50195Z" fill="#7C8B9D" fill-opacity="1.000000" fill-rule="evenodd"/>
|
||||
<path id="Icon" d="M23.3359 7.50195C25.6367 7.50195 27.502 9.36719 27.502 11.668C27.502 13.9697 25.6367 15.835 23.3359 15.835C21.0352 15.835 19.1699 13.9697 19.1699 11.668C19.1699 9.36719 21.0352 7.50195 23.3359 7.50195ZM10.2266 0.954102C8.73438 1.07617 7.46094 1.33105 6.29688 1.9248C4.41406 2.88379 2.88477 4.41309 1.92578 6.29492C1.33203 7.46094 1.07617 8.7334 0.955078 10.2256C0.835938 11.6836 0.835938 13.4902 0.835938 15.7617L0.835938 22.5742C0.835938 24.8467 0.835938 26.6533 0.955078 28.1104C1.07617 29.6035 1.33203 30.876 1.92578 32.042C2.88477 33.9229 4.41406 35.4531 6.29688 36.4121C7.46094 37.0059 8.73438 37.2607 10.2266 37.3828C11.6836 37.502 13.4902 37.502 15.7637 37.502L22.5762 37.502C24.8477 37.502 26.6543 37.502 28.1113 37.3828C29.6035 37.2607 30.877 37.0059 32.043 36.4121C33.9238 35.4531 35.4531 33.9229 36.4121 32.042C37.0059 30.876 37.2617 29.6035 37.3828 28.1104C37.502 26.6533 37.502 24.8467 37.502 22.5752L37.502 15.7617C37.502 13.4902 37.502 11.6836 37.3828 10.2256C37.2617 8.7334 37.0059 7.46094 36.4121 6.29492C35.4531 4.41309 33.9238 2.88379 32.043 1.9248C30.877 1.33105 29.6035 1.07617 28.1113 0.954102C26.6543 0.834961 24.8477 0.834961 22.5762 0.834961L15.7637 0.834961C13.4922 0.834961 11.6836 0.834961 10.2266 0.954102ZM10.498 4.27637C11.8047 4.16992 13.4746 4.16797 15.8359 4.16797L22.502 4.16797C24.8633 4.16797 26.5352 4.16992 27.8398 4.27637C29.127 4.38184 29.9121 4.58008 30.5293 4.89453C31.7832 5.53418 32.8027 6.55371 33.4434 7.80859C33.7578 8.42578 33.957 9.21094 34.0605 10.4971C34.168 11.8037 34.1699 13.4736 34.1699 15.835L34.1699 22.502C34.1699 23.0986 34.1699 23.6523 34.168 24.166L31.5254 21.5254C30.2246 20.2236 28.1133 20.2236 26.8125 21.5254L23.0918 25.2461C22.7656 25.5713 22.2383 25.5713 21.9141 25.2461L11.5254 14.8584C10.2246 13.5566 8.11328 13.5566 6.8125 14.8584L4.16992 17.502L4.16992 15.835C4.16992 13.4736 4.16992 11.8037 4.27734 10.4971C4.38281 9.21094 4.58203 8.42578 4.89648 7.80859C5.53516 6.55371 6.55469 5.53418 7.80859 4.89453C8.42578 4.58008 9.21094 4.38184 10.498 4.27637Z" stroke="#7C8B9D" stroke-opacity="1.000000" stroke-width="1.670000"/>
|
||||
</svg>
|
After Width: | Height: | Size: 4.3 KiB |
17
src/App/report/[id]/icons/map-pin.svg
Normal file
@ -0,0 +1,17 @@
|
||||
<svg width="24.000000" height="24.000000" viewBox="0 0 24 24" 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="clip824_20705">
|
||||
<rect id="map-pin" width="24.000000" height="24.000000" fill="white" fill-opacity="0"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
<rect id="map-pin" width="24.000000" height="24.000000" fill="#FFFFFF" fill-opacity="0"/>
|
||||
<g clip-path="url(#clip824_20705)">
|
||||
<path id="Vector" d="M21 10C21 17 12 23 12 23C12 23 3 17 3 10C3 7.61328 3.94727 5.32422 5.63672 3.63574C7.32422 1.94824 9.61328 1 12 1C14.3867 1 16.6758 1.94824 18.3633 3.63574C20.0527 5.32422 21 7.61328 21 10Z" fill="#3998E8" fill-opacity="1.000000" fill-rule="nonzero"/>
|
||||
<path id="Vector" d="M12 23C12 23 3 17 3 10C3 7.61328 3.94727 5.32422 5.63672 3.63574C7.32422 1.94824 9.61328 1 12 1C14.3867 1 16.6758 1.94824 18.3633 3.63574C20.0527 5.32422 21 7.61328 21 10C21 17 12 23 12 23Z" stroke="#3998E8" stroke-opacity="1.000000" stroke-width="2.000000" stroke-linejoin="round"/>
|
||||
<path id="Vector" d="M12 13C13.6562 13 15 11.6572 15 10C15 8.34277 13.6562 7 12 7C10.3438 7 9 8.34277 9 10C9 11.6572 10.3438 13 12 13Z" fill="#FFFFFF" fill-opacity="1.000000" fill-rule="nonzero"/>
|
||||
<path id="Vector" d="M12 13C10.3438 13 9 11.6572 9 10C9 8.34277 10.3438 7 12 7C13.6562 7 15 8.34277 15 10C15 11.6572 13.6562 13 12 13Z" stroke="#FFFFFF" stroke-opacity="1.000000" stroke-width="2.000000" stroke-linejoin="round"/>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 1.5 KiB |
126
src/App/report/[id]/page.tsx
Normal file
@ -0,0 +1,126 @@
|
||||
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";
|
||||
|
||||
const ReportDetails = async ({
|
||||
params,
|
||||
}: {
|
||||
params: { id: string };
|
||||
}) => {
|
||||
const getReportDetails = async () => {
|
||||
const res = await apiInstance.get<IReport>(
|
||||
`/report/${params.id}/`
|
||||
);
|
||||
|
||||
return res.data;
|
||||
};
|
||||
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} />
|
||||
</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;
|
6
src/Shared/types/author-type.ts
Normal file
@ -0,0 +1,6 @@
|
||||
export interface IAuthor {
|
||||
id: number;
|
||||
first_name: string;
|
||||
last_name: string;
|
||||
govern_status: unknown;
|
||||
}
|
@ -1,5 +1,5 @@
|
||||
export interface IList {
|
||||
count: number | null;
|
||||
previous: number | null;
|
||||
next: number | null;
|
||||
previous: string | null;
|
||||
next: string | null;
|
||||
}
|
||||
|
6
src/Shared/types/location-type.ts
Normal file
@ -0,0 +1,6 @@
|
||||
export interface ILocation {
|
||||
id: 4;
|
||||
latitude: number;
|
||||
longitude: number;
|
||||
address: string;
|
||||
}
|
18
src/Shared/types/my-reports.ts
Normal file
@ -0,0 +1,18 @@
|
||||
import { IList } from "./list-type";
|
||||
import { ILocation } from "./location-type";
|
||||
import { IReportImage } from "./report-image-type";
|
||||
|
||||
export interface IMyReports {
|
||||
id: number;
|
||||
location: ILocation[];
|
||||
image: IReportImage[];
|
||||
status: number;
|
||||
total_likes: number;
|
||||
count_reviews: number;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
}
|
||||
|
||||
export interface IMyReportsList extends IList {
|
||||
results: IMyReports[];
|
||||
}
|
9
src/Shared/types/profile-type.ts
Normal file
@ -0,0 +1,9 @@
|
||||
export interface IProfile {
|
||||
id: number;
|
||||
email: string;
|
||||
first_name: string;
|
||||
last_name: string;
|
||||
image: string;
|
||||
role: number;
|
||||
govern_status: null;
|
||||
}
|
4
src/Shared/types/report-image-type.ts
Normal file
@ -0,0 +1,4 @@
|
||||
export interface IReportImage {
|
||||
id: number;
|
||||
image: string;
|
||||
}
|
@ -1,21 +1,6 @@
|
||||
export interface IAuthor {
|
||||
id: number;
|
||||
first_name: string;
|
||||
last_name: string;
|
||||
govern_status: unknown;
|
||||
}
|
||||
|
||||
export interface ILocation {
|
||||
id: 4;
|
||||
latitude: number;
|
||||
longitude: number;
|
||||
address: string;
|
||||
}
|
||||
|
||||
export interface IReportImage {
|
||||
id: number;
|
||||
image: string;
|
||||
}
|
||||
import { IAuthor } from "./author-type";
|
||||
import { ILocation } from "./location-type";
|
||||
import { IReportImage } from "./report-image-type";
|
||||
|
||||
export interface IReport {
|
||||
results: any;
|
||||
|
11
src/Shared/variables/report-status.ts
Normal file
@ -0,0 +1,11 @@
|
||||
export const REPORT_STATUS: Record<number, string> = {
|
||||
1: "В обработке",
|
||||
2: "Одобрен",
|
||||
3: "Запрещен",
|
||||
};
|
||||
|
||||
export const REPORT_STATUS_COLORS: Record<number, string> = {
|
||||
1: "rgb(255, 172, 51)",
|
||||
2: "rgb(20, 186, 109)",
|
||||
3: "rgb(230, 68, 82)",
|
||||
};
|
@ -18,9 +18,9 @@ import geo_yellow_icon from "./icons/geo-yellow.svg";
|
||||
import { DivIcon, Icon, LatLngTuple } from "leaflet";
|
||||
import { StaticImageData } from "next/image";
|
||||
import Link from "next/link";
|
||||
import { ILocation } from "@/shared/types/report-type";
|
||||
import { useEffect, useState } from "react";
|
||||
import L from "leaflet";
|
||||
import { ILocation } from "@/shared/types/location-type";
|
||||
|
||||
interface IData {
|
||||
id: number;
|
||||
@ -72,6 +72,7 @@ const HomeMap: React.FC<IHomeMapProps> = ({
|
||||
useEffect(() => {
|
||||
setPosition(latLng);
|
||||
}, [latLng]);
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
|
@ -20,7 +20,7 @@ const NavAuth: React.FC<INavAuthProps> = ({
|
||||
{auth ? (
|
||||
<Link
|
||||
onClick={() => setOpenMenu(false)}
|
||||
href="/profile"
|
||||
href="/profile/personal"
|
||||
className={`nav-auth-profile-${
|
||||
responsible
|
||||
? `sm${pathname === "/profile" ? "_active" : ""}`
|
||||
|
@ -14,14 +14,17 @@
|
||||
|
||||
&__option,
|
||||
&__option_active {
|
||||
width: 100%;
|
||||
padding: 10px 14px;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
font-weight: 900;
|
||||
justify-content: flex-start;
|
||||
background-color: #fff;
|
||||
}
|
||||
|
||||
&__option_active {
|
||||
background-color: rgb(249, 250, 251);
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
|
@ -11,7 +11,6 @@ interface INavMenuProps {
|
||||
const NavMenu: React.FC<INavMenuProps> = ({
|
||||
setOpenMenu,
|
||||
}: INavMenuProps) => {
|
||||
const auth = false;
|
||||
const pathname = usePathname();
|
||||
return (
|
||||
<nav className="nav-menu">
|
||||
|
80
src/Widgets/ProfileForm/ChangePassword/ChangePassword.scss
Normal file
@ -0,0 +1,80 @@
|
||||
.change-password {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
z-index: 11000;
|
||||
background-color: rgba(0, 0, 0, 0.5);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
p {
|
||||
font-size: 14px;
|
||||
font-weight: 400;
|
||||
line-height: 20px;
|
||||
}
|
||||
|
||||
&__error {
|
||||
color: rgb(240, 68, 56);
|
||||
}
|
||||
|
||||
&__success {
|
||||
color: rgb(72, 159, 225);
|
||||
}
|
||||
|
||||
&__wrapper {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
max-width: 400px;
|
||||
padding: 24px;
|
||||
flex-direction: column;
|
||||
gap: 15px;
|
||||
border-radius: 12px;
|
||||
background-color: #fff;
|
||||
|
||||
h4 {
|
||||
margin-bottom: 9px;
|
||||
font-size: 18px;
|
||||
font-weight: 800;
|
||||
line-height: 28px;
|
||||
color: rgb(16, 24, 40);
|
||||
}
|
||||
|
||||
a {
|
||||
align-self: flex-end;
|
||||
font-size: 14px;
|
||||
font-weight: 400;
|
||||
line-height: 180%;
|
||||
color: rgb(72, 159, 225);
|
||||
}
|
||||
}
|
||||
&__btns {
|
||||
margin-top: 17px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
|
||||
button {
|
||||
padding: 10px 18px;
|
||||
border-radius: 8px;
|
||||
font-size: 16px;
|
||||
font-weight: 800;
|
||||
line-height: 24px;
|
||||
box-shadow: 0px 1px 2px 0px rgba(16, 24, 40, 0.05);
|
||||
}
|
||||
|
||||
button:first-child {
|
||||
border: 1px solid rgb(72, 159, 225);
|
||||
background-color: rgb(72, 159, 225);
|
||||
color: rgb(255, 255, 255);
|
||||
}
|
||||
|
||||
button:last-child {
|
||||
border: 1px solid rgb(208, 213, 221);
|
||||
background-color: rgb(255, 255, 255);
|
||||
color: rgb(52, 64, 84);
|
||||
}
|
||||
}
|
||||
}
|
154
src/Widgets/ProfileForm/ChangePassword/ChangePassword.tsx
Normal file
@ -0,0 +1,154 @@
|
||||
import "./ChangePassword.scss";
|
||||
import Link from "next/link";
|
||||
import ChangePasswordInput from "./ChangePasswordInput/ChangePasswordInput";
|
||||
import { useState } from "react";
|
||||
import { apiInstance } from "@/shared/config/apiConfig";
|
||||
import { useSession } from "next-auth/react";
|
||||
import Loader from "@/shared/ui/components/Loader/Loader";
|
||||
|
||||
interface IChangePasswordProps {
|
||||
closeWindow: (bool: boolean) => void;
|
||||
}
|
||||
|
||||
const ChangePassword: React.FC<IChangePasswordProps> = ({
|
||||
closeWindow,
|
||||
}: IChangePasswordProps) => {
|
||||
const session = useSession();
|
||||
const [oldPassword, setOldPassword] = useState<string>("");
|
||||
const [newPassword, setNewPassword] = useState<string>("");
|
||||
const [confirmNewPassword, setConfirmNewPassword] =
|
||||
useState<string>("");
|
||||
|
||||
const [warningOldPassword, setWarningOldPassword] =
|
||||
useState<string>("");
|
||||
const [warningNewPassword, setWarningNewPassword] =
|
||||
useState<string>("");
|
||||
const [warningConfirmNewPassword, setWarningConfirmNewPassword] =
|
||||
useState<string>("");
|
||||
const [error, setError] = useState<string>("");
|
||||
const [loader, setLoader] = useState<boolean>(false);
|
||||
const [success, setSuccess] = useState<boolean>(false);
|
||||
|
||||
const changePass = async () => {
|
||||
if (!oldPassword.trim()) {
|
||||
setError("");
|
||||
setWarningNewPassword("");
|
||||
setWarningConfirmNewPassword("");
|
||||
setWarningOldPassword("Пожалуйста введите старый пароль");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!newPassword.trim()) {
|
||||
setError("");
|
||||
setWarningConfirmNewPassword("");
|
||||
setWarningOldPassword("");
|
||||
setWarningNewPassword("Пожалуйста введите новый пароль");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!confirmNewPassword.trim()) {
|
||||
setError("");
|
||||
setWarningNewPassword("");
|
||||
setWarningOldPassword("");
|
||||
setWarningConfirmNewPassword(
|
||||
"Пожалуйста потвердите новый пароль"
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (confirmNewPassword !== newPassword) {
|
||||
setError("");
|
||||
setWarningNewPassword("");
|
||||
setWarningOldPassword("");
|
||||
setWarningConfirmNewPassword("Пароли отличаются");
|
||||
return;
|
||||
}
|
||||
|
||||
const data = {
|
||||
old_password: oldPassword,
|
||||
new_password1: newPassword,
|
||||
new_password2: confirmNewPassword,
|
||||
};
|
||||
|
||||
const Authorization = `Bearer ${session.data?.access_token}`;
|
||||
const config = {
|
||||
headers: {
|
||||
Authorization,
|
||||
},
|
||||
};
|
||||
|
||||
try {
|
||||
setError("");
|
||||
setWarningNewPassword("");
|
||||
setWarningOldPassword("");
|
||||
setWarningConfirmNewPassword("");
|
||||
setLoader(true);
|
||||
const res = await apiInstance.patch(
|
||||
"/users/change_password/",
|
||||
data,
|
||||
config
|
||||
);
|
||||
|
||||
if ([200, 201].includes(res.status)) return setSuccess(true);
|
||||
} catch (error: any) {
|
||||
setError(error.message);
|
||||
} finally {
|
||||
setLoader(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
onClick={() => closeWindow(false)}
|
||||
className="change-password"
|
||||
>
|
||||
<div
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
className="change-password__wrapper"
|
||||
>
|
||||
<h4>Изменить пароль</h4>
|
||||
<ChangePasswordInput
|
||||
onChange={(e) => setOldPassword(e.target.value)}
|
||||
value={oldPassword}
|
||||
placeholder="Введите старый пароль"
|
||||
label="Старый пароль"
|
||||
error={warningOldPassword}
|
||||
/>
|
||||
<Link href="/sign-in/forgot-password">Забыли пароль?</Link>
|
||||
<ChangePasswordInput
|
||||
onChange={(e) => setNewPassword(e.target.value)}
|
||||
value={newPassword}
|
||||
placeholder="Введите новый пароль"
|
||||
label="Новый пароль"
|
||||
error={warningNewPassword}
|
||||
/>
|
||||
<ChangePasswordInput
|
||||
onChange={(e) => setConfirmNewPassword(e.target.value)}
|
||||
value={confirmNewPassword}
|
||||
placeholder="Повторите новый пароль"
|
||||
label="Потвердить новый пароль"
|
||||
error={warningConfirmNewPassword}
|
||||
/>
|
||||
{error ? (
|
||||
<p className="change-password__error">{error}</p>
|
||||
) : null}
|
||||
{success ? (
|
||||
<p className="change-password__success">
|
||||
Вы успешно поменяли пароль!
|
||||
</p>
|
||||
) : null}
|
||||
|
||||
<div className="change-password__btns">
|
||||
<button type="button" onClick={changePass}>
|
||||
{loader ? <Loader /> : "Сохранить"}
|
||||
</button>
|
||||
<button type="button" onClick={() => closeWindow(false)}>
|
||||
Отмена
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ChangePassword;
|
@ -0,0 +1,43 @@
|
||||
.change-password-input {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
label {
|
||||
font-size: 16px;
|
||||
font-weight: 800;
|
||||
line-height: 20px;
|
||||
color: rgb(52, 64, 84);
|
||||
}
|
||||
|
||||
&__field,
|
||||
&__field-with-error {
|
||||
padding: 10px 14px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
border: 1px solid rgb(208, 213, 221);
|
||||
border-radius: 8px;
|
||||
box-shadow: 0px 1px 2px 0px rgba(16, 24, 40, 0.05);
|
||||
background: rgb(255, 255, 255);
|
||||
|
||||
input {
|
||||
width: 100%;
|
||||
font-size: 18px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
::placeholder {
|
||||
font-size: 16px;
|
||||
font-weight: 400;
|
||||
line-height: 24px;
|
||||
color: rgb(102, 112, 133);
|
||||
}
|
||||
}
|
||||
|
||||
p {
|
||||
font-size: 14px;
|
||||
font-weight: 400;
|
||||
line-height: 20px;
|
||||
color: rgb(240, 68, 56);
|
||||
}
|
||||
}
|
@ -0,0 +1,51 @@
|
||||
"use client";
|
||||
|
||||
import "./ChangePasswordInput.scss";
|
||||
import Image from "next/image";
|
||||
import { useState } from "react";
|
||||
import eye_off from "./icons/eye-off.svg";
|
||||
import eye_on from "./icons/eye-on.svg";
|
||||
|
||||
interface IChangePasswordInputProps
|
||||
extends React.InputHTMLAttributes<HTMLInputElement> {
|
||||
label: string;
|
||||
error: string;
|
||||
}
|
||||
|
||||
const ChangePasswordInput: React.FC<IChangePasswordInputProps> = ({
|
||||
label,
|
||||
error,
|
||||
placeholder,
|
||||
name,
|
||||
onChange,
|
||||
value,
|
||||
}: IChangePasswordInputProps) => {
|
||||
const [isOpen, setIsOpen] = useState<boolean>(false);
|
||||
return (
|
||||
<div className="change-password-input">
|
||||
<label>{label}</label>
|
||||
<div
|
||||
className={`change-password-input__field${
|
||||
error ? "-with-error" : ""
|
||||
}`}
|
||||
>
|
||||
<input
|
||||
onChange={onChange}
|
||||
value={value}
|
||||
name={name}
|
||||
placeholder={placeholder}
|
||||
type={isOpen ? "text" : "password"}
|
||||
/>
|
||||
<button
|
||||
onClick={() => setIsOpen((prev) => !prev)}
|
||||
type="button"
|
||||
>
|
||||
<Image src={isOpen ? eye_on : eye_off} alt="Eye Icon" />
|
||||
</button>
|
||||
</div>
|
||||
{error ? <p>{error}</p> : null}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ChangePasswordInput;
|
@ -0,0 +1,15 @@
|
||||
<svg width="24.000000" height="24.000000" viewBox="0 0 24 24" 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="clip1668_50526">
|
||||
<rect id="eye-off" width="24.000000" height="24.000000" fill="white" fill-opacity="0"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
<rect id="eye-off" width="24.000000" height="24.000000" fill="#FFFFFF" fill-opacity="0"/>
|
||||
<g clip-path="url(#clip1668_50526)">
|
||||
<path id="Vector" d="M17.9404 17.9404C16.2305 19.2432 14.1494 19.9648 12 20C5 20 1 12 1 12C2.24414 9.68164 3.96875 7.65625 6.05957 6.05957M9.90039 4.24023C10.5879 4.0791 11.293 3.99805 12 4C19 4 23 12 23 12C22.3926 13.1357 21.6689 14.2051 20.8398 15.1904M14.1201 14.1201C13.8457 14.415 13.5137 14.6514 13.1465 14.8154C12.7783 14.9795 12.3809 15.0674 11.9785 15.0742C11.5752 15.0811 11.1748 15.0078 10.8018 14.8564C10.4277 14.7061 10.0889 14.4814 9.80371 14.1963C9.51855 13.9111 9.29395 13.5723 9.14355 13.1982C8.99219 12.8252 8.91895 12.4248 8.92578 12.0215C8.93262 11.6191 9.02051 11.2217 9.18457 10.8535C9.34863 10.4863 9.58496 10.1543 9.87988 9.87988" stroke="#979797" stroke-opacity="1.000000" stroke-width="2.000000" stroke-linejoin="round"/>
|
||||
<path id="Vector" d="M1 1L23 23" stroke="#979797" stroke-opacity="1.000000" stroke-width="2.000000" stroke-linejoin="round" stroke-linecap="round"/>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 1.4 KiB |
@ -0,0 +1,15 @@
|
||||
<svg width="24.000000" height="24.000000" viewBox="0 0 24 24" 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="clip1668_50161">
|
||||
<rect id="eye" width="24.000000" height="24.000000" fill="white" fill-opacity="0"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
<rect id="eye" width="24.000000" height="24.000000" fill="#FFFFFF" fill-opacity="0"/>
|
||||
<g clip-path="url(#clip1668_50161)">
|
||||
<path id="Vector" d="M12 4C19 4 23 12 23 12C23 12 19 20 12 20C5 20 1 12 1 12C1 12 5 4 12 4Z" stroke="#979797" stroke-opacity="1.000000" stroke-width="2.000000" stroke-linejoin="round"/>
|
||||
<path id="Vector" d="M12 15C10.3428 15 9 13.6572 9 12C9 10.3428 10.3428 9 12 9C13.6572 9 15 10.3428 15 12C15 13.6572 13.6572 15 12 15Z" stroke="#979797" stroke-opacity="1.000000" stroke-width="2.000000" stroke-linejoin="round"/>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 901 B |
79
src/Widgets/ProfileForm/ProfileForm.scss
Normal file
@ -0,0 +1,79 @@
|
||||
.profile-form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 25px;
|
||||
|
||||
&__input {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
|
||||
label {
|
||||
font-size: 18px;
|
||||
font-weight: 400;
|
||||
line-height: 135%;
|
||||
color: #686b7c;
|
||||
}
|
||||
|
||||
div {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
input {
|
||||
width: 100%;
|
||||
max-width: 957px;
|
||||
padding: 16px;
|
||||
border: 1px solid rgb(221, 222, 226);
|
||||
border-radius: 8px;
|
||||
background: rgb(255, 255, 255);
|
||||
font-size: 18px;
|
||||
font-weight: 400;
|
||||
line-height: 135%;
|
||||
color: #09090b;
|
||||
}
|
||||
|
||||
img {
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&__btn,
|
||||
&__btn_active {
|
||||
margin-top: 50px;
|
||||
width: fit-content;
|
||||
padding: 10px;
|
||||
border-radius: 8px;
|
||||
background-color: rgb(158, 167, 175);
|
||||
cursor: auto;
|
||||
font-size: 16px;
|
||||
font-weight: 800;
|
||||
line-height: 19px;
|
||||
color: rgb(255, 255, 255);
|
||||
}
|
||||
|
||||
&__btn_active {
|
||||
background-color: rgb(57, 152, 232);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
&__logout {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 550px) {
|
||||
.profile-form {
|
||||
&__btn,
|
||||
&__btn_active {
|
||||
align-self: flex-end;
|
||||
margin-top: 15px;
|
||||
margin-bottom: 150px;
|
||||
}
|
||||
|
||||
&__logout {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
}
|
166
src/Widgets/ProfileForm/ProfileForm.tsx
Normal file
@ -0,0 +1,166 @@
|
||||
"use client";
|
||||
|
||||
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 { 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";
|
||||
|
||||
interface IProfileFormProps {
|
||||
id: number;
|
||||
email: string;
|
||||
first_name: string;
|
||||
last_name: string;
|
||||
}
|
||||
|
||||
const ProfileForm: React.FC<IProfileFormProps> = ({
|
||||
id,
|
||||
email,
|
||||
first_name,
|
||||
last_name,
|
||||
}: IProfileFormProps) => {
|
||||
const session = useSession();
|
||||
const router = useRouter();
|
||||
const [error, setError] = useState<string>("");
|
||||
const [loader, setLoader] = useState<boolean>(false);
|
||||
|
||||
const [editFirstName, setEditFirstName] = useState<boolean>(true);
|
||||
const [editLastName, setEditLastName] = useState<boolean>(true);
|
||||
|
||||
const [firstName, setFirstName] = useState<string>(first_name);
|
||||
const [lastName, setLastName] = useState<string>(last_name);
|
||||
|
||||
const [openPopup, setOpenPopup] = useState<boolean>(false);
|
||||
|
||||
const thereAreChanges = () => {
|
||||
if (firstName !== first_name || lastName !== last_name)
|
||||
return false;
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
const updateProfile: React.MouseEventHandler<
|
||||
HTMLFormElement
|
||||
> = async (e) => {
|
||||
e.preventDefault();
|
||||
|
||||
const data = {
|
||||
first_name: firstName,
|
||||
last_name: lastName,
|
||||
};
|
||||
|
||||
const Authorization = `Bearer ${session.data?.access_token}`;
|
||||
const config = {
|
||||
headers: {
|
||||
Authorization,
|
||||
},
|
||||
};
|
||||
|
||||
try {
|
||||
setLoader(true);
|
||||
const res = await apiInstance.patch(
|
||||
"/users/profile/update/",
|
||||
data,
|
||||
config
|
||||
);
|
||||
if ([200, 201].includes(res.status)) {
|
||||
router.refresh();
|
||||
}
|
||||
} catch (error: unknown) {
|
||||
if (error instanceof AxiosError) {
|
||||
setError(error.message);
|
||||
} else {
|
||||
setError("Возникла непредвиденная ошибка");
|
||||
}
|
||||
} finally {
|
||||
setLoader(false);
|
||||
}
|
||||
};
|
||||
return (
|
||||
<>
|
||||
<form onSubmit={updateProfile} className="profile-form">
|
||||
<div className="profile-form__input">
|
||||
<label>Имя</label>
|
||||
<div>
|
||||
<input
|
||||
value={firstName}
|
||||
onChange={(e) => setFirstName(e.target.value)}
|
||||
disabled={editFirstName}
|
||||
type="text"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setEditFirstName((prev) => !prev)}
|
||||
>
|
||||
<Image src={pen} alt="Pen Icon" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="profile-form__input">
|
||||
<label>Фамилия</label>
|
||||
<div>
|
||||
<input
|
||||
value={lastName}
|
||||
onChange={(e) => setLastName(e.target.value)}
|
||||
disabled={editLastName}
|
||||
type="text"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setEditLastName((prev) => !prev)}
|
||||
>
|
||||
<Image src={pen} alt="Pen Icon" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="profile-form__input">
|
||||
<label>Email</label>
|
||||
<div>
|
||||
<input value={email} disabled type="text" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="profile-form__input">
|
||||
<label>Пароль</label>
|
||||
<div>
|
||||
<input
|
||||
value={"*****************************"}
|
||||
disabled={editLastName}
|
||||
type="password"
|
||||
/>
|
||||
<button onClick={() => setOpenPopup(true)} type="button">
|
||||
<Image src={pen} alt="Pen Icon" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{openPopup && <ChangePassword closeWindow={setOpenPopup} />}
|
||||
|
||||
{error ? <p>{error}</p> : null}
|
||||
|
||||
<button
|
||||
disabled={thereAreChanges()}
|
||||
type="submit"
|
||||
className={`profile-form__btn${
|
||||
thereAreChanges() ? "" : "_active"
|
||||
}`}
|
||||
>
|
||||
{loader ? <Loader /> : "Сохранить изменения"}
|
||||
</button>
|
||||
<LogoutButton className="profile-form__logout" />
|
||||
</form>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default ProfileForm;
|
8
src/Widgets/ProfileForm/icons/pen.svg
Normal file
@ -0,0 +1,8 @@
|
||||
<svg width="21.908203" height="22.000000" viewBox="0 0 21.9082 22" fill="none" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<desc>
|
||||
Created with Pixso.
|
||||
</desc>
|
||||
<defs/>
|
||||
<path id="Vector (Stroke)" d="M14.7539 1.22754C16.3906 -0.40918 19.041 -0.40918 20.6777 1.22754C22.3145 2.86426 22.3145 5.51562 20.6777 7.15234L8.5918 19.2383C8.45508 19.375 8.28516 19.4707 8.09766 19.5166L4.51562 20.3867C4.47852 20.3984 4.44141 20.4072 4.4043 20.4141L2.23047 20.9424C1.875 21.0293 1.5 20.9238 1.24023 20.665C0.982422 20.4062 0.876953 20.0312 0.962891 19.6758L2.38867 13.8076C2.43555 13.6211 2.53125 13.4502 2.66797 13.3145L14.7539 1.22754ZM4.53711 18.2236L7.31641 17.5479L19.1953 5.66895C20.0117 4.85156 20.0117 3.52832 19.1953 2.71094C18.377 1.89355 17.0547 1.89355 16.2363 2.71094L4.35742 14.5889L3.68359 17.3652L4.53711 18.2236Z" fill="#B2B5BE" fill-opacity="1.000000" fill-rule="evenodd"/>
|
||||
<path id="Union" d="M14.5332 4.09473C14.9434 3.68652 15.6074 3.6875 16.0176 4.09766L19.123 7.21191C19.5312 7.62207 19.5312 8.28613 19.1211 8.69531C18.7109 9.10449 18.0469 9.10352 17.6367 8.69336L14.5312 5.57812C14.123 5.16797 14.123 4.50391 14.5332 4.09473ZM8.77734 19.9238C8.77734 19.3447 9.24609 18.875 9.82617 18.875L20.8594 18.875C21.4375 18.875 21.9082 19.3447 21.9082 19.9238C21.9082 20.5029 21.4375 20.9727 20.8594 20.9727L9.82617 20.9727C9.24609 20.9727 8.77734 20.5029 8.77734 19.9238Z" fill="#B2B5BE" fill-opacity="1.000000" fill-rule="evenodd"/>
|
||||
</svg>
|
After Width: | Height: | Size: 1.5 KiB |
72
src/Widgets/ProfileNav/ProfileNav.scss
Normal file
@ -0,0 +1,72 @@
|
||||
.profile-nav {
|
||||
margin-bottom: 55px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
|
||||
div {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
a:first-child {
|
||||
margin-right: 46px;
|
||||
}
|
||||
|
||||
a {
|
||||
font-size: 20px;
|
||||
font-weight: 500;
|
||||
line-height: 24px;
|
||||
color: rgb(151, 151, 151);
|
||||
}
|
||||
|
||||
span {
|
||||
margin-left: 16px;
|
||||
padding: 2px 10px;
|
||||
border-radius: 16px;
|
||||
background-color: rgb(224, 237, 248);
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
line-height: 20px;
|
||||
color: rgb(23, 92, 211);
|
||||
cursor: auto;
|
||||
text-decoration: none;
|
||||
}
|
||||
}
|
||||
|
||||
#profile-nav__link {
|
||||
text-decoration: underline;
|
||||
color: rgb(50, 48, 58);
|
||||
}
|
||||
|
||||
#profile-nav__create-report {
|
||||
padding: 10px;
|
||||
border-radius: 8px;
|
||||
background-color: rgb(57, 152, 232);
|
||||
font-size: 16px;
|
||||
font-weight: 800;
|
||||
line-height: 19px;
|
||||
color: rgb(255, 255, 255);
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 550px) {
|
||||
.profile-nav {
|
||||
div {
|
||||
a:first-child {
|
||||
margin-right: 20px;
|
||||
}
|
||||
|
||||
a {
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
span {
|
||||
margin-left: 6px;
|
||||
}
|
||||
}
|
||||
button,
|
||||
a:last-child {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
47
src/Widgets/ProfileNav/ProfileNav.tsx
Normal file
@ -0,0 +1,47 @@
|
||||
"use client";
|
||||
|
||||
import "./ProfileNav.scss";
|
||||
import LogoutButton from "@/features/LogoutButton/LogoutButton";
|
||||
import Link from "next/link";
|
||||
import { usePathname } from "next/navigation";
|
||||
|
||||
const ProfileNav = () => {
|
||||
const pathname = usePathname();
|
||||
return (
|
||||
<nav className="profile-nav">
|
||||
<div>
|
||||
<Link
|
||||
id={
|
||||
pathname === "/profile/personal"
|
||||
? "profile-nav__link"
|
||||
: ""
|
||||
}
|
||||
href="/profile/personal"
|
||||
>
|
||||
Личные данные
|
||||
</Link>
|
||||
<Link
|
||||
id={
|
||||
pathname === "/profile/my-reports"
|
||||
? "profile-nav__link"
|
||||
: ""
|
||||
}
|
||||
href="/profile/my-reports"
|
||||
>
|
||||
Мои обращения
|
||||
</Link>
|
||||
<span>3</span>
|
||||
</div>
|
||||
|
||||
{pathname === "/profile/personal" ? (
|
||||
<LogoutButton />
|
||||
) : (
|
||||
<Link id="profile-nav__create-report" href="/create-report">
|
||||
Написать обращение
|
||||
</Link>
|
||||
)}
|
||||
</nav>
|
||||
);
|
||||
};
|
||||
|
||||
export default ProfileNav;
|
134
src/Widgets/ProfileTable/ProfileTable.scss
Normal file
@ -0,0 +1,134 @@
|
||||
.profile-table {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 30px;
|
||||
&__wrapper {
|
||||
overflow: hidden;
|
||||
overflow-x: auto;
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
table {
|
||||
width: 100%;
|
||||
|
||||
thead {
|
||||
padding: 0 40px;
|
||||
width: 100%;
|
||||
height: 68px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
background-color: rgb(244, 244, 244);
|
||||
|
||||
tr {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: grid;
|
||||
grid-template-columns: 160px 271px 300px 164px 100px;
|
||||
justify-content: space-between;
|
||||
|
||||
td {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 100%;
|
||||
background-color: rgb(244, 244, 244);
|
||||
}
|
||||
|
||||
td,
|
||||
button {
|
||||
font-size: 16px;
|
||||
font-weight: 400;
|
||||
line-height: 18px;
|
||||
color: rgb(102, 112, 133);
|
||||
}
|
||||
|
||||
button {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
tbody {
|
||||
display: flex;
|
||||
|
||||
flex-direction: column;
|
||||
|
||||
tr {
|
||||
padding: 0 40px;
|
||||
height: 80px;
|
||||
display: grid;
|
||||
grid-template-columns: 160px 271px 300px 164px 100px;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
border-bottom: 1px solid rgb(241, 244, 249);
|
||||
|
||||
td {
|
||||
font-size: 18px;
|
||||
font-weight: 500;
|
||||
line-height: 20px;
|
||||
}
|
||||
|
||||
#my-report-date {
|
||||
color: rgb(102, 112, 133);
|
||||
}
|
||||
|
||||
#my-report-location {
|
||||
font-size: 20px;
|
||||
color: rgb(50, 48, 58);
|
||||
}
|
||||
|
||||
#my-report-status {
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
#my-report-reviews {
|
||||
color: rgb(72, 159, 225);
|
||||
}
|
||||
|
||||
#my-report-likes {
|
||||
color: rgb(74, 192, 63);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&__message {
|
||||
font-size: 18px;
|
||||
font-weight: 500;
|
||||
line-height: 22px;
|
||||
color: rgb(102, 102, 102);
|
||||
}
|
||||
|
||||
a {
|
||||
display: none;
|
||||
padding: 10px;
|
||||
border-radius: 8px;
|
||||
background-color: rgb(57, 152, 232);
|
||||
font-size: 16px;
|
||||
font-weight: 800;
|
||||
line-height: 19px;
|
||||
color: rgb(255, 255, 255);
|
||||
}
|
||||
}
|
||||
|
||||
.no-reports-in-profile {
|
||||
border: 1px solid rgb(197, 198, 197);
|
||||
border-radius: 10px;
|
||||
width: 100%;
|
||||
height: 150px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 550px) {
|
||||
.profile-table {
|
||||
a {
|
||||
display: flex;
|
||||
align-self: center;
|
||||
}
|
||||
}
|
||||
}
|
94
src/Widgets/ProfileTable/ProfileTable.tsx
Normal file
@ -0,0 +1,94 @@
|
||||
import Image from "next/image";
|
||||
import "./ProfileTable.scss";
|
||||
import arrows from "@/shared/icons/arrows.svg";
|
||||
import { IMyReportsList } from "@/shared/types/my-reports";
|
||||
import {
|
||||
REPORT_STATUS,
|
||||
REPORT_STATUS_COLORS,
|
||||
} from "@/shared/variables/report-status";
|
||||
import Link from "next/link";
|
||||
|
||||
interface IProfileTableProps {
|
||||
reports: IMyReportsList;
|
||||
}
|
||||
|
||||
const ProfileTable: React.FC<IProfileTableProps> = ({
|
||||
reports,
|
||||
}: IProfileTableProps) => {
|
||||
const params = [
|
||||
{ param: "Дата", handleClick() {} },
|
||||
{ param: "Адрес" },
|
||||
{ param: "Статус" },
|
||||
{ param: "Комментарии", handleClick() {} },
|
||||
{ param: "Рейтинг", handleClick() {} },
|
||||
];
|
||||
const sliceDate = (date: string) => {
|
||||
return `${date.slice(8, 10)}.${date.slice(5, 7)}.${date.slice(
|
||||
0,
|
||||
4
|
||||
)}`;
|
||||
};
|
||||
return (
|
||||
<div className="profile-table">
|
||||
<div
|
||||
className={`profile-table__wrapper ${
|
||||
reports.results.length ? "" : "no-reports-in-profile"
|
||||
}`}
|
||||
>
|
||||
{reports.results.length ? (
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
{params.map((p) => (
|
||||
<td key={p.param}>
|
||||
{p.handleClick ? (
|
||||
<button>
|
||||
{p.param}{" "}
|
||||
<Image src={arrows} alt="Arrows Icon" />
|
||||
</button>
|
||||
) : (
|
||||
p.param
|
||||
)}
|
||||
</td>
|
||||
))}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{reports.results.map((report) => (
|
||||
<tr key={report.id}>
|
||||
<td id="my-report-date">
|
||||
{sliceDate(report.created_at)}
|
||||
</td>
|
||||
<td id="my-report-location">
|
||||
{report.location[0].address}
|
||||
</td>
|
||||
<td
|
||||
id="my-report-status"
|
||||
style={{
|
||||
color: `${REPORT_STATUS_COLORS[report.status]}`,
|
||||
}}
|
||||
>
|
||||
{REPORT_STATUS[report.status]}
|
||||
</td>
|
||||
<td id="my-report-reviews">
|
||||
{report.count_reviews}
|
||||
</td>
|
||||
<td id="my-report-likes">{report.total_likes}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
) : (
|
||||
<p className="profile-table__message">
|
||||
Вы пока не оставили ни одного обращения.
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
<Link id="my-reports-link" href="/create-report">
|
||||
Написать обращение
|
||||
</Link>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ProfileTable;
|
@ -0,0 +1,7 @@
|
||||
import "./CreateReportMap.scss";
|
||||
|
||||
const CreateReportMap = () => {
|
||||
return <div>CreateReportMap</div>;
|
||||
};
|
||||
|
||||
export default CreateReportMap;
|
251
src/Widgets/ReportForm/ReportForm.scss
Normal file
@ -0,0 +1,251 @@
|
||||
.report-form {
|
||||
padding: 40px 28px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 40px;
|
||||
border: 1px solid rgb(213, 213, 213);
|
||||
border-radius: 10px;
|
||||
|
||||
label {
|
||||
font-size: 22px;
|
||||
font-weight: 400;
|
||||
line-height: 26px;
|
||||
color: rgb(50, 48, 58);
|
||||
}
|
||||
|
||||
&__input {
|
||||
width: 100%;
|
||||
max-width: 801px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 22px;
|
||||
|
||||
input,
|
||||
textarea {
|
||||
height: 52px;
|
||||
padding: 16px 20px;
|
||||
width: 100%;
|
||||
border: 1px solid rgb(197, 198, 197);
|
||||
border-radius: 6px;
|
||||
background: rgb(255, 255, 255);
|
||||
font-size: 17px;
|
||||
font-weight: 400;
|
||||
line-height: 20px;
|
||||
color: rgb(50, 48, 58);
|
||||
}
|
||||
|
||||
textarea {
|
||||
height: 276px;
|
||||
}
|
||||
|
||||
::placeholder {
|
||||
font-size: 18px;
|
||||
font-weight: 400;
|
||||
line-height: 22px;
|
||||
color: rgb(197, 198, 197);
|
||||
}
|
||||
}
|
||||
|
||||
&__map {
|
||||
width: 100%;
|
||||
height: 410px;
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 286px;
|
||||
border: 1px solid rgb(197, 198, 197);
|
||||
border-radius: 6px;
|
||||
|
||||
.leaflet-container {
|
||||
border-top-left-radius: 6px;
|
||||
border-bottom-left-radius: 6px;
|
||||
}
|
||||
}
|
||||
|
||||
&__info {
|
||||
padding: 24px;
|
||||
padding-right: 10px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 11px;
|
||||
|
||||
h4 {
|
||||
font-size: 18px;
|
||||
font-weight: 500;
|
||||
line-height: 22px;
|
||||
color: rgb(50, 48, 58);
|
||||
}
|
||||
|
||||
p {
|
||||
font-size: 16px;
|
||||
font-weight: 400;
|
||||
line-height: 140%;
|
||||
color: rgb(50, 48, 58);
|
||||
span {
|
||||
color: rgb(102, 102, 102);
|
||||
}
|
||||
}
|
||||
|
||||
p:last-child {
|
||||
margin-top: 42px;
|
||||
color: rgb(0, 0, 0);
|
||||
}
|
||||
}
|
||||
|
||||
&__add-images {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
|
||||
p {
|
||||
font-size: 18px;
|
||||
font-weight: 400;
|
||||
line-height: 22px;
|
||||
color: rgb(102, 102, 102);
|
||||
}
|
||||
|
||||
input[type="file"] {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
&__upload {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
font-size: 18px;
|
||||
font-weight: 400;
|
||||
line-height: 22px;
|
||||
color: rgb(50, 48, 58);
|
||||
cursor: pointer;
|
||||
|
||||
span {
|
||||
color: rgb(176, 176, 176);
|
||||
cursor: auto;
|
||||
}
|
||||
}
|
||||
|
||||
&__images {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 20px;
|
||||
overflow: hidden;
|
||||
overflow-x: auto;
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
div {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
img {
|
||||
width: 187px;
|
||||
height: 187px;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
button {
|
||||
width: fit-content;
|
||||
align-self: flex-end;
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
line-height: 20px;
|
||||
text-decoration: underline;
|
||||
color: rgb(50, 48, 58);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
button[type="submit"] {
|
||||
margin-top: 40px;
|
||||
padding: 15px;
|
||||
width: fit-content;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
border: 1px solid rgb(72, 159, 225), rgb(72, 159, 225);
|
||||
border-radius: 5px;
|
||||
background-color: rgb(72, 159, 225);
|
||||
color: rgb(255, 255, 255);
|
||||
font-size: 22px;
|
||||
font-weight: 400;
|
||||
line-height: 26px;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 768px) {
|
||||
.report-form {
|
||||
padding: 36px 20px;
|
||||
gap: 30px;
|
||||
|
||||
label {
|
||||
font-size: 20px;
|
||||
line-height: 24px;
|
||||
}
|
||||
|
||||
&__input {
|
||||
gap: 14px;
|
||||
}
|
||||
|
||||
&__map {
|
||||
grid-template-columns: 1fr;
|
||||
height: fit-content;
|
||||
|
||||
.leaflet-container {
|
||||
height: 410px;
|
||||
border-radius: 6px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 550px) {
|
||||
.report-form {
|
||||
border: none;
|
||||
border-radius: 0;
|
||||
padding: 0;
|
||||
gap: 26px;
|
||||
|
||||
label {
|
||||
font-size: 16px;
|
||||
line-height: 20px;
|
||||
}
|
||||
|
||||
&__input {
|
||||
gap: 6px;
|
||||
|
||||
textarea {
|
||||
height: 130px;
|
||||
}
|
||||
}
|
||||
|
||||
&__map {
|
||||
.leaflet-container {
|
||||
height: 370px;
|
||||
}
|
||||
}
|
||||
|
||||
&__info {
|
||||
padding: 20px 10px;
|
||||
h4 {
|
||||
font-size: 16px;
|
||||
line-height: 19px;
|
||||
}
|
||||
}
|
||||
|
||||
&__add-images {
|
||||
p {
|
||||
font-size: 16px;
|
||||
line-height: 19px;
|
||||
}
|
||||
}
|
||||
|
||||
button[type="submit"] {
|
||||
margin-top: 10px;
|
||||
width: 100%;
|
||||
justify-content: center;
|
||||
font-size: 19px;
|
||||
line-height: 24px;
|
||||
}
|
||||
}
|
||||
}
|
195
src/Widgets/ReportForm/ReportForm.tsx
Normal file
@ -0,0 +1,195 @@
|
||||
"use client";
|
||||
|
||||
import "./ReportForm.scss";
|
||||
import "leaflet/dist/leaflet.css";
|
||||
import Image from "next/image";
|
||||
import {
|
||||
MapContainer,
|
||||
Marker,
|
||||
Popup,
|
||||
TileLayer,
|
||||
useMapEvents,
|
||||
} from "react-leaflet";
|
||||
import clip from "./icons/clip.svg";
|
||||
import arrow_right from "./icons/arrow-right.svg";
|
||||
import pin_image from "./icons/pin-image.svg";
|
||||
import { ChangeEventHandler, useState } from "react";
|
||||
import { Icon } from "leaflet";
|
||||
import pin_icon from "./icons/pin_icon.svg";
|
||||
import axios from "axios";
|
||||
import { apiInstance } from "@/shared/config/apiConfig";
|
||||
import { useSession } from "next-auth/react";
|
||||
|
||||
interface ILatLng {
|
||||
lat: number;
|
||||
lng: number;
|
||||
}
|
||||
|
||||
const ReportForm = () => {
|
||||
const session = useSession();
|
||||
const [latLng, setLatLng] = useState<ILatLng[]>([]);
|
||||
const [displayLatLng, setDisplayLatLng] = useState<string[]>([]);
|
||||
const [images, setImages] = useState<File[]>([]);
|
||||
const position = {
|
||||
lat: 42.8746,
|
||||
lng: 74.606,
|
||||
};
|
||||
|
||||
const customIcon = new Icon({
|
||||
iconUrl: pin_icon.src,
|
||||
iconSize: [32, 32],
|
||||
});
|
||||
|
||||
const handleImages: ChangeEventHandler<HTMLInputElement> = (e) => {
|
||||
if (e.target.files) {
|
||||
const getArrayFromObject = Array.from(e.target.files);
|
||||
setImages(getArrayFromObject);
|
||||
}
|
||||
};
|
||||
|
||||
const deleteImage = (name: string) => {
|
||||
setImages((prev) => prev.filter((img) => img.name !== name));
|
||||
};
|
||||
|
||||
const createReport: React.MouseEventHandler<
|
||||
HTMLFormElement
|
||||
> = async (e) => {
|
||||
e.preventDefault();
|
||||
const Authorization = `Bearer ${session.data?.access_token}`;
|
||||
const config = {
|
||||
headers: {
|
||||
Authorization,
|
||||
},
|
||||
};
|
||||
const formData = new FormData(e.currentTarget);
|
||||
|
||||
images.forEach((image) => {
|
||||
formData.append("image", image);
|
||||
});
|
||||
formData.append("latitude1", latLng[0].lat.toString());
|
||||
formData.append("longitude1", latLng[0].lng.toString());
|
||||
formData.append("latitude2", latLng[1].lat.toString());
|
||||
formData.append("longitude2", latLng[1].lng.toString());
|
||||
const res = await apiInstance.post(
|
||||
"/report/create/",
|
||||
formData,
|
||||
config
|
||||
);
|
||||
|
||||
console.log(res.status);
|
||||
};
|
||||
|
||||
const MapPins = (e: any) => {
|
||||
useMapEvents({
|
||||
click(e) {
|
||||
if (latLng.length < 2) {
|
||||
setLatLng([...latLng, e.latlng]);
|
||||
|
||||
axios
|
||||
.get(
|
||||
`https://nominatim.openstreetmap.org/reverse?lat=${e.latlng.lat}&lon=${e.latlng.lng}&format=json`
|
||||
)
|
||||
.then((res) =>
|
||||
setDisplayLatLng([
|
||||
...displayLatLng,
|
||||
res.data.display_name,
|
||||
])
|
||||
)
|
||||
.catch((error) => console.log(error));
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
return latLng.map((l) => (
|
||||
<Marker key={l.lat} icon={customIcon} position={l} />
|
||||
));
|
||||
};
|
||||
|
||||
return (
|
||||
<form onSubmit={createReport} className="report-form">
|
||||
<div className="report-form__input">
|
||||
<label>Адрес</label>
|
||||
<input
|
||||
value={displayLatLng.join("; ")}
|
||||
disabled
|
||||
placeholder="Отметьте точки на карте"
|
||||
type="text"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="report-form__map">
|
||||
<MapContainer
|
||||
center={position}
|
||||
zoom={14}
|
||||
scrollWheelZoom={false}
|
||||
>
|
||||
<TileLayer
|
||||
attribution='© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
|
||||
url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
|
||||
/>
|
||||
<MapPins />
|
||||
</MapContainer>
|
||||
|
||||
<div className="report-form__info">
|
||||
<h4>Как отметить участок дороги? </h4>
|
||||
<Image src={pin_image} alt="Pin Image" />
|
||||
<p>
|
||||
Поставьте булавку и начните рисовать участок дороги{" "}
|
||||
<span>
|
||||
(он может состоять из любого количества ломаных линий)
|
||||
</span>
|
||||
</p>
|
||||
<p>Чтобы удалить отрезок нажмите на точки повторно.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="report-form__input">
|
||||
<label>Добавьте описание проблемы</label>
|
||||
<textarea name="description" placeholder="Введите описание" />
|
||||
</div>
|
||||
|
||||
<div className="report-form__add-images">
|
||||
<label>Добавьте фотографии</label>
|
||||
<p>
|
||||
Загрузите до 5 фотографии, связанные с дорогой, которую Вы
|
||||
хотите отметить. Фотографии помогут лучше понять проблему.
|
||||
</p>
|
||||
<label
|
||||
className="report-form__upload"
|
||||
htmlFor="report-form-upload"
|
||||
>
|
||||
<Image src={clip} alt="Paper Clip Icon" />
|
||||
Прикрепить файл
|
||||
<span>(до 5 МБ)</span>
|
||||
</label>
|
||||
<input
|
||||
onChange={handleImages}
|
||||
multiple
|
||||
type="file"
|
||||
id="report-form-upload"
|
||||
/>
|
||||
{images.length ? (
|
||||
<div className="report-form__images">
|
||||
{images.map((image) => (
|
||||
<div key={image.name}>
|
||||
<img
|
||||
src={URL.createObjectURL(image)}
|
||||
alt="Report Image"
|
||||
/>
|
||||
<button onClick={() => deleteImage(image.name)}>
|
||||
удалить
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
<button type="submit">
|
||||
Отправить на модерацию
|
||||
<Image src={arrow_right} alt="Arrow Right Icon" />
|
||||
</button>
|
||||
</form>
|
||||
);
|
||||
};
|
||||
|
||||
export default ReportForm;
|
15
src/Widgets/ReportForm/icons/arrow-right.svg
Normal file
@ -0,0 +1,15 @@
|
||||
<svg width="24.000000" height="24.000000" viewBox="0 0 24 24" 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="clip345_6572">
|
||||
<rect id="arrow-right" width="24.000000" height="24.000000" fill="white" fill-opacity="0"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
<rect id="arrow-right" width="24.000000" height="24.000000" fill="#FFFFFF" fill-opacity="0"/>
|
||||
<g clip-path="url(#clip345_6572)">
|
||||
<path id="Vector" d="M5 12L19 12" stroke="#FFFFFF" stroke-opacity="1.000000" stroke-width="2.000000" stroke-linejoin="round" stroke-linecap="round"/>
|
||||
<path id="Vector" d="M12 5L19 12L12 19" stroke="#FFFFFF" stroke-opacity="1.000000" stroke-width="2.000000" stroke-linejoin="round" stroke-linecap="round"/>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 804 B |
14
src/Widgets/ReportForm/icons/clip.svg
Normal file
@ -0,0 +1,14 @@
|
||||
<svg width="24.000000" height="24.000000" viewBox="0 0 24 24" 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="clip345_6197">
|
||||
<rect id="paperclip" width="24.000000" height="24.000000" fill="white" fill-opacity="0"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
<rect id="paperclip" width="24.000000" height="24.000000" fill="#FFFFFF" fill-opacity="0"/>
|
||||
<g clip-path="url(#clip345_6197)">
|
||||
<path id="Vector" d="M21.4404 11.0498L12.25 20.2402C11.124 21.3662 9.59766 21.998 8.00488 21.998C6.41309 21.998 4.88574 21.3662 3.76074 20.2402C2.63477 19.1143 2.00195 17.5869 2.00195 15.9951C2.00195 14.4023 2.63477 12.876 3.76074 11.75L12.9502 2.55957C13.7012 1.80957 14.7188 1.3877 15.7803 1.3877C16.8418 1.3877 17.8594 1.80957 18.6104 2.55957C19.3613 3.31055 19.7822 4.32812 19.7822 5.38965C19.7822 6.45117 19.3613 7.46973 18.6104 8.21973L9.41016 17.4102C9.03516 17.7852 8.52637 17.9961 7.99512 17.9961C7.46484 17.9961 6.95605 17.7852 6.58008 17.4102C6.20508 17.0342 5.99414 16.5254 5.99414 15.9951C5.99414 15.4639 6.20508 14.9551 6.58008 14.5801L15.0703 6.09961" stroke="#32303A" stroke-opacity="1.000000" stroke-width="2.000000" stroke-linejoin="round" stroke-linecap="round"/>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 1.2 KiB |
17
src/Widgets/ReportForm/icons/pin-image.svg
Normal file
@ -0,0 +1,17 @@
|
||||
<svg width="81.304688" height="64.551758" viewBox="0 0 81.3047 64.5518" 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="clip401_4465">
|
||||
<rect id="pin" width="50.000000" height="50.000000" fill="white" fill-opacity="0"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
<rect id="pin" width="50.000000" height="50.000000" fill="#FFFFFF" fill-opacity="0"/>
|
||||
<g clip-path="url(#clip401_4465)">
|
||||
<path id="Vector" d="M32.8125 9.375C32.8125 7.89746 32.3926 6.44922 31.6035 5.2002C30.8125 3.9502 29.6836 2.95117 28.3496 2.31738C27.0137 1.68359 25.5254 1.44238 24.0586 1.62012C22.5898 1.79883 21.2031 2.38965 20.0586 3.32422C18.9141 4.25977 18.0566 5.5 17.5898 6.90234C17.1211 8.30469 17.0605 9.81055 17.4141 11.2461C17.7676 12.6816 18.5215 13.9863 19.5879 15.0107C20.6543 16.0342 21.9883 16.7354 23.4375 17.0303L23.4375 44.6836C23.4375 45.0986 23.5195 45.5098 23.6816 45.8926L24.6641 48.2363C24.6973 48.2969 24.7461 48.3477 24.8047 48.3828C24.8633 48.417 24.9316 48.4355 25 48.4355C25.0684 48.4355 25.1367 48.417 25.1953 48.3828C25.2539 48.3477 25.3027 48.2969 25.3359 48.2363L26.3203 45.8926C26.4805 45.5098 26.5625 45.0986 26.5625 44.6836L26.5625 17.0303C28.3262 16.668 29.9102 15.709 31.0488 14.3145C32.1875 12.9199 32.8105 11.1758 32.8125 9.375ZM27.3438 9.375C26.8809 9.375 26.4277 9.23828 26.041 8.98047C25.6562 8.72266 25.3555 8.35645 25.1777 7.92871C25.002 7.5 24.9551 7.0293 25.0449 6.57422C25.1348 6.12012 25.3594 5.70215 25.6855 5.37402C26.0137 5.04688 26.4316 4.82324 26.8867 4.73242C27.3418 4.64258 27.8125 4.68848 28.2402 4.86621C28.668 5.04395 29.0352 5.34375 29.293 5.72949C29.5508 6.11523 29.6875 6.56836 29.6875 7.03125C29.6875 7.65332 29.4414 8.24902 29.002 8.68848C28.5605 9.12891 27.9648 9.375 27.3438 9.375Z" fill="#E64452" fill-opacity="1.000000" fill-rule="nonzero"/>
|
||||
</g>
|
||||
<circle id="Ellipse 129" cx="25.000000" cy="51.000000" r="5.000000" fill="#FFFFFF" fill-opacity="1.000000"/>
|
||||
<circle id="Ellipse 129" cx="25.000000" cy="51.000000" r="4.750000" stroke="#000000" stroke-opacity="1.000000" stroke-width="0.500000"/>
|
||||
<line id="Line 12" x1="33.294922" y1="53.529297" x2="81.009766" y2="63.081055" stroke="#E64452" stroke-opacity="1.000000" stroke-width="3.000000" stroke-dasharray="6.000000,6.000000"/>
|
||||
</svg>
|
After Width: | Height: | Size: 2.3 KiB |
14
src/Widgets/ReportForm/icons/pin_icon.svg
Normal file
@ -0,0 +1,14 @@
|
||||
<svg width="50.000000" height="50.000000" viewBox="0 0 50 50" 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="clip345_6143">
|
||||
<rect id="pin" width="50.000000" height="50.000000" fill="white" fill-opacity="0"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
<rect id="pin" width="50.000000" height="50.000000" fill="#FFFFFF" fill-opacity="0"/>
|
||||
<g clip-path="url(#clip345_6143)">
|
||||
<path id="Vector" d="M32.8125 9.375C32.8125 7.89746 32.3926 6.44922 31.6025 5.2002C30.8125 3.9502 29.6846 2.95117 28.3486 2.31738C27.0127 1.68359 25.5254 1.44238 24.0576 1.62012C22.5908 1.79883 21.2041 2.38965 20.0586 3.32422C18.9141 4.25977 18.0576 5.5 17.5898 6.90234C17.1211 8.30469 17.0605 9.81055 17.415 11.2461C17.7686 12.6816 18.5225 13.9863 19.5879 15.0107C20.6543 16.0342 21.9893 16.7354 23.4375 17.0303L23.4375 44.6836C23.4375 45.0986 23.5205 45.5098 23.6807 45.8926L24.6641 48.2363C24.6973 48.2969 24.7451 48.3477 24.8047 48.3828C24.8643 48.417 24.9316 48.4355 25 48.4355C25.0684 48.4355 25.1357 48.417 25.1953 48.3828C25.2549 48.3477 25.3027 48.2969 25.3359 48.2363L26.3193 45.8926C26.4795 45.5098 26.5625 45.0986 26.5625 44.6836L26.5625 17.0303C28.3262 16.668 29.9102 15.709 31.0488 14.3145C32.1875 12.9199 32.8105 11.1758 32.8125 9.375ZM27.3438 9.375C26.8799 9.375 26.4268 9.23828 26.042 8.98047C25.6562 8.72266 25.3555 8.35645 25.1787 7.92871C25.001 7.5 24.9551 7.0293 25.0449 6.57422C25.1357 6.12012 25.3584 5.70215 25.6865 5.37402C26.0146 5.04688 26.4316 4.82324 26.8867 4.73242C27.3408 4.64258 27.8125 4.68848 28.2402 4.86621C28.6689 5.04395 29.0352 5.34375 29.293 5.72949C29.5498 6.11523 29.6875 6.56836 29.6875 7.03125C29.6875 7.65332 29.4404 8.24902 29.001 8.68848C28.5615 9.12891 27.9658 9.375 27.3438 9.375Z" fill="#E64452" fill-opacity="1.000000" fill-rule="nonzero"/>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 1.8 KiB |
@ -31,6 +31,7 @@
|
||||
padding: 26px 18px;
|
||||
border: 1px solid rgb(197, 198, 197);
|
||||
border-radius: 12px;
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
button {
|
||||
|
@ -50,7 +50,7 @@ const SignInForm = () => {
|
||||
setLoader(false);
|
||||
|
||||
if (res?.ok && !res.error) {
|
||||
router.push("/profile");
|
||||
router.push("/profile/personal");
|
||||
} else if (res?.status.toString().slice(0, 1) === "4") {
|
||||
setError("Неверный Email или Пароль");
|
||||
} else {
|
||||
|
15
src/features/LogoutButton/LogoutButton.scss
Normal file
@ -0,0 +1,15 @@
|
||||
.logout-btn {
|
||||
padding: 10px;
|
||||
border: 1px solid rgb(230, 68, 82);
|
||||
border-radius: 8px;
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
line-height: 19px;
|
||||
color: rgb(230, 68, 82);
|
||||
}
|
||||
|
||||
@media screen and (max-width: 550px) {
|
||||
.logout-btn {
|
||||
align-self: flex-end;
|
||||
}
|
||||
}
|
26
src/features/LogoutButton/LogoutButton.tsx
Normal file
@ -0,0 +1,26 @@
|
||||
import { signOut } from "next-auth/react";
|
||||
import "./LogoutButton.scss";
|
||||
|
||||
interface ILogoutButtonProps {
|
||||
className?: string;
|
||||
}
|
||||
|
||||
const LogoutButton: React.FC<ILogoutButtonProps> = ({
|
||||
className,
|
||||
}: ILogoutButtonProps) => {
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
className={`logout-btn ${className}`}
|
||||
onClick={() =>
|
||||
signOut({
|
||||
callbackUrl: "/",
|
||||
})
|
||||
}
|
||||
>
|
||||
Выйти из аккаунта
|
||||
</button>
|
||||
);
|
||||
};
|
||||
|
||||
export default LogoutButton;
|
37
src/features/ProfileAvatar/ProfileAvatar.scss
Normal file
@ -0,0 +1,37 @@
|
||||
.profile-avatar {
|
||||
margin-bottom: 25px;
|
||||
position: relative;
|
||||
width: 135px;
|
||||
height: 135px;
|
||||
|
||||
&__image {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border-radius: 50%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
input[type="file"] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
label {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
box-shadow: 0px 0px 9px 0px rgba(0, 0, 0, 0.1);
|
||||
background: rgb(255, 255, 255);
|
||||
border-radius: 50%;
|
||||
cursor: pointer;
|
||||
|
||||
img {
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
}
|
||||
}
|
||||
}
|
61
src/features/ProfileAvatar/ProfileAvatar.tsx
Normal file
@ -0,0 +1,61 @@
|
||||
"use client";
|
||||
|
||||
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 { useRouter } from "next/navigation";
|
||||
|
||||
interface IProfileAvatarProps {
|
||||
img: string;
|
||||
}
|
||||
|
||||
const ProfileAvatar: React.FC<IProfileAvatarProps> = ({
|
||||
img,
|
||||
}: IProfileAvatarProps) => {
|
||||
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);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="profile-avatar">
|
||||
<img
|
||||
className="profile-avatar__image"
|
||||
src={img}
|
||||
alt="User Image"
|
||||
/>
|
||||
<label htmlFor="profile-image">
|
||||
<Image src={pen} alt="Pen Icon" />
|
||||
</label>
|
||||
<input onChange={changeImage} id="profile-image" type="file" />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ProfileAvatar;
|
8
src/features/ProfileAvatar/icons/pen.svg
Normal file
@ -0,0 +1,8 @@
|
||||
<svg width="21.908203" height="22.000000" viewBox="0 0 21.9082 22" fill="none" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<desc>
|
||||
Created with Pixso.
|
||||
</desc>
|
||||
<defs/>
|
||||
<path id="Vector (Stroke)" d="M14.7539 1.22754C16.3906 -0.40918 19.041 -0.40918 20.6777 1.22754C22.3145 2.86426 22.3145 5.51562 20.6777 7.15234L8.5918 19.2383C8.45508 19.375 8.28516 19.4707 8.09766 19.5166L4.51562 20.3867C4.47852 20.3984 4.44141 20.4072 4.4043 20.4141L2.23047 20.9424C1.875 21.0293 1.5 20.9238 1.24023 20.665C0.982422 20.4062 0.876953 20.0312 0.962891 19.6758L2.38867 13.8076C2.43555 13.6211 2.53125 13.4502 2.66797 13.3145L14.7539 1.22754ZM4.53711 18.2236L7.31641 17.5479L19.1953 5.66895C20.0117 4.85156 20.0117 3.52832 19.1953 2.71094C18.377 1.89355 17.0547 1.89355 16.2363 2.71094L4.35742 14.5889L3.68359 17.3652L4.53711 18.2236Z" fill="#B2B5BE" fill-opacity="1.000000" fill-rule="evenodd"/>
|
||||
<path id="Union" d="M14.5332 4.09473C14.9434 3.68652 15.6074 3.6875 16.0176 4.09766L19.123 7.21191C19.5312 7.62207 19.5312 8.28613 19.1211 8.69531C18.7109 9.10449 18.0469 9.10352 17.6367 8.69336L14.5312 5.57812C14.123 5.16797 14.123 4.50391 14.5332 4.09473ZM8.77734 19.9238C8.77734 19.3447 9.24609 18.875 9.82617 18.875L20.8594 18.875C21.4375 18.875 21.9082 19.3447 21.9082 19.9238C21.9082 20.5029 21.4375 20.9727 20.8594 20.9727L9.82617 20.9727C9.24609 20.9727 8.77734 20.5029 8.77734 19.9238Z" fill="#B2B5BE" fill-opacity="1.000000" fill-rule="evenodd"/>
|
||||
</svg>
|
After Width: | Height: | Size: 1.5 KiB |
13
src/features/ReportLike/ReportLike.scss
Normal file
@ -0,0 +1,13 @@
|
||||
.report-like {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 7px;
|
||||
|
||||
span {
|
||||
align-self: center;
|
||||
font-size: 15px;
|
||||
font-weight: 500;
|
||||
line-height: 18px;
|
||||
color: rgb(74, 192, 63);
|
||||
}
|
||||
}
|
24
src/features/ReportLike/ReportLike.tsx
Normal file
@ -0,0 +1,24 @@
|
||||
import "./ReportLike.scss";
|
||||
import Image from "next/image";
|
||||
import like from "./icons/like.svg";
|
||||
import { apiInstance } from "@/shared/config/apiConfig";
|
||||
|
||||
interface IReportLikeProps {
|
||||
count: number;
|
||||
}
|
||||
|
||||
const ReportLike: React.FC<IReportLikeProps> = ({
|
||||
count,
|
||||
}: IReportLikeProps) => {
|
||||
const likeReport = async () => {
|
||||
const res = await apiInstance.post("");
|
||||
};
|
||||
return (
|
||||
<button className="report-like">
|
||||
<Image src={like} alt="Like Icon" />
|
||||
<span>{count}</span>
|
||||
</button>
|
||||
);
|
||||
};
|
||||
|
||||
export default ReportLike;
|
8
src/features/ReportLike/icons/like.svg
Normal file
@ -0,0 +1,8 @@
|
||||
<svg width="26.000000" height="26.000000" viewBox="0 0 26 26" fill="none" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<desc>
|
||||
Created with Pixso.
|
||||
</desc>
|
||||
<defs/>
|
||||
<rect id="thumbs-up" width="26.000000" height="26.000000" fill="#FFFFFF" fill-opacity="0"/>
|
||||
<path id="Vector" d="M7.58301 11.917L4.33301 11.917C3.7583 11.917 3.20752 12.1455 2.80127 12.5518C2.39502 12.958 2.1665 13.5088 2.1665 14.084L2.1665 21.667C2.1665 22.2412 2.39502 22.793 2.80127 23.1992C3.20752 23.6055 3.7583 23.834 4.33301 23.834L7.58301 23.834L19.8027 23.834C20.3262 23.8398 20.833 23.6562 21.2305 23.3184C21.6289 22.9795 21.8916 22.5088 21.9697 21.9922L23.4648 12.2422C23.5117 11.9316 23.4912 11.6143 23.4033 11.3125C23.3154 11.0107 23.1641 10.7324 22.958 10.4951C22.752 10.2578 22.4961 10.0684 22.21 9.94043C21.9229 9.81152 21.6123 9.74707 21.2979 9.75L15.167 9.75L15.167 5.41699C15.167 4.55469 14.8242 3.72852 14.2148 3.11914C13.6055 2.50977 12.7783 2.16699 11.916 2.16699L7.58301 11.917L7.58301 23.834" stroke="#4AC03F" stroke-opacity="1.000000" stroke-width="2.000000" stroke-linejoin="round"/>
|
||||
</svg>
|
After Width: | Height: | Size: 1.1 KiB |
@ -1,3 +1,9 @@
|
||||
export { default } from "next-auth/middleware";
|
||||
|
||||
export const config = { matcher: ["/profile"] };
|
||||
export const config = {
|
||||
matcher: [
|
||||
"/profile/personal",
|
||||
"/profile/my-reports",
|
||||
"/create-report",
|
||||
],
|
||||
};
|
||||
|