russ_react/src/pages/ArticlePage.tsx

268 lines
9.6 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { useState, useEffect } from 'react';
import { useParams, Link, useLocation, useNavigate } from 'react-router-dom';
import { ArrowLeft, Clock } from 'lucide-react';
import { Header } from '../components/Header';
import { ReactionButtons } from '../components/ReactionButtons';
import { PhotoGallery } from '../components/PhotoGallery';
import { Article, CategoryTitles, CityTitles } from '../types';
import { SEO } from '../components/SEO';
import { useScrollStore } from '../stores/scrollStore';
import { ArticleContent } from '../components/ArticleContent';
import { ShareButton } from '../components/ShareButton';
import { BookmarkButton } from '../components/BookmarkButton';
import MinutesWord from '../components/Words/MinutesWord';
import axios from "axios";
import api from "../utils/api";
export function ArticlePage() {
const { id } = useParams();
const location = useLocation();
const navigate = useNavigate();
const [articleData, setArticleData] = useState<Article | null>(null);
const [error, setError] = useState<string | null>(null);
const [loading, setLoading] = useState(true);
const restoreHomeScrollPosition = useScrollStore(state => state.restoreHomeScrollPosition);
// Получть предыдущее состояние местоположения или создать путь по умолчанию
const backTo = location.state?.from || '/';
const handleBackClick = (e: React.MouseEvent) => {
e.preventDefault();
// При возврате на главную страницу восстановливается положение скролинга
if (backTo === '/' || backTo.startsWith('/?')) {
navigate(backTo);
// Восстановление положения скролинга после навигации
setTimeout(() => {
restoreHomeScrollPosition();
}, 50);
} else {
navigate(backTo);
}
};
useEffect(() => {
const fetchArticle = async () => {
try {
const response = await axios.get(`/api/articles/${id}`);
setArticleData(response.data);
} catch (error) {
setError('Не удалось загрузить статью');
console.error(error);
} finally {
setLoading(false);
}
};
fetchArticle();
}, [id]);
useEffect(() => {
window.scrollTo(0, 0);
}, [id]);
if (loading) {
return (
<div className="min-h-screen flex items-center justify-center text-gray-500">
Загрузка статьи...
</div>
);
}
if (error || !articleData) {
return (
<div className="min-h-screen flex items-center justify-center">
<div className="text-center">
<h2 className="text-2xl font-bold text-gray-900 mb-4">
{error || 'Статья не найдена'}
</h2>
<Link to={backTo} className="text-blue-600 hover:text-blue-800">
Назад на главную
</Link>
</div>
</div>
);
}
const handleReaction = async (reaction: 'like' | 'dislike') => {
if (!articleData || !id) return;
try {
const { data: updatedArticle } = await api.put(`/articles/react/${id}`, {
reaction: articleData.userReaction === reaction ? null : reaction,
likes: articleData.likes,
dislikes: articleData.dislikes,
});
setArticleData(prev => {
if (!prev) return prev;
const newArticle = { ...prev };
if (prev.userReaction === 'like') newArticle.likes--;
if (prev.userReaction === 'dislike') newArticle.dislikes--;
if (prev.userReaction !== reaction) {
if (reaction === 'like') newArticle.likes = updatedArticle.likes;
if (reaction === 'dislike') newArticle.dislikes = updatedArticle.dislikes;
newArticle.userReaction = reaction;
} else {
newArticle.userReaction = null;
}
return newArticle;
});
} catch (error) {
setError('Ошибка оценки статьи. Попробуйте еще раз.');
console.error('Ошибка оценки статьи:', error);
}
};
const writerAuthors = articleData.authors
.filter(a => a.role === 'WRITER')
.sort((a, b) => (a.author.order ?? 0) - (b.author.order ?? 0));
return (
<div className="min-h-screen bg-white">
<SEO
title={articleData.title}
description={articleData.excerpt}
image={articleData.coverImage}
type="article"
keywords={[CategoryTitles[Number(articleData.categoryId)], 'культура', 'статья', CityTitles[Number(articleData.cityId)].toLowerCase()]}
/>
<Header />
<main className="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 py-12">
<button
onClick={handleBackClick}
className="inline-flex items-center text-gray-600 hover:text-gray-900 mb-8 transition-colors"
>
<ArrowLeft size={20} className="mr-2" />
К списку статей
</button>
<article>
{/* Article Header */}
<div className="mb-8">
<div className="flex items-center gap-4 mb-6">
{writerAuthors.map((authorLink) => (
<img
key={authorLink.author.id}
src={authorLink.author.avatarUrl}
alt={authorLink.author.displayName}
className="w-12 h-12 rounded-full"
/>
))}
<div>
<p className="text-sm font-medium text-gray-900">
{writerAuthors.map((a, i) => (
<span key={a.author.id}>
{a.author.displayName}
{i < writerAuthors.length - 1 ? ', ' : ''}
</span>
))}
</p>
<div className="flex items-center text-sm text-gray-500">
<Clock size={14} className="mr-1" />
{articleData.readTime} <MinutesWord minutes={articleData.readTime}/> на чтение ·{' '}
{new Date(articleData.publishedAt).toLocaleDateString('ru-RU', {
month: 'long',
day: 'numeric',
year: 'numeric',
})}
</div>
</div>
</div>
<h1 className="text-4xl font-bold text-gray-900 mb-4">{articleData.title}</h1>
<p className="text-xl text-gray-600 mb-6">{articleData.excerpt}</p>
<div className="flex items-center gap-4 mb-8">
<span className="bg-blue-100 text-blue-800 px-3 py-1 rounded-full text-sm font-medium">
{CategoryTitles[articleData.categoryId]}
</span>
<div className="flex-1" />
<ShareButton title={articleData.title} url={window.location.href} />
<BookmarkButton article={articleData} />
</div>
</div>
{/* Cover Image */}
<img
src={articleData.coverImage}
alt={articleData.title}
className="w-full h-[28rem] object-cover rounded-xl mb-8"
/>
{/* Article Content */}
<div className="prose prose-lg max-w-none mb-8">
<div className="text-gray-800 leading-relaxed">
<ArticleContent content={articleData.content} />
</div>
</div>
{/* Photo Gallery */}
{articleData.gallery && articleData.gallery.length > 0 && (
<div className="mb-8">
<h2 className="text-2xl font-bold text-gray-900 mb-4">Фото галерея</h2>
<PhotoGallery images={articleData.gallery} />
</div>
)}
{/* Article Footer */}
<div className="border-t pt-4">
{error && (
<div className="mb-4 text-sm text-red-600">
{error}
</div>
)}
{/* Авторы */}
<div className="text-sm text-gray-600 text-right">
{(() => {
const photographer = articleData.authors?.find(a => a.role === 'PHOTOGRAPHER');
return (
<>
{writerAuthors && (
<p className="text-base font-medium text-gray-900">
{writerAuthors.map((a, i) => (
<span key={a.author.id}>
{a.author.displayName}
{i < writerAuthors.length - 1 ? ', ' : ''}
</span>
))}
</p>
)}
{photographer && (
<p className="text-base font-medium text-gray-900">
<span className="font-semibold">Фото:</span>{' '}
{photographer.author.displayName}
</p>
)}
</>
);
})()}
</div>
<ReactionButtons
likes={articleData.likes}
dislikes={articleData.dislikes}
userReaction={articleData.userReaction}
onReact={handleReaction}
/>
</div>
</article>
<div className="pt-8"/>
<Link
to={backTo}
className="inline-flex items-center text-gray-600 hover:text-gray-900 mb-8"
>
<ArrowLeft size={20} className="mr-2" />
К списку статей
</Link>
</main>
</div>
);
}