forked from Transparency/kgroad-frontend2
made news details page, made review section
This commit is contained in:
1 parent
083ef42f9e
commit
bb852f5119
30 files changed
+710
-123
No files matched your search
@@ -14,6 +14,7 @@
|
||||
"react": "^18",
|
||||
"react-dom": "^18",
|
||||
"sass": "^1.70.0",
|
||||
"swr": "^2.2.4",
|
||||
"zustand": "^4.5.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
"use client";
|
||||
|
||||
import Image, { StaticImageData } from "next/image";
|
||||
import "./NewsCard.scss";
|
||||
import Link from "next/link";
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
.review-card {
|
||||
padding: 15px 20px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 15px;
|
||||
border-radius: 12px;
|
||||
border: 1px solid #c5c6c5;
|
||||
|
||||
&__header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
|
||||
&_left {
|
||||
min-width: 60px;
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
border-radius: 50%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
&_right {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
|
||||
h4 {
|
||||
color: #3e3232;
|
||||
font-size: 16px;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
span {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
|
||||
color: rgba(62, 50, 50, 0.75);
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
line-height: 20px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
p {
|
||||
color: rgba(0, 0, 0, 0.75);
|
||||
font-size: 18px;
|
||||
font-weight: 500;
|
||||
line-height: 20px;
|
||||
letter-spacing: 0.25px;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
import Image from "next/image";
|
||||
import "./ReviewCard.scss";
|
||||
import calendar_icon from "./icons/calendar-icon.svg";
|
||||
|
||||
interface IReviewCardProps {
|
||||
item: IReview;
|
||||
}
|
||||
|
||||
interface IReview {
|
||||
id: number;
|
||||
author: IAuthor;
|
||||
review: string;
|
||||
created_at: string;
|
||||
}
|
||||
|
||||
interface IAuthor {
|
||||
id: number;
|
||||
first_name: string;
|
||||
last_name: string;
|
||||
image: string;
|
||||
}
|
||||
|
||||
const ReviewCard: React.FC<IReviewCardProps> = ({
|
||||
item,
|
||||
}: IReviewCardProps) => {
|
||||
const months: Record<string, string> = {
|
||||
"01": "Январь",
|
||||
"02": "Февраль",
|
||||
"03": "Март",
|
||||
"04": "Апрель",
|
||||
"05": "Май",
|
||||
"06": "Июнь",
|
||||
"07": "Июль",
|
||||
"08": "Август",
|
||||
"09": "Сентябрь",
|
||||
"10": "Октябрь",
|
||||
"11": "Ноябрь",
|
||||
"12": "Декабрь",
|
||||
};
|
||||
|
||||
const year = item?.created_at.slice(0, 4);
|
||||
const month = item?.created_at.slice(5, 7);
|
||||
const day = item?.created_at.slice(8, 10);
|
||||
return (
|
||||
<div className="review-card">
|
||||
<div className="review-card__header">
|
||||
<img
|
||||
className="review-card__header_left"
|
||||
src={item.author.image}
|
||||
alt="Author Image"
|
||||
/>
|
||||
<div className="review-card__header_right">
|
||||
<h4>
|
||||
{item.author.first_name} {item.author.last_name}
|
||||
</h4>
|
||||
<span>
|
||||
<Image src={calendar_icon} alt="Calendar Icon" />
|
||||
{month && months[month]} {day}, {year}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<p>{item.review}</p>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ReviewCard;
|
||||
@@ -0,0 +1,8 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g id="calendar">
|
||||
<path id="Vector" d="M12.6667 2.66699H3.33333C2.59695 2.66699 2 3.26395 2 4.00033V13.3337C2 14.07 2.59695 14.667 3.33333 14.667H12.6667C13.403 14.667 14 14.07 14 13.3337V4.00033C14 3.26395 13.403 2.66699 12.6667 2.66699Z" stroke="#6E6565" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path id="Vector_2" d="M10.666 1.33301V3.99967" stroke="#6E6565" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path id="Vector_3" d="M5.33398 1.33301V3.99967" stroke="#6E6565" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path id="Vector_4" d="M2 6.66699H14" stroke="#6E6565" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 747 B |
@@ -0,0 +1,48 @@
|
||||
.create-review {
|
||||
margin-top: 70px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 40px;
|
||||
|
||||
h3 {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
color: #3e3232;
|
||||
font-size: 24px;
|
||||
font-weight: 500;
|
||||
|
||||
span {
|
||||
width: 4px;
|
||||
height: 10px;
|
||||
border-radius: 12px;
|
||||
background: #3998e8;
|
||||
}
|
||||
}
|
||||
|
||||
div {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 18px;
|
||||
|
||||
textarea {
|
||||
height: 258px;
|
||||
padding: 27px 18px;
|
||||
resize: none;
|
||||
border-radius: 12px;
|
||||
border: 1px solid #c5c6c5;
|
||||
font-size: 16px;
|
||||
outline: none;
|
||||
}
|
||||
::placeholder {
|
||||
color: #c5c6c5;
|
||||
font-size: 16px;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
button {
|
||||
width: fit-content;
|
||||
align-self: flex-end;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
"use client";
|
||||
|
||||
import "./CreateReview.scss";
|
||||
import { useState } from "react";
|
||||
import Button from "@/Shared/UI/Button/Button";
|
||||
import { createReviewAction } from "./actions";
|
||||
import { StaticImageData } from "next/image";
|
||||
|
||||
interface ICreateReviewProps {
|
||||
endpoint: string;
|
||||
id: number | null | undefined;
|
||||
}
|
||||
|
||||
const CreateReview: React.FC<ICreateReviewProps> = ({
|
||||
endpoint,
|
||||
id,
|
||||
}: ICreateReviewProps) => {
|
||||
const [review, setReview] = useState<string>("");
|
||||
|
||||
return (
|
||||
<div className="create-review">
|
||||
<h3>
|
||||
<span />
|
||||
Написать комментарий
|
||||
</h3>
|
||||
<div>
|
||||
<textarea
|
||||
onChange={(e) => setReview(e.target.value)}
|
||||
value={review}
|
||||
placeholder="Напишите комментарий"
|
||||
/>
|
||||
<Button
|
||||
disabled={createReviewAction.isLoading}
|
||||
onClick={() => {
|
||||
setReview("");
|
||||
createReviewAction.createReview(endpoint, id, review);
|
||||
}}
|
||||
>
|
||||
Отправить
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default CreateReview;
|
||||
@@ -0,0 +1,59 @@
|
||||
import { baseAPI } from "@/Shared/API/baseAPI";
|
||||
import { getTokens } from "@/Shared/helpers/getTokens";
|
||||
import axios, { AxiosError } from "axios";
|
||||
|
||||
class CreateReview {
|
||||
response: string;
|
||||
isLoading: boolean;
|
||||
error: string;
|
||||
constructor() {
|
||||
this.response = "";
|
||||
this.isLoading = false;
|
||||
this.error = "";
|
||||
}
|
||||
|
||||
async createReview(
|
||||
endpoint: string,
|
||||
id: number | null | undefined,
|
||||
review: string
|
||||
) {
|
||||
try {
|
||||
this.isLoading = true;
|
||||
|
||||
const access_token = getTokens()?.access_token;
|
||||
|
||||
const Authorization = `Bearer ${access_token}`;
|
||||
|
||||
const config = {
|
||||
headers: {
|
||||
Authorization,
|
||||
},
|
||||
};
|
||||
|
||||
const body = {
|
||||
review,
|
||||
};
|
||||
|
||||
const response = await axios.post(
|
||||
`${baseAPI}/${endpoint}/${id}/reviews/`,
|
||||
body,
|
||||
config
|
||||
);
|
||||
|
||||
this.response = response.data;
|
||||
|
||||
console.log(response);
|
||||
} catch (error: unknown) {
|
||||
console.log(error);
|
||||
if (error instanceof AxiosError) {
|
||||
this.error = error.message;
|
||||
} else {
|
||||
this.error = "An error ocured";
|
||||
}
|
||||
} finally {
|
||||
this.isLoading = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const createReviewAction = new CreateReview();
|
||||
@@ -43,7 +43,7 @@ export const useSignIn = create<SignInStore>((set) => ({
|
||||
user
|
||||
);
|
||||
|
||||
localStorage.setItem("user", JSON.stringify(response.data));
|
||||
localStorage.setItem("tokens", JSON.stringify(response.data));
|
||||
|
||||
set({ emailError: "" });
|
||||
set({ passwordError: "" });
|
||||
|
||||
@@ -86,7 +86,7 @@ export const useSignUp = create<SignUpStore>((set) => ({
|
||||
user
|
||||
);
|
||||
|
||||
localStorage.setItem("user", JSON.stringify(response.data));
|
||||
localStorage.setItem("tokens", JSON.stringify(response.data));
|
||||
|
||||
set({ emailError: "" });
|
||||
set({ passwordError: "" });
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
.news-details-page {
|
||||
padding: 118px 90px 0 90px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 40px;
|
||||
|
||||
&__wrapper {
|
||||
max-width: 1070px;
|
||||
display: flex;
|
||||
align-self: center;
|
||||
flex-direction: column;
|
||||
gap: 40px;
|
||||
}
|
||||
|
||||
&__image {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 30px;
|
||||
&_main {
|
||||
width: 100%;
|
||||
height: 600px;
|
||||
border-radius: 8px;
|
||||
object-fit: cover;
|
||||
}
|
||||
}
|
||||
|
||||
&__date-and-reviews {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 80px;
|
||||
span {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
color: rgba(62, 50, 50, 0.75);
|
||||
font-size: 15px;
|
||||
font-weight: 500;
|
||||
line-height: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
&__description {
|
||||
color: #3e3232;
|
||||
font-size: 20px;
|
||||
line-height: 34px;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 1024px) {
|
||||
.news-details-page {
|
||||
padding: 118px 30px 0 30px;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 768px) {
|
||||
.news-details-page {
|
||||
padding: 112px 30px 0 30px;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 550px) {
|
||||
.news-details-page {
|
||||
padding: 112px 16px 0 16px;
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,69 @@
|
||||
import { newsDetailsStore } from "./store";
|
||||
import "./NewsDetailsPage.scss";
|
||||
import HeaderText from "@/Shared/UI/HeaderText/HeaderText";
|
||||
import message_icon from "./icons/message-icon.svg";
|
||||
import calendar_icon from "./icons/calendar-icon.svg";
|
||||
import Image from "next/image";
|
||||
import NewsReviewsSection from "@/Widgets/NewsReviewsSection/NewsReviewsSection";
|
||||
|
||||
const NewsDetailsPage = () => {
|
||||
return <div>NewsDetailsPage</div>;
|
||||
const NewsDetailsPage = async ({
|
||||
params,
|
||||
}: {
|
||||
params: { id: string };
|
||||
}) => {
|
||||
const data = await newsDetailsStore.getNewsDetails(params.id);
|
||||
|
||||
console.log(data);
|
||||
|
||||
const months: Record<string, string> = {
|
||||
"01": "Январь",
|
||||
"02": "Февраль",
|
||||
"03": "Март",
|
||||
"04": "Апрель",
|
||||
"05": "Май",
|
||||
"06": "Июнь",
|
||||
"07": "Июль",
|
||||
"08": "Август",
|
||||
"09": "Сентябрь",
|
||||
"10": "Октябрь",
|
||||
"11": "Ноябрь",
|
||||
"12": "Декабрь",
|
||||
};
|
||||
|
||||
const year = data?.created_at.slice(0, 4);
|
||||
const month = data?.created_at.slice(5, 7);
|
||||
const day = data?.created_at.slice(8, 10);
|
||||
|
||||
return (
|
||||
<div className="news-details-page">
|
||||
<HeaderText>{data?.title}</HeaderText>
|
||||
<div className="news-details-page__wrapper">
|
||||
<div className="news-details-page__image">
|
||||
<img
|
||||
src={data?.image}
|
||||
alt="News Image"
|
||||
className="news-details-page__image_main"
|
||||
/>
|
||||
<div className="news-details-page__date-and-reviews">
|
||||
<span>
|
||||
<Image src={calendar_icon} alt="Calendar Icon" />
|
||||
{month && months[month]} {day}, {year}
|
||||
</span>
|
||||
<span>
|
||||
<Image src={message_icon} alt="Message Icon" />
|
||||
Комментарии: {data?.count_reviews}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p className="news-details-page__description">
|
||||
{data?.description}
|
||||
</p>
|
||||
|
||||
<NewsReviewsSection id={data?.id} list={data?.news_review} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default NewsDetailsPage;
|
||||
@@ -0,0 +1,8 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g id="calendar">
|
||||
<path id="Vector" d="M12.6667 2.66699H3.33333C2.59695 2.66699 2 3.26395 2 4.00033V13.3337C2 14.07 2.59695 14.667 3.33333 14.667H12.6667C13.403 14.667 14 14.07 14 13.3337V4.00033C14 3.26395 13.403 2.66699 12.6667 2.66699Z" stroke="#6E6565" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path id="Vector_2" d="M10.666 1.33301V3.99967" stroke="#6E6565" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path id="Vector_3" d="M5.33398 1.33301V3.99967" stroke="#6E6565" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path id="Vector_4" d="M2 6.66699H14" stroke="#6E6565" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 747 B |
@@ -0,0 +1,5 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g id="message-circle">
|
||||
<path id="Vector" d="M14 7.66669C14.0023 8.5466 13.7967 9.41461 13.4 10.2C12.9296 11.1412 12.2065 11.9328 11.3116 12.4862C10.4168 13.0396 9.3855 13.3329 8.33333 13.3334C7.45342 13.3356 6.58541 13.1301 5.8 12.7334L2 14L3.26667 10.2C2.86995 9.41461 2.66437 8.5466 2.66667 7.66669C2.66707 6.61452 2.96041 5.58325 3.51381 4.68839C4.06722 3.79352 4.85884 3.0704 5.8 2.60002C6.58541 2.20331 7.45342 1.99772 8.33333 2.00002H8.66667C10.0562 2.07668 11.3687 2.66319 12.3528 3.64726C13.3368 4.63132 13.9233 5.94379 14 7.33335V7.66669Z" stroke="#6E6565" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 724 B |
@@ -0,0 +1 @@
|
||||
export * from "./store";
|
||||
@@ -0,0 +1,54 @@
|
||||
import { baseAPI } from "@/Shared/API/baseAPI";
|
||||
import axios, { AxiosError } from "axios";
|
||||
import { StaticImport } from "next/dist/shared/lib/get-img-props";
|
||||
import { StaticImageData } from "next/image";
|
||||
|
||||
interface IDetails {
|
||||
id: number | null;
|
||||
image: string;
|
||||
title: string;
|
||||
description: string;
|
||||
news_review: IReview[];
|
||||
created_at: string;
|
||||
count_reviews: number;
|
||||
}
|
||||
|
||||
interface IReview {
|
||||
id: number;
|
||||
author: IAuthor;
|
||||
review: string;
|
||||
created_at: string;
|
||||
}
|
||||
|
||||
interface IAuthor {
|
||||
id: number;
|
||||
first_name: string;
|
||||
last_name: string;
|
||||
image: string;
|
||||
}
|
||||
|
||||
class NewsDetailsStore {
|
||||
error: string;
|
||||
constructor() {
|
||||
this.error = "";
|
||||
}
|
||||
|
||||
async getNewsDetails(id: string) {
|
||||
try {
|
||||
const response = await axios.get<IDetails>(
|
||||
`${baseAPI}/news/${id}/`
|
||||
);
|
||||
|
||||
return response.data;
|
||||
} catch (error: unknown) {
|
||||
if (error instanceof AxiosError) {
|
||||
this.error = error.message;
|
||||
} else {
|
||||
console.log(error);
|
||||
this.error = "An error occured";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const newsDetailsStore = new NewsDetailsStore();
|
||||
@@ -1,8 +1,8 @@
|
||||
import HeaderText from "@/Shared/UI/HeaderText/HeaderText";
|
||||
import "./NewsPage.scss";
|
||||
import NewsList from "@/Widgets/NewsList/NewsList";
|
||||
import HeaderText from "@/Shared/UI/HeaderText/HeaderText";
|
||||
|
||||
const NewsPage = () => {
|
||||
const NewsPage = async () => {
|
||||
return (
|
||||
<div className="news-page">
|
||||
<HeaderText>Новости</HeaderText>
|
||||
|
||||
@@ -4,8 +4,15 @@ interface IButton extends React.AllHTMLAttributes<HTMLButtonElement> {
|
||||
children: React.ReactNode | null;
|
||||
}
|
||||
|
||||
const Button: React.FC<IButton> = ({ children }: IButton) => {
|
||||
return <button className="ui-btn">{children}</button>;
|
||||
const Button: React.FC<IButton> = ({
|
||||
children,
|
||||
onClick,
|
||||
}: IButton) => {
|
||||
return (
|
||||
<button className="ui-btn" onClick={onClick}>
|
||||
{children}
|
||||
</button>
|
||||
);
|
||||
};
|
||||
|
||||
export default Button;
|
||||
@@ -0,0 +1,20 @@
|
||||
type Tokens = {
|
||||
access_token: string;
|
||||
refresh_token: string;
|
||||
};
|
||||
|
||||
export const getTokens = (): Tokens | null => {
|
||||
try {
|
||||
const storage: string | null = localStorage.getItem("tokens");
|
||||
|
||||
if (storage === null) {
|
||||
throw new Error("Tokens not found");
|
||||
}
|
||||
|
||||
const parsed: Tokens = JSON.parse(storage);
|
||||
return parsed;
|
||||
} catch (error: any) {
|
||||
console.error("Error retrieving tokens:", error.message);
|
||||
return null;
|
||||
}
|
||||
};
|
||||
@@ -1,19 +1,17 @@
|
||||
"use client";
|
||||
|
||||
import "./NewsList.scss";
|
||||
import { useEffect } from "react";
|
||||
import { useNews } from "./news.store";
|
||||
import NewsCard from "@/Entities/NewsCard/NewsCard";
|
||||
import "./NewsList.scss";
|
||||
import { newsStore } from "./store";
|
||||
|
||||
const NewsList = () => {
|
||||
const { data, getNews } = useNews();
|
||||
const NewsList = async () => {
|
||||
const data = await newsStore.getNews();
|
||||
|
||||
if (newsStore.error) {
|
||||
return <div className="news-list">{newsStore.error}</div>;
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
getNews();
|
||||
}, []);
|
||||
return (
|
||||
<ul className="news-list">
|
||||
{data.results.map((card) => (
|
||||
{data?.results.map((card) => (
|
||||
<li key={card.id}>
|
||||
<NewsCard
|
||||
id={card.id}
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
export * from "./store";
|
||||
@@ -0,0 +1,39 @@
|
||||
import { baseAPI } from "@/Shared/API/baseAPI";
|
||||
import { IList } from "@/Shared/types";
|
||||
import axios, { AxiosError } from "axios";
|
||||
import { StaticImageData } from "next/image";
|
||||
|
||||
interface INews extends IList {
|
||||
results: IResult[];
|
||||
}
|
||||
|
||||
interface IResult {
|
||||
id: number;
|
||||
image: StaticImageData;
|
||||
title: string;
|
||||
description: string;
|
||||
created_at: string;
|
||||
}
|
||||
|
||||
class NewsStore {
|
||||
error: string;
|
||||
constructor() {
|
||||
this.error = "";
|
||||
}
|
||||
|
||||
async getNews() {
|
||||
try {
|
||||
const response = await axios.get<INews>(`${baseAPI}/news/`);
|
||||
|
||||
return response.data;
|
||||
} catch (error: unknown) {
|
||||
if (error instanceof AxiosError) {
|
||||
this.error = error.message;
|
||||
} else {
|
||||
this.error = "An error occurred.";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const newsStore = new NewsStore();
|
||||
@@ -0,0 +1,34 @@
|
||||
.news-reviews-section {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 70px;
|
||||
|
||||
h3 {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
|
||||
color: #3e3232;
|
||||
font-size: 24px;
|
||||
font-weight: 500;
|
||||
|
||||
span {
|
||||
width: 4px;
|
||||
height: 10px;
|
||||
border-radius: 12px;
|
||||
background: #3998e8;
|
||||
}
|
||||
}
|
||||
|
||||
&__container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 24px;
|
||||
}
|
||||
|
||||
&__list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 30px;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
import ReviewCard from "@/Entities/ReviewCard/ReviewCard";
|
||||
import "./NewsReviewsSection.scss";
|
||||
import CreateReview from "@/Features/CreateReview/CreateReview";
|
||||
|
||||
interface INewsReviewsSectionProps {
|
||||
id: number | null | undefined;
|
||||
list: IReview[] | undefined;
|
||||
}
|
||||
|
||||
interface IReview {
|
||||
id: number;
|
||||
author: IAuthor;
|
||||
review: string;
|
||||
created_at: string;
|
||||
}
|
||||
|
||||
interface IAuthor {
|
||||
id: number;
|
||||
first_name: string;
|
||||
last_name: string;
|
||||
image: string;
|
||||
}
|
||||
|
||||
const NewsReviewsSection: React.FC<INewsReviewsSectionProps> = ({
|
||||
id,
|
||||
list,
|
||||
}: INewsReviewsSectionProps) => {
|
||||
return (
|
||||
<div className="news-reviews-section">
|
||||
<CreateReview endpoint="news" id={id} />
|
||||
<div className="news-reviews-section__container">
|
||||
<h3>
|
||||
<span />
|
||||
Комментарии
|
||||
</h3>
|
||||
<div>
|
||||
{list?.map((item) => (
|
||||
<ReviewCard key={item.id} item={item} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default NewsReviewsSection;
|
||||
@@ -33,7 +33,7 @@
|
||||
background-color: #f1f1f1;
|
||||
}
|
||||
|
||||
&_active {
|
||||
#statistics-table__location_active {
|
||||
background-color: #f1f1f1;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,18 +10,20 @@ import { useStatistics } from "./statistics.store";
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
const StatisticsTable = () => {
|
||||
const [location, setLocation] = useState("city");
|
||||
const [location, setLocation] = useState("village");
|
||||
const [locationMenu, setLocationMenu] = useState(false);
|
||||
const {
|
||||
data,
|
||||
getStatisticsForCity,
|
||||
getStatisticsForState,
|
||||
getStatisticsForVillage,
|
||||
} = useStatistics();
|
||||
const { data, getStatistics } = useStatistics();
|
||||
|
||||
const locations = [
|
||||
{ id: 1, name: "Регион", type: "state" },
|
||||
{ id: 2, name: "Город", type: "city" },
|
||||
{ id: 3, name: "Деревня", type: "village" },
|
||||
];
|
||||
|
||||
useEffect(() => {
|
||||
getStatisticsForState();
|
||||
getStatistics();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="statistics-table">
|
||||
{locationMenu && (
|
||||
@@ -29,54 +31,26 @@ const StatisticsTable = () => {
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
className="statistics-table__location"
|
||||
>
|
||||
<li
|
||||
className={
|
||||
location === "state"
|
||||
? "statistics-table__location_active"
|
||||
: ""
|
||||
}
|
||||
>
|
||||
<button
|
||||
onClick={() => {
|
||||
setLocation("state");
|
||||
getStatisticsForState();
|
||||
}}
|
||||
{locations.map((loc) => (
|
||||
<li
|
||||
key={loc.id}
|
||||
id={
|
||||
location === loc.type
|
||||
? "statistics-table__location_active"
|
||||
: ""
|
||||
}
|
||||
>
|
||||
Регион
|
||||
</button>
|
||||
</li>
|
||||
<li
|
||||
className={
|
||||
location === "city"
|
||||
? "statistics-table__location_active"
|
||||
: ""
|
||||
}
|
||||
>
|
||||
<button
|
||||
onClick={() => {
|
||||
setLocation("city");
|
||||
getStatisticsForCity();
|
||||
}}
|
||||
>
|
||||
Город
|
||||
</button>
|
||||
</li>
|
||||
<li
|
||||
className={
|
||||
location === "village"
|
||||
? "statistics-table__location_active"
|
||||
: ""
|
||||
}
|
||||
>
|
||||
<button
|
||||
onClick={() => {
|
||||
setLocation("village");
|
||||
getStatisticsForVillage();
|
||||
}}
|
||||
>
|
||||
Деревня
|
||||
</button>
|
||||
</li>
|
||||
<button
|
||||
onClick={() => {
|
||||
setLocation(loc.type);
|
||||
getStatistics(loc.type);
|
||||
setLocationMenu(false);
|
||||
}}
|
||||
>
|
||||
{loc.name}
|
||||
</button>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
<div className="statistics-table__wrapper">
|
||||
@@ -168,17 +142,25 @@ const StatisticsTable = () => {
|
||||
</thead>
|
||||
|
||||
<tbody>
|
||||
{data.map((statistic) => (
|
||||
{data.length !== 0 ? (
|
||||
data.map((statistic) => (
|
||||
<tr>
|
||||
<td>{statistic.name}</td>
|
||||
<td>{statistic.broken_road_1}</td>
|
||||
<td>{statistic.local_defect_3}</td>
|
||||
<td>{statistic.hotbed_of_accidents_2}</td>
|
||||
<td>{statistic.local_defect_fixed_6}</td>
|
||||
<td>{statistic.repair_plans_4}</td>
|
||||
<td>{statistic.repaired_5}</td>
|
||||
</tr>
|
||||
))
|
||||
) : (
|
||||
<tr>
|
||||
<td>{statistic.name}</td>
|
||||
<td>{statistic.broken_road_1}</td>
|
||||
<td>{statistic.local_defect_3}</td>
|
||||
<td>{statistic.hotbed_of_accidents_2}</td>
|
||||
<td>{statistic.local_defect_fixed_6}</td>
|
||||
<td>{statistic.repair_plans_4}</td>
|
||||
<td>{statistic.repaired_5}</td>
|
||||
<td style={{ textAlign: "center" }} colSpan={7}>
|
||||
Looks like there is no data for {location}
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
@@ -15,51 +15,19 @@ interface IStatistic {
|
||||
|
||||
interface IStatisticStore extends IFetch {
|
||||
data: IStatistic[];
|
||||
getStatisticsForCity: () => Promise<void>;
|
||||
getStatisticsForState: () => Promise<void>;
|
||||
getStatisticsForVillage: () => Promise<void>;
|
||||
getStatistics: (location?: string) => Promise<void>;
|
||||
}
|
||||
|
||||
export const useStatistics = create<IStatisticStore>((set) => ({
|
||||
data: [],
|
||||
loading: false,
|
||||
error: "",
|
||||
getStatisticsForCity: async () => {
|
||||
getStatistics: async (location: string = "village") => {
|
||||
try {
|
||||
set({ loading: true });
|
||||
|
||||
const response = await axios.get<IStatistic[]>(
|
||||
`${baseAPI}/report/city/stats/`
|
||||
);
|
||||
|
||||
set({ data: response.data });
|
||||
} catch (error: any) {
|
||||
set({ error: error.message });
|
||||
} finally {
|
||||
set({ loading: false });
|
||||
}
|
||||
},
|
||||
getStatisticsForState: async () => {
|
||||
try {
|
||||
set({ loading: true });
|
||||
|
||||
const response = await axios.get<IStatistic[]>(
|
||||
`${baseAPI}/report/state/stats/`
|
||||
);
|
||||
|
||||
set({ data: response.data });
|
||||
} catch (error: any) {
|
||||
set({ error: error.message });
|
||||
} finally {
|
||||
set({ loading: false });
|
||||
}
|
||||
},
|
||||
getStatisticsForVillage: async () => {
|
||||
try {
|
||||
set({ loading: true });
|
||||
|
||||
const response = await axios.get<IStatistic[]>(
|
||||
`${baseAPI}/report/village/stats/`
|
||||
`${baseAPI}/report/${location}/stats/`
|
||||
);
|
||||
|
||||
set({ data: response.data });
|
||||
|
||||
@@ -25,7 +25,7 @@ const Navbar = () => {
|
||||
<li
|
||||
key={page.id}
|
||||
className={
|
||||
page.path === pathname
|
||||
page.path === pathname?.slice(0, page.path.length)
|
||||
? "nav__pages-item_active"
|
||||
: "nav__pages-item"
|
||||
}
|
||||
|
||||
+6
-1
@@ -21,6 +21,11 @@
|
||||
"@/*": ["./src/*"]
|
||||
}
|
||||
},
|
||||
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
|
||||
"include": [
|
||||
"next-env.d.ts",
|
||||
"**/*.ts",
|
||||
"**/*.tsx",
|
||||
".next/types/**/*.ts"
|
||||
],
|
||||
"exclude": ["node_modules"]
|
||||
}
|
||||
@@ -526,7 +526,7 @@ chalk@^4.0.0:
|
||||
optionalDependencies:
|
||||
fsevents "~2.3.2"
|
||||
|
||||
client-only@0.0.1:
|
||||
client-only@0.0.1, client-only@^0.0.1:
|
||||
version "0.0.1"
|
||||
resolved "https://registry.yarnpkg.com/client-only/-/client-only-0.0.1.tgz#38bba5d403c41ab150bff64a95c85013cf73bca1"
|
||||
integrity sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==
|
||||
@@ -2193,6 +2193,14 @@ supports-preserve-symlinks-flag@^1.0.0:
|
||||
resolved "https://registry.yarnpkg.com/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz#6eda4bd344a3c94aea376d4cc31bc77311039e09"
|
||||
integrity sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==
|
||||
|
||||
swr@^2.2.4:
|
||||
version "2.2.4"
|
||||
resolved "https://registry.yarnpkg.com/swr/-/swr-2.2.4.tgz#03ec4c56019902fbdc904d78544bd7a9a6fa3f07"
|
||||
integrity sha512-njiZ/4RiIhoOlAaLYDqwz5qH/KZXVilRLvomrx83HjzCWTfa+InyfAjv05PSFxnmLzZkNO9ZfvgoqzAaEI4sGQ==
|
||||
dependencies:
|
||||
client-only "^0.0.1"
|
||||
use-sync-external-store "^1.2.0"
|
||||
|
||||
tapable@^2.2.0:
|
||||
version "2.2.1"
|
||||
resolved "https://registry.yarnpkg.com/tapable/-/tapable-2.2.1.tgz#1967a73ef4060a82f12ab96af86d52fdb76eeca0"
|
||||
@@ -2308,7 +2316,7 @@ uri-js@^4.2.2:
|
||||
dependencies:
|
||||
punycode "^2.1.0"
|
||||
|
||||
use-sync-external-store@1.2.0:
|
||||
use-sync-external-store@1.2.0, use-sync-external-store@^1.2.0:
|
||||
version "1.2.0"
|
||||
resolved "https://registry.yarnpkg.com/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz#7dbefd6ef3fe4e767a0cf5d7287aacfb5846928a"
|
||||
integrity sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA==
|
||||
|
||||
Reference in new issue
Block a user