forked from Transparency/kgroad-frontend2
made news details page, made review section
This commit is contained in:
parent
083ef42f9e
commit
bb852f5119
@ -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";
|
||||
|
53
src/Entities/ReviewCard/ReviewCard.scss
Normal file
53
src/Entities/ReviewCard/ReviewCard.scss
Normal file
@ -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;
|
||||
}
|
||||
}
|
67
src/Entities/ReviewCard/ReviewCard.tsx
Normal file
67
src/Entities/ReviewCard/ReviewCard.tsx
Normal file
@ -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;
|
8
src/Entities/ReviewCard/icons/calendar-icon.svg
Normal file
8
src/Entities/ReviewCard/icons/calendar-icon.svg
Normal file
@ -0,0 +1,8 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g id="calendar">
|
||||
<path id="Vector" d="M12.6667 2.66699H3.33333C2.59695 2.66699 2 3.26395 2 4.00033V13.3337C2 14.07 2.59695 14.667 3.33333 14.667H12.6667C13.403 14.667 14 14.07 14 13.3337V4.00033C14 3.26395 13.403 2.66699 12.6667 2.66699Z" stroke="#6E6565" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path id="Vector_2" d="M10.666 1.33301V3.99967" stroke="#6E6565" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path id="Vector_3" d="M5.33398 1.33301V3.99967" stroke="#6E6565" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path id="Vector_4" d="M2 6.66699H14" stroke="#6E6565" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 747 B |
48
src/Features/CreateReview/CreateReview.scss
Normal file
48
src/Features/CreateReview/CreateReview.scss
Normal file
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
46
src/Features/CreateReview/CreateReview.tsx
Normal file
46
src/Features/CreateReview/CreateReview.tsx
Normal file
@ -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;
|
59
src/Features/CreateReview/actions.ts
Normal file
59
src/Features/CreateReview/actions.ts
Normal file
@ -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;
|
||||
|
8
src/Pages/NewsDetailsPage/icons/calendar-icon.svg
Normal file
8
src/Pages/NewsDetailsPage/icons/calendar-icon.svg
Normal file
@ -0,0 +1,8 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g id="calendar">
|
||||
<path id="Vector" d="M12.6667 2.66699H3.33333C2.59695 2.66699 2 3.26395 2 4.00033V13.3337C2 14.07 2.59695 14.667 3.33333 14.667H12.6667C13.403 14.667 14 14.07 14 13.3337V4.00033C14 3.26395 13.403 2.66699 12.6667 2.66699Z" stroke="#6E6565" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path id="Vector_2" d="M10.666 1.33301V3.99967" stroke="#6E6565" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path id="Vector_3" d="M5.33398 1.33301V3.99967" stroke="#6E6565" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path id="Vector_4" d="M2 6.66699H14" stroke="#6E6565" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 747 B |
5
src/Pages/NewsDetailsPage/icons/message-icon.svg
Normal file
5
src/Pages/NewsDetailsPage/icons/message-icon.svg
Normal file
@ -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 |
1
src/Pages/NewsDetailsPage/store/index.ts
Normal file
1
src/Pages/NewsDetailsPage/store/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export * from "./store";
|
54
src/Pages/NewsDetailsPage/store/store.ts
Normal file
54
src/Pages/NewsDetailsPage/store/store.ts
Normal file
@ -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;
|
||||
|
20
src/Shared/helpers/getTokens.ts
Normal file
20
src/Shared/helpers/getTokens.ts
Normal file
@ -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}
|
||||
|
1
src/Widgets/NewsList/store/index.ts
Normal file
1
src/Widgets/NewsList/store/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export * from "./store";
|
39
src/Widgets/NewsList/store/store.ts
Normal file
39
src/Widgets/NewsList/store/store.ts
Normal file
@ -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();
|
34
src/Widgets/NewsReviewsSection/NewsReviewsSection.scss
Normal file
34
src/Widgets/NewsReviewsSection/NewsReviewsSection.scss
Normal file
@ -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;
|
||||
}
|
||||
}
|
46
src/Widgets/NewsReviewsSection/NewsReviewsSection.tsx
Normal file
46
src/Widgets/NewsReviewsSection/NewsReviewsSection.tsx
Normal file
@ -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"
|
||||
}
|
||||
|
@ -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"]
|
||||
}
|
||||
|
12
yarn.lock
12
yarn.lock
@ -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==
|
||||
|
Loading…
Reference in New Issue
Block a user