added next-intl, composed next-auth and next-intl in middleware, fixed some bugs, completed details page, added google auth

This commit is contained in:
Alibek committed 2024-02-29 14:09:37 +06:00
1 parent 9cd7b2a67d
commit 6637ab4eae
172 files changed
+1772 -502

No files matched your search

+7 -5
View File
@@ -1,6 +1,5 @@
import "./Footer.scss";
import logo from "../../shared/assets/logo.svg";
import Link from "next/link";
import Image from "next/image";
import { LINKS } from "@/shared/variables/links";
import youtube from "./icons/youtube.svg";
@@ -8,17 +7,20 @@ import facebook from "./icons/facebook.svg";
import instagram from "./icons/instagram.svg";
import app_store_btn from "./icons/app-store-btn.svg";
import play_market_btn from "./icons/play-market-btn.svg";
import { Link } from "@/shared/config/navigation";
import { useTranslations } from "next-intl";
const Footer = () => {
const t = useTranslations("general");
return (
<footer className="footer">
<Link href="/">
<Image src={logo} alt="Logo" />
</Link>
<div className="footer__links">
<h4>Навигация</h4>
<h4>{t("navigation")}</h4>
<ul>
{LINKS.map((link) => (
{LINKS().map((link) => (
<li key={link.id}>
<Link href={link.pathname} key={link.id}>
{link.pagename}
@@ -29,7 +31,7 @@ const Footer = () => {
</div>
<div className="footer__contacts">
<h4>Контакты</h4>
<h4>{t("contacts")}</h4>
<ul>
<li>namename@gmail.com</li>
<li>+09646895467</li>
@@ -44,7 +46,7 @@ const Footer = () => {
</div>
<div className="footer__apps">
<h4>Скачивай наше приложение</h4>
<h4>{t("download_our_app")}</h4>
<div className="footer__apps-btns">
{[app_store_btn, play_market_btn].map((app, i) => (
<Link key={i} href="#">
+5 -3
View File
@@ -1,7 +1,8 @@
import Link from "next/link";
import "./NavAuth.scss";
import { usePathname } from "next/navigation";
import { useSession } from "next-auth/react";
import { Link } from "@/shared/config/navigation";
import { useTranslations } from "next-intl";
interface INavAuthProps {
responsible?: boolean;
@@ -12,6 +13,7 @@ const NavAuth: React.FC<INavAuthProps> = ({
responsible,
setOpenMenu,
}: INavAuthProps) => {
const t = useTranslations("navigation");
const session = useSession();
const auth = session.status === "authenticated" ? true : false;
const pathname = usePathname();
@@ -27,7 +29,7 @@ const NavAuth: React.FC<INavAuthProps> = ({
: "lg"
}`}
>
Профиль
{t("profile")}
</Link>
) : (
<Link
@@ -39,7 +41,7 @@ const NavAuth: React.FC<INavAuthProps> = ({
: "lg"
}`}
>
Войти
{t("login")}
</Link>
)}
</>
+44 -9
View File
@@ -1,20 +1,52 @@
"use client";
import "./NavLanguage.scss";
import Image from "next/image";
import { useState } from "react";
import { useEffect, useRef, useState, useTransition } from "react";
import globus from "./icons/globus.svg";
import chevron from "./icons/chevron-down.svg";
import check from "./icons/check.svg";
import { useParams, useRouter } from "next/navigation";
import { LANGUAGES } from "./variables";
import { usePathname } from "@/shared/config/navigation";
const NavLanguage = () => {
const [language, setLanguage] = useState<string>("ru");
const [openMenu, setOpenMenu] = useState<boolean>(false);
const LANGUAGES = [
{ id: 1, language: "Русский", index: "ru" },
{ id: 2, language: "Кыргызча", index: "kg" },
{ id: 3, language: "English", index: "en" },
];
const menuRef: React.RefObject<HTMLDivElement> = useRef(null);
const router = useRouter();
const pathname = usePathname();
const { locale } = useParams();
const [_, startTransition] = useTransition();
const [language, setLanguage] = useState<string>(locale as string);
useEffect(() => {
const params = window.location.search || "";
startTransition(() => {
router.replace(`/${language}${pathname}${params}`, {
scroll: false,
});
});
}, [language]);
useEffect(() => {
function handleClickOutside(event: any) {
if (
menuRef.current &&
!menuRef.current.contains(event.target)
) {
setOpenMenu(false);
}
}
document.addEventListener("mousedown", handleClickOutside);
return () => {
document.removeEventListener("mousedown", handleClickOutside);
};
}, [menuRef]);
return (
<div className="nav-language">
<div ref={menuRef} className="nav-language">
<button
onClick={() => setOpenMenu((prev) => !prev)}
className="nav-language__btn"
@@ -29,7 +61,10 @@ const NavLanguage = () => {
className={`nav-language__option${
language === lang.index ? "_active" : ""
}`}
onClick={() => setLanguage(lang.index)}
onClick={() => {
setLanguage(lang.index);
setOpenMenu(false);
}}
key={lang.id}
>
{lang.language}
@@ -0,0 +1,11 @@
interface ILangs {
id: number;
language: string;
index: string;
}
export const LANGUAGES: ILangs[] = [
{ id: 1, language: "Русский", index: "ru" },
{ id: 2, language: "Кыргызча", index: "kg" },
{ id: 3, language: "English", index: "en" },
];
+5 -5
View File
@@ -1,8 +1,7 @@
import { LINKS } from "@/shared/variables/links";
import "./NavMenu.scss";
import Link from "next/link";
import { usePathname } from "next/navigation";
import NavAuth from "../NavAuth/NavAuth";
import { Link, usePathname } from "@/shared/config/navigation";
interface INavMenuProps {
setOpenMenu: (boolean: boolean) => void;
@@ -12,15 +11,16 @@ const NavMenu: React.FC<INavMenuProps> = ({
setOpenMenu,
}: INavMenuProps) => {
const pathname = usePathname();
return (
<nav className="nav-menu">
{LINKS.map((link) => (
{LINKS().map((link) => (
<Link
onClick={() => setOpenMenu(false)}
className={`nav-menu__link${
pathname === link.pathname ? "_active" : ""
pathname.slice(4) === link.pathname ? "_active" : ""
}`}
href={link.pathname}
href={`${pathname.slice(1, 3)}/${link.pathname}`}
key={link.id}
>
{link.pagename}
+3 -4
View File
@@ -2,15 +2,14 @@
import "./Navbar.scss";
import Image from "next/image";
import Link from "next/link";
import { usePathname } from "next/navigation";
import { Link } from "@/shared/config/navigation";
import { usePathname } from "@/shared/config/navigation";
import logo from "@/shared/assets/logo.svg";
import { LINKS } from "@/shared/variables/links";
import NavLanguage from "./NavLanguage/NavLanguage";
import NavAuth from "./NavAuth/NavAuth";
import menu from "./icons/menu.svg";
import cross from "./icons/cross.svg";
import NavMenu from "./NavMenu/NavMenu";
import { useState } from "react";
@@ -25,7 +24,7 @@ const Navbar = () => {
</Link>
<nav className="navbar__links">
{LINKS.map((link) => (
{LINKS().map((link) => (
<Link
className={`navbar__link${
pathname === link.pathname ? "_active" : ""
+1 -2
View File
@@ -2,8 +2,7 @@
import "./ProfileNav.scss";
import LogoutButton from "@/features/LogoutButton/LogoutButton";
import Link from "next/link";
import { usePathname } from "next/navigation";
import { Link, usePathname } from "@/shared/config/navigation";
interface IProfileNavProps {
report_count: number;
@@ -20,6 +20,32 @@
}
}
&__auth-warning,
&__warning {
height: 150px;
margin-bottom: 70px;
text-align: center;
display: flex;
align-items: center;
justify-content: center;
gap: 4px;
font-size: 18px;
font-weight: 500;
line-height: 22px;
border: 1px solid #c5c6c5;
border-radius: 10px;
button {
text-decoration: underline;
color: #0077b6;
font-size: 18px;
font-weight: 500;
line-height: 22px;
}
}
form {
margin-bottom: 70px;
display: flex;
+64 -35
View File
@@ -3,10 +3,15 @@
import "./ReviewSection.scss";
import { apiInstance } from "@/shared/config/apiConfig";
import { IReviewList } from "@/shared/types/review-type";
import { useSession } from "next-auth/react";
import { signIn, useSession } from "next-auth/react";
import { useEffect, useState } from "react";
import calendar from "./icons/calendar.svg";
import Image from "next/image";
import {
Link,
usePathname,
useRouter,
} from "@/shared/config/navigation";
interface IReviewsSectionProps {
endpoint: string;
@@ -18,6 +23,7 @@ const ReviewSection: React.FC<IReviewsSectionProps> = ({
id,
}: IReviewsSectionProps) => {
const [reviews, setReviews] = useState<IReviewList>();
const pathname = usePathname();
const session = useSession();
const handleSubmit: React.MouseEventHandler<
HTMLFormElement
@@ -33,6 +39,10 @@ const ReviewSection: React.FC<IReviewsSectionProps> = ({
},
};
if (session.status === "unauthenticated") {
signIn(undefined, { callbackUrl: pathname });
}
if (!formData.get("review")) {
return;
}
@@ -53,7 +63,7 @@ const ReviewSection: React.FC<IReviewsSectionProps> = ({
const getReviews = async () => {
const response = await apiInstance.get<IReviewList>(
`/${endpoint}/${id}/reviews/`
`/${endpoint}/${id}/reviews/?page_size=8`
);
setReviews(response.data);
@@ -83,47 +93,66 @@ const ReviewSection: React.FC<IReviewsSectionProps> = ({
<span id="blue-point" /> Написать комментарий
</h3>
<form onSubmit={handleSubmit}>
<textarea name="review" />
<button type="submit">Отправить</button>
</form>
{session.status === "authenticated" ? (
<form onSubmit={handleSubmit}>
<textarea name="review" />
<button type="submit">Отправить</button>
</form>
) : (
<p className="review-section__auth-warning">
Перед тем как оставить комментарий, пожалуйста
<button
onClick={() =>
signIn(undefined, { callbackUrl: pathname })
}
>
войдите в аккаунт
</button>
</p>
)}
<div className="review-section__list">
<h3>
<span id="blue-point" /> Комментарии
</h3>
<ul>
{reviews?.results.map((review) => (
<li key={review.id} className="review">
<div className="review__author">
<img
id="author-img"
src={review.author.image}
alt="Author Image"
/>
<div className="review__header">
<h5 className="review__author-name">
{review.author.first_name}{" "}
{review.author.last_name}
</h5>
<div className="review__date">
<Image src={calendar} alt="Calendar Icon" />
<p>
{months[review.created_at.slice(5, 7)]}{" "}
{review.created_at.slice(5, 7).slice(0, 1) ===
"0"
? review.created_at.slice(6, 7)
: review.created_at.slice(5, 7)}
, {review.created_at.slice(0, 4)}
</p>
{reviews?.results.length !== 0 ? (
<ul>
{reviews?.results.map((review) => (
<li key={review.id} className="review">
<div className="review__author">
<img
id="author-img"
src={review.author.image}
alt="Author Image"
/>
<div className="review__header">
<h5 className="review__author-name">
{review.author.first_name}{" "}
{review.author.last_name}
</h5>
<div className="review__date">
<Image src={calendar} alt="Calendar Icon" />
<p>
{months[review.created_at.slice(5, 7)]}{" "}
{review.created_at.slice(5, 7).slice(0, 1) ===
"0"
? review.created_at.slice(6, 7)
: review.created_at.slice(5, 7)}
, {review.created_at.slice(0, 4)}
</p>
</div>
</div>
</div>
</div>
<p className="review__description">{review.review}</p>
</li>
))}
</ul>
<p className="review__description">{review.review}</p>
</li>
))}
</ul>
) : (
<p className="review-section__warning">
Оставьте комментарий первым :)
</p>
)}
</div>
</section>
);
@@ -1,10 +1,10 @@
"use client";
import { useEffect, useState } from "react";
import "./ConfirmEmailForm.scss";
import { useEffect, useState } from "react";
import { apiInstance } from "@/shared/config/apiConfig";
import { useRouter } from "next/navigation";
import Loader from "@/shared/ui/components/Loader/Loader";
import { useRouter } from "@/shared/config/navigation";
interface IConfirmEmailFormProps {
email: string;
@@ -5,8 +5,8 @@ import "./confirm-code.scss";
import { useState } from "react";
import { apiInstance } from "@/shared/config/apiConfig";
import Loader from "@/shared/ui/components/Loader/Loader";
import { useRouter } from "next/navigation";
import { ITokens } from "@/shared/types/token-type";
import { useRouter } from "@/shared/config/navigation";
interface IConfirmCodeProps {
setChangeForm: (boolean: boolean) => void;
@@ -3,16 +3,14 @@
import "./ProfileForm.scss";
import Image from "next/image";
import pen from "./icons/pen.svg";
import eye_off from "./icons/eye-off.svg";
import eye_on from "./icons/eye-on.svg";
import { useState } from "react";
import { apiInstance } from "@/shared/config/apiConfig";
import { apiInstance, authInstanse } from "@/shared/config/apiConfig";
import { AxiosError } from "axios";
import Loader from "@/shared/ui/components/Loader/Loader";
import { useSession } from "next-auth/react";
import { useRouter } from "next/navigation";
import LogoutButton from "@/features/LogoutButton/LogoutButton";
import ChangePassword from "./ChangePassword/ChangePassword";
import { useRouter } from "@/shared/config/navigation";
interface IProfileFormProps {
id: number;
@@ -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='&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

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

+13
View File
@@ -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: {
Loaded 100 of 172 files, more files were not shown because too many files have changed in this diff. Show more