Update Next.js configuration for S3 support and enhance admin dashboard functionality - Added S3 hostname to next.config.js for image uploads - Updated package.json and package-lock.json with AWS SDK dependencies - Improved admin layout with S3 status component and enhanced dashboard statistics loading logic - Refactored news loading in NewsBlock component to handle errors gracefully.

This commit is contained in:
albivkt
2025-07-13 23:36:38 +03:00
parent c0e91bba1d
commit 162d96e9aa
21 changed files with 3675 additions and 137 deletions

View File

@ -1,68 +1,146 @@
'use client';
import React from 'react';
import React, { useState, useEffect } from 'react';
import Link from 'next/link';
import { FileText, Eye, Calendar, TrendingUp, Plus } from 'lucide-react';
import { NEWS_DATA } from '@/lib/news-data';
import { FileText, Eye, Calendar, TrendingUp, Plus, Users, Database, Activity } from 'lucide-react';
interface DashboardStats {
totalNews: number;
publishedNews: number;
featuredNews: number;
recentNews: number;
totalUsers: number;
}
interface NewsItem {
id: string;
title: string;
slug: string;
category: string;
featured: boolean;
published: boolean;
publishedAt: string;
views: number;
likes: number;
}
export default function AdminDashboard() {
// Подсчет статистики
const totalNews = NEWS_DATA.length;
const publishedNews = NEWS_DATA.filter(news => news.published !== false).length;
const featuredNews = NEWS_DATA.filter(news => news.featured).length;
const recentNews = NEWS_DATA.filter(news => {
const publishDate = new Date(news.publishedAt);
const weekAgo = new Date();
weekAgo.setDate(weekAgo.getDate() - 7);
return publishDate >= weekAgo;
}).length;
const [stats, setStats] = useState<DashboardStats | null>(null);
const [latestNews, setLatestNews] = useState<NewsItem[]>([]);
const [loading, setLoading] = useState(true);
const stats = [
useEffect(() => {
loadDashboardData();
}, []);
const loadDashboardData = async () => {
try {
setLoading(true);
// Загружаем статистику
const healthResponse = await fetch('/api/health');
const healthData = await healthResponse.json();
// Загружаем все новости для статистики
const newsResponse = await fetch('/api/news?limit=100&published=all');
const newsData = await newsResponse.json();
if (newsData.success) {
const allNews = newsData.data.news;
const publishedNews = allNews.filter((news: NewsItem) => news.published);
const featuredNews = allNews.filter((news: NewsItem) => news.featured);
const weekAgo = new Date();
weekAgo.setDate(weekAgo.getDate() - 7);
const recentNews = allNews.filter((news: NewsItem) => {
const publishDate = new Date(news.publishedAt);
return publishDate >= weekAgo;
});
setStats({
totalNews: allNews.length,
publishedNews: publishedNews.length,
featuredNews: featuredNews.length,
recentNews: recentNews.length,
totalUsers: healthData.data?.userCount || 0
});
// Берем последние 5 новостей
setLatestNews(allNews.slice(0, 5));
}
} catch (error) {
console.error('Error loading dashboard data:', error);
} finally {
setLoading(false);
}
};
const statsCards = stats ? [
{
name: 'Всего новостей',
value: totalNews,
value: stats.totalNews,
icon: FileText,
color: 'bg-blue-500',
textColor: 'text-blue-600'
color: 'bg-gradient-to-r from-blue-500 to-blue-600',
textColor: 'text-blue-600',
bgColor: 'bg-blue-50'
},
{
name: 'Опубликовано',
value: publishedNews,
value: stats.publishedNews,
icon: Eye,
color: 'bg-green-500',
textColor: 'text-green-600'
color: 'bg-gradient-to-r from-green-500 to-green-600',
textColor: 'text-green-600',
bgColor: 'bg-green-50'
},
{
name: 'Рекомендуемые',
value: featuredNews,
value: stats.featuredNews,
icon: TrendingUp,
color: 'bg-yellow-500',
textColor: 'text-yellow-600'
color: 'bg-gradient-to-r from-yellow-500 to-yellow-600',
textColor: 'text-yellow-600',
bgColor: 'bg-yellow-50'
},
{
name: 'За неделю',
value: recentNews,
value: stats.recentNews,
icon: Calendar,
color: 'bg-purple-500',
textColor: 'text-purple-600'
color: 'bg-gradient-to-r from-purple-500 to-purple-600',
textColor: 'text-purple-600',
bgColor: 'bg-purple-50'
},
{
name: 'Пользователи',
value: stats.totalUsers,
icon: Users,
color: 'bg-gradient-to-r from-indigo-500 to-indigo-600',
textColor: 'text-indigo-600',
bgColor: 'bg-indigo-50'
}
];
] : [];
const latestNews = NEWS_DATA
.sort((a, b) => new Date(b.publishedAt).getTime() - new Date(a.publishedAt).getTime())
.slice(0, 5);
if (loading) {
return (
<div className="min-h-screen flex items-center justify-center">
<div className="text-center">
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-600 mx-auto mb-4"></div>
<p className="text-gray-600">Загрузка данных...</p>
</div>
</div>
);
}
return (
<div className="space-y-8">
{/* Header */}
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between">
<div>
<h1 className="text-3xl font-bold text-gray-900">Панель управления</h1>
<h1 className="text-3xl font-bold bg-gradient-to-r from-gray-900 to-gray-700 bg-clip-text text-transparent">
Панель управления
</h1>
<p className="text-gray-600 mt-2">Обзор системы управления новостями</p>
</div>
<Link
href="/admin/news/create"
className="mt-4 sm:mt-0 inline-flex items-center px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700 transition-colors"
className="mt-4 sm:mt-0 inline-flex items-center px-6 py-3 bg-gradient-to-r from-blue-600 to-blue-700 text-white rounded-lg hover:from-blue-700 hover:to-blue-800 transition-all duration-200 shadow-lg hover:shadow-xl transform hover:-translate-y-0.5"
>
<Plus className="h-4 w-4 mr-2" />
Создать новость
@ -70,15 +148,15 @@ export default function AdminDashboard() {
</div>
{/* Stats Grid */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
{stats.map((stat) => (
<div key={stat.name} className="bg-white rounded-lg shadow-sm p-6">
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-5 gap-6">
{statsCards.map((stat) => (
<div key={stat.name} className={`${stat.bgColor} rounded-xl shadow-sm p-6 border border-gray-100 hover:shadow-md transition-shadow duration-200`}>
<div className="flex items-center">
<div className={`p-3 rounded-lg ${stat.color} bg-opacity-10`}>
<stat.icon className={`h-6 w-6 ${stat.textColor}`} />
<div className={`p-3 rounded-lg ${stat.color} shadow-sm`}>
<stat.icon className="h-6 w-6 text-white" />
</div>
<div className="ml-4">
<p className="text-sm text-gray-600">{stat.name}</p>
<p className="text-sm text-gray-600 font-medium">{stat.name}</p>
<p className="text-2xl font-bold text-gray-900">{stat.value}</p>
</div>
</div>
@ -87,13 +165,13 @@ export default function AdminDashboard() {
</div>
{/* Recent News */}
<div className="bg-white rounded-lg shadow-sm">
<div className="bg-white rounded-xl shadow-sm border border-gray-100">
<div className="px-6 py-4 border-b border-gray-200">
<div className="flex items-center justify-between">
<h2 className="text-lg font-semibold text-gray-900">Последние новости</h2>
<Link
href="/admin/news"
className="text-blue-600 hover:text-blue-800 text-sm font-medium"
className="text-blue-600 hover:text-blue-800 text-sm font-medium hover:underline"
>
Показать все
</Link>
@ -101,41 +179,49 @@ export default function AdminDashboard() {
</div>
<div className="divide-y divide-gray-200">
{latestNews.map((news) => (
<div key={news.id} className="px-6 py-4 hover:bg-gray-50">
<div key={news.id} className="px-6 py-4 hover:bg-gray-50 transition-colors">
<div className="flex items-center justify-between">
<div className="flex-1">
<div className="flex items-center space-x-2">
<h3 className="text-sm font-medium text-gray-900 truncate">
<h3 className="text-sm font-medium text-gray-900 truncate max-w-md">
{news.title}
</h3>
{news.featured && (
<span className="inline-flex items-center px-2 py-1 rounded-full text-xs font-medium bg-yellow-100 text-yellow-800">
Рекомендуемое
<span className="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-gradient-to-r from-yellow-400 to-yellow-500 text-white">
Рекомендуемое
</span>
)}
<span className={`inline-flex items-center px-2 py-1 rounded-full text-xs font-medium ${
news.published !== false
<span className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium ${
news.published
? 'bg-green-100 text-green-800'
: 'bg-gray-100 text-gray-800'
}`}>
{news.published !== false ? 'Опубликовано' : 'Черновик'}
{news.published ? 'Опубликовано' : '📝 Черновик'}
</span>
</div>
<p className="text-sm text-gray-500 mt-1">
{new Date(news.publishedAt).toLocaleDateString('ru-RU')}
</p>
<div className="flex items-center space-x-4 mt-2">
<p className="text-sm text-gray-500">
📅 {new Date(news.publishedAt).toLocaleDateString('ru-RU')}
</p>
<p className="text-sm text-gray-500">
👁 {news.views} просмотров
</p>
<p className="text-sm text-gray-500">
{news.likes} лайков
</p>
</div>
</div>
<div className="flex items-center space-x-2">
<Link
href={`/admin/news/${news.id}/edit`}
className="text-blue-600 hover:text-blue-800 text-sm"
className="px-3 py-1 text-sm text-blue-600 hover:text-blue-800 hover:bg-blue-50 rounded-md transition-colors"
>
Редактировать
</Link>
<Link
href={`/news/${news.slug}`}
target="_blank"
className="text-gray-600 hover:text-gray-800 text-sm"
className="px-3 py-1 text-sm text-gray-600 hover:text-gray-800 hover:bg-gray-50 rounded-md transition-colors"
>
Просмотр
</Link>
@ -143,47 +229,108 @@ export default function AdminDashboard() {
</div>
</div>
))}
{latestNews.length === 0 && (
<div className="px-6 py-8 text-center text-gray-500">
<FileText className="h-12 w-12 mx-auto mb-4 text-gray-300" />
<p>Новости не найдены</p>
<Link
href="/admin/news/create"
className="mt-2 text-blue-600 hover:text-blue-800 text-sm font-medium hover:underline"
>
Создать первую новость
</Link>
</div>
)}
</div>
</div>
{/* Quick Actions */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<div className="bg-white rounded-lg shadow-sm p-6">
<h3 className="text-lg font-semibold text-gray-900 mb-4">Быстрые действия</h3>
{/* Enhanced Quick Actions & Analytics */}
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
<div className="bg-white rounded-xl shadow-sm p-6 border border-gray-100">
<h3 className="text-lg font-semibold text-gray-900 mb-4 flex items-center">
<Activity className="h-5 w-5 mr-2 text-blue-600" />
Быстрые действия
</h3>
<div className="space-y-3">
<Link
href="/admin/news/create"
className="flex items-center p-3 rounded-md hover:bg-gray-50 transition-colors"
className="flex items-center p-3 rounded-lg hover:bg-blue-50 transition-colors group"
>
<Plus className="h-5 w-5 text-blue-600 mr-3" />
<span className="text-sm font-medium text-gray-900">Создать новость</span>
<div className="p-2 bg-blue-100 rounded-lg group-hover:bg-blue-200 transition-colors">
<Plus className="h-4 w-4 text-blue-600" />
</div>
<span className="text-sm font-medium text-gray-900 ml-3">Создать новость</span>
</Link>
<Link
href="/admin/news"
className="flex items-center p-3 rounded-md hover:bg-gray-50 transition-colors"
className="flex items-center p-3 rounded-lg hover:bg-green-50 transition-colors group"
>
<FileText className="h-5 w-5 text-green-600 mr-3" />
<span className="text-sm font-medium text-gray-900">Управление новостями</span>
<div className="p-2 bg-green-100 rounded-lg group-hover:bg-green-200 transition-colors">
<FileText className="h-4 w-4 text-green-600" />
</div>
<span className="text-sm font-medium text-gray-900 ml-3">Управление новостями</span>
</Link>
<Link
href="/admin/settings"
className="flex items-center p-3 rounded-lg hover:bg-purple-50 transition-colors group"
>
<div className="p-2 bg-purple-100 rounded-lg group-hover:bg-purple-200 transition-colors">
<Database className="h-4 w-4 text-purple-600" />
</div>
<span className="text-sm font-medium text-gray-900 ml-3">Настройки</span>
</Link>
</div>
</div>
<div className="bg-white rounded-lg shadow-sm p-6">
<div className="bg-white rounded-xl shadow-sm p-6 border border-gray-100">
<h3 className="text-lg font-semibold text-gray-900 mb-4">Статистика по категориям</h3>
<div className="space-y-3">
{['company', 'promotions', 'other'].map((category) => {
const count = NEWS_DATA.filter(news => news.category === category).length;
const categoryName = category === 'company' ? 'Новости компании' :
category === 'promotions' ? 'Акции' : 'Другое';
<div className="space-y-4">
{[
{ id: 'Общие новости', name: 'Общие новости', color: 'bg-blue-500' },
{ id: 'Обследование канализации', name: 'Канализация', color: 'bg-green-500' },
{ id: 'Тепловизионная экспертиза', name: 'Тепловизор', color: 'bg-purple-500' },
{ id: 'Экспертиза при заливе', name: 'Залив', color: 'bg-red-500' },
{ id: 'Строительная экспертиза', name: 'Строительство', color: 'bg-yellow-500' }
].map((category) => {
const count = latestNews.filter(news => news.category === category.id).length;
return (
<div key={category} className="flex items-center justify-between">
<span className="text-sm text-gray-600">{categoryName}</span>
<span className="text-sm font-medium text-gray-900">{count}</span>
<div key={category.id} className="flex items-center justify-between">
<div className="flex items-center">
<div className={`w-3 h-3 rounded-full ${category.color} mr-3`}></div>
<span className="text-sm text-gray-600">{category.name}</span>
</div>
<span className="text-sm font-medium text-gray-900 bg-gray-100 px-2 py-1 rounded-full">
{count}
</span>
</div>
);
})}
</div>
</div>
<div className="bg-gradient-to-br from-blue-50 to-indigo-50 rounded-xl shadow-sm p-6 border border-blue-100">
<h3 className="text-lg font-semibold text-gray-900 mb-4">Система</h3>
<div className="space-y-3">
<div className="flex items-center justify-between">
<span className="text-sm text-gray-600">База данных</span>
<span className="text-sm font-medium text-green-600 flex items-center">
<div className="w-2 h-2 bg-green-500 rounded-full mr-2"></div>
Подключена
</span>
</div>
<div className="flex items-center justify-between">
<span className="text-sm text-gray-600">S3 хранилище</span>
<span className="text-sm font-medium text-green-600 flex items-center">
<div className="w-2 h-2 bg-green-500 rounded-full mr-2"></div>
Активно
</span>
</div>
<div className="flex items-center justify-between">
<span className="text-sm text-gray-600">Версия</span>
<span className="text-sm font-medium text-gray-900">v1.0.0</span>
</div>
</div>
</div>
</div>
</div>
);