added next-intl, composed next-auth and next-intl in middleware, fixed some bugs, completed details page, added google auth
No files matched your search
@@ -1,6 +1,5 @@
|
||||
import "./Footer.scss";
|
||||
import logo from "../../shared/assets/logo.svg";
|
||||
import Link from "next/link";
|
||||
import Image from "next/image";
|
||||
import { LINKS } from "@/shared/variables/links";
|
||||
import youtube from "./icons/youtube.svg";
|
||||
@@ -8,17 +7,20 @@ import facebook from "./icons/facebook.svg";
|
||||
import instagram from "./icons/instagram.svg";
|
||||
import app_store_btn from "./icons/app-store-btn.svg";
|
||||
import play_market_btn from "./icons/play-market-btn.svg";
|
||||
import { Link } from "@/shared/config/navigation";
|
||||
import { useTranslations } from "next-intl";
|
||||
|
||||
const Footer = () => {
|
||||
const t = useTranslations("general");
|
||||
return (
|
||||
<footer className="footer">
|
||||
<Link href="/">
|
||||
<Image src={logo} alt="Logo" />
|
||||
</Link>
|
||||
<div className="footer__links">
|
||||
<h4>Навигация</h4>
|
||||
<h4>{t("navigation")}</h4>
|
||||
<ul>
|
||||
{LINKS.map((link) => (
|
||||
{LINKS().map((link) => (
|
||||
<li key={link.id}>
|
||||
<Link href={link.pathname} key={link.id}>
|
||||
{link.pagename}
|
||||
@@ -29,7 +31,7 @@ const Footer = () => {
|
||||
</div>
|
||||
|
||||
<div className="footer__contacts">
|
||||
<h4>Контакты</h4>
|
||||
<h4>{t("contacts")}</h4>
|
||||
<ul>
|
||||
<li>namename@gmail.com</li>
|
||||
<li>+09646895467</li>
|
||||
@@ -44,7 +46,7 @@ const Footer = () => {
|
||||
</div>
|
||||
|
||||
<div className="footer__apps">
|
||||
<h4>Скачивай наше приложение</h4>
|
||||
<h4>{t("download_our_app")}</h4>
|
||||
<div className="footer__apps-btns">
|
||||
{[app_store_btn, play_market_btn].map((app, i) => (
|
||||
<Link key={i} href="#">
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import Link from "next/link";
|
||||
import "./NavAuth.scss";
|
||||
import { usePathname } from "next/navigation";
|
||||
import { useSession } from "next-auth/react";
|
||||
import { Link } from "@/shared/config/navigation";
|
||||
import { useTranslations } from "next-intl";
|
||||
|
||||
interface INavAuthProps {
|
||||
responsible?: boolean;
|
||||
@@ -12,6 +13,7 @@ const NavAuth: React.FC<INavAuthProps> = ({
|
||||
responsible,
|
||||
setOpenMenu,
|
||||
}: INavAuthProps) => {
|
||||
const t = useTranslations("navigation");
|
||||
const session = useSession();
|
||||
const auth = session.status === "authenticated" ? true : false;
|
||||
const pathname = usePathname();
|
||||
@@ -27,7 +29,7 @@ const NavAuth: React.FC<INavAuthProps> = ({
|
||||
: "lg"
|
||||
}`}
|
||||
>
|
||||
Профиль
|
||||
{t("profile")}
|
||||
</Link>
|
||||
) : (
|
||||
<Link
|
||||
@@ -39,7 +41,7 @@ const NavAuth: React.FC<INavAuthProps> = ({
|
||||
: "lg"
|
||||
}`}
|
||||
>
|
||||
Войти
|
||||
{t("login")}
|
||||
</Link>
|
||||
)}
|
||||
</>
|
||||
|
||||
@@ -1,20 +1,52 @@
|
||||
"use client";
|
||||
|
||||
import "./NavLanguage.scss";
|
||||
import Image from "next/image";
|
||||
import { useState } from "react";
|
||||
import { useEffect, useRef, useState, useTransition } from "react";
|
||||
import globus from "./icons/globus.svg";
|
||||
import chevron from "./icons/chevron-down.svg";
|
||||
import check from "./icons/check.svg";
|
||||
import { useParams, useRouter } from "next/navigation";
|
||||
import { LANGUAGES } from "./variables";
|
||||
import { usePathname } from "@/shared/config/navigation";
|
||||
|
||||
const NavLanguage = () => {
|
||||
const [language, setLanguage] = useState<string>("ru");
|
||||
const [openMenu, setOpenMenu] = useState<boolean>(false);
|
||||
const LANGUAGES = [
|
||||
{ id: 1, language: "Русский", index: "ru" },
|
||||
{ id: 2, language: "Кыргызча", index: "kg" },
|
||||
{ id: 3, language: "English", index: "en" },
|
||||
];
|
||||
const menuRef: React.RefObject<HTMLDivElement> = useRef(null);
|
||||
|
||||
const router = useRouter();
|
||||
const pathname = usePathname();
|
||||
const { locale } = useParams();
|
||||
|
||||
const [_, startTransition] = useTransition();
|
||||
const [language, setLanguage] = useState<string>(locale as string);
|
||||
|
||||
useEffect(() => {
|
||||
const params = window.location.search || "";
|
||||
startTransition(() => {
|
||||
router.replace(`/${language}${pathname}${params}`, {
|
||||
scroll: false,
|
||||
});
|
||||
});
|
||||
}, [language]);
|
||||
|
||||
useEffect(() => {
|
||||
function handleClickOutside(event: any) {
|
||||
if (
|
||||
menuRef.current &&
|
||||
!menuRef.current.contains(event.target)
|
||||
) {
|
||||
setOpenMenu(false);
|
||||
}
|
||||
}
|
||||
document.addEventListener("mousedown", handleClickOutside);
|
||||
return () => {
|
||||
document.removeEventListener("mousedown", handleClickOutside);
|
||||
};
|
||||
}, [menuRef]);
|
||||
|
||||
return (
|
||||
<div className="nav-language">
|
||||
<div ref={menuRef} className="nav-language">
|
||||
<button
|
||||
onClick={() => setOpenMenu((prev) => !prev)}
|
||||
className="nav-language__btn"
|
||||
@@ -29,7 +61,10 @@ const NavLanguage = () => {
|
||||
className={`nav-language__option${
|
||||
language === lang.index ? "_active" : ""
|
||||
}`}
|
||||
onClick={() => setLanguage(lang.index)}
|
||||
onClick={() => {
|
||||
setLanguage(lang.index);
|
||||
setOpenMenu(false);
|
||||
}}
|
||||
key={lang.id}
|
||||
>
|
||||
{lang.language}
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
interface ILangs {
|
||||
id: number;
|
||||
language: string;
|
||||
index: string;
|
||||
}
|
||||
|
||||
export const LANGUAGES: ILangs[] = [
|
||||
{ id: 1, language: "Русский", index: "ru" },
|
||||
{ id: 2, language: "Кыргызча", index: "kg" },
|
||||
{ id: 3, language: "English", index: "en" },
|
||||
];
|
||||
@@ -1,8 +1,7 @@
|
||||
import { LINKS } from "@/shared/variables/links";
|
||||
import "./NavMenu.scss";
|
||||
import Link from "next/link";
|
||||
import { usePathname } from "next/navigation";
|
||||
import NavAuth from "../NavAuth/NavAuth";
|
||||
import { Link, usePathname } from "@/shared/config/navigation";
|
||||
|
||||
interface INavMenuProps {
|
||||
setOpenMenu: (boolean: boolean) => void;
|
||||
@@ -12,15 +11,16 @@ const NavMenu: React.FC<INavMenuProps> = ({
|
||||
setOpenMenu,
|
||||
}: INavMenuProps) => {
|
||||
const pathname = usePathname();
|
||||
|
||||
return (
|
||||
<nav className="nav-menu">
|
||||
{LINKS.map((link) => (
|
||||
{LINKS().map((link) => (
|
||||
<Link
|
||||
onClick={() => setOpenMenu(false)}
|
||||
className={`nav-menu__link${
|
||||
pathname === link.pathname ? "_active" : ""
|
||||
pathname.slice(4) === link.pathname ? "_active" : ""
|
||||
}`}
|
||||
href={link.pathname}
|
||||
href={`${pathname.slice(1, 3)}/${link.pathname}`}
|
||||
key={link.id}
|
||||
>
|
||||
{link.pagename}
|
||||
|
||||
@@ -2,15 +2,14 @@
|
||||
|
||||
import "./Navbar.scss";
|
||||
import Image from "next/image";
|
||||
import Link from "next/link";
|
||||
import { usePathname } from "next/navigation";
|
||||
import { Link } from "@/shared/config/navigation";
|
||||
import { usePathname } from "@/shared/config/navigation";
|
||||
import logo from "@/shared/assets/logo.svg";
|
||||
import { LINKS } from "@/shared/variables/links";
|
||||
import NavLanguage from "./NavLanguage/NavLanguage";
|
||||
import NavAuth from "./NavAuth/NavAuth";
|
||||
import menu from "./icons/menu.svg";
|
||||
import cross from "./icons/cross.svg";
|
||||
|
||||
import NavMenu from "./NavMenu/NavMenu";
|
||||
import { useState } from "react";
|
||||
|
||||
@@ -25,7 +24,7 @@ const Navbar = () => {
|
||||
</Link>
|
||||
|
||||
<nav className="navbar__links">
|
||||
{LINKS.map((link) => (
|
||||
{LINKS().map((link) => (
|
||||
<Link
|
||||
className={`navbar__link${
|
||||
pathname === link.pathname ? "_active" : ""
|
||||
|
||||
@@ -2,8 +2,7 @@
|
||||
|
||||
import "./ProfileNav.scss";
|
||||
import LogoutButton from "@/features/LogoutButton/LogoutButton";
|
||||
import Link from "next/link";
|
||||
import { usePathname } from "next/navigation";
|
||||
import { Link, usePathname } from "@/shared/config/navigation";
|
||||
|
||||
interface IProfileNavProps {
|
||||
report_count: number;
|
||||
|
||||
@@ -20,6 +20,32 @@
|
||||
}
|
||||
}
|
||||
|
||||
&__auth-warning,
|
||||
&__warning {
|
||||
height: 150px;
|
||||
margin-bottom: 70px;
|
||||
text-align: center;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 4px;
|
||||
|
||||
font-size: 18px;
|
||||
font-weight: 500;
|
||||
line-height: 22px;
|
||||
|
||||
border: 1px solid #c5c6c5;
|
||||
border-radius: 10px;
|
||||
|
||||
button {
|
||||
text-decoration: underline;
|
||||
color: #0077b6;
|
||||
font-size: 18px;
|
||||
font-weight: 500;
|
||||
line-height: 22px;
|
||||
}
|
||||
}
|
||||
|
||||
form {
|
||||
margin-bottom: 70px;
|
||||
display: flex;
|
||||
|
||||
@@ -3,10 +3,15 @@
|
||||
import "./ReviewSection.scss";
|
||||
import { apiInstance } from "@/shared/config/apiConfig";
|
||||
import { IReviewList } from "@/shared/types/review-type";
|
||||
import { useSession } from "next-auth/react";
|
||||
import { signIn, useSession } from "next-auth/react";
|
||||
import { useEffect, useState } from "react";
|
||||
import calendar from "./icons/calendar.svg";
|
||||
import Image from "next/image";
|
||||
import {
|
||||
Link,
|
||||
usePathname,
|
||||
useRouter,
|
||||
} from "@/shared/config/navigation";
|
||||
|
||||
interface IReviewsSectionProps {
|
||||
endpoint: string;
|
||||
@@ -18,6 +23,7 @@ const ReviewSection: React.FC<IReviewsSectionProps> = ({
|
||||
id,
|
||||
}: IReviewsSectionProps) => {
|
||||
const [reviews, setReviews] = useState<IReviewList>();
|
||||
const pathname = usePathname();
|
||||
const session = useSession();
|
||||
const handleSubmit: React.MouseEventHandler<
|
||||
HTMLFormElement
|
||||
@@ -33,6 +39,10 @@ const ReviewSection: React.FC<IReviewsSectionProps> = ({
|
||||
},
|
||||
};
|
||||
|
||||
if (session.status === "unauthenticated") {
|
||||
signIn(undefined, { callbackUrl: pathname });
|
||||
}
|
||||
|
||||
if (!formData.get("review")) {
|
||||
return;
|
||||
}
|
||||
@@ -53,7 +63,7 @@ const ReviewSection: React.FC<IReviewsSectionProps> = ({
|
||||
|
||||
const getReviews = async () => {
|
||||
const response = await apiInstance.get<IReviewList>(
|
||||
`/${endpoint}/${id}/reviews/`
|
||||
`/${endpoint}/${id}/reviews/?page_size=8`
|
||||
);
|
||||
|
||||
setReviews(response.data);
|
||||
@@ -83,47 +93,66 @@ const ReviewSection: React.FC<IReviewsSectionProps> = ({
|
||||
<span id="blue-point" /> Написать комментарий
|
||||
</h3>
|
||||
|
||||
<form onSubmit={handleSubmit}>
|
||||
<textarea name="review" />
|
||||
<button type="submit">Отправить</button>
|
||||
</form>
|
||||
{session.status === "authenticated" ? (
|
||||
<form onSubmit={handleSubmit}>
|
||||
<textarea name="review" />
|
||||
<button type="submit">Отправить</button>
|
||||
</form>
|
||||
) : (
|
||||
<p className="review-section__auth-warning">
|
||||
Перед тем как оставить комментарий, пожалуйста
|
||||
<button
|
||||
onClick={() =>
|
||||
signIn(undefined, { callbackUrl: pathname })
|
||||
}
|
||||
>
|
||||
войдите в аккаунт
|
||||
</button>
|
||||
</p>
|
||||
)}
|
||||
|
||||
<div className="review-section__list">
|
||||
<h3>
|
||||
<span id="blue-point" /> Комментарии
|
||||
</h3>
|
||||
|
||||
<ul>
|
||||
{reviews?.results.map((review) => (
|
||||
<li key={review.id} className="review">
|
||||
<div className="review__author">
|
||||
<img
|
||||
id="author-img"
|
||||
src={review.author.image}
|
||||
alt="Author Image"
|
||||
/>
|
||||
<div className="review__header">
|
||||
<h5 className="review__author-name">
|
||||
{review.author.first_name}{" "}
|
||||
{review.author.last_name}
|
||||
</h5>
|
||||
<div className="review__date">
|
||||
<Image src={calendar} alt="Calendar Icon" />
|
||||
<p>
|
||||
{months[review.created_at.slice(5, 7)]}{" "}
|
||||
{review.created_at.slice(5, 7).slice(0, 1) ===
|
||||
"0"
|
||||
? review.created_at.slice(6, 7)
|
||||
: review.created_at.slice(5, 7)}
|
||||
, {review.created_at.slice(0, 4)}
|
||||
</p>
|
||||
{reviews?.results.length !== 0 ? (
|
||||
<ul>
|
||||
{reviews?.results.map((review) => (
|
||||
<li key={review.id} className="review">
|
||||
<div className="review__author">
|
||||
<img
|
||||
id="author-img"
|
||||
src={review.author.image}
|
||||
alt="Author Image"
|
||||
/>
|
||||
<div className="review__header">
|
||||
<h5 className="review__author-name">
|
||||
{review.author.first_name}{" "}
|
||||
{review.author.last_name}
|
||||
</h5>
|
||||
<div className="review__date">
|
||||
<Image src={calendar} alt="Calendar Icon" />
|
||||
<p>
|
||||
{months[review.created_at.slice(5, 7)]}{" "}
|
||||
{review.created_at.slice(5, 7).slice(0, 1) ===
|
||||
"0"
|
||||
? review.created_at.slice(6, 7)
|
||||
: review.created_at.slice(5, 7)}
|
||||
, {review.created_at.slice(0, 4)}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<p className="review__description">{review.review}</p>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
<p className="review__description">{review.review}</p>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
) : (
|
||||
<p className="review-section__warning">
|
||||
Оставьте комментарий первым :)
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import "./ConfirmEmailForm.scss";
|
||||
import { useEffect, useState } from "react";
|
||||
import { apiInstance } from "@/shared/config/apiConfig";
|
||||
import { useRouter } from "next/navigation";
|
||||
import Loader from "@/shared/ui/components/Loader/Loader";
|
||||
import { useRouter } from "@/shared/config/navigation";
|
||||
|
||||
interface IConfirmEmailFormProps {
|
||||
email: string;
|
||||
@@ -5,8 +5,8 @@ import "./confirm-code.scss";
|
||||
import { useState } from "react";
|
||||
import { apiInstance } from "@/shared/config/apiConfig";
|
||||
import Loader from "@/shared/ui/components/Loader/Loader";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { ITokens } from "@/shared/types/token-type";
|
||||
import { useRouter } from "@/shared/config/navigation";
|
||||
|
||||
interface IConfirmCodeProps {
|
||||
setChangeForm: (boolean: boolean) => void;
|
||||
@@ -3,16 +3,14 @@
|
||||
import "./ProfileForm.scss";
|
||||
import Image from "next/image";
|
||||
import pen from "./icons/pen.svg";
|
||||
import eye_off from "./icons/eye-off.svg";
|
||||
import eye_on from "./icons/eye-on.svg";
|
||||
import { useState } from "react";
|
||||
import { apiInstance } from "@/shared/config/apiConfig";
|
||||
import { apiInstance, authInstanse } from "@/shared/config/apiConfig";
|
||||
import { AxiosError } from "axios";
|
||||
import Loader from "@/shared/ui/components/Loader/Loader";
|
||||
import { useSession } from "next-auth/react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import LogoutButton from "@/features/LogoutButton/LogoutButton";
|
||||
import ChangePassword from "./ChangePassword/ChangePassword";
|
||||
import { useRouter } from "@/shared/config/navigation";
|
||||
|
||||
interface IProfileFormProps {
|
||||
id: number;
|
||||
@@ -19,8 +19,8 @@ import axios, { AxiosError } from "axios";
|
||||
import { apiInstance } from "@/shared/config/apiConfig";
|
||||
import { useSession } from "next-auth/react";
|
||||
import { IDisplayMap } from "@/shared/types/map-type";
|
||||
import { useRouter } from "next/navigation";
|
||||
import Loader from "@/shared/ui/components/Loader/Loader";
|
||||
import { useRouter } from "@/shared/config/navigation";
|
||||
|
||||
interface ILatLng {
|
||||
lat: number;
|
||||
@@ -5,8 +5,8 @@ import "./ResetCodeForm.scss";
|
||||
import { useState } from "react";
|
||||
import { apiInstance } from "@/shared/config/apiConfig";
|
||||
import { ITokens } from "@/shared/types/token-type";
|
||||
import { useRouter } from "next/navigation";
|
||||
import Loader from "@/shared/ui/components/Loader/Loader";
|
||||
import { useRouter } from "@/shared/config/navigation";
|
||||
|
||||
const ResetCodeForm = () => {
|
||||
const [passwordWarning, setPasswordWarning] = useState<string>("");
|
||||
@@ -3,10 +3,9 @@
|
||||
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 { useState } from "react";
|
||||
|
||||
const SignInForm = () => {
|
||||
@@ -5,9 +5,9 @@ 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 { useRouter } from "@/shared/config/navigation";
|
||||
|
||||
const SignUpForm = () => {
|
||||
const [emailWarning, setEmailWarning] = useState<string>("");
|
||||
@@ -72,7 +72,7 @@ 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")}`
|
||||
);
|
||||
@@ -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>
|
||||
Borem ipsum dolor sit amet, consectetur adipiscing elit.
|
||||
</h1>
|
||||
<p>
|
||||
Borem ipsum dolor sit amet, consectetur adipiscing elit.
|
||||
</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;
|
||||
@@ -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,6 +30,9 @@ interface IRatingSectionProps {
|
||||
const RatingSection: React.FC<IRatingSectionProps> = ({
|
||||
searchParams,
|
||||
}: IRatingSectionProps) => {
|
||||
const tHome = useTranslations("home");
|
||||
const tGeneral = useTranslations("general");
|
||||
|
||||
const [ratingSearch, setRatingSearch] = useState<string>(
|
||||
searchParams["поиск-рейтинг"] || ""
|
||||
);
|
||||
@@ -107,7 +104,7 @@ const RatingSection: React.FC<IRatingSectionProps> = ({
|
||||
const params = [
|
||||
{
|
||||
id: 1,
|
||||
name: "Дата",
|
||||
name: tGeneral("date"),
|
||||
handleClick() {
|
||||
if (filter.option !== "date") {
|
||||
return setFilter({ option: "date", toggle: false });
|
||||
@@ -119,12 +116,12 @@ const RatingSection: React.FC<IRatingSectionProps> = ({
|
||||
getReports(ratingSearch, activePage, filter);
|
||||
},
|
||||
},
|
||||
{ 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 });
|
||||
@@ -138,7 +135,7 @@ const RatingSection: React.FC<IRatingSectionProps> = ({
|
||||
},
|
||||
{
|
||||
id: 6,
|
||||
name: "Рейтинг",
|
||||
name: tGeneral("rating"),
|
||||
handleClick() {
|
||||
if (filter.option !== "rating") {
|
||||
return setFilter({ option: "rating", toggle: false });
|
||||
@@ -152,18 +149,25 @@ const RatingSection: React.FC<IRatingSectionProps> = ({
|
||||
},
|
||||
];
|
||||
|
||||
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 +193,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)}...`;
|
||||
|
||||
@@ -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='© <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 |
@@ -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 |
@@ -0,0 +1,17 @@
|
||||
<svg width="118" height="130" viewBox="0 0 118 130" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g filter="url(#filter0_d_122_42709)">
|
||||
<path d="M59 20C48.5233 20 40 28.7481 40 39.501C40 52.8456 57.0031 68.5901 57.7271 69.4175C58.407 70.1948 59.5942 70.1935 60.2729 69.4175C60.9969 68.5901 78 52.8456 78 39.501C77.9998 28.7481 69.4766 20 59 20Z" fill="#3998E8"/>
|
||||
</g>
|
||||
<ellipse cx="58.7872" cy="39" rx="9.88775" ry="10" fill="white"/>
|
||||
<defs>
|
||||
<filter id="filter0_d_122_42709" x="0" y="0" width="118" height="130" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
|
||||
<feOffset dy="20"/>
|
||||
<feGaussianBlur stdDeviation="20"/>
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 0.314315 0 0 0 0 0.376772 0 0 0 0 0.406356 0 0 0 0.5 0"/>
|
||||
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_122_42709"/>
|
||||
<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow_122_42709" result="shape"/>
|
||||
</filter>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.1 KiB |
@@ -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 |
@@ -0,0 +1,13 @@
|
||||
import { create } from "zustand";
|
||||
|
||||
interface useReportStore {
|
||||
showMap: boolean;
|
||||
setShowMap: (bool: boolean) => void;
|
||||
}
|
||||
|
||||
export const useReportStore = create<useReportStore>((set) => ({
|
||||
showMap: false,
|
||||
setShowMap: (bool: boolean) => {
|
||||
set({ showMap: bool });
|
||||
},
|
||||
}));
|
||||
@@ -7,13 +7,12 @@ import {
|
||||
REPORT_STATUS,
|
||||
REPORT_STATUS_COLORS,
|
||||
} from "@/shared/variables/report-status";
|
||||
import Link from "next/link";
|
||||
import Pagination from "@/features/Pagination/Pagination";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useSession } from "next-auth/react";
|
||||
import { sliceDate } from "./helpers";
|
||||
import { useProfileReportsStore } from "./profile-reports.store";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { Link, useRouter } from "@/shared/config/navigation";
|
||||
|
||||
interface IProfileTableProps {
|
||||
searchParams: { ["страница-обращений"]: string };
|
||||
@@ -1,6 +1,5 @@
|
||||
"use client";
|
||||
|
||||
import { IStatistics } from "@/shared/types/statistics-type";
|
||||
import "./StatisticsTable.scss";
|
||||
import { useStatisticsStore } from "./statistics.store";
|
||||
import { useShallow } from "zustand/react/shallow";
|
||||
@@ -10,7 +9,7 @@ import arrows from "@/shared/icons/arrows.svg";
|
||||
import Image from "next/image";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useDebounce } from "use-debounce";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useRouter } from "@/shared/config/navigation";
|
||||
|
||||
interface IStatisticsTableProps {
|
||||
searchParams: {
|
||||