release004

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

No files matched your search

+13
View File
@@ -4,6 +4,19 @@
grid-template-columns: 1fr 1fr 1fr 1fr;
gap: 30px;
background-color: rgb(15, 23, 42);
&__logo {
display: flex;
flex-direction: column;
gap: 24px;
p {
color: white;
font-family: "Inter", sans-serif;
font-size: 16px;
}
}
a,
li,
h4 {
+19 -13
View File
@@ -1,6 +1,5 @@
import "./Footer.scss";
import logo from "../../shared/assets/logo.svg";
import Link from "next/link";
import Image from "next/image";
import { LINKS } from "@/shared/variables/links";
import youtube from "./icons/youtube.svg";
@@ -8,18 +7,27 @@ import facebook from "./icons/facebook.svg";
import instagram from "./icons/instagram.svg";
import app_store_btn from "./icons/app-store-btn.svg";
import play_market_btn from "./icons/play-market-btn.svg";
import { Link } from "@/shared/config/navigation";
import { useTranslations } from "next-intl";
import NetKgTracker from "@/widgets/NetKgTracker/NetKgTracker";
const Footer = () => {
const t = useTranslations("general");
const tDisclaimer = useTranslations("disclaimer");
const tRights = useTranslations("rights");
return (
<footer className="footer">
<Link href="/">
<Image src={logo} alt="Logo" />
</Link>
<div className="footer__logo">
<Link href="/">
<Image src={logo} alt="Logo" />
</Link>
<p>© {tRights("text")}</p>
<p>{tDisclaimer("text")}</p>
</div>
<div className="footer__links">
<h4>Навигация</h4>
<h4>{t("navigation")}</h4>
<ul>
{LINKS.map((link) => (
{LINKS().map((link) => (
<li key={link.id}>
<Link href={link.pathname} key={link.id}>
{link.pagename}
@@ -31,11 +39,11 @@ const Footer = () => {
</div>
<div className="footer__contacts">
<h4>Контакты</h4>
<h4>{t("contacts")}</h4>
<ul>
<li>admin@kgroad.org</li>
<li>+9960312394038</li>
{/*
<li>
{[youtube, facebook, instagram].map((net, i) => (
<Link key={i} href="#">
@@ -43,13 +51,12 @@ const Footer = () => {
</Link>
))}
</li>
*/}
<li>Photo By ThomasG, CC BY-SA 3.0</li>
</ul>
<p className="text-white">Photo By ThomasG, CC BY-SA 3.0</p>
</div>
{/*
<div className="footer__apps">
<h4>Скачивай наше приложение</h4>
<h4>{t("download_our_app")}</h4>
<div className="footer__apps-btns">
{[app_store_btn, play_market_btn].map((app, i) => (
<Link key={i} href="#">
@@ -58,7 +65,6 @@ const Footer = () => {
))}
</div>
</div>
*/}
</footer>
);
};
+7 -3
View File
@@ -1,7 +1,8 @@
import Link from "next/link";
import "./NavAuth.scss";
import { usePathname } from "next/navigation";
import { useSession } from "next-auth/react";
import { Link } from "@/shared/config/navigation";
import { useTranslations } from "next-intl";
interface INavAuthProps {
responsible?: boolean;
@@ -12,8 +13,11 @@ const NavAuth: React.FC<INavAuthProps> = ({
responsible,
setOpenMenu,
}: INavAuthProps) => {
const t = useTranslations("navigation");
const session = useSession();
const auth = session.status === "authenticated" ? true : false;
const pathname = usePathname();
return (
<>
@@ -27,7 +31,7 @@ const NavAuth: React.FC<INavAuthProps> = ({
: "lg"
}`}
>
Профиль
{t("profile")}
</Link>
) : (
<Link
@@ -39,7 +43,7 @@ const NavAuth: React.FC<INavAuthProps> = ({
: "lg"
}`}
>
Войти
{t("login")}
</Link>
)}
</>
+44 -9
View File
@@ -1,20 +1,52 @@
"use client";
import "./NavLanguage.scss";
import Image from "next/image";
import { useState } from "react";
import { useEffect, useRef, useState, useTransition } from "react";
import globus from "./icons/globus.svg";
import chevron from "./icons/chevron-down.svg";
import check from "./icons/check.svg";
import { useParams, useRouter } from "next/navigation";
import { LANGUAGES } from "./variables";
import { usePathname } from "@/shared/config/navigation";
const NavLanguage = () => {
const [language, setLanguage] = useState<string>("ru");
const [openMenu, setOpenMenu] = useState<boolean>(false);
const LANGUAGES = [
{ id: 1, language: "Русский", index: "ru" },
{ id: 2, language: "Кыргызча", index: "kg" },
{ id: 3, language: "English", index: "en" },
];
const menuRef: React.RefObject<HTMLDivElement> = useRef(null);
const router = useRouter();
const pathname = usePathname();
const { locale } = useParams();
const [_, startTransition] = useTransition();
const [language, setLanguage] = useState<string>(locale as string);
useEffect(() => {
const params = window.location.search || "";
startTransition(() => {
router.replace(`/${language}${pathname}${params}`, {
scroll: false,
});
});
}, [language]);
useEffect(() => {
function handleClickOutside(event: any) {
if (
menuRef.current &&
!menuRef.current.contains(event.target)
) {
setOpenMenu(false);
}
}
document.addEventListener("mousedown", handleClickOutside);
return () => {
document.removeEventListener("mousedown", handleClickOutside);
};
}, [menuRef]);
return (
<div className="nav-language">
<div ref={menuRef} className="nav-language">
<button
onClick={() => setOpenMenu((prev) => !prev)}
className="nav-language__btn"
@@ -29,7 +61,10 @@ const NavLanguage = () => {
className={`nav-language__option${
language === lang.index ? "_active" : ""
}`}
onClick={() => setLanguage(lang.index)}
onClick={() => {
setLanguage(lang.index);
setOpenMenu(false);
}}
key={lang.id}
>
{lang.language}
@@ -0,0 +1,11 @@
interface ILangs {
id: number;
language: string;
index: string;
}
export const LANGUAGES: ILangs[] = [
{ id: 1, language: "Русский", index: "ru" },
{ id: 2, language: "Кыргызча", index: "kg" },
{ id: 3, language: "English", index: "en" },
];
+5 -5
View File
@@ -1,8 +1,7 @@
import { LINKS } from "@/shared/variables/links";
import "./NavMenu.scss";
import Link from "next/link";
import { usePathname } from "next/navigation";
import NavAuth from "../NavAuth/NavAuth";
import { Link, usePathname } from "@/shared/config/navigation";
interface INavMenuProps {
setOpenMenu: (boolean: boolean) => void;
@@ -12,15 +11,16 @@ const NavMenu: React.FC<INavMenuProps> = ({
setOpenMenu,
}: INavMenuProps) => {
const pathname = usePathname();
return (
<nav className="nav-menu">
{LINKS.map((link) => (
{LINKS().map((link) => (
<Link
onClick={() => setOpenMenu(false)}
className={`nav-menu__link${
pathname === link.pathname ? "_active" : ""
pathname.slice(4) === link.pathname ? "_active" : ""
}`}
href={link.pathname}
href={`${pathname.slice(1, 3)}/${link.pathname}`}
key={link.id}
>
{link.pagename}
+23 -1
View File
@@ -11,6 +11,18 @@
align-items: center;
background-color: #fff;
&__logo {
display: flex;
align-items: center;
gap: 10px;
&_last {
min-width: 78px;
max-width: 78px;
height: 100%;
}
}
&__links {
height: 40%;
display: flex;
@@ -46,6 +58,16 @@
}
}
@media screen and (max-width: 1220px) {
.navbar {
padding: 0 60px;
&__links {
gap: 40px;
}
}
}
@media screen and (max-width: 1024px) {
.navbar {
padding: 0 30px;
@@ -60,7 +82,7 @@
}
}
@media screen and (max-width: 768px) {
@media screen and (max-width: 900px) {
.navbar {
height: 72px;
+25 -8
View File
@@ -1,31 +1,48 @@
"use client";
import "./Navbar.scss";
import Image from "next/image";
import Link from "next/link";
import { usePathname } from "next/navigation";
import Image, { StaticImageData } from "next/image";
import { Link } from "@/shared/config/navigation";
import { usePathname } from "@/shared/config/navigation";
import logo from "@/shared/assets/logo.svg";
import { LINKS } from "@/shared/variables/links";
import NavLanguage from "./NavLanguage/NavLanguage";
import NavAuth from "./NavAuth/NavAuth";
import menu from "./icons/menu.svg";
import cross from "./icons/cross.svg";
import NavMenu from "./NavMenu/NavMenu";
import { useState } from "react";
import founded_ru from "./assets/founded-ru.png";
import founded_en from "./assets/founded-en.png";
import founded_kg from "./assets/founded-kg.png";
import { useParams } from "next/navigation";
const Navbar = () => {
const pathname = usePathname();
const [openMenu, setOpenMenu] = useState<boolean>(false);
const { locale } = useParams();
const FOUNDED: Record<string, StaticImageData> = {
ru: founded_ru,
kg: founded_kg,
en: founded_en,
};
return (
<section className="navbar">
<Link href="/">
<Image src={logo} alt="Logo" />
</Link>
<div className="navbar__logo">
<Link href="/">
<Image src={logo} alt="Logo" />
</Link>
<Image
className="navbar__logo_last"
src={FOUNDED[locale as string]}
alt="Founded by EU Image"
/>
</div>
<nav className="navbar__links">
{LINKS.map((link) => (
{LINKS().map((link) => (
<Link
className={`navbar__link${
pathname === link.pathname ? "_active" : ""
Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 74 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

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

After

Width:  |  Height:  |  Size: 5.9 KiB

+1 -2
View File
@@ -2,8 +2,7 @@
import "./ProfileNav.scss";
import LogoutButton from "@/features/LogoutButton/LogoutButton";
import Link from "next/link";
import { usePathname } from "next/navigation";
import { Link, usePathname } from "@/shared/config/navigation";
interface IProfileNavProps {
report_count: number;
@@ -1,106 +0,0 @@
import { apiInstance } from "@/shared/config/apiConfig";
import { IFetch } from "@/shared/types/fetch-type";
import {
IMyReports,
IMyReportsList,
} from "@/shared/types/my-reports";
import { AxiosError } from "axios";
import { create } from "zustand";
const filterCategories: Record<string, string> = {
count_reviews: "count_reviews",
total_likes: "total_likes",
};
interface IProfileReportsStore extends IFetch {
data: IMyReportsList;
getMyReports: (
filter: { option: string; toggle: boolean },
page: number,
access_token: string
) => void;
}
export const useProfileReportsStore = create<IProfileReportsStore>(
(set) => ({
isLoading: false,
error: "",
data: {
count: 0,
previous: null,
next: null,
results: [],
},
getMyReports: async (
filter: {
option: string;
toggle: boolean;
},
page: number,
access_token: string
) => {
try {
const Authorization = `Bearer ${access_token}`;
const config = {
headers: {
Authorization,
},
};
set({ isLoading: true });
const res = await apiInstance.get<IMyReportsList>(
`/users/reports/?page=${page}`,
config
);
let data = res.data;
if (filter.option === "date" && filter.toggle === false) {
data.results = data.results.sort((a, b) => {
const dateA = new Date(a.created_at) as unknown as number;
const dateB = new Date(b.created_at) as unknown as number;
return dateA - dateB;
});
} else if (
filter.option === "date" &&
filter.toggle === true
) {
data.results = data.results.sort((a, b) => {
const dateA = new Date(a.created_at) as unknown as number;
const dateB = new Date(b.created_at) as unknown as number;
return dateB - dateA;
});
}
if (
filter.option === filterCategories[filter.option] &&
filter.toggle === false
) {
data.results = data.results.sort((a, b) => {
const optionKey = filter.option as keyof IMyReports;
return (Number(a[optionKey]) -
Number(b[optionKey])) as number;
});
} else if (
filter.option === "rating" &&
filter.toggle === true
) {
data.results = data.results.sort((a, b) => {
const optionKey = filter.option as keyof IMyReports;
return (Number(a[optionKey]) -
Number(b[optionKey])) as number;
});
}
set({ data: data });
} catch (error: unknown) {
if (error instanceof AxiosError) {
set({ error: error.message });
} else {
set({ error: "an error ocured" });
}
} finally {
set({ isLoading: false });
}
},
})
);
@@ -1,100 +0,0 @@
import { apiInstance } from "@/shared/config/apiConfig";
import { IFetch } from "@/shared/types/fetch-type";
import { IList } from "@/shared/types/list-type";
import { IReport } from "@/shared/types/report-type";
import { create } from "zustand";
interface IFetchReports extends IList {
results: IReport[];
}
interface IRatingStore extends IFetch {
data: IFetchReports;
getReports: (
categories: string,
page: number,
filter: { option: string; toggle: boolean }
) => Promise<void>;
}
export const useRatingStore = create<IRatingStore>((set) => ({
data: {
count: 0,
previous: null,
next: null,
results: [],
},
isLoading: false,
error: "",
getReports: async (
query: string = "",
page: number = 1,
filter: { option: string; toggle: boolean }
) => {
try {
set({ isLoading: true });
const data = (
await apiInstance.get<IFetchReports>(
`/report/?page=${page}&page_size=${8}`
)
).data;
const searched = data.results.filter((rating) => {
return rating.location.some((location) => {
return location.address
.toLowerCase()
.includes(query.toLowerCase());
});
});
data.results = [...searched];
if (filter.option === "date" && filter.toggle === false) {
data.results = data.results.sort((a, b) => {
const dateA = new Date(a.created_at) as unknown as number;
const dateB = new Date(b.created_at) as unknown as number;
return dateA - dateB;
});
} else if (filter.option === "date" && filter.toggle === true) {
data.results = data.results.sort((a, b) => {
const dateA = new Date(a.created_at) as unknown as number;
const dateB = new Date(b.created_at) as unknown as number;
return dateB - dateA;
});
}
if (filter.option === "reviews" && filter.toggle === false) {
data.results = data.results.sort(
(a, b) => a.count_reviews - b.count_reviews
);
} else if (
filter.option === "reviews" &&
filter.toggle === true
) {
data.results = data.results.sort(
(a, b) => b.count_reviews - a.count_reviews
);
}
if (filter.option === "rating" && filter.toggle === false) {
data.results = data.results.sort(
(a, b) => a.total_likes - b.total_likes
);
} else if (
filter.option === "rating" &&
filter.toggle === true
) {
data.results = data.results.sort(
(a, b) => b.total_likes - a.total_likes
);
}
set({ data: data });
} catch (error: any) {
set({ error: error.message });
} finally {
set({ isLoading: false });
}
},
}));
Whitespace-only changes.
@@ -1,7 +0,0 @@
import "./CreateReportMap.scss";
const CreateReportMap = () => {
return <div>CreateReportMap</div>;
};
export default CreateReportMap;
@@ -20,6 +20,37 @@
}
}
&__auth-warning,
&__warning {
height: 150px;
margin-bottom: 70px;
text-align: center;
display: flex;
align-items: center;
justify-content: center;
gap: 4px;
font-size: 18px;
font-weight: 500;
line-height: 22px;
border: 1px solid #c5c6c5;
border-radius: 10px;
button {
text-decoration: underline;
color: #0077b6;
font-size: 18px;
font-weight: 500;
line-height: 22px;
}
}
&__loader {
display: flex;
justify-content: center;
}
form {
margin-bottom: 70px;
display: flex;
+133 -40
View File
@@ -2,11 +2,17 @@
import "./ReviewSection.scss";
import { apiInstance } from "@/shared/config/apiConfig";
import { IReview } from "@/shared/types/review-type";
import { useSession } from "next-auth/react";
import { useEffect, useState } from "react";
import { IReviewList } from "@/shared/types/review-type";
import { signIn, useSession } from "next-auth/react";
import { useEffect, useRef, useState } from "react";
import calendar from "./icons/calendar.svg";
import Image from "next/image";
import {
Link,
usePathname,
useRouter,
} from "@/shared/config/navigation";
import Loader from "@/shared/ui/components/Loader/Loader";
interface IReviewsSectionProps {
endpoint: string;
@@ -17,7 +23,31 @@ const ReviewSection: React.FC<IReviewsSectionProps> = ({
endpoint,
id,
}: IReviewsSectionProps) => {
const [reviews, setReviews] = useState<IReview[]>();
const lastElement = useRef<HTMLDivElement | null>(null);
const observer = useRef<IntersectionObserver | null>(null);
const [reviews, setReviews] = useState<IReviewList>({
count: 0,
next: null,
previous: null,
results: [
{
id: 0,
author: {
image: "",
id: 0,
first_name: "",
last_name: "",
govern_status: null,
},
review: "",
created_at: "",
},
],
});
const [pageSize, setPageSize] = useState<number>(8);
const [reviewCount, setReviewCount] = useState<number>(0);
const [loader, setLoader] = useState<boolean>(false);
const pathname = usePathname();
const session = useSession();
const handleSubmit: React.MouseEventHandler<
HTMLFormElement
@@ -33,6 +63,10 @@ const ReviewSection: React.FC<IReviewsSectionProps> = ({
},
};
if (session.status === "unauthenticated") {
signIn(undefined, { callbackUrl: pathname });
}
if (!formData.get("review")) {
return;
}
@@ -45,6 +79,7 @@ const ReviewSection: React.FC<IReviewsSectionProps> = ({
formData,
config
);
getReviews();
} catch (error) {
console.log(error);
@@ -52,15 +87,44 @@ const ReviewSection: React.FC<IReviewsSectionProps> = ({
};
const getReviews = async () => {
const response = await apiInstance.get<IReview[]>(
`/${endpoint}/${id}/reviews/`
setLoader(true);
const response = await apiInstance.get<IReviewList>(
`/${endpoint}/${id}/reviews/?page_size=${pageSize}`
);
setReviewCount(response.data.count);
setLoader(false);
setReviews({
count: response.data.count,
next: response.data.next,
previous: response.data.previous,
results: [...reviews?.results, ...response.data.results],
});
setReviews(response?.data);
};
useEffect(() => {
if (loader) return;
if (observer.current) observer.current.disconnect();
const callback = function (entries: any) {
if (
entries[0].isIntersecting &&
(reviews?.results.length as number) <= reviewCount
) {
if (pageSize < reviewCount) {
setPageSize((prev) => prev + 8);
} else {
setPageSize(reviewCount);
}
}
};
observer.current = new IntersectionObserver(callback);
observer.current.observe(lastElement.current as HTMLDivElement);
}, [loader]);
useEffect(() => {
getReviews();
}, []);
}, [pageSize]);
const months: Record<string, string> = {
"01": "Январь",
@@ -76,53 +140,82 @@ const ReviewSection: React.FC<IReviewsSectionProps> = ({
"11": "Ноябрь",
"12": "Декабрь",
};
return (
<section className="review-section">
<h3>
<span id="blue-point" /> Написать комментарий
</h3>
<form onSubmit={handleSubmit}>
<textarea name="review" />
<button type="submit">Отправить</button>
</form>
{session.status === "authenticated" ? (
<form onSubmit={handleSubmit}>
<textarea name="review" />
<button type="submit">Отправить</button>
</form>
) : (
<p className="review-section__auth-warning">
Перед тем как оставить комментарий, пожалуйста
<button
onClick={() =>
signIn(undefined, { callbackUrl: pathname })
}
>
войдите в аккаунт
</button>
</p>
)}
<div className="review-section__list">
<h3>
<span id="blue-point" /> Комментарии
</h3>
<ul>
{reviews?.map((review) => (
<li key={review.id} className="review">
<div className="review__author">
<img
id="author-img"
src={review.author.image}
alt="Author Image"
/>
<div className="review__header">
<h5 className="review__author-name">
{review.author.first_name}{" "}
{review.author.last_name}
</h5>
<div className="review__date">
<Image src={calendar} alt="Calendar Icon" />
<p>
{months[review.created_at.slice(5, 7)]}{" "}
{review.created_at.slice(5, 7).slice(0, 1) ===
"0"
? review.created_at.slice(6, 7)
: review.created_at.slice(5, 7)}
, {review.created_at.slice(0, 4)}
</p>
{reviews?.results.length !== 0 ? (
<ul key="unuqie key">
{reviews?.results.map((review) => (
<li key={review.id} className="review">
<div className="review__author">
<img
id="author-img"
src={review.author.image}
alt="Author Image"
/>
<div className="review__header">
<h5 className="review__author-name">
{review.author.first_name}{" "}
{review.author.last_name}
</h5>
<div className="review__date">
<Image src={calendar} alt="Calendar Icon" />
<p>
{months[review.created_at.slice(5, 7)]}{" "}
{review.created_at.slice(5, 7).slice(0, 1) ===
"0"
? review.created_at.slice(6, 7)
: review.created_at.slice(5, 7)}
, {review.created_at.slice(0, 4)}
</p>
</div>
</div>
</div>
</div>
<p className="review__description">{review.review}</p>
</li>
))}
</ul>
<p className="review__description">{review.review}</p>
</li>
))}
</ul>
) : (
<p className="review-section__warning">
Оставьте комментарий первым :)
</p>
)}
{loader ? (
<div className="review-section__loader">
<Loader color="#000" />
</div>
) : null}
<div
ref={lastElement}
className="review-section__list_bottom"
></div>
</div>
</section>
);
@@ -1,10 +1,11 @@
"use client";
import { useEffect, useState } from "react";
import "./ConfirmEmailForm.scss";
import { useEffect, useState } from "react";
import { apiInstance } from "@/shared/config/apiConfig";
import { useRouter } from "next/navigation";
import Loader from "@/shared/ui/components/Loader/Loader";
import { useRouter } from "@/shared/config/navigation";
import { AxiosError } from "axios";
interface IConfirmEmailFormProps {
email: string;
@@ -54,24 +55,38 @@ const ConfirmEmailForm: React.FC<IConfirmEmailFormProps> = ({
if (response.status === 200 || response.status === 201) {
router.push("/sign-in");
}
} catch (error) {
setError("Возникла ошибка");
} catch (error: unknown) {
if (error instanceof AxiosError) {
if ([400, 404].includes(error.response?.status as number)) {
setError("Неверный код подтверждения");
} else {
setError("Ошибка на стороне сервера");
}
} else {
setError("Произошла непредвиденная ошибка");
}
} finally {
setLoader(false);
}
};
const handleClick = async () => {
const data = {
email,
};
const response = await apiInstance.post(
"/users/resend_code/",
data
);
try {
const data = {
email,
};
const response = await apiInstance.post(
"/users/resend_code/",
data
);
if (response.status === 200 || response.status === 201) {
setMinutes(1);
if ([200, 201].includes(response.status)) {
setMinutes(1);
}
} catch (error) {
setError(
"Проблема на стороне сервера или вы достигли максимальное количество попыток"
);
}
};
@@ -5,8 +5,9 @@ import "./confirm-code.scss";
import { useState } from "react";
import { apiInstance } from "@/shared/config/apiConfig";
import Loader from "@/shared/ui/components/Loader/Loader";
import { useRouter } from "next/navigation";
import { ITokens } from "@/shared/types/token-type";
import { useRouter } from "@/shared/config/navigation";
import { AxiosError } from "axios";
interface IConfirmCodeProps {
setChangeForm: (boolean: boolean) => void;
@@ -44,9 +45,17 @@ const ConfirmCode: React.FC<IConfirmCodeProps> = ({
);
router.push("/sign-in/reset-code");
}
} catch (error) {
setError("An error ocured");
} finally {
setLoader(false);
} catch (error: unknown) {
if (error instanceof AxiosError) {
if (error.response?.status === 400) {
setError("Неверный код");
} else {
setError("Ошибка на стороне сервера");
}
} else {
setError("Произошла непредвиденная ошибка");
}
setLoader(false);
}
};
@@ -5,6 +5,7 @@ import "./send-email.scss";
import { useState } from "react";
import { apiInstance } from "@/shared/config/apiConfig";
import Loader from "@/shared/ui/components/Loader/Loader";
import { AxiosError } from "axios";
interface ISendEmailProps {
setChangeForm: (boolean: boolean) => void;
@@ -34,11 +35,21 @@ const SendEmail: React.FC<ISendEmailProps> = ({
formData
);
if (res.status === 200 || res.status === 201) {
if ([200, 201].includes(res.status)) {
console.log(res.data);
setChangeForm(true);
}
} catch (error) {
setError("An error ocured");
} catch (error: unknown) {
if (error instanceof AxiosError) {
console.log(error);
if (error.response?.status === 400) {
setError("Пользователь с таким email не найден");
} else {
setError("Ошибка на стороне сервера");
}
} else {
setError("Произошла непредвиденная ошибка");
}
} finally {
setLoader(false);
}
@@ -57,7 +68,7 @@ const SendEmail: React.FC<ISendEmailProps> = ({
<form onSubmit={handleSubmit} className="send-email">
<AuthInput
type="text"
type="email"
name="email"
label="Email"
error={error}
@@ -5,6 +5,7 @@ import { useState } from "react";
import { apiInstance } from "@/shared/config/apiConfig";
import { useSession } from "next-auth/react";
import Loader from "@/shared/ui/components/Loader/Loader";
import { AxiosError } from "axios";
interface IChangePasswordProps {
closeWindow: (bool: boolean) => void;
@@ -90,8 +91,16 @@ const ChangePassword: React.FC<IChangePasswordProps> = ({
);
if ([200, 201].includes(res.status)) return setSuccess(true);
} catch (error: any) {
setError(error.message);
} catch (error: unknown) {
if (error instanceof AxiosError) {
if (error.response?.status === 400) {
setError(
"Некорректный старый пароль или недопустимый новый пароль"
);
}
} else {
setError("Произошла непредвиденная ошибка");
}
} finally {
setLoader(false);
}
@@ -3,16 +3,14 @@
import "./ProfileForm.scss";
import Image from "next/image";
import pen from "./icons/pen.svg";
import eye_off from "./icons/eye-off.svg";
import eye_on from "./icons/eye-on.svg";
import { useState } from "react";
import { apiInstance } from "@/shared/config/apiConfig";
import { apiInstance, authInstanse } from "@/shared/config/apiConfig";
import { AxiosError } from "axios";
import Loader from "@/shared/ui/components/Loader/Loader";
import { useSession } from "next-auth/react";
import { useRouter } from "next/navigation";
import LogoutButton from "@/features/LogoutButton/LogoutButton";
import ChangePassword from "./ChangePassword/ChangePassword";
import { useRouter } from "@/shared/config/navigation";
interface IProfileFormProps {
id: number;
@@ -76,7 +74,11 @@ const ProfileForm: React.FC<IProfileFormProps> = ({
}
} catch (error: unknown) {
if (error instanceof AxiosError) {
setError(error.message);
if (error.response?.status === 400) {
setError("Были введены неккоректные данные");
} else {
setError("Ошибка на стороне сервера");
}
} else {
setError("Возникла непредвиденная ошибка");
}
@@ -171,9 +171,10 @@
button[type="submit"] {
margin-top: 40px;
padding: 15px;
width: fit-content;
max-width: 320px;
display: flex;
align-items: center;
justify-content: center;
gap: 10px;
border: 1px solid rgb(72, 159, 225), rgb(72, 159, 225);
border-radius: 5px;
@@ -182,6 +183,10 @@
font-size: 22px;
font-weight: 400;
line-height: 26px;
div {
flex: 1;
}
}
}
@@ -6,6 +6,7 @@ import Image from "next/image";
import {
MapContainer,
Marker,
Popup,
TileLayer,
useMapEvents,
} from "react-leaflet";
@@ -19,8 +20,8 @@ import axios, { AxiosError } from "axios";
import { apiInstance } from "@/shared/config/apiConfig";
import { useSession } from "next-auth/react";
import { IDisplayMap } from "@/shared/types/map-type";
import { useRouter } from "next/navigation";
import Loader from "@/shared/ui/components/Loader/Loader";
import { useRouter } from "@/shared/config/navigation";
interface ILatLng {
lat: number;
@@ -66,7 +67,7 @@ const ReportForm = () => {
e.preventDefault();
const formData = new FormData(e.currentTarget);
if (displayLatLng.length === 0) {
if (displayLatLng.length === 0 || latLng.length === 0) {
setDescriptionWarning("");
setImageWarning("");
setError("");
@@ -109,8 +110,10 @@ const ReportForm = () => {
});
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());
if (latLng.length === 2) {
formData.append("latitude2", latLng[1].lat.toString());
formData.append("longitude2", latLng[1].lng.toString());
}
try {
setLoader(true);
@@ -125,6 +128,7 @@ const ReportForm = () => {
}
} catch (error: unknown) {
if (error instanceof AxiosError) {
console.log(error);
setError(error.message);
} else {
setError("Произошла непредвиденная ошибка");
@@ -144,12 +148,20 @@ const ReportForm = () => {
.get(
`https://nominatim.openstreetmap.org/reverse?lat=${e.latlng.lat}&lon=${e.latlng.lng}&format=json`
)
.then((res) =>
.then((res) => {
console.log(res.data);
if (res.data.address.country_code !== "kg") {
setLocationWarning("Выберите точку в Кыргызстане");
return;
} else if (res.data.address.road === undefined) {
setLocationWarning("Выберите точку на дороге");
return;
}
setDisplayLatLng([
...displayLatLng,
res.data.display_name,
])
)
]);
})
.catch((error) => console.log(error));
}
},
@@ -272,8 +284,14 @@ const ReportForm = () => {
) : null}
</div>
<button disabled={loader} type="submit">
{loader ? <Loader /> : "Отправить на модерацию"}
<Image src={arrow_right} alt="Arrow Right Icon" />
{loader ? (
<Loader />
) : (
<>
Отправить на модерацию
<Image src={arrow_right} alt="Arrow Right Icon" />
</>
)}
</button>
{error ? <p className="report-form__error">{error}</p> : null}
</form>
@@ -5,8 +5,9 @@ import "./ResetCodeForm.scss";
import { useState } from "react";
import { apiInstance } from "@/shared/config/apiConfig";
import { ITokens } from "@/shared/types/token-type";
import { useRouter } from "next/navigation";
import Loader from "@/shared/ui/components/Loader/Loader";
import { useRouter } from "@/shared/config/navigation";
import { AxiosError } from "axios";
const ResetCodeForm = () => {
const [passwordWarning, setPasswordWarning] = useState<string>("");
@@ -62,12 +63,28 @@ const ResetCodeForm = () => {
config
);
if (response.status === 200 || response.status === 201) {
if ([200, 201].includes(response.status)) {
localStorage.removeItem("transitional");
router.push("/sign-in");
}
} catch (error) {
setError("An error ocured");
if (error instanceof AxiosError) {
if (
[500, 501, 502, 503, 504].includes(
error.response?.status as number
)
) {
setError("Ошибка на стороне сервера");
} else if (
[400, 404].includes(error.response?.status as number)
) {
setError(
"Слабый пароль, прошу избегайте очевидных паролей"
);
}
} else {
setError("Произошла непредвиденная ошибка");
}
} finally {
setLoader(false);
}
@@ -3,13 +3,13 @@
import "./SignInForm.scss";
import AuthInput from "@/features/AuthInput/AuthInput";
import GoogleButton from "@/features/GoogleButton/GoogleButton";
import { Link, useRouter } from "@/shared/config/navigation";
import Loader from "@/shared/ui/components/Loader/Loader";
import { signIn } from "next-auth/react";
import Link from "next/link";
import { useRouter } from "next/navigation";
import { signIn, useSession } from "next-auth/react";
import { useState } from "react";
const SignInForm = () => {
const session = useSession();
const [emailWarning, setEmailWarning] = useState<string>("");
const [passwordWarning, setPasswordWarning] = useState<string>("");
const [error, setError] = useState<string>("");
@@ -51,7 +51,7 @@ const SignInForm = () => {
if (res?.ok && !res.error) {
router.push("/profile/personal");
} else if (res?.status.toString().slice(0, 1) === "4") {
} else if ([400, 401, 404].includes(res?.status as number)) {
setError("Неверный Email или Пароль");
} else {
setError("Произошла непредвиденная ошибка");
@@ -89,9 +89,7 @@ const SignInForm = () => {
{loader ? <Loader /> : "Войти"}
</button>
{/*
<GoogleButton />
*/}
</div>
</form>
);
@@ -6,6 +6,42 @@
display: flex;
flex-direction: column;
&__policy {
margin-top: 30px;
display: flex;
align-items: center;
gap: 12px;
p,
a {
font-size: 14px;
font-weight: 500;
line-height: 20px;
}
p {
color: #344054;
}
a {
color: #0077b6;
}
}
&__checkbox {
width: 16px;
height: 16px;
display: flex;
align-items: center;
justify-content: center;
border: 1px solid #d0d5dd;
border-radius: 4px;
img {
width: 10px;
height: 10px;
}
}
&__error {
font-size: 14px;
font-weight: 400;
@@ -5,11 +5,14 @@ import { useState } from "react";
import AuthInput from "@/features/AuthInput/AuthInput";
import Loader from "@/shared/ui/components/Loader/Loader";
import GoogleButton from "@/features/GoogleButton/GoogleButton";
import { useRouter } from "next/navigation";
import { apiInstance } from "@/shared/config/apiConfig";
import { AxiosError } from "axios";
import { Link, useRouter } from "@/shared/config/navigation";
import check from "./icons/check.svg";
import Image from "next/image";
const SignUpForm = () => {
const [checkbox, setCheckbox] = useState<boolean>(false);
const [emailWarning, setEmailWarning] = useState<string>("");
const [passwordWarning, setPasswordWarning] = useState<string>("");
const [passwordConfirmWarning, setPasswordConfirmWarning] =
@@ -24,6 +27,7 @@ const SignUpForm = () => {
> = async (e) => {
e.preventDefault();
const formData = new FormData(e.currentTarget);
const regex = /^(?=.*\d)(?=.*[a-z])(?=.*[A-Z])/;
if (!formData.get("email")?.toString()) {
setError("");
@@ -41,6 +45,26 @@ const SignUpForm = () => {
return;
}
if ((formData.get("password")?.toString().length as number) < 8) {
setError("");
setEmailWarning("");
setPasswordConfirmWarning("");
setPasswordWarning(
"Пароль должен содержать минимум 8 символов"
);
return;
}
if (!regex.test(formData.get("password")?.toString() as string)) {
setError("");
setEmailWarning("");
setPasswordConfirmWarning("");
setPasswordWarning(
"Пароль должен содержать по меньшей мере 1 прописную букву, одну заглавную букву и одну цифру"
);
return;
}
if (!formData.get("password2")?.toString()) {
setError("");
setEmailWarning("");
@@ -60,6 +84,14 @@ const SignUpForm = () => {
return;
}
if (!checkbox) {
setEmailWarning("");
setPasswordWarning("");
setPasswordConfirmWarning("");
setError("Необходимо принять политику конфиденциальности");
return;
}
try {
setError("");
setEmailWarning("");
@@ -72,16 +104,22 @@ const SignUpForm = () => {
formData
);
if (res.status === 200 || res.status === 201) {
if ([200, 201].includes(res.status)) {
router.push(
`/sign-up/confirm-email/?email=${formData.get("email")}`
);
}
} catch (error: unknown) {
if (error instanceof AxiosError) {
setError("Произошла непредвиденная ошибка");
if ([401, 400].includes(error.response?.status as number)) {
setError("Такой пользователь уже существует");
} else if (
error.response?.status.toString().slice(0, 1) === "5"
) {
setError("Ошибка на стороне сервера");
}
} else {
setError("An error ocured");
setError("Непредвиденная ошибка");
}
} finally {
setLoader(false);
@@ -114,15 +152,26 @@ const SignUpForm = () => {
/>
</div>
{error ? <p className="sign-up-form__error">{error}</p> : null}
<div className="sign-up-form__policy">
<button
onClick={() => setCheckbox((prev) => !prev)}
type="button"
className="sign-up-form__checkbox"
>
{checkbox ? <Image src={check} alt="Check Icon" /> : null}
</button>
<p>
Я ознакомлен(-а) с{" "}
<Link href="#">Политикой конфиденциальности</Link>
</p>
</div>
<div className="sign-up-form__btns">
<button className="sign-up-form__btns_first" type="submit">
{loader ? <Loader /> : "Войти"}
{loader ? <Loader /> : "Зарегистрироваться"}
</button>
{/*
<GoogleButton />
*/}
</div>
</form>
);
@@ -0,0 +1,7 @@
<svg width="15.007812" height="10.834961" viewBox="0 0 15.0078 10.835" 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="M14.1699 0.835938L5.00391 10.0029L0.836914 5.83594" stroke="#3695D8" stroke-opacity="1.000000" stroke-width="1.666667" stroke-linejoin="round" stroke-linecap="round"/>
</svg>

After

Width:  |  Height:  |  Size: 406 B

@@ -13,6 +13,7 @@
}
&__text {
width: 100%;
padding: 0 90px;
height: 100%;
position: relative;
@@ -2,8 +2,10 @@ import "./Header.scss";
import Image from "next/image";
import background from "./assets/background.svg";
import HeaderLink from "./HeaderLink/HeaderLink";
import { useTranslations } from "next-intl";
const Header = () => {
const t = useTranslations("home");
return (
<header className="header">
<Image
@@ -12,12 +14,8 @@ const Header = () => {
alt="Background Image"
/>
<div className="header__text">
<h1>
Дороги Кыргызстана
</h1>
<p>
Сделаем дороги безопасными! Актуальная информация о состоянии дорог.
</p>
<h1>{t("title")}</h1>
<p>{t("subtitle")}</p>
</div>
<HeaderLink />
@@ -1,12 +1,15 @@
import "./HeaderLink.scss";
import Image from "next/image";
import arrow_right from "./icons/arrow-right-circle.svg";
import Link from "next/link";
import { Link } from "@/shared/config/navigation";
import { useTranslations } from "next-intl";
const HeaderLink = () => {
const t = useTranslations("home");
return (
<Link href="/create-report" className="header-link">
Отметить разбитую дорогу
{t("report_broken_road")}
<Image src={arrow_right} alt="Arrow Right Icon" />
</Link>
);
@@ -17,19 +17,13 @@ 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 geo_white_icon from "./icons/geo-white.svg";
import {
DivIcon,
Icon,
LatLngExpression,
LatLngTuple,
} from "leaflet";
import { DivIcon, Icon, LatLngTuple } from "leaflet";
import { StaticImageData } from "next/image";
import Link from "next/link";
import { Fragment, useEffect, useState } from "react";
import { useEffect, useState } from "react";
import L from "leaflet";
import { ILocation } from "@/shared/types/location-type";
import { useMapStore } from "../mapSectionStore";
import { Link } from "@/shared/config/navigation";
interface IData {
id: number;
@@ -58,7 +52,7 @@ const HomeMap: React.FC<IHomeMapProps> = ({
const customIcon = new Icon({
iconUrl: icon.src,
iconSize: [32, 32],
iconAnchor: [16, 32]
iconAnchor: [16, 32],
});
return customIcon;
@@ -6,7 +6,8 @@ import search from "./icons/search.svg";
import { useMapStore } from "../mapSectionStore";
import { useEffect, useState } from "react";
import { useDebounce } from "use-debounce";
import { useRouter } from "next/navigation";
import { useRouter } from "@/shared/config/navigation";
import { useTranslations } from "next-intl";
interface IMapSearchProps
extends React.InputHTMLAttributes<HTMLInputElement> {
@@ -20,7 +21,9 @@ interface IMapSearchProps
const MapSearch: React.FC<IMapSearchProps> = ({
searchParams,
placeholder,
}: IMapSearchProps) => {
const t = useTranslations("general");
const router = useRouter();
const [searchMap, setSearchMap] = useState<string>(
searchParams["поиск-на-карте"] || ""
@@ -76,13 +79,13 @@ const MapSearch: React.FC<IMapSearchProps> = ({
<Image src={search} alt="Search Icon" />
<input
value={searchMap}
placeholder="Введите город, село или регион"
placeholder={placeholder}
onChange={(e) => setSearchMap(e.target.value)}
name="map-search"
type="text"
/>
</div>
<button type="submit">Поиск</button>
<button type="submit">{t("search")}</button>
</form>
{options.length !== 0 ? (
@@ -1,15 +1,14 @@
import Typography from "@/shared/ui/components/Typography/Typography";
import "./MapSection.scss";
import Typography from "@/shared/ui/components/Typography/Typography";
import Paragraph from "@/shared/ui/components/Paragraph/Paragraph";
import MapSearch from "./MapSearch/MapSearch";
import dynamic from "next/dynamic";
import { apiInstance } from "@/shared/config/apiConfig";
import { IReport } from "@/shared/types/report-type";
import Switch from "./Switch/Switch";
import {
ROAD_TYPES,
ROAD_TYPES_COLORS,
} from "@/shared/variables/road-types";
import { ROAD_TYPES_COLORS } from "@/shared/variables/road-types";
import { useTranslations } from "next-intl";
import { getTranslations } from "next-intl/server";
const DynamicMap = dynamic(() => import("./HomeMap/HomeMap"), {
ssr: false,
@@ -27,6 +26,8 @@ interface IMapSectionProps {
const MapSection: React.FC<IMapSectionProps> = async ({
searchParams,
}: IMapSectionProps) => {
const t = await getTranslations("home");
const getReports = async (categories: string) => {
const res = await apiInstance<IReport[]>(
`/report/?category=${categories}`
@@ -56,14 +57,20 @@ const MapSection: React.FC<IMapSectionProps> = async ({
return categories.join(",");
};
const ROAD_TYPES: Record<number, string> = {
1: t("broken_roads"),
2: t("accident_hotspots"),
3: t("local_defects"),
4: t("repair_plans"),
5: t("repaired"),
6: t("fixed_local_defects"),
};
return (
<div className="map-section">
<div className="map-section__header">
<Typography element="h3">Карта дорог</Typography>
<Paragraph>
Будьте в курсе последних новостей о дорожном движении,
строительствах и мероприятиях!
</Paragraph>
<Typography element="h3">{t("road_map")}</Typography>
<Paragraph>{t("latest_news")}</Paragraph>
</div>
<div className="map-section__categories">
@@ -125,7 +132,10 @@ const MapSection: React.FC<IMapSectionProps> = async ({
</ul>
</div>
<MapSearch searchParams={searchParams} />
<MapSearch
searchParams={searchParams}
placeholder={t("enter_location")}
/>
<div className="map-section__categories"></div>
@@ -1,4 +1,4 @@
@import "../../shared/ui/variables.scss";
@import "@/shared/ui/variables.scss";
.news-section {
padding: 60px 90px 100px 90px;
@@ -1,16 +1,20 @@
import "./NewsSection.scss";
import Image from "next/image";
import Link from "next/link";
import { getNews } from "./newsSectionStore";
import arrow_icon from "./icons/arrow-right.svg";
import NewsCard from "@/entities/NewsCard/NewsCard";
import Typography from "@/shared/ui/components/Typography/Typography";
import { Link } from "@/shared/config/navigation";
import { getTranslations } from "next-intl/server";
const NewsSection = async () => {
const tHome = await getTranslations("home");
const tGeneral = await getTranslations("general");
const news = await getNews();
return (
<section className="news-section">
<Typography element="h3">Новости</Typography>
<Typography element="h3">{tGeneral("news")}</Typography>
<ul className="news-section__list">
{news?.map((article) => (
<li key={article.id} className="news-section__card">
@@ -25,7 +29,7 @@ const NewsSection = async () => {
))}
</ul>
<Link href="/news" className="news-section__read-more-btn">
Читать
{tHome("read_more")}
<Image src={arrow_icon} alt="Arrow Right Icon" />
</Link>
</section>
@@ -1,4 +1,4 @@
@import "../../shared/ui/variables.scss";
@import "@/shared/ui/variables.scss";
.rating-section {
padding: 0 90px;
@@ -1,28 +1,22 @@
"use client";
import "./RatingSection.scss";
import Image from "next/image";
import { useEffect, useState } from "react";
import { useRouter } from "next/navigation";
import SearchForm from "@/features/SearchForm/SearchForm";
import like_icon from "./icons/like.svg";
import message_icon from "./icons/message.svg";
import { useRatingStore } from "./ratingSectionStore";
import { useShallow } from "zustand/react/shallow";
import Link from "next/link";
import {
ROAD_TYPES,
ROAD_TYPES_COLORS,
} from "@/shared/variables/road-types";
import { ROAD_TYPES_COLORS } from "@/shared/variables/road-types";
import RoadType from "@/entities/RoadType/RoadType";
import Typography from "@/shared/ui/components/Typography/Typography";
import Paragraph from "@/shared/ui/components/Paragraph/Paragraph";
import arrows from "../../shared/icons/arrows.svg";
import arrows from "@/shared/icons/arrows.svg";
import Pagination from "@/features/Pagination/Pagination";
import {
sliceDate,
sliceDescription,
sliceLocation,
} from "./helpers";
import { sliceDescription, sliceLocation } from "./helpers";
import { Link, useRouter } from "@/shared/config/navigation";
import { useTranslations } from "next-intl";
interface IRatingSectionProps {
searchParams: {
@@ -36,16 +30,16 @@ interface IRatingSectionProps {
const RatingSection: React.FC<IRatingSectionProps> = ({
searchParams,
}: IRatingSectionProps) => {
const tHome = useTranslations("home");
const tGeneral = useTranslations("general");
const [ratingSearch, setRatingSearch] = useState<string>(
searchParams["поиск-рейтинг"] || ""
);
const [activePage, setActivePage] = useState<number>(
+searchParams["страница-рейтинга"] || 1
);
const [filter, setFilter] = useState({
option: "date",
toggle: false,
});
const [sort, setSort] = useState<string>("");
const router = useRouter();
const reports = useRatingStore(useShallow((state) => state.data));
@@ -54,8 +48,8 @@ const RatingSection: React.FC<IRatingSectionProps> = ({
);
useEffect(() => {
getReports(ratingSearch, activePage, filter);
}, []);
getReports(ratingSearch, activePage, sort);
}, [sort]);
const handleSubmit: React.FormEventHandler<
HTMLFormElement
@@ -77,7 +71,7 @@ const RatingSection: React.FC<IRatingSectionProps> = ({
}
);
getReports(ratingSearch, activePage, filter);
getReports(ratingSearch, activePage, sort);
if (
reports.results.length < 8 &&
@@ -101,69 +95,55 @@ const RatingSection: React.FC<IRatingSectionProps> = ({
}
);
getReports(ratingSearch, activePage, filter);
getReports(ratingSearch, activePage, sort);
}, [activePage]);
const params = [
{
id: 1,
name: "Дата",
name: tGeneral("date"),
handleClick() {
if (filter.option !== "date") {
return setFilter({ option: "date", toggle: false });
}
setFilter((prev) => {
return { option: "date", toggle: !prev.toggle };
});
getReports(ratingSearch, activePage, filter);
setSort("");
},
},
{ id: 2, name: "Адрес" },
{ id: 3, name: "Статус" },
{ id: 4, name: "Описание" },
{ id: 2, name: tGeneral("address") },
{ id: 3, name: tGeneral("status") },
{ id: 4, name: tGeneral("description") },
{
id: 5,
name: "Комментарии",
name: tGeneral("reviews"),
handleClick() {
if (filter.option !== "reviews") {
return setFilter({ option: "reviews", toggle: false });
}
setFilter((prev) => {
return { option: "reviews", toggle: !prev.toggle };
});
getReports(ratingSearch, activePage, filter);
setSort("reviews");
},
},
{
id: 6,
name: "Рейтинг",
name: tGeneral("rating"),
handleClick() {
if (filter.option !== "rating") {
return setFilter({ option: "rating", toggle: false });
}
setFilter((prev) => {
return { option: "rating", toggle: !prev.toggle };
});
getReports(ratingSearch, activePage, filter);
setSort("likes");
},
},
];
const ROAD_TYPES: Record<number, string> = {
1: tHome("broken_roads"),
2: tHome("accident_hotspots"),
3: tHome("local_defects"),
4: tHome("repair_plans"),
5: tHome("repaired"),
6: tHome("fixed_local_defects"),
};
return (
<section className="rating-section">
<div className="rating-section__header">
<Typography element="h3">Рейтинг</Typography>
<Paragraph>
Обсуждаем дороги: рейтинг, опыт, комфорт в пути!
</Paragraph>
<Typography element="h3">{tHome("rating")}</Typography>
<Paragraph>{tHome("road_discussions")}</Paragraph>
</div>
<SearchForm
onChange={(e) => setRatingSearch(e.target.value)}
name="rating-search"
placeholder="Введите адрес"
placeholder={tHome("enter_address")}
value={ratingSearch}
handleSubmit={handleSubmit}
/>
@@ -189,7 +169,7 @@ const RatingSection: React.FC<IRatingSectionProps> = ({
{reports.results.map((report) => (
<tr key={report.id}>
<td id="rating-section-date">
{sliceDate(report.created_at)}
{new Date(report.created_at).toLocaleDateString()}
</td>
<td id="rating-section-link">
<Link href={`/report/${report.id}`}>
@@ -1,10 +1,3 @@
export const sliceDate = (date: string) => {
return `${date.slice(8, 10)}.${date.slice(5, 7)}.${date.slice(
0,
4
)}`;
};
export const sliceLocation = (location: string) => {
if (location.length > 15) return `${location.slice(0, 15)}...`;
@@ -0,0 +1,60 @@
import { apiInstance } from "@/shared/config/apiConfig";
import { IFetch } from "@/shared/types/fetch-type";
import { IList } from "@/shared/types/list-type";
import { IReport } from "@/shared/types/report-type";
import { create } from "zustand";
interface IFetchReports extends IList {
results: IReport[];
}
interface IRatingStore extends IFetch {
data: IFetchReports;
getReports: (
categories: string,
page: number,
sort: string
) => Promise<void>;
}
export const useRatingStore = create<IRatingStore>((set) => ({
data: {
count: 0,
previous: null,
next: null,
results: [],
},
isLoading: false,
error: "",
getReports: async (
query: string = "",
page: number = 1,
sort: string = ""
) => {
try {
set({ isLoading: true });
const data = (
await apiInstance.get<IFetchReports>(
`/report/list_sort?sort_by=${sort}&page=${page}`
)
).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 {
set({ isLoading: false });
}
},
}));
@@ -1,4 +1,4 @@
@import "../../shared/ui/variables.scss";
@import "@/shared/ui/variables.scss";
.statistics-section {
padding: 120px 90px 110px 90px;
@@ -1,6 +1,7 @@
import { ROAD_TYPES_STATS } from "@/shared/variables/road-types";
import "./StatisticsSection.scss";
import { apiInstance } from "@/shared/config/apiConfig";
import { useTranslations } from "next-intl";
import { getTranslations } from "next-intl/server";
interface ICategoryStatistics {
category: number;
@@ -8,6 +9,8 @@ interface ICategoryStatistics {
}
const StatisticsSection = async () => {
const t = await getTranslations("general");
const getCategoryStatistics = async () => {
const res = await apiInstance.get<ICategoryStatistics[]>(
"/report/category_count/"
@@ -18,6 +21,15 @@ const StatisticsSection = async () => {
const statistics = await getCategoryStatistics();
const ROAD_TYPES_STATS: Record<number, string> = {
1: t("broken_roads"),
2: t("accident_hotspots"),
3: t("local_defects"),
4: t("repair_plans"),
5: t("repaired"),
6: t("fixed_local_defects"),
};
return (
<section className="statistics-section">
<ul className="statistics-section__list">
@@ -0,0 +1,52 @@
.report-images {
max-width: 540px;
width: 100%;
display: flex;
flex-direction: column;
gap: 20px;
img {
object-fit: cover;
border-radius: 10px;
}
&__main {
width: 100%;
height: 441px;
grid-column: 1 / 5;
}
&__list {
display: grid;
grid-template-columns: 1fr 1fr 1fr 1fr;
gap: 20px;
}
&__item {
height: 102px;
}
}
@media screen and (max-width: 1024px) {
.report-images {
max-width: 100%;
}
}
@media screen and (max-width: 550px) {
.report-images {
&__main {
height: 162px;
grid-column: 1 / 2;
}
&__list {
grid-template-columns: 1fr 1fr;
}
&__item {
width: 100%;
height: 162px;
}
}
}
@@ -0,0 +1,56 @@
"use client";
import "./ReportImages.scss";
import { useEffect, useState } from "react";
interface IReportImagesProps {
images: IImage[];
}
interface IImage {
id: number;
image: string;
}
const ReportImages: React.FC<IReportImagesProps> = ({
images,
}: IReportImagesProps) => {
const [mainImage, setMainImage] = useState<IImage>(images[0]);
const [width, setWidth] = useState<number>(0);
const filterImages = images.filter((i) => i.id !== mainImage.id);
useEffect(() => {
setWidth(window.innerWidth);
}, []);
const handleMainImage = (image: IImage) => {
if (width <= 550) return;
setMainImage(image);
};
return (
<div className="report-images">
<div className="report-images__list">
<img
className="report-images__main"
src={mainImage.image}
alt="Report Main Image"
/>
{filterImages.map((image) => (
<button
key={image.id}
onClick={() => handleMainImage(image)}
>
<img
className="report-images__item"
src={image.image}
alt="Report Image"
/>
</button>
))}
</div>
</div>
);
};
export default ReportImages;
@@ -0,0 +1,107 @@
.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);
}
}
@media screen and (max-width: 1024px) {
.report-information {
max-width: 100%;
h2 {
font-size: 36px;
line-height: 43px;
}
&__description {
font-size: 18px;
}
}
}
@media screen and (max-width: 550px) {
.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;
}
}
}
@@ -0,0 +1,75 @@
import "./ReportInformation.scss";
import RoadType from "@/entities/RoadType/RoadType";
import ReportLike from "@/features/ReportLike/ReportLike";
import { IAuthor } from "@/shared/types/author-type";
import { ILocation } from "@/shared/types/location-type";
import calendar from "./icons/calendar.svg";
import { ROAD_TYPES_COLORS } from "@/shared/variables/road-types";
import Image from "next/image";
import { MONTHS } from "@/shared/variables/month";
import ShowMapButton from "@/features/ShowMapButton/ShowMapButton";
import { getTranslations } from "next-intl/server";
interface IReportInformationProps {
id: number;
location: ILocation;
date: string;
description: string;
category: number;
total_likes: number;
author: IAuthor;
}
const ReportInformation: React.FC<IReportInformationProps> = async ({
id,
location,
date,
description,
category,
total_likes,
author,
}: IReportInformationProps) => {
const t = await getTranslations("home");
const ROAD_TYPES: Record<number, string> = {
1: t("broken_roads"),
2: t("accident_hotspots"),
3: t("local_defects"),
4: t("repair_plans"),
5: t("repaired"),
6: t("fixed_local_defects"),
};
return (
<div className="report-information">
<RoadType color={ROAD_TYPES_COLORS[category]}>
{ROAD_TYPES[category]}
</RoadType>
<h2>{location.address}</h2>
<div className="report-information__date-and-like">
<div className="report-information__date">
<Image src={calendar} alt="Calendar Icon" />
<p>
{MONTHS[date.slice(5, 7)]}{" "}
{date.slice(5, 7).slice(0, 1) === "0"
? date.slice(6, 7)
: date.slice(5, 7)}
, {date.slice(0, 4)}
</p>
</div>
<ReportLike count={total_likes} report_id={id} />
</div>
<p className="report-information__description">{description}</p>
<p className="report-information__author">
Автор обращения:{" "}
<span>
{author.first_name} {author.last_name.slice(0, 1)}.
</span>
</p>
<ShowMapButton />
</div>
);
};
export default ReportInformation;
@@ -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

@@ -0,0 +1,44 @@
.report-map {
width: 100%;
height: 580px;
border-radius: 10px;
.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;
p {
text-decoration: underline;
color: #3998e8;
font-size: 12px;
font-weight: 400;
line-height: 18px;
}
}
}
}
@media screen and (max-width: 770px) {
.report-map {
height: 416px;
}
}
@media screen and (max-width: 550px) {
.report-map {
height: 370px;
}
}
@@ -0,0 +1,108 @@
"use client";
import "./ReportMap.scss";
import "leaflet/dist/leaflet.css";
import {
MapContainer,
Marker,
Polyline,
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 { ILocation } from "@/shared/types/location-type";
import { Link } from "@/shared/config/navigation";
import { useReportStore } from "../reportStore";
interface IReportMapProps {
location: ILocation[];
category: number;
}
const ReportMap: React.FC<IReportMapProps> = ({
location,
category,
}: IReportMapProps) => {
const { showMap } = useReportStore();
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_yellow_icon),
6: createCustomIcon(geo_green_icon),
};
const categoryToPolyline: Record<number, { color: string }> = {
1: { color: "rgba(230, 68, 82, 0.8)" },
2: { color: "rgba(198, 152, 224, 0.8)" },
3: { color: "rgba(135, 40, 157, 0.8)" },
4: { color: "rgba(247, 181, 84, 0.8)" },
5: { color: "rgba(254, 211, 99, 0.8)" },
6: { color: "rgba(158, 221, 128, 0.8)" },
};
const position = {
lat: +location[0].latitude,
lng: +location[0].longitude,
};
return showMap ? (
<MapContainer
center={position}
zoom={14}
scrollWheelZoom={false}
className="report-map"
>
<TileLayer
attribution='&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
/>
{location.length === 2 ? (
<Polyline
pathOptions={categoryToPolyline[category]}
positions={[
[
parseFloat(location[0].latitude),
parseFloat(location[0].longitude),
],
[
parseFloat(location[1].latitude),
parseFloat(location[1].longitude),
],
]}
/>
) : null}
{location.map((marker) => (
<Marker
key={marker.id}
icon={icons[category]}
position={
[+marker.latitude, +marker.longitude] as LatLngTuple
}
>
<Popup>
<p>{marker.address}</p>
</Popup>
</Marker>
))}
</MapContainer>
) : null;
};
export default ReportMap;
@@ -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

@@ -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

@@ -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

@@ -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

Loaded 100 of 204 files, more files were not shown because too many files have changed in this diff. Show more