Версия 1.2.15 Добавлен фон под названием сайта и анонсы.
This commit is contained in:
parent
e95f1d52a2
commit
0251d5788f
2042
package-lock.json
generated
2042
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "vite-react-typescript-starter",
|
||||
"private": true,
|
||||
"version": "1.2.14",
|
||||
"version": "1.2.15",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
@ -17,7 +17,7 @@
|
||||
"prisma": {
|
||||
"seed": "npx ts-node --project tsconfig.node.json --compiler-options {\"module\":\"CommonJS\"} prisma/seed.ts"
|
||||
},
|
||||
"dependencies": {
|
||||
"dependencies": {
|
||||
"@aws-sdk/client-s3": "^3.525.0",
|
||||
"@aws-sdk/s3-request-presigner": "^3.525.0",
|
||||
"@headlessui/react": "^2.2.2",
|
||||
@ -80,6 +80,7 @@
|
||||
"tailwindcss": "^3.4.17",
|
||||
"typescript": "^5.5.3",
|
||||
"typescript-eslint": "^8.3.0",
|
||||
"vite": "^5.4.14"
|
||||
"vite": "^5.4.14",
|
||||
"vite-plugin-prerender": "^1.0.8"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 651 KiB After Width: | Height: | Size: 460 KiB |
BIN
public/images/panorama-hero-2.webp
Normal file
BIN
public/images/panorama-hero-2.webp
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 392 KiB |
@ -8,6 +8,7 @@ import { AdminPage } from './pages/AdminPage';
|
||||
import { LoginPage } from './pages/LoginPage';
|
||||
import { UserManagementPage } from './pages/UserManagementPage';
|
||||
import { AuthorManagementPage } from './pages/AuthorManagementPage';
|
||||
import { AnnouncementManagementPage } from './pages/AnnouncementManagementPage';
|
||||
import { SearchPage } from './pages/SearchPage';
|
||||
import { BookmarksPage } from './pages/BookmarksPage';
|
||||
import { Footer } from './components/Footer';
|
||||
@ -78,6 +79,14 @@ function App() {
|
||||
</AuthGuard>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="/admin/announcements"
|
||||
element={
|
||||
<AuthGuard>
|
||||
<AnnouncementManagementPage />
|
||||
</AuthGuard>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="/admin/import"
|
||||
element={
|
||||
|
||||
@ -64,13 +64,13 @@ export function ArticleCard({ article, featured = false }: ArticleCardProps) {
|
||||
))}
|
||||
</p>
|
||||
<div className="flex items-center text-sm text-gray-500">
|
||||
<Clock size={14} className="mr-1" />
|
||||
{article.readTime} <MinutesWord minutes={article.readTime} /> ·{' '}
|
||||
{new Date(article.publishedAt).toLocaleDateString('ru-RU', {
|
||||
month: 'long',
|
||||
day: 'numeric',
|
||||
year: 'numeric',
|
||||
})}
|
||||
})} ·{' '}
|
||||
<Clock size={14} className="mr-1" />
|
||||
{article.readTime} <MinutesWord minutes={article.readTime} /> чтения
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import React, { useState, useEffect } from "react";
|
||||
import { Author, User } from "../types/auth.ts";
|
||||
import { Author, User } from "../types/auth";
|
||||
|
||||
|
||||
interface AuthorModalProps {
|
||||
|
||||
@ -225,13 +225,18 @@ const AuthorSelectionModal: React.FC<AuthorSelectionModalProps> = ({
|
||||
<div id="other-authors-section" key={`other-${activeTab}`}>
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<h3 className="text-lg font-semibold text-gray-700">
|
||||
Все {roleLabels[activeTab].toLowerCase()}
|
||||
Остальные {roleLabels[activeTab].toLowerCase()}
|
||||
</h3>
|
||||
{showPagination && (
|
||||
<span className="text-sm text-gray-500">
|
||||
Страница {currentPage} из {totalPages}
|
||||
<div className="flex items-center space-x-4">
|
||||
<span className="text-sm text-gray-500">
|
||||
Всего: {totalAuthors}
|
||||
</span>
|
||||
)}
|
||||
{showPagination && (
|
||||
<span className="text-sm text-gray-500">
|
||||
Страница {currentPage} из {totalPages}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Кнопки с первыми буквами */}
|
||||
|
||||
56
src/components/Header/AnnouncementBanner.tsx
Normal file
56
src/components/Header/AnnouncementBanner.tsx
Normal file
@ -0,0 +1,56 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { X } from 'lucide-react';
|
||||
import axios from 'axios';
|
||||
|
||||
interface Announcement {
|
||||
id: string;
|
||||
message: string;
|
||||
}
|
||||
|
||||
export function AnnouncementBanner() {
|
||||
const [announcement, setAnnouncement] = useState<Announcement | null>(null);
|
||||
const [isVisible, setIsVisible] = useState(true);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchAnnouncement = async () => {
|
||||
try {
|
||||
const response = await axios.get('/api/announcements/active');
|
||||
if (response.data) {
|
||||
setAnnouncement(response.data);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch announcement:', error);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
fetchAnnouncement();
|
||||
}, []);
|
||||
|
||||
if (isLoading || !announcement || !isVisible) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="relative bg-gradient-to-r from-blue-600 to-blue-700 text-white">
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div className="flex items-center justify-between py-3 sm:py-4">
|
||||
<div className="flex-1 text-center">
|
||||
<p className="text-sm sm:text-base font-medium">
|
||||
{announcement.message}
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => setIsVisible(false)}
|
||||
className="ml-4 inline-flex text-white hover:bg-white/20 rounded-full p-1 transition-colors"
|
||||
aria-label="Close announcement"
|
||||
>
|
||||
<X size={20} />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
21
src/components/Header/BannerHero.tsx
Normal file
21
src/components/Header/BannerHero.tsx
Normal file
@ -0,0 +1,21 @@
|
||||
export function BannerHero() {
|
||||
return (
|
||||
<div className="relative w-full h-16 sm:h-24 md:h-32 lg:h-42 xl:h-56 overflow-hidden bg-gray-900">
|
||||
<img
|
||||
src="/images/panorama-hero-2.webp"
|
||||
alt="Культура Двух Столиц"
|
||||
className="absolute inset-0 w-full h-full object-cover"
|
||||
/>
|
||||
|
||||
<div className="absolute inset-0 bg-gradient-to-r from-black/70 via-black/60 to-black/50" />
|
||||
|
||||
<div className="relative h-full flex items-center justify-center px-4 sm:px-6 lg:px-8">
|
||||
<div className="text-center">
|
||||
<h1 className="text-3xl sm:text-4xl md:text-5xl lg:text-6xl font-bold text-white uppercase tracking-tight">
|
||||
Культура Двух Столиц
|
||||
</h1>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -26,31 +26,23 @@ export function Header() {
|
||||
const handleCityChange = (event: React.ChangeEvent<HTMLSelectElement>) => {
|
||||
const city = event.target.value;
|
||||
const params = new URLSearchParams(location.search);
|
||||
|
||||
|
||||
if (city) {
|
||||
params.set('city', city);
|
||||
} else {
|
||||
params.delete('city');
|
||||
}
|
||||
|
||||
|
||||
window.location.href = `/?${params.toString()}`;
|
||||
};
|
||||
|
||||
return (
|
||||
<header className="sticky top-0 z-50 bg-white shadow-sm">
|
||||
{/* Название блога над навигацией */}
|
||||
<div className="bg-white border-b border-gray-100">
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-2">
|
||||
<h1 className="text-center text-xl sm:text-2xl font-semibold text-gray-800 tracking-tight bg-gradient-to-b from-white to-gray-100 border-b border-gray-100 pb-2">
|
||||
Культура Двух Столиц
|
||||
</h1>
|
||||
</div>
|
||||
</div>
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
{/* Main header row */}
|
||||
<div className="flex justify-between items-center h-16">
|
||||
<div className="flex items-center">
|
||||
<button
|
||||
<button
|
||||
className="p-2 rounded-md text-gray-500 xl:hidden hover:bg-gray-100"
|
||||
onClick={() => setIsMobileMenuOpen(!isMobileMenuOpen)}
|
||||
>
|
||||
@ -69,10 +61,10 @@ export function Header() {
|
||||
|
||||
<div className="h-6 w-px bg-gray-200" />
|
||||
|
||||
<Navigation
|
||||
<Navigation
|
||||
categories={CategoryIds}
|
||||
currentCategory={currentCategory}
|
||||
currentCity={currentCity}
|
||||
currentCategory={currentCategory}
|
||||
currentCity={currentCity}
|
||||
/>
|
||||
</nav>
|
||||
|
||||
@ -114,7 +106,7 @@ export function Header() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<MobileMenu
|
||||
<MobileMenu
|
||||
isOpen={isMobileMenuOpen}
|
||||
categories={CategoryIds}
|
||||
cities={CityIds}
|
||||
|
||||
@ -7,6 +7,6 @@ export const BackgroundImages: Record<number, string> = {
|
||||
6: '/images/pack/legend-bn.webp?auto=format&fit=crop&q=80&w=2070',
|
||||
7: '/images/pack/anniversary-bn.webp?auto=format&fit=crop&q=80&w=2070',
|
||||
8: '/images/pack/memory-bn.webp?auto=format&fit=crop&q=80&w=2070',
|
||||
0: 'https://images.unsplash.com/photo-1460661419201-fd4cecdf8a8bу?auto=format&fit=crop&q=80&w=2070'
|
||||
0: ''
|
||||
};
|
||||
|
||||
|
||||
@ -7,7 +7,7 @@ import { GalleryModal } from '../components/GalleryManager/GalleryModal';
|
||||
import { ArticleDeleteModal } from '../components/ArticleDeleteModal';
|
||||
import { Article, Author, GalleryImage, ArticleData, AuthorRole, AuthorLink, ViewMode } from '../types';
|
||||
import { usePermissions } from '../hooks/usePermissions';
|
||||
import { FileJson, Users, UserSquare2 } from "lucide-react";
|
||||
import { FileJson, Users, UserSquare2, Bell } from "lucide-react";
|
||||
import { Link } from 'react-router-dom';
|
||||
import { useAuthStore } from "../stores/authStore";
|
||||
|
||||
@ -216,7 +216,7 @@ export function AdminPage() {
|
||||
|
||||
{/* Блок кнопок навигации */}
|
||||
{viewMode === 'list' && isAdmin && (
|
||||
<div className="flex gap-4 mb-8">
|
||||
<div className="flex flex-wrap gap-4 mb-8">
|
||||
<Link
|
||||
to="/admin/users"
|
||||
className="inline-flex items-center px-4 py-2 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-blue-600 hover:bg-blue-700"
|
||||
@ -231,6 +231,13 @@ export function AdminPage() {
|
||||
<UserSquare2 className="h-5 w-5 mr-2" />
|
||||
Авторы
|
||||
</Link>
|
||||
<Link
|
||||
to="/admin/announcements"
|
||||
className="inline-flex items-center px-4 py-2 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-blue-600 hover:bg-blue-700"
|
||||
>
|
||||
<Bell className="h-5 w-5 mr-2" />
|
||||
Анонсы
|
||||
</Link>
|
||||
<Link
|
||||
to="/admin/import"
|
||||
className="inline-flex items-center px-4 py-2 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-blue-600 hover:bg-blue-700"
|
||||
|
||||
326
src/pages/AnnouncementManagementPage.tsx
Normal file
326
src/pages/AnnouncementManagementPage.tsx
Normal file
@ -0,0 +1,326 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { Header } from '../components/Header';
|
||||
import { Plus, Pencil, Trash2, ArrowLeft, ToggleLeft, ToggleRight } from 'lucide-react';
|
||||
import axios from 'axios';
|
||||
|
||||
interface Announcement {
|
||||
id: string;
|
||||
message: string;
|
||||
isActive: boolean;
|
||||
createdAt: string;
|
||||
}
|
||||
|
||||
type ViewMode = 'list' | 'create' | 'edit';
|
||||
|
||||
export function AnnouncementManagementPage() {
|
||||
const [viewMode, setViewMode] = useState<ViewMode>('list');
|
||||
const [announcements, setAnnouncements] = useState<Announcement[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [message, setMessage] = useState('');
|
||||
const [editingId, setEditingId] = useState<string | null>(null);
|
||||
const [showDeleteModal, setShowDeleteModal] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
fetchAnnouncements();
|
||||
}, []);
|
||||
|
||||
const fetchAnnouncements = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
const token = localStorage.getItem('token');
|
||||
const response = await axios.get('/api/announcements', {
|
||||
headers: { Authorization: `Bearer ${token}` }
|
||||
});
|
||||
setAnnouncements(response.data);
|
||||
setError(null);
|
||||
} catch (err) {
|
||||
setError('Сбой загрузки Анонсов');
|
||||
console.error('Ошибка загрузки Анонсов:', err);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleCreateNew = () => {
|
||||
setMessage('');
|
||||
setEditingId(null);
|
||||
setViewMode('create');
|
||||
setError(null);
|
||||
};
|
||||
|
||||
const handleEdit = (id: string) => {
|
||||
const announcement = announcements.find(a => a.id === id);
|
||||
if (announcement) {
|
||||
setMessage(announcement.message);
|
||||
setEditingId(id);
|
||||
setViewMode('edit');
|
||||
setError(null);
|
||||
}
|
||||
};
|
||||
|
||||
const handleCancel = () => {
|
||||
setViewMode('list');
|
||||
setMessage('');
|
||||
setEditingId(null);
|
||||
setError(null);
|
||||
};
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
|
||||
if (!message.trim()) {
|
||||
setError('Message cannot be empty');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const token = localStorage.getItem('token');
|
||||
|
||||
if (editingId) {
|
||||
await axios.put(
|
||||
`/api/announcements/${editingId}`,
|
||||
{ message: message.trim() },
|
||||
{ headers: { Authorization: `Bearer ${token}` } }
|
||||
);
|
||||
} else {
|
||||
await axios.post(
|
||||
'/api/announcements',
|
||||
{ message: message.trim() },
|
||||
{ headers: { Authorization: `Bearer ${token}` } }
|
||||
);
|
||||
}
|
||||
|
||||
await fetchAnnouncements();
|
||||
handleCancel();
|
||||
} catch (err) {
|
||||
setError('Failed to save announcement');
|
||||
console.error('Error saving announcement:', err);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDelete = async (id: string) => {
|
||||
try {
|
||||
const token = localStorage.getItem('token');
|
||||
await axios.delete(
|
||||
`/api/announcements/${id}`,
|
||||
{ headers: { Authorization: `Bearer ${token}` } }
|
||||
);
|
||||
|
||||
await fetchAnnouncements();
|
||||
setShowDeleteModal(null);
|
||||
} catch (err) {
|
||||
setError('Failed to delete announcement');
|
||||
console.error('Error deleting announcement:', err);
|
||||
}
|
||||
};
|
||||
|
||||
const handleToggleActive = async (id: string) => {
|
||||
try {
|
||||
const announcement = announcements.find(a => a.id === id);
|
||||
if (!announcement) return;
|
||||
|
||||
const token = localStorage.getItem('token');
|
||||
await axios.put(
|
||||
`/api/announcements/${id}`,
|
||||
{ isActive: !announcement.isActive },
|
||||
{ headers: { Authorization: `Bearer ${token}` } }
|
||||
);
|
||||
|
||||
await fetchAnnouncements();
|
||||
} catch (err) {
|
||||
setError('Failed to update announcement');
|
||||
console.error('Error updating announcement:', err);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-50">
|
||||
<Header />
|
||||
<main className="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 py-12">
|
||||
{/* Form View */}
|
||||
{(viewMode === 'create' || viewMode === 'edit') && (
|
||||
<div className="bg-white rounded-lg shadow-sm p-6">
|
||||
<div className="flex items-center justify-between mb-6">
|
||||
<div className="flex items-center">
|
||||
<button
|
||||
onClick={handleCancel}
|
||||
className="mr-4 p-2 text-gray-500 hover:text-gray-700 rounded-full hover:bg-gray-100"
|
||||
>
|
||||
<ArrowLeft size={20} />
|
||||
</button>
|
||||
<h1 className="text-2xl font-bold text-gray-900">
|
||||
{viewMode === 'edit' ? 'Редактирование Анонса' : 'Создание Анонса'}
|
||||
</h1>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{error && (
|
||||
<div className="mb-6 bg-red-50 text-red-700 p-4 rounded-md">
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<form onSubmit={handleSubmit} className="space-y-6">
|
||||
<div>
|
||||
<label htmlFor="message" className="block text-sm font-medium text-gray-700">
|
||||
Сообщение Анонса
|
||||
</label>
|
||||
<textarea
|
||||
id="message"
|
||||
rows={6}
|
||||
value={message}
|
||||
onChange={(e) => setMessage(e.target.value)}
|
||||
placeholder="Ведите сообщение ананса (например, 'Нам 10 лет!')"
|
||||
className="mt-1 block w-full rounded-md border border-gray-300 px-3 py-2 shadow-sm focus:border-blue-500 focus:ring-blue-500 focus:outline-none"
|
||||
required
|
||||
/>
|
||||
<p className="mt-2 text-sm text-gray-500">
|
||||
Это сообщение будет отображено в баннере в верхней части главной страницы.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-end gap-4">
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleCancel}
|
||||
className="px-4 py-2 border border-gray-300 rounded-md shadow-sm text-sm font-medium text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500"
|
||||
>
|
||||
Отмена
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
className="inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md shadow-sm text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500"
|
||||
>
|
||||
{viewMode === 'edit' ? 'Изменить' : 'Создать'}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* List View */}
|
||||
{viewMode === 'list' && (
|
||||
<div className="bg-white rounded-lg shadow-sm">
|
||||
<div className="px-6 py-4 border-b border-gray-200">
|
||||
<div className="flex items-center justify-between">
|
||||
<h2 className="text-lg font-medium text-gray-900">
|
||||
Анонсы
|
||||
</h2>
|
||||
<button
|
||||
onClick={handleCreateNew}
|
||||
className="inline-flex items-center px-4 py-2 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-blue-600 hover:bg-blue-700"
|
||||
>
|
||||
<Plus className="h-4 w-4 mr-2" />
|
||||
Новый
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{loading ? (
|
||||
<div className="px-6 py-12 text-center">
|
||||
<p className="text-gray-500">Загрузка анонсов...</p>
|
||||
</div>
|
||||
) : error ? (
|
||||
<div className="px-6 py-12">
|
||||
<div className="bg-red-50 text-red-700 p-4 rounded-md">
|
||||
{error}
|
||||
</div>
|
||||
</div>
|
||||
) : announcements.length === 0 ? (
|
||||
<div className="px-6 py-12 text-center">
|
||||
<p className="text-gray-500">Анонсов еще нет.</p>
|
||||
<button
|
||||
onClick={handleCreateNew}
|
||||
className="mt-4 inline-flex items-center px-4 py-2 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-blue-600 hover:bg-blue-700"
|
||||
>
|
||||
<Plus className="h-4 w-4 mr-2" />
|
||||
Создать первый Анонс
|
||||
</button>
|
||||
</div>
|
||||
) : (
|
||||
<ul className="divide-y divide-gray-200">
|
||||
{announcements.map((announcement) => (
|
||||
<li
|
||||
key={announcement.id}
|
||||
className={`px-6 py-4 ${!announcement.isActive ? 'bg-gray-50' : ''}`}
|
||||
>
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex-1 min-w-0">
|
||||
<p className="text-sm font-medium text-gray-900 break-words">
|
||||
{announcement.message}
|
||||
</p>
|
||||
<p className="text-xs text-gray-500 mt-1">
|
||||
Created: {new Date(announcement.createdAt).toLocaleDateString()} at {new Date(announcement.createdAt).toLocaleTimeString()}
|
||||
</p>
|
||||
{announcement.isActive && (
|
||||
<span className="inline-block mt-2 px-2 py-1 text-xs font-medium text-white bg-green-600 rounded">
|
||||
Currently Active
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex items-center gap-4 ml-4">
|
||||
<button
|
||||
onClick={() => handleToggleActive(announcement.id)}
|
||||
className={`p-2 rounded-full hover:bg-gray-100 transition-colors ${
|
||||
announcement.isActive ? 'text-green-600' : 'text-gray-400'
|
||||
}`}
|
||||
title={announcement.isActive ? 'Deactivate' : 'Activate'}
|
||||
>
|
||||
{announcement.isActive ? <ToggleRight size={18} /> : <ToggleLeft size={18} />}
|
||||
</button>
|
||||
<button
|
||||
onClick={() => handleEdit(announcement.id)}
|
||||
className="p-2 text-gray-400 hover:text-blue-600 rounded-full hover:bg-blue-50 transition-colors"
|
||||
title="Edit"
|
||||
>
|
||||
<Pencil size={18} />
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setShowDeleteModal(announcement.id)}
|
||||
className="p-2 text-gray-400 hover:text-red-600 rounded-full hover:bg-red-50 transition-colors"
|
||||
title="Delete"
|
||||
>
|
||||
<Trash2 size={18} />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Delete Confirmation Modal */}
|
||||
{showDeleteModal && (
|
||||
<div className="fixed inset-0 bg-gray-500 bg-opacity-75 flex items-center justify-center p-4">
|
||||
<div className="bg-white rounded-lg max-w-md w-full p-6">
|
||||
<h3 className="text-lg font-medium text-gray-900 mb-4">
|
||||
Удаление Анонса
|
||||
</h3>
|
||||
|
||||
<p className="text-sm text-gray-500 mb-6">
|
||||
Вы уверены, что хотите удалить этот Анонс? Это действие невозможно отменить.
|
||||
</p>
|
||||
<div className="flex justify-end gap-4">
|
||||
<button
|
||||
onClick={() => setShowDeleteModal(null)}
|
||||
className="px-4 py-2 border border-gray-300 rounded-md shadow-sm text-sm font-medium text-gray-700 bg-white hover:bg-gray-50"
|
||||
>
|
||||
Отмена
|
||||
</button>
|
||||
<button
|
||||
onClick={() => handleDelete(showDeleteModal)}
|
||||
className="px-4 py-2 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-red-600 hover:bg-red-700"
|
||||
>
|
||||
Удалить
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -8,6 +8,9 @@ import { BackgroundImages } from '../hooks/useBackgroundImage';
|
||||
import { useScrollStore } from '../stores/scrollStore';
|
||||
import { CategoryDescription, CategoryText, CategoryTitles } from '../types';
|
||||
import { SEO } from '../components/SEO';
|
||||
import { BannerHero } from "../components/Header/BannerHero";
|
||||
import { AnnouncementBanner } from '../components/Header/AnnouncementBanner';
|
||||
|
||||
//import { MasterBio } from "../components/MasterBio";
|
||||
|
||||
|
||||
@ -45,19 +48,21 @@ export function HomePage() {
|
||||
|
||||
const getSEODescription = () => {
|
||||
if (categoryId) {
|
||||
return `Explore the latest ${CategoryTitles[Number(categoryId)].toLowerCase()} stories, events, and cultural highlights from around the globe.`;
|
||||
return `Ознакомьтесь с последними историями, событиями ${CategoryTitles[Number(categoryId)].toLowerCase()} Москвы и Петербурга.`;
|
||||
}
|
||||
return 'Discover the latest in art, music, theater, and cultural events from around the globe. Your premier destination for arts and culture coverage.';
|
||||
return 'Узнайте о последних достижениях искусства, музыки, театра и культурных мероприятиях Москвы и Петербурга. Это лучшее место для знакомства с искусством и культурой.';
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-cover bg-center bg-fixed bg-white/95" style={{ backgroundImage: `url('/images/gpt_main-bg.webp')` }}>
|
||||
<div className="min-h-screen bg-cover bg-center bg-fixed bg-white/95" style={{ backgroundImage: `url('/images/main-bg.webp')` }}>
|
||||
<SEO
|
||||
title={categoryId ? `${CategoryTitles[Number(categoryId)]} Статьи - Культура двух Столиц` : undefined}
|
||||
description={getSEODescription()}
|
||||
keywords={categoryId ? [CategoryTitles[Number(categoryId)].toLowerCase(), 'культура', 'искусство', 'события'] : undefined}
|
||||
image={backgroundImage}
|
||||
/>
|
||||
<AnnouncementBanner />
|
||||
<BannerHero />
|
||||
<Header />
|
||||
<main>
|
||||
<div className="max-w-7xl mx-auto py-6 sm:px-6 lg:px-8">
|
||||
|
||||
@ -2,7 +2,19 @@
|
||||
export default {
|
||||
content: ['./index.html', './src/**/*.{js,ts,jsx,tsx}'],
|
||||
theme: {
|
||||
extend: {},
|
||||
extend: {
|
||||
// Ключевые кадры для анимации
|
||||
keyframes: {
|
||||
fadeInUp: {
|
||||
'0%': { opacity: '0', transform: 'translateY(30px)' },
|
||||
'100%': { opacity: '1', transform: 'translateY(0)' },
|
||||
}
|
||||
},
|
||||
// Название анимации для использования в классах
|
||||
animation: {
|
||||
'fade-in-up': 'fadeInUp 1s ease-out',
|
||||
}
|
||||
},
|
||||
},
|
||||
plugins: [],
|
||||
};
|
||||
};
|
||||
Loading…
x
Reference in New Issue
Block a user