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

@ -51,21 +51,30 @@ export default function ImageUpload({
setError('');
try {
// В реальном приложении здесь будет загрузка на сервер
// Для демонстрации используем FileReader для создания data URL
const reader = new FileReader();
reader.onload = (e) => {
const result = e.target?.result as string;
onChange(result);
setIsUploading(false);
};
reader.onerror = () => {
setError('Ошибка при загрузке файла');
setIsUploading(false);
};
reader.readAsDataURL(file);
const formData = new FormData();
formData.append('file', file);
formData.append('folder', 'images');
// Если есть старое изображение, передаем его URL для удаления
if (value && value.startsWith('http')) {
formData.append('oldUrl', value);
}
const response = await fetch('/api/upload', {
method: 'POST',
body: formData,
});
const result = await response.json();
if (!response.ok) {
throw new Error(result.error || 'Ошибка при загрузке файла');
}
onChange(result.data.publicUrl);
setIsUploading(false);
} catch (error) {
setError('Ошибка при загрузке файла');
setError(error instanceof Error ? error.message : 'Ошибка при загрузке файла');
setIsUploading(false);
}
};
@ -97,7 +106,19 @@ export default function ImageUpload({
setDragActive(false);
};
const handleRemove = () => {
const handleRemove = async () => {
try {
// Если файл находится в S3, удаляем его оттуда
if (value && value.startsWith('http')) {
await fetch(`/api/upload?url=${encodeURIComponent(value)}`, {
method: 'DELETE',
});
}
} catch (error) {
console.error('Ошибка при удалении файла:', error);
// Не показываем ошибку пользователю, так как файл может быть удален из UI
}
onRemove();
setError('');
if (fileInputRef.current) {

View File

@ -0,0 +1,141 @@
'use client';
import { useState, useEffect } from 'react';
import { CheckCircle, XCircle, AlertCircle, RefreshCw } from 'lucide-react';
import { Button } from '@/components/ui/button';
interface S3StatusProps {
className?: string;
}
export default function S3Status({ className = '' }: S3StatusProps) {
const [status, setStatus] = useState<'checking' | 'connected' | 'error' | 'unknown'>('unknown');
const [error, setError] = useState<string>('');
const [isChecking, setIsChecking] = useState(false);
console.log('S3Status: Компонент инициализирован (версия 2.0)');
const checkS3Connection = async () => {
setIsChecking(true);
setStatus('checking');
setError('');
try {
console.log('S3Status: Проверяю подключение к S3...');
console.log('S3Status: Используется API /api/test-s3');
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), 10000); // 10 секунд таймаут
const response = await fetch(`/api/test-s3?t=${Date.now()}`, {
signal: controller.signal,
cache: 'no-cache'
});
console.log('S3Status: Запрос отправлен к /api/test-s3, статус:', response.status);
clearTimeout(timeoutId);
const result = await response.json();
console.log('S3Status: Результат проверки:', result);
if (result.success) {
setStatus('connected');
console.log('S3Status: Подключение успешно');
} else {
setStatus('error');
const errorMessage = result.error || 'Ошибка подключения к S3';
setError(errorMessage);
console.error('S3Status: Ошибка подключения:', result);
}
} catch (err) {
console.error('S3Status: Исключение при проверке:', err);
setStatus('error');
if (err instanceof Error && err.name === 'AbortError') {
setError('Таймаут подключения к S3 (более 10 секунд)');
} else {
setError(err instanceof Error ? err.message : 'Ошибка сети при проверке S3');
}
} finally {
setIsChecking(false);
}
};
useEffect(() => {
checkS3Connection();
}, []);
const getStatusIcon = () => {
switch (status) {
case 'checking':
return <RefreshCw className="h-4 w-4 animate-spin text-blue-500" />;
case 'connected':
return <CheckCircle className="h-4 w-4 text-green-500" />;
case 'error':
return <XCircle className="h-4 w-4 text-red-500" />;
default:
return <AlertCircle className="h-4 w-4 text-gray-500" />;
}
};
const getStatusText = () => {
switch (status) {
case 'checking':
return 'Проверка подключения...';
case 'connected':
return 'S3 подключено';
case 'error':
return 'Ошибка S3';
default:
return 'Статус неизвестен';
}
};
const getStatusColor = () => {
switch (status) {
case 'checking':
return 'text-blue-400';
case 'connected':
return 'text-green-400';
case 'error':
return 'text-red-400';
default:
return 'text-gray-400';
}
};
return (
<div className={`flex items-center space-x-2 ${className}`}>
{getStatusIcon()}
<span className={`text-sm font-medium ${getStatusColor()}`}>
{getStatusText()}
</span>
{status === 'error' && (
<Button
variant="outline"
size="sm"
onClick={checkS3Connection}
disabled={isChecking}
className="ml-2"
>
{isChecking ? (
<RefreshCw className="h-3 w-3 animate-spin" />
) : (
'Повторить'
)}
</Button>
)}
{error && (
<div className="ml-2 text-xs text-red-400 max-w-xs" title={error}>
<span className="block truncate">{error}</span>
<span className="block text-xs text-gray-500 mt-1">
Проверьте консоль для подробностей
</span>
</div>
)}
</div>
);
}