This commit is contained in:
Alibek 2024-02-02 17:35:09 +06:00
parent bb852f5119
commit eeb9cc7ff8
92 changed files with 2076 additions and 279 deletions

2
.env Normal file
View File

@ -0,0 +1,2 @@
NEXTAUTH_SECRET=";sadmfxflpdk"
NEXTAUTH_URL="http://localhost:3000"

View File

@ -0,0 +1,91 @@
import axios from "axios";
import NextAuth, { NextAuthOptions } from "next-auth";
import { JWT } from "next-auth/jwt";
import CredentialsProvider from "next-auth/providers/credentials";
interface IToken {
access: string;
}
const refreshToken = async (token: JWT): Promise<JWT> => {
const data = {
refresh: token.refresh_token,
};
const response = await axios.post<IToken>(
"https://api.kgroaduat.fishrungames.com/api/v1/token/refresh/",
data
);
return {
...token,
access_token: response.data.access,
};
};
export const authOptions: NextAuthOptions = {
providers: [
CredentialsProvider({
name: "Credentials",
credentials: {
email: {
label: "Email",
type: "text",
placeholder: "jsmith@example.com",
},
password: { label: "Password", type: "password" },
},
async authorize(credentials, req) {
if (!credentials?.email || !credentials?.password)
return null;
const { email, password } = credentials as any;
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,
}),
}
);
if (res.status === 401) {
console.log(res.status);
return null;
}
const user = await res.json();
return user;
},
}),
],
pages: {
signIn: "/sign-in",
},
session: {
strategy: "jwt",
},
callbacks: {
async jwt({ token, user }) {
if (user) return { ...token, ...user };
return refreshToken(token);
},
async session({ token, session }) {
session.access_token = token.access_token;
session.refresh_token = token.refresh_token;
return session;
},
},
};
const handler = NextAuth(authOptions);
export { handler as GET, handler as POST };

View File

@ -0,0 +1,3 @@
import CreateReportPage from "@/Pages/CreateReportPage/CreateReportPage";
export default CreateReportPage;

3
app/profile/layout.tsx Normal file
View File

@ -0,0 +1,3 @@
import ProfileLayout from "@/Pages/profile/ProfilePage/ProfileLayout";
export default ProfileLayout;

View File

@ -0,0 +1,3 @@
import MyReportsPage from "@/Pages/profile/MyReportsPage/MyReportsPage";
export default MyReportsPage;

View File

@ -1,7 +0,0 @@
import React from "react";
const page = () => {
return <div>page</div>;
};
export default page;

View File

@ -0,0 +1,3 @@
import PersonalDataPage from "@/Pages/profile/PersonalDataPage/PersonalDataPage";
export default PersonalDataPage;

3
app/report/[id]/page.tsx Normal file
View File

@ -0,0 +1,3 @@
import ReportDetailsPage from "@/Pages/ReportDetailsPage/ReportDetailsPage";
export default ReportDetailsPage;

18
lib/next-auth.d.ts vendored Normal file
View File

@ -0,0 +1,18 @@
import NextAuth from "next-auth";
declare module "next-auth" {
interface Session {
refresh_token: string;
access_token: string;
}
}
import { JWT } from "next-auth/jwt";
declare module "next-auth/jwt" {
interface JWT {
refresh_token: string;
access_token: string;
exp: number;
}
}

View File

