Добавлены bookmarks. Улучшения интерфейса, логотип. Есть дублирующмй код Header в отдельной папке, разбитый на отдельные компоненты. Сейчас используется монолит.

This commit is contained in:
anibilag 2025-03-19 22:06:42 +03:00
parent 7fb5daf210
commit e4d5029e72
21 changed files with 197 additions and 122 deletions

30
.nginx/nginx.conf Normal file
View File

@ -0,0 +1,30 @@
server {
listen 80 default_server;
listen [::]:80 default_server;
root /var/www/dist;
index index.html;
server_name russcult.anibilag.ru;
location / {
try_files $uri /index.html;
}
location /api/ {
proxy_pass http://192.168.1.67:5000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
gzip on;
gzip_vary on;
gzip_proxied any;
gzip_comp_level 6;
gzip_buffers 16 8k;
gzip_http_version 1.1;
gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript;
error_page 404 /index.html;
}

BIN
public/images/Logo-1.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 142 KiB

BIN
public/images/Logo-2.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 182 KiB

Binary file not shown.

View File

@ -13,6 +13,7 @@ import { Footer } from './components/Footer';
import { AuthGuard } from './components/AuthGuard';
import { ImportArticlesPage } from "./pages/ImportArticlesPage";
const API_URL = import.meta.env.VITE_API_URL || 'http://localhost:5000';
function App() {

View File

@ -1,4 +1,4 @@
import { Clock, ThumbsUp, MapPin } from 'lucide-react';
import { Clock, ThumbsUp, MapPin, ThumbsDown } from 'lucide-react';
import { Link } from 'react-router-dom';
import { Article, CategoryTitles, CityTitles } from '../types';
import MinutesWord from './MinutesWord';
@ -9,30 +9,28 @@ interface ArticleCardProps {
}
export function ArticleCard({ article, featured = false }: ArticleCardProps) {
const categoryName = CategoryTitles[article.categoryId];
return (
<article className={`overflow-hidden rounded-lg shadow-lg transition-transform hover:scale-[1.02] ${
featured ? 'col-span-2 row-span-2' : ''
}`}>
} flex flex-col`}>
<div className="relative pt-7">
<img
src={article.coverImage}
alt={article.title}
className={`w-full object-cover ${featured ? 'h-96' : 'h-64'}`}
/>
<div className="absolute top-1 left-0 flex gap-2 z-10">
<span className="bg-gray-100 shadow px-3 py-1 rounded-full text-sm font-medium">
{categoryName}
<div className="absolute bottom-0 left-0 w-full flex justify-between items-center z-10 px-4 py-2 bg-gradient-to-t from-black/70 to-transparent">
<span className="text-white text-sm md:text-base font-medium">
{CategoryTitles[article.categoryId]}
</span>
<span className="bg-gray-100 shadow px-3 py-1 rounded-full text-sm font-medium flex items-center">
<MapPin size={14} className="mr-1" />
<span className="text-white text-sm md:text-base font-medium flex items-center">
<MapPin size={14} className="mr-1 text-white" />
{CityTitles[article.cityId]}
</span>
</div>
</div>
<div className="p-6">
<div className="p-6 flex flex-col flex-grow">
<div className="flex items-center gap-4 mb-4">
<img
src={article.author.avatarUrl}
@ -58,7 +56,7 @@ export function ArticleCard({ article, featured = false }: ArticleCardProps) {
</h2>
<p className="text-gray-600 line-clamp-2">{article.excerpt}</p>
<div className="mt-4 flex items-center justify-between">
<div className="p-4 mt-auto flex items-center justify-between">
<Link
to={`/article/${article.id}`}
className="text-blue-600 font-medium hover:text-blue-800"
@ -68,6 +66,10 @@ export function ArticleCard({ article, featured = false }: ArticleCardProps) {
<div className="flex items-center text-gray-500">
<ThumbsUp size={16} className="mr-1" />
<span>{article.likes}</span>
<span className="ml-2">
<ThumbsDown size={16} className="mr-1" />
</span>
<span>{article.dislikes}</span>
</div>
</div>
</div>

View File

@ -36,7 +36,7 @@ export function AuthGuard({ children, requiredPermissions }: AuthGuardProps) {
<div className="text-center">
<h2 className="text-2xl font-bold text-gray-900 mb-4">Access Denied</h2>
<p className="text-gray-600">
У вас нет прав на {action} статьи в {categoryId} категории.
У вас нет прав на {action} статьи в разделе {categoryId}.
</p>
</div>
</div>

View File

@ -27,14 +27,14 @@ export function AuthorsSection() {
<div className="text-center mb-12">
<h2 className="text-3xl font-bold text-gray-900">Наши авторы</h2>
<p className="mt-4 text-lg text-gray-600 max-w-2xl mx-auto">
Познакомьтесь с талантливыми писателями и экспертами, работающими для вас
Познакомьтесь с талантливыми писателями и экспертами, работающими для Вас
</p>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8">
{authors.map((author) => (
<div key={author.id} className="bg-white rounded-lg shadow-md overflow-hidden transition-transform hover:scale-[1.02]">
<div className="p-6">
<div key={author.id} className="bg-white rounded-lg shadow-md overflow-hidden transition-transform hover:scale-[1.02] h-full">
<div className="p-6 flex flex-col flex-grow min-h-[200px] h-full">
<div className="flex items-center mb-4">
<img
src={author.avatarUrl}
@ -58,7 +58,7 @@ export function AuthorsSection() {
</div>
<p className="text-gray-600">{author.bio}</p>
<div className="mt-6 pt-6 border-t border-gray-100">
<div className="mt-auto pt-6 border-t border-gray-100">
<div className="flex justify-between items-center">
<span className="text-sm text-gray-500">
{author.articlesCount} статей

View File

@ -1,4 +1,3 @@
import React from 'react';
import { Bookmark } from 'lucide-react';
import { useBookmarkStore } from '../stores/bookmarkStore';
import { Article } from '../types';

View File

@ -68,7 +68,7 @@ export function FeaturedSection() {
<div className="flex justify-between items-center mb-8">
<h2 className="text-3xl font-bold text-gray-900">
{city ? `${CityTitles[Number(city)]} ` : ''}
{category ? `${CategoryTitles[Number(category)]} Статьи` : 'Тематические статьи'}
{category ? `${CategoryTitles[Number(category)]} Статьи` : 'Читайте сегодня'}
</h2>
<p className="font-bold text-gray-600">
Статьи {Math.min((currentPage - 1) * ARTICLES_PER_PAGE + 1, totalArticles)}

View File

@ -1,7 +1,9 @@
import React, { useState } from 'react';
import { Menu, Search, X, ChevronDown } from 'lucide-react';
import { Logo } from "./Header/Logo";
import { Menu, Search, X, ChevronDown, Bookmark } from 'lucide-react';
import { Link, useLocation, useNavigate } from 'react-router-dom';
import { CategoryIds, CategoryTitles, CityIds, CityTitles } from '../types';
import { useBookmarkStore } from "../stores/bookmarkStore";
export function Header() {
@ -12,6 +14,7 @@ export function Header() {
const searchParams = new URLSearchParams(location.search);
const currentCategory = searchParams.get('category');
const currentCity = searchParams.get('city');
const { bookmarks } = useBookmarkStore();
const handleCityChange = (event: React.ChangeEvent<HTMLSelectElement>) => {
const city = event.target.value;
@ -50,9 +53,9 @@ export function Header() {
>
{isMobileMenuOpen ? <X size={24} /> : <Menu size={24} />}
</button>
<Link to="/" className="ml-2 flex items-center space-x-2">
<h1 className="text-2xl font-bold text-gray-900">Культура</h1>
</Link>
<div className="ml-2">
<Logo />
</div>
</div>
<nav className="hidden lg:flex items-center space-x-8">
@ -80,26 +83,16 @@ export function Header() {
<div className="h-6 w-px bg-gray-200" />
{/* Menu */}
<div className="flex items-center space-x-4 overflow-x-auto">
<span className="text-sm font-medium text-gray-500"></span>
<Link
to={currentCity ? `/?city=${currentCity}` : '/'}
className={`px-3 py-2 text-sm font-medium transition-colors whitespace-nowrap ${
!currentCategory
? 'text-blue-600 hover:text-blue-800'
: 'text-gray-600 hover:text-gray-900'
}`}
>
Все категории
</Link>
{CategoryIds.map((category) => (
<Link
key={category}
to={`/?category=${category}${currentCity ? `&city=${currentCity}` : ''}`}
className={`px-3 py-2 text-sm font-medium transition-colors whitespace-nowrap ${
className={`px-3 py-2 font-medium transition-colors whitespace-nowrap ${
Number(currentCategory) === category
? 'text-blue-600 hover:text-blue-800'
: 'text-gray-600 hover:text-gray-900'
? 'text-base md:text-lg lg:text-xl font-semibold text-blue-600 hover:text-blue-800 underline'
: 'text-sm md:text-base lg:text-lg font-medium text-gray-600 hover:text-gray-900'
}`}
>
{CategoryTitles[category]}
@ -112,7 +105,7 @@ export function Header() {
<form onSubmit={handleSearch} className="relative">
<input
type="text"
placeholder="Search..."
placeholder="Поиск..."
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
onKeyDown={handleSearchKeyPress}
@ -125,6 +118,17 @@ export function Header() {
<Search size={18} />
</button>
</form>
<Link
to="/bookmarks"
className="relative p-2 rounded-full hover:bg-gray-100 transition-colors"
>
<Bookmark size={20} className="text-gray-500" />
{bookmarks.length > 0 && (
<span className="absolute -top-1 -right-1 bg-blue-600 text-white text-xs w-4 h-4 flex items-center justify-center rounded-full">
{bookmarks.length}
</span>
)}
</Link>
</div>
</div>
</div>
@ -137,7 +141,7 @@ export function Header() {
>
<div className="px-2 pt-2 pb-3 space-y-1">
<div className="px-3 py-2">
<h3 className="text-sm font-medium text-gray-500 mb-2">City</h3>
<h3 className="text-sm font-medium text-gray-500 mb-2">Столицы</h3>
<select
value={currentCity || ''}
onChange={handleCityChange}
@ -153,18 +157,8 @@ export function Header() {
</div>
<div className="px-3 py-2">
<h3 className="text-sm font-medium text-gray-500 mb-2">Categories</h3>
<div className="space-y-1">
<Link
to={currentCity ? `/?city=${currentCity}` : '/'}
className={`block px-3 py-2 rounded-md text-base font-medium ${
!currentCategory
? 'bg-blue-50 text-blue-600'
: 'text-gray-600 hover:bg-gray-50'
}`}
>
Все категории
</Link>
<h3 className="text-sm font-medium text-gray-500 mb-2">Разделы</h3>
<div className="space-y-0">
{CategoryIds.map((category) => (
<Link
key={category}

View File

@ -1,10 +1,10 @@
import React from 'react';
import { useNavigate } from 'react-router-dom';
import { ChevronDown } from 'lucide-react';
import { City } from '../../types';
import { CityTitles } from '../../types';
interface CitySelectorProps {
cities: City[];
cities: number[];
currentCity: string | null;
}
@ -34,7 +34,7 @@ export function CitySelector({ cities, currentCity }: CitySelectorProps) {
<option value="">All Cities</option>
{cities.map((city) => (
<option key={city} value={city}>
{city}
{CityTitles[city]}
</option>
))}
</select>

View File

@ -1,11 +1,14 @@
import { Link } from 'react-router-dom';
import { Palette } from 'lucide-react';
export function Logo() {
return (
<Link to="/" className="flex items-center space-x-2 text-gray-900">
<Palette size={32} className="text-blue-600" />
<span className="text-2xl font-bold">CultureScope</span>
<Link to="/" className="flex items-center space-x-4 text-gray-900 min-w-0">
<img
src="/images/Logo-2.webp"
alt="Культура двух столиц"
className="h-10 w-10 md:h-12 md:w-12 flex-shrink-0 transition-transform duration-300 hover:scale-150"
/>
<span className="invisible">_____</span>
</Link>
);
}

View File

@ -1,11 +1,11 @@
import React from 'react';
import { Link } from 'react-router-dom';
import { Category, City } from '../../types';
import { CityTitles, CategoryTitles } from '../../types';
interface MobileMenuProps {
isOpen: boolean;
categories: Category[];
cities: City[];
categories: number[];
cities: number[];
currentCategory: string | null;
currentCity: string | null;
onCityChange: (event: React.ChangeEvent<HTMLSelectElement>) => void;
@ -34,7 +34,7 @@ export function MobileMenu({
<option value="">All Cities</option>
{cities.map((city) => (
<option key={city} value={city}>
{city}
{CityTitles[city]}
</option>
))}
</select>
@ -58,12 +58,12 @@ export function MobileMenu({
key={category}
to={`/?category=${category}${currentCity ? `&city=${currentCity}` : ''}`}
className={`block px-3 py-2 rounded-md text-base font-medium ${
currentCategory === category
Number(currentCategory) === category
? 'bg-blue-50 text-blue-600'
: 'text-gray-600 hover:bg-gray-50'
}`}
>
{category}
{CategoryTitles[category]}
</Link>
))}
</div>

View File

@ -1,8 +1,8 @@
import { Link, useLocation } from 'react-router-dom';
import { Category } from '../../types';
import { Link } from 'react-router-dom';
import { CategoryTitles } from '../../types';
interface NavigationProps {
categories: Category[];
categories: number[];
currentCategory: string | null;
currentCity: string | null;
}
@ -26,12 +26,12 @@ export function Navigation({ categories, currentCategory, currentCity }: Navigat
key={category}
to={`/?category=${category}${currentCity ? `&city=${currentCity}` : ''}`}
className={`px-3 py-2 text-sm font-medium transition-colors whitespace-nowrap ${
currentCategory === category
Number(currentCategory) === category
? 'text-blue-600 hover:text-blue-800'
: 'text-gray-600 hover:text-gray-900'
}`}
>
{category}
{CategoryTitles[category]}
</Link>
))}
</div>

View File

@ -1,16 +1,14 @@
import React, { useState } from 'react';
import { Menu, X, Bookmark } from 'lucide-react';
import { Link, useLocation } from 'react-router-dom';
import { Category, City } from '../../types';
import { Logo } from './Logo';
import { SearchBar } from './SearchBar';
import { Navigation } from './Navigation';
import { CitySelector } from './CitySelector';
import { MobileMenu } from './MobileMenu';
import { CategoryIds, CityIds } from '../../types';
import { useBookmarkStore } from '../../stores/bookmarkStore';
const categories: Category[] = ['Film', 'Theater', 'Music', 'Sports', 'Art', 'Legends', 'Anniversaries', 'Memory'];
const cities: City[] = ['New York', 'London'];
export function Header() {
const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false);
@ -52,13 +50,13 @@ export function Header() {
<nav className="hidden lg:flex items-center space-x-8">
<div className="flex items-center space-x-4">
<span className="text-sm font-medium text-gray-500"></span>
<CitySelector cities={cities} currentCity={currentCity} />
<CitySelector cities={CityIds} currentCity={currentCity} />
</div>
<div className="h-6 w-px bg-gray-200" />
<Navigation
categories={categories}
categories={CategoryIds}
currentCategory={currentCategory}
currentCity={currentCity}
/>
@ -83,8 +81,8 @@ export function Header() {
<MobileMenu
isOpen={isMobileMenuOpen}
categories={categories}
cities={cities}
categories={CategoryIds}
cities={CityIds}
currentCategory={currentCategory}
currentCity={currentCity}
onCityChange={handleCityChange}

View File

@ -0,0 +1,46 @@
import { useState } from 'react';
import { Share2 } from 'lucide-react';
interface ShareButtonProps {
title: string;
url: string;
className?: string;
}
export function ShareButton({ title, url, className = '' }: ShareButtonProps) {
const [showTooltip, setShowTooltip] = useState(false);
const handleShare = async () => {
try {
if (navigator.share) {
await navigator.share({
title,
url
});
} else {
await navigator.clipboard.writeText(url);
setShowTooltip(true);
setTimeout(() => setShowTooltip(false), 2000);
}
} catch (error) {
console.error('Error sharing:', error);
}
};
return (
<div className="relative">
<button
onClick={handleShare}
className={`p-2 text-gray-500 hover:text-gray-700 rounded-full hover:bg-gray-100 ${className}`}
aria-label="Share article"
>
<Share2 size={20} />
</button>
{showTooltip && (
<div className="absolute bottom-full left-1/2 transform -translate-x-1/2 mb-2 px-2 py-1 bg-gray-800 text-white text-xs rounded whitespace-nowrap">
Ссылка скопированна!
</div>
)}
</div>
);
}

View File

@ -1,11 +1,13 @@
import { useState, useEffect } from 'react';
import { useParams, Link } from 'react-router-dom';
import { ArrowLeft, Clock, Share2, Bookmark } from 'lucide-react';
import { ArrowLeft, Clock } from 'lucide-react';
import { Header } from '../components/Header';
import { ReactionButtons } from '../components/ReactionButtons';
import { PhotoGallery } from '../components/PhotoGallery';
import { Article, CategoryTitles } from '../types';
import { ArticleContent } from '../components/ArticleContent';
import { ShareButton } from '../components/ShareButton';
import { BookmarkButton } from '../components/BookmarkButton';
import MinutesWord from '../components/MinutesWord';
import axios from "axios";
import api from "../utils/api";
@ -122,12 +124,8 @@ export function ArticlePage() {
{CategoryTitles[articleData.categoryId]}
</span>
<div className="flex-1" />
<button className="p-2 text-gray-500 hover:text-gray-700 rounded-full hover:bg-gray-100">
<Share2 size={20} />
</button>
<button className="p-2 text-gray-500 hover:text-gray-700 rounded-full hover:bg-gray-100">
<Bookmark size={20} />
</button>
<ShareButton title={articleData.title} url={window.location.href} />
<BookmarkButton article={articleData} />
</div>
</div>

View File

@ -12,7 +12,7 @@ export function BookmarksPage() {
<main className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-12">
<div className="flex items-center space-x-4 mb-8">
<Bookmark size={24} className="text-blue-600" />
<h1 className="text-3xl font-bold text-gray-900">Your Bookmarks</h1>
<h1 className="text-3xl font-bold text-gray-900">Мои закладки</h1>
</div>
{bookmarks.length > 0 ? (

View File

@ -30,13 +30,13 @@ export function HomePage() {
const { main, sub, description } = getHeroTitle();
return (
<>
<div className="min-h-screen bg-cover bg-center bg-fixed bg-white/95" style={{ backgroundImage: `url('/images/main-bg.jpg')` }}>
<Header />
<main>
<div className="max-w-7xl mx-auto py-6 sm:px-6 lg:px-8">
<div className="px-4 py-6 sm:px-0">
<div className="relative">
<div className="absolute inset-0">
<div
className="w-full h-full bg-cover bg-center transition-all duration-500 ease-in-out"
@ -47,6 +47,7 @@ export function HomePage() {
<div className="absolute inset-0 bg-gradient-to-r from-gray-900/90 to-gray-900/60 backdrop-blur-sm" />
</div>
</div>
<div className="relative">
<div className="text-center max-w-4xl mx-auto py-24 px-4 sm:py-32 sm:px-6 lg:px-8">
<h1 className="text-4xl font-extrabold tracking-tight text-white sm:text-5xl md:text-6xl">
@ -60,6 +61,7 @@ export function HomePage() {
</div>
</div>
</div>
</div>
<div className="relative bg-white/95">
@ -73,6 +75,5 @@ export function HomePage() {
</div>
</main>
</div>
</>
);
}

View File

@ -6,6 +6,9 @@ export default defineConfig(({ mode }) => {
return {
plugins: [react()],
esbuild: {
minifyIdentifiers: false, // Отключаем переименование переменных
},
optimizeDeps: {
exclude: ['@prisma/client', 'lucide-react'],
},