@ -10,17 +10,22 @@
},
"dependencies": {
"axios": "^1.6.5",
"leaflet": "^1.9.4",
"next": "14.1.0",
"next-auth": "^4.24.5",
"react": "^18",
"react-dom": "^18",
"react-leaflet": "^4.2.1",
"sass": "^1.70.0",
"swr": "^2.2.4",
"use-debounce": "^10.0.0",
"zustand": "^4.5.0"
},
"devDependencies": {
"@types/leaflet": "^1.9.8",
"@types/node": "^20",
"@types/react": "^18",
"@types/react-dom": "^18",
"@types/react-leaflet": "^3.0.0",
"eslint": "^8",
"eslint-config-next": "14.1.0",
"typescript": "^5"

View File

@ -2,6 +2,7 @@ import { Montserrat } from "next/font/google";
import "./globals.scss";
import Navbar from "@/Widgets/general/Navbar/Navbar";
import Footer from "@/Widgets/general/Footer/Footer";
import { Providers } from "./Providers";
const montserrat = Montserrat({ subsets: ["latin"] });
@ -13,9 +14,11 @@ export default function RootLayout({
return (
<html lang="en">
<body className={montserrat.className}>
<Providers>
<Navbar />
{children}
<Footer />
</Providers>
</body>
</html>
);

11
src/App/Providers.tsx Normal file
View File

@ -0,0 +1,11 @@
"use client";
import { SessionProvider } from "next-auth/react";
export const Providers = ({
children,
}: {
children: React.ReactNode;
}) => {
return <SessionProvider>{children}</SessionProvider>;
};

View File

@ -1,11 +1,12 @@
"use client";
import Link from "next/link";
import "./NavAuthBtn.scss";
import { useSession } from "next-auth/react";
const NavAuthBtn = () => {
const auth = false;
return auth ? (
<Link href="/profile" className="nav-profile">
const session = useSession();
return session.status === "authenticated" ? (
<Link href="/profile/personal-data" className="nav-profile">
Профиль
</Link>
) : (

View File

@ -1,13 +1,17 @@
@import "../../Shared/variables.scss";
.news-card {
min-height: 420px;
height: 100%;
display: flex;
flex-direction: column;
justify-content: space-between;
gap: 24px;
img {
width: 100%;
height: 160px;
min-height: 160px;
max-height: 160px;
object-fit: cover;
}

View File

@ -19,6 +19,13 @@ const NewsCard: React.FC<INewsCard> = ({
description,
date,
}: INewsCard) => {
const sliceTitle = (title: string) => {
if (title.length > 35) {
return `${title.slice(0, 35)}...`;
}
return title;
};
const sliceDate = (date: string) => {
return `${date.slice(8, 10)}/${date.slice(5, 7)}/${date.slice(
0,
@ -45,7 +52,7 @@ const NewsCard: React.FC<INewsCard> = ({
<div className="news-card__text">
<h5 className="news-card__date">{sliceDate(date)}</h5>
<h4>{title}</h4>
<h4>{sliceTitle(title)}</h4>
<p>{sliceDescription(description)}</p>
</div>

View File

@ -51,3 +51,11 @@
letter-spacing: 0.25px;
}
}
@media screen and (max-width: 550px) {
.review-card {
p {
font-size: 16px;
}
}
}

View File

@ -0,0 +1,8 @@
.road-type {
padding: 4px 12px;
width: fit-content;
color: white;
font-size: 14px;
font-weight: 400;
border-radius: 20px;
}

View File

@ -0,0 +1,19 @@
import "./RoadType.scss";
interface IRoadTypeProps {
children: React.ReactNode;
color: string;
}
const RoadType: React.FC<IRoadTypeProps> = ({
children,
color,
}: IRoadTypeProps) => {
return (
<div className="road-type" style={{ backgroundColor: color }}>
{children}
</div>
);
};
export default RoadType;

View File

@ -1,16 +1,31 @@
"use client";
import Link from "next/link";
import "./Switch.scss";
import { useState } from "react";
enum ESwitch {
BUTTON,
A,
}
interface ISwitch {
color?: string;
onClick?: () => void;
defaultState?: boolean;
type?: ESwitch;
href?: ESwitch extends ESwitch.A ? string : string | undefined;
}
const Switch: React.FC<ISwitch> = ({ color, onClick }) => {
const [toggle, setToggle] = useState(false);
return (
const Switch: React.FC<ISwitch> = ({
color,
onClick,
defaultState,
type,
href,
}: ISwitch) => {
const [toggle, setToggle] = useState(defaultState);
return type === ESwitch.BUTTON ? (
<button
style={{
backgroundColor: !toggle
@ -27,6 +42,25 @@ const Switch: React.FC<ISwitch> = ({ color, onClick }) => {
>
<div className="switch__thumb"></div>
</button>
) : (
<Link
scroll={false}
href={href ? href : "?category"}
style={{
backgroundColor: !toggle
? "#32303A"
: color
? color
: "#e64452",
}}
onClick={() => {
setToggle((prev) => !prev);
onClick && onClick();
}}
className={`switch ${toggle ? "switch-active" : ""}`}
>
<div className="switch__thumb"></div>
</Link>
);
};

View File

@ -0,0 +1,31 @@
.create-report-map {
height: 410px;
display: grid;
grid-template-columns: 1fr 290px;
border-radius: 6px;
border: 1px solid #c5c6c5;
&__info {
padding: 20px 5px 20px 20px;
display: flex;
flex-direction: column;
gap: 11px;
h4 {
color: #32303a;
font-size: 18px;
font-weight: 500;
}
p {
color: #32303a;
font-size: 16px;
font-weight: 400;
line-height: 140%;
span {
color: #666;
}
}
}
}

View File

@ -0,0 +1,80 @@
import "./CreateReportMap.scss";
import "leaflet/dist/leaflet.css";
import {
MapContainer,
Marker,
TileLayer,
useMapEvents,
} from "react-leaflet";
import { LatLng, LatLngTuple, LeafletMouseEvent } from "leaflet";
import pin from "./assets/pin.svg";
import Image from "next/image";
import { Dispatch, SetStateAction } from "react";
interface ICreateReportMapProps {
markers: LatLng[];
setMarkers: Dispatch<SetStateAction<LatLng[]>>;
}
const CreateReportMap: React.FC<ICreateReportMapProps> = ({
markers,
setMarkers,
}: ICreateReportMapProps) => {
const position = [42.8746, 74.606];
const handleMapClick = (e: LeafletMouseEvent) => {
if (markers.length <= 2) {
setMarkers((prev) => [...prev, e.latlng]);
}
};
const LocationMarker = () => {
useMapEvents({
click: handleMapClick,
});
return (
<>
{markers.map((marker) => (
<Marker
key={marker.lat}
position={position as LatLngTuple}
></Marker>
))}
</>
);
};
return (
<div className="create-report-map">
<MapContainer
center={position as LatLngTuple}
zoom={13}
scrollWheelZoom={false}
className="create-report-map__container"
>
<TileLayer
attribution='&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
/>
<LocationMarker />
</MapContainer>
<div className="create-report-map__info">
<h4>Как отметить участок дороги?</h4>
<Image src={pin} alt="Pin Image" />
<p>
Поставьте булавку и начните рисовать участок дороги{" "}
<span>
(он может состоять из любого количества ломаных линий)
</span>
</p>
<p>Чтобы удалить отрезок нажмите на точки повторно.</p>
</div>
</div>
);
};
export default CreateReportMap;

View File

@ -0,0 +1,5 @@
<svg width="82" height="65" viewBox="0 0 82 65" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M32.8125 9.37536C32.8124 7.89715 32.3929 6.4493 31.6027 5.19999C30.8126 3.95067 29.6842 2.95117 28.3487 2.31755C27.0132 1.68394 25.5253 1.44223 24.0578 1.6205C22.5904 1.79876 21.2037 2.38969 20.0587 3.32464C18.9137 4.25959 18.0575 5.50019 17.5894 6.90235C17.1214 8.30451 17.0608 9.81069 17.4146 11.2459C17.7684 12.6812 18.5222 13.9866 19.5883 15.0106C20.6544 16.0345 21.9892 16.735 23.4375 17.0306V44.684C23.4377 45.0991 23.5203 45.51 23.6807 45.8929L24.6641 48.2367C24.697 48.297 24.7456 48.3473 24.8047 48.3824C24.8638 48.4174 24.9313 48.436 25 48.436C25.0687 48.436 25.1362 48.4174 25.1953 48.3824C25.2544 48.3473 25.303 48.297 25.3359 48.2367L26.3193 45.8929C26.4797 45.51 26.5623 45.0991 26.5625 44.684V17.0306C28.3259 16.6683 29.9104 15.709 31.0489 14.3145C32.1874 12.92 32.8103 11.1756 32.8125 9.37536ZM27.3438 9.37536C26.8802 9.37536 26.4271 9.2379 26.0416 8.98037C25.6562 8.72284 25.3558 8.35679 25.1784 7.92853C25.001 7.50026 24.9546 7.02901 25.045 6.57437C25.1355 6.11973 25.3587 5.70211 25.6865 5.37433C26.0142 5.04655 26.4319 4.82333 26.8865 4.7329C27.3412 4.64246 27.8124 4.68888 28.2407 4.86627C28.6689 5.04366 29.035 5.34407 29.2925 5.7295C29.55 6.11492 29.6875 6.56806 29.6875 7.03161C29.6875 7.65321 29.4406 8.24936 29.001 8.6889C28.5615 9.12843 27.9654 9.37536 27.3438 9.37536Z" fill="#E64452"/>
<circle cx="25" cy="51" r="4.75" fill="white" stroke="black" stroke-width="0.5"/>
<line x1="33.2944" y1="53.5292" x2="81.0099" y2="63.081" stroke="#E64452" stroke-width="3" stroke-dasharray="6 6"/>
</svg>

After

Width:  |  Height:  |  Size: 1.6 KiB

View File

@ -46,3 +46,19 @@
}
}
}
@media screen and (max-width: 768px) {
.create-review {
gap: 30px;
}
}
@media screen and (max-width: 550px) {
.create-review {
gap: 6px;
div {
gap: 12px;
}
}
}

View File

@ -0,0 +1,44 @@
.home-map {
width: 100%;
height: 580px;
border-radius: 8px;
.leaflet-popup {
margin-bottom: 30px;
&-content-wrapper {
border-radius: 8px;
max-width: 180px;
}
&-tip,
&-close-button {
display: none;
}
&-content {
padding: 12px 16px;
margin: 0;
text-align: center;
a {
text-decoration: underline;
color: #3998e8;
font-size: 12px;
font-weight: 400;
line-height: 18px;
}
}
}
}
@media screen and (max-width: 768px) {
.home-map {
height: 416px;
}
}
@media screen and (max-width: 550px) {
.home-map {
height: 370px;
}
}

View File

@ -0,0 +1,98 @@
"use client";
import "./HomeMap.scss";
import "leaflet/dist/leaflet.css";
import {
MapContainer,
Marker,
Popup,
TileLayer,
} from "react-leaflet";
import geo_green_icon from "./icons/geo-green.svg";
import geo_orange_icon from "./icons/geo-orange.svg";
import geo_pink_icon from "./icons/geo-pink.svg";
import geo_purple_icon from "./icons/geo-purple.svg";
import geo_red_icon from "./icons/geo-red.svg";
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";
interface ILocation {
id: number;
latitude: string;
longitude: string;
address: string;
}
interface IData {
id: number;
created_at: string;
location: ILocation[];
category: number;
description: string;
count_reviews: number;
total_likes: number;
}
interface IHomeMapProps {
data: IData[] | undefined;
}
const HomeMap: React.FC<IHomeMapProps> = ({
data,
}: IHomeMapProps) => {
const createCustomIcon = (icon: StaticImageData) => {
const customIcon = new Icon({
iconUrl: icon.src,
iconSize: [32, 32],
});
return customIcon;
};
const icons: Record<string, DivIcon> = {
1: createCustomIcon(geo_red_icon),
2: createCustomIcon(geo_pink_icon),
3: createCustomIcon(geo_purple_icon),
4: createCustomIcon(geo_orange_icon),
5: createCustomIcon(geo_green_icon),
6: createCustomIcon(geo_yellow_icon),
};
const position = [42.8746, 74.606];
return (
<MapContainer
center={position as LatLngTuple}
zoom={14}
scrollWheelZoom={false}
className="home-map"
>
<TileLayer
attribution='&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
/>
{data?.map((marker) => (
<Marker
key={marker.id}
icon={icons[marker.category]}
position={
[
+marker.location[0].latitude,
+marker.location[0].longitude,
] as LatLngTuple
}
>
<Popup>
<Link href={`/report/${marker.location[0].id}`}>
{marker.location[0].address}
</Link>
</Popup>
</Marker>
))}
</MapContainer>
);
};
export default HomeMap;

View File

@ -0,0 +1,5 @@
<svg width="38" height="50" viewBox="0 0 38 50" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M19 0C8.52334 0 0 8.74812 0 19.501C0 32.8456 17.0031 48.5901 17.7271 49.4175C18.407 50.1948 19.5942 50.1935 20.2729 49.4175C20.9969 48.5901 38 32.8456 38 19.501C37.9998 8.74812 29.4766 0 19 0Z" fill="#3998E8" fill-opacity="0.7"/>
<ellipse cx="18.7867" cy="19" rx="9.88775" ry="10" fill="white"/>
<circle cx="18.5" cy="18.5" r="7.5" fill="#8FDE6A"/>
</svg>

After

Width:  |  Height:  |  Size: 461 B

View File

@ -0,0 +1,5 @@
<svg width="38" height="50" viewBox="0 0 38 50" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M19 0C8.52334 0 0 8.74812 0 19.501C0 32.8456 17.0031 48.5901 17.7271 49.4175C18.407 50.1948 19.5942 50.1935 20.2729 49.4175C20.9969 48.5901 38 32.8456 38 19.501C37.9998 8.74812 29.4766 0 19 0Z" fill="#3998E8" fill-opacity="0.7"/>
<ellipse cx="18.7867" cy="19" rx="9.88775" ry="10" fill="white"/>
<circle cx="18.5" cy="18.5" r="7.5" fill="#FFAC33"/>
</svg>

After

Width:  |  Height:  |  Size: 461 B

View File

@ -0,0 +1,5 @@
<svg width="38" height="50" viewBox="0 0 38 50" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M19 0C8.52334 0 0 8.74812 0 19.501C0 32.8456 17.0031 48.5901 17.7271 49.4175C18.407 50.1948 19.5942 50.1935 20.2729 49.4175C20.9969 48.5901 38 32.8456 38 19.501C37.9998 8.74812 29.4766 0 19 0Z" fill="#3998E8" fill-opacity="0.7"/>
<ellipse cx="18.7867" cy="19" rx="9.88775" ry="10" fill="white"/>
<circle cx="18.5" cy="18.5" r="7.5" fill="#C288E2"/>
</svg>

After

Width:  |  Height:  |  Size: 461 B

View File

@ -0,0 +1,5 @@
<svg width="38" height="50" viewBox="0 0 38 50" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M19 0C8.52334 0 0 8.74812 0 19.501C0 32.8456 17.0031 48.5901 17.7271 49.4175C18.407 50.1948 19.5942 50.1935 20.2729 49.4175C20.9969 48.5901 38 32.8456 38 19.501C37.9998 8.74812 29.4766 0 19 0Z" fill="#3998E8" fill-opacity="0.7"/>
<ellipse cx="18.7867" cy="19" rx="9.88775" ry="10" fill="white"/>
<circle cx="18.5" cy="18.5" r="7.5" fill="#87289D"/>
</svg>

After

Width:  |  Height:  |  Size: 461 B

View File

@ -0,0 +1,5 @@
<svg width="38" height="50" viewBox="0 0 38 50" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M19 0C8.52334 0 0 8.74812 0 19.501C0 32.8456 17.0031 48.5901 17.7271 49.4175C18.407 50.1948 19.5942 50.1935 20.2729 49.4175C20.9969 48.5901 38 32.8456 38 19.501C37.9998 8.74812 29.4766 0 19 0Z" fill="#3998E8" fill-opacity="0.7"/>
<ellipse cx="18.7867" cy="19" rx="9.88775" ry="10" fill="white"/>
<circle cx="18.5" cy="18.5" r="7.5" fill="#E64452"/>
</svg>

After

Width:  |  Height:  |  Size: 461 B

View File

@ -0,0 +1,5 @@
<svg width="38" height="50" viewBox="0 0 38 50" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M19 0C8.52334 0 0 8.74812 0 19.501C0 32.8456 17.0031 48.5901 17.7271 49.4175C18.407 50.1948 19.5942 50.1935 20.2729 49.4175C20.9969 48.5901 38 32.8456 38 19.501C37.9998 8.74812 29.4766 0 19 0Z" fill="#3998E8" fill-opacity="0.7"/>
<ellipse cx="18.7867" cy="19" rx="9.88775" ry="10" fill="white"/>
<circle cx="18.5" cy="18.5" r="7.5" fill="#FED363"/>
</svg>

After

Width:  |  Height:  |  Size: 461 B

View File

@ -0,0 +1,10 @@
.report-like {
display: flex;
align-items: center;
gap: 6px;
color: rgb(74, 192, 63);
font-size: 15px;
font-weight: 500;
line-height: 18px;
}

View File

@ -0,0 +1,22 @@
import "./ReportLike.scss";
import Image from "next/image";
import like_icon from "./icons/like-icon.svg";
interface IReportLikeProps {
count_likes: number;
}
const ReportLike: React.FC<IReportLikeProps> = ({
count_likes,
}: IReportLikeProps) => {
return (
<div className="report-like">
<button>
<Image src={like_icon} alt="Like Icon" />
</button>
{count_likes}
</div>
);
};
export default ReportLike;

View File

@ -0,0 +1,5 @@
<svg width="26" height="27" viewBox="0 0 26 27" fill="none" xmlns="http://www.w3.org/2000/svg">
<g id="thumbs-up">
<path id="Vector" d="M7.58317 12.417L11.9165 2.66699C12.7785 2.66699 13.6051 3.0094 14.2146 3.6189C14.8241 4.22839 15.1665 5.05504 15.1665 5.91699V10.2503H21.2982C21.6122 10.2468 21.9233 10.3115 22.2099 10.4401C22.4964 10.5688 22.7516 10.7581 22.9577 10.9951C23.1638 11.2321 23.3159 11.5111 23.4035 11.8128C23.491 12.1144 23.512 12.4315 23.4648 12.742L21.9698 22.492C21.8915 23.0086 21.6291 23.4796 21.2309 23.818C20.8327 24.1564 20.3257 24.3396 19.8032 24.3337H7.58317M7.58317 12.417V24.3337M7.58317 12.417H4.33317C3.75853 12.417 3.20743 12.6453 2.80111 13.0516C2.39478 13.4579 2.1665 14.009 2.1665 14.5837V22.167C2.1665 22.7416 2.39478 23.2927 2.80111 23.6991C3.20743 24.1054 3.75853 24.3337 4.33317 24.3337H7.58317" stroke="#4AC03F" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 929 B

View File

@ -5,17 +5,26 @@ import search_icon from "./icons/search-icon.svg";
interface ISearchBar {
placeholder?: string;
style?: object;
value: string;
setValue: (search: string) => any;
}
const SearchBar: React.FC<ISearchBar> = ({
placeholder,
style,
value,
setValue,
}: ISearchBar) => {
return (
<div style={style} className="search-bar">
<div className="search-bar__input">
<Image src={search_icon} alt="Search Icon" />
<input type="text" placeholder={placeholder} />
<input
value={value}
onChange={(e) => setValue(e.target.value)}
type="text"
placeholder={placeholder}
/>
</div>
<button>Поиск</button>
</div>

View File

@ -9,56 +9,56 @@ import CustomLink from "@/Entities/CustomLink/CustomLink";
import GoogleButton from "@/Entities/GoogleButton/GoogleButton";
import { useSignIn } from "./sign-in.store";
import DefaultLoader from "@/Shared/UI/DefaultLoader/DefaultLoader";
import { signIn } from "next-auth/react";
const SignInForm = () => {
const [email, setEmail] = useState<string>("");
const [password, setPassword] = useState<string>("");
const {
login,
emailError,
passwordError,
error,
loading,
cleanRedirect,
redirect,
} = useSignIn();
const router = useRouter();
useEffect(() => {
if (redirect) {
const handleSubmit: React.FormEventHandler<
HTMLFormElement
> = async (e) => {
e.preventDefault();
const formData = new FormData(e.currentTarget);
console.log(formData.get("email"), formData.get("email"));
const res = await signIn("credentials", {
email: formData.get("email"),
password: formData.get("password"),
redirect: false,
});
if (res && !res.error) {
router.push("/profile");
cleanRedirect();
} else {
console.log(res?.error);
}
}, [redirect]);
};
return (
<form
className="sign-in-form"
onSubmit={(e) => {
e.preventDefault();
login(email, password);
}}
>
<form className="sign-in-form" onSubmit={handleSubmit}>
<div className="sign-in-form__inputs">
<InputWithLabel
name="email"
label="Email"
value={email}
onChange={(e) => setEmail(e.target.value)}
placeholder="Введите email"
error={emailError}
error=""
/>
{emailError ? <p>{emailError}</p> : null}
<InputWithLabel
name="password"
label="Пароль"
value={password}
onChange={(e) => setPassword(e.target.value)}
placeholder="Введите пароль"
secret
error={passwordError}
error=""
/>
{error ? <p>{error}</p> : null}
</div>
<CustomLink
@ -69,9 +69,7 @@ const SignInForm = () => {
</CustomLink>
<div className="sign-in-form__btns">
<Button type="submit">
{!loading ? "Войти" : <DefaultLoader />}
</Button>
<Button type="submit">Войти</Button>
<GoogleButton>Войти через Google</GoogleButton>
</div>

View File

@ -0,0 +1,133 @@
.create-report {
padding: 118px 90px 0px 90px;
display: flex;
flex-direction: column;
gap: 38px;
&__wrapper {
padding: 28px;
display: flex;
flex-direction: column;
gap: 20px;
border-radius: 10px;
border: 1px solid #d5d5d5;
h5 {
color: #32303a;
font-size: 22px;
font-weight: 400;
}
.input-label {
max-width: 800px;
display: flex;
flex-direction: column;
gap: 22px;
input {
padding: 16px 20px;
border-radius: 6px;
border: 1px solid #c5c6c5;
background: #fff;
color: #32303a;
font-size: 17px;
font-weight: 400;
::placeholder {
color: #c5c6c5;
font-size: 18px;
font-weight: 400;
}
}
}
}
&__add-image {
max-width: 800px;
display: flex;
flex-direction: column;
gap: 18px;
p {
color: #666;
font-size: 18px;
font-weight: 400;
}
input[type="file"] {
display: none;
}
label {
display: flex;
align-items: center;
gap: 16px;
color: #32303a;
font-size: 18px;
font-weight: 400;
cursor: pointer;
span {
color: #b0b0b0;
}
}
}
&__images {
display: flex;
gap: 20px;
div {
width: 187px;
display: flex;
flex-direction: column;
gap: 8px;
img {
width: 100%;
height: 187px;
object-fit: cover;
}
button {
color: #32303a;
font-size: 16px;
font-weight: 500;
text-decoration-line: underline;
align-self: flex-end;
}
}
}
&__btn {
width: 330px;
padding: 15px;
gap: 10px;
background-color: #489fe1;
border-radius: 5px;
color: #fff;
text-align: center;
font-size: 22px;
font-weight: 400;
}
}
@media screen and (max-width: 1024px) {
.create-report {
padding: 118px 30px 0px 30px;
}
}
@media screen and (max-width: 768px) {
.create-report {
padding: 112px 30px 0px 30px;
}
}
@media screen and (max-width: 550px) {
.create-report {
padding: 112px 16px 0px 16px;
}
}

View File

@ -0,0 +1,94 @@
"use client";
import "./CreateReportPage.scss";
import HeaderText from "@/Shared/UI/HeaderText/HeaderText";
import CreateReportMap from "@/Features/CreateReportMap/CreateReportMap";
import paperclip__icon from "./icons/paperclip.svg";
import { useState } from "react";
import Image from "next/image";
import arrow_right_icon from "./icons/arrow-right.svg";
import { LatLng } from "leaflet";
const CreateReportPage = () => {
const [location, setLocation] = useState<LatLng[]>([]);
const [images, setImages] = useState<File[]>();
const [description, setdescription] = useState<string>("");
const handleImages = (e: React.ChangeEvent<HTMLInputElement>) => {
if (e.target.files) {
setImages(Array.from(e.target.files));
}
};
const deleteImage = (name: string) => {
setImages((prev) => prev?.filter((image) => image.name !== name));
};
return (
<div className="create-report">
<HeaderText>Написать обращение</HeaderText>
<div className="create-report__wrapper">
<div className="input-label">
<h5>Адрес</h5>
<input type="text" placeholder="Выберите точки на карте" />
</div>
<CreateReportMap
markers={location}
setMarkers={setLocation}
/>
<div className="input-label">
<h5>Добавьте описание проблемы</h5>
<input
value={description}
onChange={(e) => setdescription(e.target.value)}
type="text"
placeholder="Введите описание"
/>
</div>
<div className="create-report__add-image">
<h5>Добавьте фотографии</h5>
<p>
Загрузите до 5 фотографии, связанные с дорогой, которую Вы
хотите отметить. Фотографии помогут лучше понять проблему.
</p>
<input
onChange={handleImages}
type="file"
id="report-add-image"
multiple
/>
<label htmlFor="report-add-image">
<Image src={paperclip__icon} alt="Paper Clip Icon" />
Прикрепить файл <span>(до 5 МБ)</span>
</label>
<div className="create-report__images">
{images?.map((image) => (
<div>
<img
src={URL.createObjectURL(image)}
key={image.name}
alt="Report Image"
/>
<button onClick={() => deleteImage(image.name)}>
удалить
</button>
</div>
))}
</div>
<button className="create-report__btn">
Отправить на модерацию
<Image src={arrow_right_icon} alt="Arrow Right Icon" />
</button>
</div>
</div>
</div>
);
};
export default CreateReportPage;

View File

@ -0,0 +1,6 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<g id="arrow-right">
<path id="Vector" d="M5 12H19" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path id="Vector_2" d="M12 5L19 12L12 19" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 364 B

View File

@ -0,0 +1,5 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<g id="paperclip">
<path id="Vector" d="M21.4403 11.0499L12.2503 20.2399C11.1244 21.3658 9.59747 21.9983 8.00529 21.9983C6.41311 21.9983 4.88613 21.3658 3.76029 20.2399C2.63445 19.1141 2.00195 17.5871 2.00195 15.9949C2.00195 14.4027 2.63445 12.8758 3.76029 11.7499L12.9503 2.55992C13.7009 1.80936 14.7188 1.3877 15.7803 1.3877C16.8417 1.3877 17.8597 1.80936 18.6103 2.55992C19.3609 3.31048 19.7825 4.32846 19.7825 5.38992C19.7825 6.45138 19.3609 7.46936 18.6103 8.21992L9.41029 17.4099C9.03501 17.7852 8.52602 17.996 7.99529 17.996C7.46456 17.996 6.95557 17.7852 6.58029 17.4099C6.20501 17.0346 5.99418 16.5256 5.99418 15.9949C5.99418 15.4642 6.20501 14.9552 6.58029 14.5799L15.0703 6.09992" stroke="#32303A" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 883 B

View File

@ -5,13 +5,29 @@ import RatingSection from "@/Widgets/home/RatingSection/RatingSection";
import StatisticsSection from "@/Widgets/home/StatisticsSection/StatisticsSection";
import NewsSection from "@/Widgets/home/NewsSection/NewsSection";
const Homepage = () => {
const Homepage = ({
searchParams,
}: {
searchParams: {
"тип-дороги": string;
"карта-дорог": string;
рейтинг: string;
};
}) => {
return (
<div className="home">
<Header />
<StatisticsSection />
<MapSection />
<RatingSection />
<MapSection
categories={searchParams["тип-дороги"]}
queryMap={searchParams["карта-дорог"]}
queryRating={searchParams["рейтинг"]}
/>
<RatingSection
categories={searchParams["тип-дороги"]}
queryMap={searchParams["карта-дорог"]}
queryRating={searchParams["рейтинг"]}
/>
<NewsSection />
</div>
);

View File

@ -12,33 +12,6 @@
gap: 40px;
}
&__image {
display: flex;
flex-direction: column;
gap: 30px;
&_main {
width: 100%;
height: 600px;
border-radius: 8px;
object-fit: cover;
}
}
&__date-and-reviews {
display: flex;
align-items: center;
gap: 80px;
span {
display: flex;
align-items: center;
gap: 4px;
color: rgba(62, 50, 50, 0.75);
font-size: 15px;
font-weight: 500;
line-height: 20px;
}
}
&__description {
color: #3e3232;
font-size: 20px;
@ -55,11 +28,13 @@
@media screen and (max-width: 768px) {
.news-details-page {
padding: 112px 30px 0 30px;
gap: 35px;
}
}
@media screen and (max-width: 550px) {
.news-details-page {
padding: 112px 16px 0 16px;
gap: 20px;
}
}

View File

@ -1,10 +1,9 @@
import { newsDetailsStore } from "./store";
import "./NewsDetailsPage.scss";
import HeaderText from "@/Shared/UI/HeaderText/HeaderText";
import message_icon from "./icons/message-icon.svg";
import calendar_icon from "./icons/calendar-icon.svg";
import Image from "next/image";
import NewsReviewsSection from "@/Widgets/NewsReviewsSection/NewsReviewsSection";
import ReviewsSection from "@/Widgets/general/ReviewsSection/ReviewsSection";
import NewsHeader from "@/Widgets/NewsHeader/NewsHeader";
const NewsDetailsPage = async ({
params,
@ -13,54 +12,21 @@ const NewsDetailsPage = async ({
}) => {
const data = await newsDetailsStore.getNewsDetails(params.id);
console.log(data);
const months: Record<string, string> = {
"01": "Январь",
"02": "Февраль",
"03": "Март",
"04": "Апрель",
"05": "Май",
"06": "Июнь",
"07": "Июль",
"08": "Август",
"09": "Сентябрь",
"10": "Октябрь",
"11": "Ноябрь",
"12": "Декабрь",
};
const year = data?.created_at.slice(0, 4);
const month = data?.created_at.slice(5, 7);
const day = data?.created_at.slice(8, 10);
return (
<div className="news-details-page">
<HeaderText>{data?.title}</HeaderText>
<div className="news-details-page__wrapper">
<div className="news-details-page__image">
<img
src={data?.image}
alt="News Image"
className="news-details-page__image_main"
/>
<div className="news-details-page__date-and-reviews">
<span>
<Image src={calendar_icon} alt="Calendar Icon" />
{month && months[month]} {day}, {year}
</span>
<span>
<Image src={message_icon} alt="Message Icon" />
Комментарии: {data?.count_reviews}
</span>
</div>
</div>
<div className="news-details-page__wrapper">
<NewsHeader
date={data?.created_at}
image={data?.image}
count_reviews={data?.count_reviews}
/>
<p className="news-details-page__description">
{data?.description}
</p>
<NewsReviewsSection id={data?.id} list={data?.news_review} />
<ReviewsSection id={data?.id} endpoint="news" />
</div>
</div>
);

View File

@ -0,0 +1,36 @@
.report-details-page {
padding: 118px 90px 0 90px;
&__container {
display: grid;
grid-template-columns: 52% 1fr;
gap: 76px;
}
}
@media screen and (max-width: 1024px) {
.report-details-page {
padding: 118px 30px 0 30px;
}
}
@media screen and (max-width: 768px) {
.report-details-page {
padding: 112px 30px 0 30px;
&__container {
grid-template-columns: 1fr;
gap: 45px;
}
}
}
@media screen and (max-width: 550px) {
.report-details-page {
padding: 112px 16px 0px 16px;
&__container {
gap: 40px;
}
}
}

View File

@ -0,0 +1,32 @@
import ReportInfo from "@/Widgets/ReportInfo/ReportInfo";
import "./ReportDetailsPage.scss";
import { reportDetailsStore } from "./report-details.store";
import ReportImages from "@/Widgets/ReportImages/ReportImages";
import ReviewsSection from "@/Widgets/general/ReviewsSection/ReviewsSection";
const ReportDetailsPage = async ({
params,
}: {
params: { id: string };
}) => {
const data = await reportDetailsStore.getReportDetails(params.id);
return (
<div className="report-details-page">
<div className="report-details-page__container">
<ReportInfo
description={data?.description}
date={data?.created_at}
category={data?.category}
count_likes={data?.total_likes}
author={data?.author}
location={data?.location}
/>
<ReportImages images={data?.image!} />
</div>
<ReviewsSection id={data?.id} endpoint="report" />
</div>
);
};
export default ReportDetailsPage;

View File

@ -0,0 +1,57 @@
import { baseAPI } from "@/Shared/API/baseAPI";
import axios, { AxiosError } from "axios";
interface IAuthor {
id: number;
first_name: string;
last_name: string;
govern_status: any;
}
interface ILocation {
id: number;
latitude: string;
longitude: string;
address: string;
}
interface IImage {
id: number;
image: string;
}
interface IReport {
id: number;
created_at: string;
location: ILocation[];
category: number;
description: string;
total_likes: number;
author: IAuthor;
image: IImage[];
}
class ReportDetailsStore {
error: string;
constructor() {
this.error = "";
}
async getReportDetails(id: string) {
try {
const response = await axios.get<IReport>(
`${baseAPI}/report/${id}/`
);
return response.data;
} catch (error: unknown) {
if (error instanceof AxiosError) {
this.error = error.message;
} else {
this.error = "An error occured";
}
}
}
}
export const reportDetailsStore = new ReportDetailsStore();

View File

@ -0,0 +1,7 @@
import "./MyReportsPage.scss";
const MyReportsPage = () => {
return <div className="my-reports-page">MyReportsPage</div>;
};
export default MyReportsPage;

View File

@ -0,0 +1,7 @@
import "./PersonalDataPage.scss";
const PersonalDataPage = () => {
return <div className="personal-data-page">PersonalDataPage</div>;
};
export default PersonalDataPage;

View File

@ -0,0 +1,24 @@
.profile-layout {
padding: 118px 90px 0 90px;
display: flex;
flex-direction: column;
gap: 50px;
}
@media screen and (max-width: 1024px) {
.personal-data-page {
padding: 118px 30px 0 30px;
}
}
@media screen and (max-width: 768px) {
.personal-data-page {
padding: 112px 30px 0 30px;
}
}
@media screen and (max-width: 550px) {
.personal-data-page {
padding: 112px 16px 0 16px;
}
}

View File

@ -0,0 +1,17 @@
import HeaderText from "@/Shared/UI/HeaderText/HeaderText";
import "./ProfileLayout.scss";
import ProfileNav from "@/Widgets/ProfileNav/ProfileNav";
export default function ProfileLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
return (
<div className="profile-layout">
<HeaderText>Личный кабинет</HeaderText>
<ProfileNav />
{children}
</div>
);
}

View File

View File

@ -1,27 +0,0 @@
import { useRef } from "react";
import { DependencyList, EffectCallback, useEffect } from "react";
export function useIsFirstRender(): boolean {
const isFirst = useRef(true);
if (isFirst.current) {
isFirst.current = false;
return true;
}
return isFirst.current;
}
export function useUpdateEffect(
effect: EffectCallback,
deps?: DependencyList
) {
const isFirst = useIsFirstRender();
useEffect(() => {
if (!isFirst) {
return effect();
}
}, deps);
}

View File

@ -0,0 +1,17 @@
export const ROAD_TYPES_COLORS: Record<number, string> = {
1: "#E64452",
2: "#C288E2",
3: "#87289D",
4: "#FFAC33",
5: "#8FDE6A",
6: "#FED363",
};
export const ROAD_TYPES: Record<number, string> = {
1: "Разбитая дорога",
2: "Очаг аварийности",
3: "Локальный дефект",
4: "В планах ремонта",
5: "Отремонтировано",
6: "Локальный дефект исправлен",
};

View File

@ -0,0 +1,55 @@
.news-header {
display: flex;
flex-direction: column;
gap: 30px;
&__main-image {
width: 100%;
height: 600px;
border-radius: 8px;
object-fit: cover;
}
&__date-and-reviews {
display: flex;
align-items: center;
gap: 80px;
span {
display: flex;
align-items: center;
gap: 4px;
color: rgba(62, 50, 50, 0.75);
font-size: 15px;
font-weight: 500;
line-height: 20px;
}
}
}
@media screen and (max-width: 768px) {
.news-header {
gap: 25px;
&__main-image {
height: 392px;
}
&__date-and-reviews {
gap: 20px;
}
}
}
@media screen and (max-width: 375px) {
.news-header {
gap: 20px;
&__main-image {
height: 231px;
}
&__date-and-reviews {
flex-direction: column;
align-items: flex-start;
}
}
}

View File

@ -0,0 +1,58 @@
import "./NewsHeader.scss";
import Image from "next/image";
import message_icon from "./icons/message-icon.svg";
import calendar_icon from "./icons/calendar-icon.svg";
import React from "react";
interface INewsHeaderProps {
date: string | undefined;
image: string | undefined;
count_reviews: number | undefined;
}
const NewsHeader: React.FC<INewsHeaderProps> = ({
date,
image,
count_reviews,
}: INewsHeaderProps) => {
const months: Record<string, string> = {
"01": "Январь",
"02": "Февраль",
"03": "Март",
"04": "Апрель",
"05": "Май",
"06": "Июнь",
"07": "Июль",
"08": "Август",
"09": "Сентябрь",
"10": "Октябрь",
"11": "Ноябрь",
"12": "Декабрь",
};
const year = date?.slice(0, 4);
const month = date?.slice(5, 7);
const day = date?.slice(8, 10);
return (
<div className="news-header">
<img
src={image}
alt="News Image"
className="news-header__main-image"
/>
<div className="news-header__date-and-reviews">
<span>
<Image src={calendar_icon} alt="Calendar Icon" />
{month && months[month]} {day}, {year}
</span>
<span>
<Image src={message_icon} alt="Message Icon" />
Комментарии: {count_reviews}
</span>
</div>
</div>
);
};
export default NewsHeader;

View File

Before

Width:  |  Height:  |  Size: 747 B

After

Width:  |  Height:  |  Size: 747 B

View File

Before

Width:  |  Height:  |  Size: 724 B

After

Width:  |  Height:  |  Size: 724 B

View File

@ -1,46 +0,0 @@
import ReviewCard from "@/Entities/ReviewCard/ReviewCard";
import "./NewsReviewsSection.scss";
import CreateReview from "@/Features/CreateReview/CreateReview";
interface INewsReviewsSectionProps {
id: number | null | undefined;
list: IReview[] | undefined;
}
interface IReview {
id: number;
author: IAuthor;
review: string;
created_at: string;
}
interface IAuthor {
id: number;
first_name: string;
last_name: string;
image: string;
}
const NewsReviewsSection: React.FC<INewsReviewsSectionProps> = ({
id,
list,
}: INewsReviewsSectionProps) => {
return (
<div className="news-reviews-section">
<CreateReview endpoint="news" id={id} />
<div className="news-reviews-section__container">
<h3>
<span />
Комментарии
</h3>
<div>
{list?.map((item) => (
<ReviewCard key={item.id} item={item} />
))}
</div>
</div>
</div>
);
};
export default NewsReviewsSection;

View File

@ -0,0 +1,47 @@
.profile-nav {
display: flex;
align-items: center;
justify-content: space-between;
&__links {
display: flex;
align-items: center;
gap: 46px;
&_right {
display: flex;
align-items: center;
gap: 16px;
div {
padding: 2px 10px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 16px;
background: rgb(224, 237, 248);
color: rgb(23, 92, 211);
font-size: 14px;
font-weight: 500;
}
}
a {
color: rgb(50, 48, 58);
font-size: 20px;
font-weight: 500;
line-height: 24px;
}
}
&__logout {
padding: 10px;
border: 1px solid rgb(230, 68, 82);
border-radius: 8px;
color: rgb(230, 68, 82);
font-size: 16px;
font-weight: 500;
}
}

View File

@ -0,0 +1,51 @@
"use client";
import { usePathname } from "next/navigation";
import "./ProfileNav.scss";
import Link from "next/link";
import { signOut } from "next-auth/react";
const ProfileNav = () => {
const pathname = usePathname();
return (
<div className="profile-nav">
<div className="profile-nav__links">
<Link
style={{
textDecoration: `${
pathname === "/profile/personal-data" ? "underline" : ""
}`,
}}
href="/profile/personal-data"
>
Личные данные
</Link>
<div className="profile-nav__links_right">
<Link
style={{
textDecoration: `${
pathname === "/profile/my-reports" ? "underline" : ""
}`,
}}
href="/profile/my-reports"
>
Мои обращения
</Link>
<div>3</div>
</div>
</div>
<button
onClick={() =>
signOut({
callbackUrl: "/",
})
}
className="profile-nav__logout"
>
Выйти из аккаунта
</button>
</div>
);
};
export default ProfileNav;

View File

@ -0,0 +1,42 @@
.report-images {
display: grid;
grid-template-columns: 1fr 1fr 1fr 1fr;
gap: 20px;
img {
border-radius: 10px;
object-fit: cover;
}
&-1 {
width: 100%;
grid-column: 1 / 5;
height: 441px;
}
&-default-1,
&-default-2,
&-default-3,
&-default-4,
&-default-5 {
height: 102px;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 10px;
border-radius: 10px;
background-color: rgb(209, 217, 226);
img {
height: 36px;
width: 36px;
}
}
&-default-1 {
grid-column: 1 / 5;
height: 441px;
}
}

View File

@ -0,0 +1,50 @@
import Image from "next/image";
import "./ReportImages.scss";
import default_image_icon from "./icons/default-image.svg";
interface IImage {
id: number;
image: string;
}
interface IReportImagesProps {
images: IImage[];
}
const ReportImages: React.FC<IReportImagesProps> = ({
images,
}: IReportImagesProps) => {
const showImages = () => {
let result = [];
for (let i = 0; i < 5; i++) {
if (images.length > i) {
result.push(
<img
className={`report-images-${i + 1}`}
key={i}
src={images[i].image}
alt="Road Image"
/>
);
} else {
result.push(
<div className={`report-images-default-${i + 1}`} key={i}>
<Image
src={default_image_icon}
alt="Default Image Icon"
/>
{i + 1 === 1
? "Пользователь не загрузил изображения"
: ""}
</div>
);
}
}
return result;
};
return <div className="report-images">{showImages()}</div>;
};
export default ReportImages;

View 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

View File

@ -0,0 +1,61 @@
@import "../../Shared/variables.scss";
.report-info {
padding-top: 50px;
display: flex;
flex-direction: column;
gap: 24px;
&__date-and-likes {
display: flex;
gap: 80px;
}
&__date {
display: flex;
align-items: center;
gap: 10px;
color: rgba(62, 50, 50, 0.75);
font-size: 15px;
font-weight: 500;
line-height: 20px;
}
p {
color: rgb(62, 50, 50);
font-size: 20px;
font-weight: 400;
line-height: 34px;
}
h5 {
font-size: 20px;
font-weight: 400;
line-height: 34px;
color: rgb(102, 102, 102);
span {
color: $light-blue;
text-decoration: underline;
}
}
&__show-map {
width: fit-content;
display: flex;
align-items: center;
gap: 12px;
span {
color: rgb(102, 102, 102);
font-size: 20px;
font-weight: 400;
text-decoration: underline;
}
}
h5 {
margin-bottom: 40px;
}
}

View File

@ -0,0 +1,94 @@
import "./ReportInfo.scss";
import Image from "next/image";
import RoadType from "@/Entities/RoadType/RoadType";
import HeaderText from "@/Shared/UI/HeaderText/HeaderText";
import ReportLike from "@/Features/ReportLike/ReportLike";
import calendar_icon from "./icons/calendar-icon.svg";
import map_pin_icon from "./icons/map-pin.svg";
import {
ROAD_TYPES,
ROAD_TYPES_COLORS,
} from "@/Shared/variables/road-types";
interface IAuthor {
id: number;
first_name: string;
last_name: string;
govern_status: any;
}
interface ILocation {
id: number;
latitude: string;
longitude: string;
address: string;
}
interface IReportInfoProps {
description: string | undefined;
date: string | undefined;
category: number | undefined;
count_likes: number | undefined;
author: IAuthor | undefined;
location: ILocation[] | undefined;
}
const ReportInfo: React.FC<IReportInfoProps> = ({
description,
date,
category = 1,
count_likes,
author,
location = [],
}: IReportInfoProps) => {
const months: Record<string, string> = {
"01": "Январь",
"02": "Февраль",
"03": "Март",
"04": "Апрель",
"05": "Май",
"06": "Июнь",
"07": "Июль",
"08": "Август",
"09": "Сентябрь",
"10": "Октябрь",
"11": "Ноябрь",
"12": "Декабрь",
};
const year = date?.slice(0, 4);
const month = date?.slice(5, 7);
const day = date?.slice(8, 10);
return (
<div className="report-info">
<RoadType color={ROAD_TYPES_COLORS[category]}>
{ROAD_TYPES[category]}
</RoadType>
<HeaderText>{location[0].address}</HeaderText>
<div className="report-info__date-and-likes">
<div className="report-info__date">
<Image src={calendar_icon} alt="Calendar Icon" />
{day} {months[month!]}, {year}
</div>
<ReportLike count_likes={count_likes!} />
</div>
<p>{description}</p>
<h5>
Автор обращения:{" "}
<span>
{author?.first_name}. {author?.last_name.slice(0, 1)}
</span>
</h5>
<button className="report-info__show-map">
<Image src={map_pin_icon} alt="Map Pin Icon" />
<span>Показать на карте</span>
</button>
</div>
);
};
export default ReportInfo;

View File

@ -0,0 +1,8 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<g id="calendar">
<path id="Vector" d="M12.6667 2.66699H3.33333C2.59695 2.66699 2 3.26395 2 4.00033V13.3337C2 14.07 2.59695 14.667 3.33333 14.667H12.6667C13.403 14.667 14 14.07 14 13.3337V4.00033C14 3.26395 13.403 2.66699 12.6667 2.66699Z" stroke="#6E6565" stroke-linecap="round" stroke-linejoin="round"/>
<path id="Vector_2" d="M10.666 1.33301V3.99967" stroke="#6E6565" stroke-linecap="round" stroke-linejoin="round"/>
<path id="Vector_3" d="M5.33398 1.33301V3.99967" stroke="#6E6565" stroke-linecap="round" stroke-linejoin="round"/>
<path id="Vector_4" d="M2 6.66699H14" stroke="#6E6565" stroke-linecap="round" stroke-linejoin="round"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 747 B

View 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

View File

@ -2,7 +2,7 @@
.nav {
position: fixed;
z-index: 15;
z-index: 10000;
width: 100%;
padding: 0 90px;
height: 78px;

View File

@ -1,4 +1,4 @@
.news-reviews-section {
.reviews-section {
display: flex;
flex-direction: column;
gap: 70px;
@ -20,15 +20,35 @@
}
}
&__container {
&__container,
&__list {
display: flex;
flex-direction: column;
gap: 24px;
}
&__list {
display: flex;
flex-direction: column;
gap: 30px;
}
}
@media screen and (max-width: 768px) {
.reviews-section {
gap: 40px;
&__list {
gap: 25px;
}
}
}
@media screen and (max-width: 550px) {
.reviews-section {
h3 {
font-size: 20px;
}
&__list {
gap: 13px;
}
}
}

View File

@ -0,0 +1,34 @@
import "./ReviewsSection.scss";
import ReviewCard from "@/Entities/ReviewCard/ReviewCard";
import CreateReview from "@/Features/CreateReview/CreateReview";
import { newsReviewStore } from "./store";
interface INewsReviewsSectionProps {
id: number | null | undefined;
endpoint: string;
}
const ReviewsSection: React.FC<INewsReviewsSectionProps> = async ({
id,
endpoint,
}: INewsReviewsSectionProps) => {
const list = await newsReviewStore.getReviews(endpoint, id);
return (
<div className="reviews-section">
<CreateReview endpoint="news" id={id} />
<div className="reviews-section__container">
<h3>
<span />
Комментарии
</h3>
<div className="reviews-section__list">
{list?.results.map((item) => (
<ReviewCard key={item.id} item={item} />
))}
</div>
</div>
</div>
);
};
export default ReviewsSection;

View File

@ -0,0 +1,46 @@
import { baseAPI } from "@/Shared/API/baseAPI";
import { IList } from "@/Shared/types";
import axios, { AxiosError } from "axios";
interface IReview extends IList {
results: IResult[];
}
interface IResult {
id: number;
author: IAuthor;
review: string;
created_at: string;
}
interface IAuthor {
id: number;
first_name: string;
last_name: string;
image: string;
}
class NewsReviewsStore {
error: string;
constructor() {
this.error = "";
}
async getReviews(endpoint: string, id: number | null | undefined) {
try {
const response = await axios.get<IReview>(
`${baseAPI}/${endpoint}/${id}/reviews/`
);
return response.data;
} catch (error: unknown) {
if (error instanceof AxiosError) {
this.error = error.message;
} else {
this.error = "An error ocured";
}
}
}
}
export const newsReviewStore = new NewsReviewsStore();

View File

@ -22,7 +22,7 @@ const Header = () => {
</p>
</div>
<Link href="/send-report" className="header__report">
<Link href="/create-report" className="header__report">
Отметить разбитую дорогу
<Image src={arrow_icon} alt="Arrow Right Icon" />
</Link>

View File

@ -28,14 +28,6 @@
font-weight: 400;
}
}
&__map {
height: 580px;
width: 100%;
border-radius: 10px;
object-fit: cover;
}
}
@media screen and (max-width: 1024px) {

View File

@ -1,52 +1,123 @@
import Image from "next/image";
"use client";
import "./MapSection.scss";
import search_icon from "./icons/search-icon.svg";
import Switch from "@/Entities/Switch/Switch";
import map_image from "./assets/map.jpg";
import SectionHeader from "@/Shared/UI/SectionHeader/SectionHeader";
import SearchBar from "@/Features/SearchBar/SearchBar";
import HomeMap from "@/Features/HomeMap/HomeMap";
import { useEffect, useState } from "react";
import { useMapStore } from "./map.store";
import { useShallow } from "zustand/react/shallow";
import { useDebounce } from "use-debounce";
import { useRouter } from "next/navigation";
enum ESwitch {
BUTTON,
A,
}
interface IMapSection {
[key: string]: string;
}
const MapSection: React.FC<IMapSection> = ({
queryMap,
queryRating,
categories = "1,2,3,4,5,6",
}: IMapSection) => {
const [searchLocation, setSearchLocation] =
useState<string>(queryMap);
const [query] = useDebounce(searchLocation, 500);
const router = useRouter();
const switches = [
{ title: "Разбитая дорога", category: "1", color: "#E64452" },
{ title: "В планах ремонта", category: "4", color: "#FFAC33" },
{ title: "Очаг аварийности", category: "2", color: "#C288E2" },
{ title: "Отремонтировано", category: "5", color: "#8FDE6A" },
{ title: "Локальный дефект", category: "3", color: "#87289D" },
{
title: "Локальный дефект исправлен'",
category: "6",
color: "#FED363",
},
];
const data = useMapStore(useShallow((state) => state.data));
const getRatings = useMapStore(
useShallow((state) => state.getRatings)
);
useEffect(() => {
getRatings(categories);
}, [categories]);
useEffect(() => {
router.push(
`/?тип-дороги=${categories}${
query ? `&карта-дорог=${query}` : ""
}${queryRating ? `&рейтинг=${queryRating}` : ""}`,
{
scroll: false,
}
);
}, [query, router, categories]);
const setSearchParams = (category: string) => {
const availableCategories: Record<string, number> = {
"1": 1,
"2": 2,
"3": 3,
"4": 4,
"5": 5,
"6": 6,
};
if (!categories || !availableCategories[category])
return categories;
if (categories?.includes(category)) {
const updatedString = categories
?.replace(category + ",", "")
.replace("," + category, "")
.replace(category, "");
return updatedString;
} else {
const newValue = category + ",";
const updatedString = newValue + categories;
return updatedString;
}
};
const MapSection = () => {
return (
<div className="map-section">
<SectionHeader description="Yorem ipsum dolor sit amet, consectetur adipiscing elit.">
Карта дорог
</SectionHeader>
<SearchBar placeholder="Введите город, село или регион" />
<SearchBar
value={searchLocation}
setValue={setSearchLocation}
placeholder="Введите город, село или регион"
/>
<div className="map-section__filters">
<div className="map-section__switch">
<Switch color="#E64452" />
<h4>Разбитая дорога</h4>
</div>
<div className="map-section__switch">
<Switch color="#FFAC33" />
<h4>В планах ремонта</h4>
</div>
<div className="map-section__switch">
<Switch color="#C288E2" />
<h4>Очаг аварийности</h4>
</div>
<div className="map-section__switch">
<Switch color="#FED363" />
<h4>Локальный дефект исправлен</h4>
</div>
<div className="map-section__switch">
<Switch color="#87289D" />
<h4>Локальный дефект</h4>
</div>
<div className="map-section__switch">
<Switch color="#8FDE6A" />
<h4>Отремонтировано</h4>
{switches.map((sw) => (
<div key={sw.category} className="map-section__switch">
<Switch
defaultState={
categories?.includes(sw.category) || categories === ""
}
href={`/?тип-дороги=${setSearchParams(sw.category)}`}
type={ESwitch.A}
color={sw.color}
/>
<h4>{sw.title}</h4>
</div>
))}
</div>
<Image
className="map-section__map"
src={map_image}
alt="Map Image"
/>
<HomeMap data={data?.results} />
</div>
);
};

View File

@ -0,0 +1,5 @@
<svg width="38" height="50" viewBox="0 0 38 50" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M19 0C8.52334 0 0 8.74812 0 19.501C0 32.8456 17.0031 48.5901 17.7271 49.4175C18.407 50.1948 19.5942 50.1935 20.2729 49.4175C20.9969 48.5901 38 32.8456 38 19.501C37.9998 8.74812 29.4766 0 19 0Z" fill="#3998E8" fill-opacity="0.7"/>
<ellipse cx="18.7867" cy="19" rx="9.88775" ry="10" fill="white"/>
<circle cx="18.5" cy="18.5" r="7.5" fill="#8FDE6A"/>
</svg>

After

Width:  |  Height:  |  Size: 461 B

View File

@ -0,0 +1,5 @@
<svg width="38" height="50" viewBox="0 0 38 50" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M19 0C8.52334 0 0 8.74812 0 19.501C0 32.8456 17.0031 48.5901 17.7271 49.4175C18.407 50.1948 19.5942 50.1935 20.2729 49.4175C20.9969 48.5901 38 32.8456 38 19.501C37.9998 8.74812 29.4766 0 19 0Z" fill="#3998E8" fill-opacity="0.7"/>
<ellipse cx="18.7867" cy="19" rx="9.88775" ry="10" fill="white"/>
<circle cx="18.5" cy="18.5" r="7.5" fill="#FFAC33"/>
</svg>

After

Width:  |  Height:  |  Size: 461 B

View File

@ -0,0 +1,5 @@
<svg width="38" height="50" viewBox="0 0 38 50" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M19 0C8.52334 0 0 8.74812 0 19.501C0 32.8456 17.0031 48.5901 17.7271 49.4175C18.407 50.1948 19.5942 50.1935 20.2729 49.4175C20.9969 48.5901 38 32.8456 38 19.501C37.9998 8.74812 29.4766 0 19 0Z" fill="#3998E8" fill-opacity="0.7"/>
<ellipse cx="18.7867" cy="19" rx="9.88775" ry="10" fill="white"/>
<circle cx="18.5" cy="18.5" r="7.5" fill="#C288E2"/>
</svg>

After

Width:  |  Height:  |  Size: 461 B

View File

@ -0,0 +1,5 @@
<svg width="38" height="50" viewBox="0 0 38 50" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M19 0C8.52334 0 0 8.74812 0 19.501C0 32.8456 17.0031 48.5901 17.7271 49.4175C18.407 50.1948 19.5942 50.1935 20.2729 49.4175C20.9969 48.5901 38 32.8456 38 19.501C37.9998 8.74812 29.4766 0 19 0Z" fill="#3998E8" fill-opacity="0.7"/>
<ellipse cx="18.7867" cy="19" rx="9.88775" ry="10" fill="white"/>
<circle cx="18.5" cy="18.5" r="7.5" fill="#87289D"/>
</svg>

After

Width:  |  Height:  |  Size: 461 B

View File

@ -0,0 +1,5 @@
<svg width="38" height="50" viewBox="0 0 38 50" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M19 0C8.52334 0 0 8.74812 0 19.501C0 32.8456 17.0031 48.5901 17.7271 49.4175C18.407 50.1948 19.5942 50.1935 20.2729 49.4175C20.9969 48.5901 38 32.8456 38 19.501C37.9998 8.74812 29.4766 0 19 0Z" fill="#3998E8" fill-opacity="0.7"/>
<ellipse cx="18.7867" cy="19" rx="9.88775" ry="10" fill="white"/>
<circle cx="18.5" cy="18.5" r="7.5" fill="#E64452"/>
</svg>

After

Width:  |  Height:  |  Size: 461 B

View File

@ -0,0 +1,5 @@
<svg width="38" height="50" viewBox="0 0 38 50" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M19 0C8.52334 0 0 8.74812 0 19.501C0 32.8456 17.0031 48.5901 17.7271 49.4175C18.407 50.1948 19.5942 50.1935 20.2729 49.4175C20.9969 48.5901 38 32.8456 38 19.501C37.9998 8.74812 29.4766 0 19 0Z" fill="#3998E8" fill-opacity="0.7"/>
<ellipse cx="18.7867" cy="19" rx="9.88775" ry="10" fill="white"/>
<circle cx="18.5" cy="18.5" r="7.5" fill="#FED363"/>
</svg>

After

Width:  |  Height:  |  Size: 461 B

View File

@ -0,0 +1,39 @@
import { baseAPI } from "@/Shared/API/baseAPI";
import { IFetch } from "@/Shared/types";
import axios from "axios";
import { create } from "zustand";
import { ILocation, IReport } from "./types";
interface IMapStore extends IFetch {
data: IReport;
getRatings: (categories: string) => Promise<void>;
}
export const useMapStore = create<IMapStore>((set) => ({
data: {
count: 0,
previous: null,
next: null,
results: [],
},
loading: false,
error: "",
getRatings: async (
categories: string = "1,2,3,4,5,6",
query: string = ""
) => {
try {
set({ loading: true });
const response = await axios.get<IReport>(
`${baseAPI}/report/?category=${categories}`
);
set({ data: response.data });
} catch (error: any) {
set({ error: error.message });
} finally {
set({ loading: false });
}
},
}));

View File

@ -0,0 +1,22 @@
import { IList } from "@/Shared/types";
export interface ILocation {
id: number;
latitude: string;
longitude: string;
address: string;
}
interface IResults {
id: number;
created_at: string;
location: ILocation[];
category: number;
description: string;
count_reviews: number;
total_likes: number;
}
export interface IReport extends IList {
results: IResults[];
}

View File

@ -23,7 +23,7 @@ const NewsSection = () => {
<ul className="news-section__list">
{data.map((card) => (
<li key={card.id} className="news-card">
<li key={card.id} className="news-section__card">
<NewsCard
id={card.id}
title={card.title}

View File

@ -76,15 +76,6 @@
#report-type {
min-width: 210px;
span {
width: 120px;
padding: 4px 12px;
font-size: 14px;
border-radius: 20px;
background: rgba(230, 68, 82, 0.8);
color: #fff;
}
}
#report-description {

View File

@ -9,15 +9,45 @@ import like_icon from "./icons/like-icon.svg";
import message_icon from "./icons/message-icon.svg";
import Image from "next/image";
import { useRating } from "./rating.store";
import { useEffect } from "react";
import React, { useEffect, useState } from "react";
import Link from "next/link";
import { useRouter } from "next/navigation";
import { useDebounce } from "use-debounce";
import RoadType from "@/Entities/RoadType/RoadType";
import {
ROAD_TYPES,
ROAD_TYPES_COLORS,
} from "@/Shared/variables/road-types";
const RatingSection = () => {
interface IRatingSection {
[key: string]: string;
}
const RatingSection: React.FC<IRatingSection> = ({
categories = "1,2,3,4,5,6",
queryMap,
queryRating,
}: IRatingSection) => {
const { data: reports, getRatings } = useRating();
const [search, setSearch] = useState<string>(queryRating);
const [query] = useDebounce(search, 500);
const router = useRouter();
useEffect(() => {
getRatings();
}, []);
getRatings(query);
}, [query]);
useEffect(() => {
router.push(
`/?тип-дороги=${categories}${
queryMap ? `&карта-дорог=${queryMap}` : ""
}${query ? `&рейтинг=${query}` : ""}`,
{
scroll: false,
}
);
}, [query, router, categories]);
const sliceDate = (date: string) => {
return `${date.slice(8, 10)}.${date.slice(5, 7)}.${date.slice(
@ -45,7 +75,11 @@ const RatingSection = () => {
<SectionHeader description="Yorem ipsum dolor sit amet, consectetur adipiscing elit.">
Рейтинг
</SectionHeader>
<SearchBar placeholder="Введите адрес" />
<SearchBar
value={search}
setValue={setSearch}
placeholder="Введите адрес"
/>
<div className="rating-section__table">
<table>
@ -89,7 +123,11 @@ const RatingSection = () => {
</Link>
</td>
<td id="report-type">
<span>Разбитая дорога</span>
<RoadType
color={ROAD_TYPES_COLORS[report.category]}
>
{ROAD_TYPES[report.category]}
</RoadType>
</td>
<td id="report-description">
<p>{sliceDescription(report.description)}</p>

View File

@ -6,23 +6,36 @@ import { IReport } from "./types";
interface IRatingStore extends IFetch {
data: IReport;
getRatings: () => Promise<void>;
getRatings: (query: string) => Promise<void>;
}
export const useRating = create<IRatingStore>((set) => ({
data: {
count: 0,
previous: null,
next: null,
results: [],
},
loading: false,
error: "",
getRatings: async () => {
getRatings: async (query: string = "") => {
try {
set({ loading: true });
const response = await axios.get<IReport>(`${baseAPI}/report/`);
const data = (await axios.get<IReport>(`${baseAPI}/report/`))
.data;
set({ data: response.data });
const searched = data.results.filter((rating) => {
return rating.location.some((location) => {
return location.address
.toLowerCase()
.includes(query.toLowerCase());
});
});
data.results = [...searched];
set({ data: data });
} catch (error: any) {
set({ error: error.message });
} finally {

View File

@ -1,13 +1,12 @@
import { IList } from "@/Shared/types";
export interface ILocation {
id: number;
latitude: string;
longitude: string;
address: string;
}
export interface IReport {
count: number;
results: IResults[];
}
interface IResults {
id: number;
created_at: string;
@ -17,3 +16,7 @@ interface IResults {
count_reviews: number;
total_likes: number;
}
export interface IReport extends IList {
results: IResults[];
}

3
src/middleware.ts Normal file
View File

@ -0,0 +1,3 @@
export { default } from "next-auth/middleware";
export const config = { matcher: ["/profile/personal-data"] };

View File

@ -1,5 +1,6 @@
{
"compilerOptions": {
"target": "ES2015",
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,

137
yarn.lock
View File

@ -7,6 +7,13 @@
resolved "https://registry.yarnpkg.com/@aashutoshrathi/word-wrap/-/word-wrap-1.2.6.tgz#bd9154aec9983f77b3a034ecaa015c2e4201f6cf"
integrity sha512-1Yjs2SvM8TflER/OD3cOjhWWOZb58A2t7wpE2S9XfBYTiIl+XFhQG2bjy4Pu1I+EAlCNUzRDYDdFwFYUKvXcIA==
"@babel/runtime@^7.20.13":
version "7.23.9"
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.23.9.tgz#47791a15e4603bb5f905bc0753801cf21d6345f7"
integrity sha512-0CX6F+BI2s9dkUqr08KFrAIZgNFj75rdBU/DjCyYLIaV/quFjkk6T+EJ2LkZHyZTbEV4L5p97mNkUsHl2wLFAw==
dependencies:
regenerator-runtime "^0.14.0"
"@babel/runtime@^7.23.2":
version "7.23.8"
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.23.8.tgz#8ee6fe1ac47add7122902f257b8ddf55c898f650"
@ -155,11 +162,21 @@
"@nodelib/fs.scandir" "2.1.5"
fastq "^1.6.0"
"@panva/hkdf@^1.0.2":
version "1.1.1"
resolved "https://registry.yarnpkg.com/@panva/hkdf/-/hkdf-1.1.1.tgz#ab9cd8755d1976e72fc77a00f7655a64efe6cd5d"
integrity sha512-dhPeilub1NuIG0X5Kvhh9lH4iW3ZsHlnzwgwbOlgwQ2wG1IqFzsgHqmKPk3WzsdWAeaxKJxgM0+W433RmN45GA==
"@pkgjs/parseargs@^0.11.0":
version "0.11.0"
resolved "https://registry.yarnpkg.com/@pkgjs/parseargs/-/parseargs-0.11.0.tgz#a77ea742fab25775145434eb1d2328cf5013ac33"
integrity sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==
"@react-leaflet/core@^2.1.0":
version "2.1.0"
resolved "https://registry.yarnpkg.com/@react-leaflet/core/-/core-2.1.0.tgz#383acd31259d7c9ae8fb1b02d5e18fe613c2a13d"
integrity sha512-Qk7Pfu8BSarKGqILj4x7bCSZ1pjuAPZ+qmRwH5S7mDS91VSbVVsJSrW4qA+GPrro8t69gFYVMWb1Zc4yFmPiVg==
"@rushstack/eslint-patch@^1.3.3":
version "1.7.0"
resolved "https://registry.yarnpkg.com/@rushstack/eslint-patch/-/eslint-patch-1.7.0.tgz#b5bc1e081428794f6a4d239707b359404be35ce2"
@ -172,11 +189,23 @@
dependencies:
tslib "^2.4.0"
"@types/geojson@*":
version "7946.0.13"
resolved "https://registry.yarnpkg.com/@types/geojson/-/geojson-7946.0.13.tgz#e6e77ea9ecf36564980a861e24e62a095988775e"
integrity sha512-bmrNrgKMOhM3WsafmbGmC+6dsF2Z308vLFsQ3a/bT8X8Sv5clVYpPars/UPq+sAaJP+5OoLAYgwbkS5QEJdLUQ==
"@types/json5@^0.0.29":
version "0.0.29"
resolved "https://registry.yarnpkg.com/@types/json5/-/json5-0.0.29.tgz#ee28707ae94e11d2b827bcbe5270bcea7f3e71ee"
integrity sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==
"@types/leaflet@^1.9.8":
version "1.9.8"
resolved "https://registry.yarnpkg.com/@types/leaflet/-/leaflet-1.9.8.tgz#32162a8eaf305c63267e99470b9603b5883e63e8"
integrity sha512-EXdsL4EhoUtGm2GC2ZYtXn+Fzc6pluVgagvo2VC1RHWToLGlTRwVYoDpqS/7QXa01rmDyBjJk3Catpf60VMkwg==
dependencies:
"@types/geojson" "*"
"@types/node@^20":
version "20.11.5"
resolved "https://registry.yarnpkg.com/@types/node/-/node-20.11.5.tgz#be10c622ca7fcaa3cf226cf80166abc31389d86e"
@ -196,6 +225,13 @@
dependencies:
"@types/react" "*"
"@types/react-leaflet@^3.0.0":
version "3.0.0"
resolved "https://registry.yarnpkg.com/@types/react-leaflet/-/react-leaflet-3.0.0.tgz#b27a50abb6e3ae734d3c15399a26c77c161cab1c"
integrity sha512-p8R9mVKbCDDqOdW+M6GyJJuFn6q+IgDFYavFiOIvaWHuOe5kIHZEtCy1pfM43JIA6JiB3D/aDoby7C51eO+XSg==
dependencies:
react-leaflet "*"
"@types/react@*", "@types/react@^18":
version "18.2.48"
resolved "https://registry.yarnpkg.com/@types/react/-/react-18.2.48.tgz#11df5664642d0bd879c1f58bc1d37205b064e8f1"
@ -526,7 +562,7 @@ chalk@^4.0.0:
optionalDependencies:
fsevents "~2.3.2"
client-only@0.0.1, client-only@^0.0.1:
client-only@0.0.1:
version "0.0.1"
resolved "https://registry.yarnpkg.com/client-only/-/client-only-0.0.1.tgz#38bba5d403c41ab150bff64a95c85013cf73bca1"
integrity sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==
@ -555,6 +591,11 @@ concat-map@0.0.1:
resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b"
integrity sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==
cookie@^0.5.0:
version "0.5.0"
resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.5.0.tgz#d1f5d71adec6558c58f389987c366aa47e994f8b"
integrity sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==
cross-spawn@^7.0.0, cross-spawn@^7.0.2:
version "7.0.3"
resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.3.tgz#f73a85b9d5d41d045551c177e2882d4ac85728a6"
@ -1491,6 +1532,11 @@ jackspeak@^2.3.5:
optionalDependencies:
"@pkgjs/parseargs" "^0.11.0"
jose@^4.11.4, jose@^4.15.4:
version "4.15.4"
resolved "https://registry.yarnpkg.com/jose/-/jose-4.15.4.tgz#02a9a763803e3872cf55f29ecef0dfdcc218cc03"
integrity sha512-W+oqK4H+r5sITxfxpSU+MMdr/YSWGvgZMQDIsNoBDGGy4i7GBPTtvFKibQzW06n3U3TqHjhvBJsirShsEJ6eeQ==
"js-tokens@^3.0.0 || ^4.0.0":
version "4.0.0"
resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499"
@ -1554,6 +1600,11 @@ language-tags@^1.0.9:
dependencies:
language-subtag-registry "^0.3.20"
leaflet@^1.9.4:
version "1.9.4"
resolved "https://registry.yarnpkg.com/leaflet/-/leaflet-1.9.4.tgz#23fae724e282fa25745aff82ca4d394748db7d8d"
integrity sha512-nxS1ynzJOmOlHp+iL3FyWqK89GtNL8U8rvlMOsQdTTssxZwCXh8N2NB3GDQOL+YR3XnWyZAxwQixURb+FA74PA==
levn@^0.4.1:
version "0.4.1"
resolved "https://registry.yarnpkg.com/levn/-/levn-0.4.1.tgz#ae4562c007473b932a6200d403268dd2fffc6ade"
@ -1662,6 +1713,21 @@ natural-compare@^1.4.0:
resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7"
integrity sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==
next-auth@^4.24.5:
version "4.24.5"
resolved "https://registry.yarnpkg.com/next-auth/-/next-auth-4.24.5.tgz#1fd1bfc0603c61fd2ba6fd81b976af690edbf07e"
integrity sha512-3RafV3XbfIKk6rF6GlLE4/KxjTcuMCifqrmD+98ejFq73SRoj2rmzoca8u764977lH/Q7jo6Xu6yM+Re1Mz/Og==
dependencies:
"@babel/runtime" "^7.20.13"
"@panva/hkdf" "^1.0.2"
cookie "^0.5.0"
jose "^4.11.4"
oauth "^0.9.15"
openid-client "^5.4.0"
preact "^10.6.3"
preact-render-to-string "^5.1.19"
uuid "^8.3.2"
next@14.1.0:
version "14.1.0"
resolved "https://registry.yarnpkg.com/next/-/next-14.1.0.tgz#b31c0261ff9caa6b4a17c5af019ed77387174b69"
@ -1690,11 +1756,21 @@ normalize-path@^3.0.0, normalize-path@~3.0.0:
resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-3.0.0.tgz#0dcd69ff23a1c9b11fd0978316644a0388216a65"
integrity sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==
oauth@^0.9.15:
version "0.9.15"
resolved "https://registry.yarnpkg.com/oauth/-/oauth-0.9.15.tgz#bd1fefaf686c96b75475aed5196412ff60cfb9c1"
integrity sha512-a5ERWK1kh38ExDEfoO6qUHJb32rd7aYmPHuyCu3Fta/cnICvYmgd2uhuKXvPD+PXB+gCEYYEaQdIRAjCOwAKNA==
object-assign@^4.1.1:
version "4.1.1"
resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863"
integrity sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==
object-hash@^2.2.0:
version "2.2.0"
resolved "https://registry.yarnpkg.com/object-hash/-/object-hash-2.2.0.tgz#5ad518581eefc443bd763472b8ff2e9c2c0d54a5"
integrity sha512-gScRMn0bS5fH+IuwyIFgnh9zBdo4DV+6GhygmWM9HyNJSgS0hScp1f5vjtm7oIIOiT9trXrShAkLFSc2IqKNgw==
object-inspect@^1.13.1, object-inspect@^1.9.0:
version "1.13.1"
resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.13.1.tgz#b96c6109324ccfef6b12216a956ca4dc2ff94bc2"
@ -1760,6 +1836,11 @@ object.values@^1.1.6, object.values@^1.1.7:
define-properties "^1.2.0"
es-abstract "^1.22.1"
oidc-token-hash@^5.0.3:
version "5.0.3"
resolved "https://registry.yarnpkg.com/oidc-token-hash/-/oidc-token-hash-5.0.3.tgz#9a229f0a1ce9d4fc89bcaee5478c97a889e7b7b6"
integrity sha512-IF4PcGgzAr6XXSff26Sk/+P4KZFJVuHAJZj3wgO3vX2bMdNVp/QXTP3P7CEm9V1IdG8lDLY3HhiqpsE/nOwpPw==
once@^1.3.0:
version "1.4.0"
resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1"
@ -1767,6 +1848,16 @@ once@^1.3.0:
dependencies:
wrappy "1"
openid-client@^5.4.0:
version "5.6.4"
resolved "https://registry.yarnpkg.com/openid-client/-/openid-client-5.6.4.tgz#b2c25e6d5338ba3ce00e04341bb286798a196177"
integrity sha512-T1h3B10BRPKfcObdBklX639tVz+xh34O7GjofqrqiAQdm7eHsQ00ih18x6wuJ/E6FxdtS2u3FmUGPDeEcMwzNA==
dependencies:
jose "^4.15.4"
lru-cache "^6.0.0"
object-hash "^2.2.0"
oidc-token-hash "^5.0.3"
optionator@^0.9.3:
version "0.9.3"
resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.9.3.tgz#007397d44ed1872fdc6ed31360190f81814e2c64"
@ -1852,11 +1943,28 @@ postcss@8.4.31:
picocolors "^1.0.0"
source-map-js "^1.0.2"
preact-render-to-string@^5.1.19:
version "5.2.6"
resolved "https://registry.yarnpkg.com/preact-render-to-string/-/preact-render-to-string-5.2.6.tgz#0ff0c86cd118d30affb825193f18e92bd59d0604"
integrity sha512-JyhErpYOvBV1hEPwIxc/fHWXPfnEGdRKxc8gFdAZ7XV4tlzyzG847XAyEZqoDnynP88akM4eaHcSOzNcLWFguw==
dependencies:
pretty-format "^3.8.0"
preact@^10.6.3:
version "10.19.3"
resolved "https://registry.yarnpkg.com/preact/-/preact-10.19.3.tgz#7a7107ed2598a60676c943709ea3efb8aaafa899"
integrity sha512-nHHTeFVBTHRGxJXKkKu5hT8C/YWBkPso4/Gad6xuj5dbptt9iF9NZr9pHbPhBrnT2klheu7mHTxTZ/LjwJiEiQ==
prelude-ls@^1.2.1:
version "1.2.1"
resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.2.1.tgz#debc6489d7a6e6b0e7611888cec880337d316396"
integrity sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==
pretty-format@^3.8.0:
version "3.8.0"
resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-3.8.0.tgz#bfbed56d5e9a776645f4b1ff7aa1a3ac4fa3c385"
integrity sha512-WuxUnVtlWL1OfZFQFuqvnvs6MiAGk9UNsBostyBOB0Is9wb5uRESevA6rnl/rkksXaGX3GzZhPup5d6Vp1nFew==
prop-types@^15.8.1:
version "15.8.1"
resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.8.1.tgz#67d87bf1a694f48435cf332c24af10214a3140b5"
@ -1894,6 +2002,13 @@ react-is@^16.13.1:
resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4"
integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==
react-leaflet@*, react-leaflet@^4.2.1:
version "4.2.1"
resolved "https://registry.yarnpkg.com/react-leaflet/-/react-leaflet-4.2.1.tgz#c300e9eccaf15cb40757552e181200aa10b94780"
integrity sha512-p9chkvhcKrWn/H/1FFeVSqLdReGwn2qmiobOQGO3BifX+/vV/39qhY8dGqbdcPh1e6jxh/QHriLXr7a4eLFK4Q==
dependencies:
"@react-leaflet/core" "^2.1.0"
react@^18:
version "18.2.0"
resolved "https://registry.yarnpkg.com/react/-/react-18.2.0.tgz#555bd98592883255fa00de14f1151a917b5d77d5"
@ -2193,14 +2308,6 @@ supports-preserve-symlinks-flag@^1.0.0:
resolved "https://registry.yarnpkg.com/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz#6eda4bd344a3c94aea376d4cc31bc77311039e09"
integrity sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==
swr@^2.2.4:
version "2.2.4"
resolved "https://registry.yarnpkg.com/swr/-/swr-2.2.4.tgz#03ec4c56019902fbdc904d78544bd7a9a6fa3f07"
integrity sha512-njiZ/4RiIhoOlAaLYDqwz5qH/KZXVilRLvomrx83HjzCWTfa+InyfAjv05PSFxnmLzZkNO9ZfvgoqzAaEI4sGQ==
dependencies:
client-only "^0.0.1"
use-sync-external-store "^1.2.0"
tapable@^2.2.0:
version "2.2.1"
resolved "https://registry.yarnpkg.com/tapable/-/tapable-2.2.1.tgz#1967a73ef4060a82f12ab96af86d52fdb76eeca0"
@ -2316,11 +2423,21 @@ uri-js@^4.2.2:
dependencies:
punycode "^2.1.0"
use-sync-external-store@1.2.0, use-sync-external-store@^1.2.0:
use-debounce@^10.0.0:
version "10.0.0"
resolved "https://registry.yarnpkg.com/use-debounce/-/use-debounce-10.0.0.tgz#5091b18d6c16292605f588bae3c0d2cfae756ff2"
integrity sha512-XRjvlvCB46bah9IBXVnq/ACP2lxqXyZj0D9hj4K5OzNroMDpTEBg8Anuh1/UfRTRs7pLhQ+RiNxxwZu9+MVl1A==
use-sync-external-store@1.2.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz#7dbefd6ef3fe4e767a0cf5d7287aacfb5846928a"
integrity sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA==
uuid@^8.3.2:
version "8.3.2"
resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.3.2.tgz#80d5b5ced271bb9af6c445f21a1a04c606cefbe2"
integrity sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==
which-boxed-primitive@^1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/which-boxed-primitive/-/which-boxed-primitive-1.0.2.tgz#13757bc89b209b049fe5d86430e21cf40a89a8e6"