create-ex-den-kit
Version:
Create exam projects with Ex-Den-Kit
298 lines (275 loc) • 10.4 kB
JSX
import React, { useState, useEffect } from 'react';
import { useNavigate } from 'react-router-dom';
import { motion, AnimatePresence } from 'framer-motion';
import axios from 'axios';
import toast from 'react-hot-toast';
import { useTheme } from '../contexts/ThemeContext';
import {
Plus,
Trash2,
MessageSquare,
Calendar,
Clock,
AlertCircle,
CheckCircle,
XCircle,
Archive
} from 'lucide-react';
import { format } from 'date-fns';
import { ru } from 'date-fns/locale';
const Dashboard = () => {
const { config } = useTheme();
const navigate = useNavigate();
const [entries, setEntries] = useState([]);
const [loading, setLoading] = useState(true);
const [showReviewModal, setShowReviewModal] = useState(false);
const [selectedEntry, setSelectedEntry] = useState(null);
const [review, setReview] = useState('');
useEffect(() => {
fetchEntries();
}, []);
const fetchEntries = async () => {
try {
const response = await axios.get('/api/entries/my');
setEntries(response.data);
} catch (error) {
toast.error('Ошибка при загрузке записей');
} finally {
setLoading(false);
}
};
const handleDelete = async (id) => {
if (!window.confirm('Вы уверены, что хотите удалить эту запись?')) return;
try {
await axios.delete(`/api/entries/${id}`);
toast.success('Запись перемещена в архив');
fetchEntries();
} catch (error) {
toast.error('Ошибка при удалении записи');
}
};
const handleReview = async () => {
if (!review.trim()) {
toast.error('Введите текст отзыва');
return;
}
try {
await axios.post(`/api/entries/${selectedEntry.id}/review`, { review });
toast.success('Отзыв добавлен');
setShowReviewModal(false);
setReview('');
fetchEntries();
} catch (error) {
toast.error('Ошибка при добавлении отзыва');
}
};
const getStatusIcon = (status) => {
switch (status) {
case 'new':
return <AlertCircle className="text-yellow-500" size={20} />;
case 'approved':
case 'studying':
case 'visited':
case 'in_work':
return <CheckCircle className="text-green-500" size={20} />;
case 'rejected':
case 'cancelled':
return <XCircle className="text-red-500" size={20} />;
case 'completed':
return <CheckCircle className="text-blue-500" size={20} />;
default:
return null;
}
};
const getStatusLabel = (status) => {
const statusLabels = {
new: 'Новая',
approved: 'Одобрена',
rejected: 'Отклонена',
studying: 'Идет обучение',
completed: 'Обучение завершено',
visited: 'Посещение состоялось',
cancelled: 'Отменена',
in_work: 'В работе',
};
return statusLabels[status] || status;
};
const canAddReview = (entry) => {
if (!config.features.reviews) return false;
if (entry.data.review) return false;
const allowedStatuses = config.features.statusRestrictions?.review || [];
return allowedStatuses.includes(entry.status);
};
if (loading) {
return (
<div className="flex justify-center items-center h-64">
<motion.div
animate={{ rotate: 360 }}
transition={{ duration: 1, repeat: Infinity, ease: 'linear' }}
className="w-16 h-16 border-4 border-primary border-t-transparent rounded-full"
/>
</div>
);
}
return (
<div>
<div className="flex justify-between items-center mb-8">
<h1 className="text-3xl font-bold text-gray-900">Мои записи</h1>
<motion.button
whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.95 }}
onClick={() => navigate('/create')}
className="btn-primary flex items-center space-x-2"
>
<Plus size={20} />
<span>Создать запись</span>
</motion.button>
</div>
{entries.length === 0 ? (
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
className="text-center py-12"
>
<div className="text-6xl mb-4 opacity-20">📋</div>
<h3 className="text-xl font-medium text-gray-600 mb-2">
У вас пока нет записей
</h3>
<p className="text-gray-500 mb-6">
Создайте первую запись, нажав кнопку выше
</p>
</motion.div>
) : (
<div className="grid gap-6 md:grid-cols-2 lg:grid-cols-3">
<AnimatePresence>
{entries.map((entry, index) => (
<motion.div
key={entry.id}
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, scale: 0.9 }}
transition={{ delay: index * 0.1 }}
className="card hover:shadow-2xl"
>
<div className="flex justify-between items-start mb-4">
<div className="flex items-center space-x-2">
{getStatusIcon(entry.status)}
<span className="font-medium text-gray-700">
{getStatusLabel(entry.status)}
</span>
</div>
<div className="flex space-x-2">
{canAddReview(entry) && (
<button
onClick={() => {
setSelectedEntry(entry);
setShowReviewModal(true);
}}
className="text-blue-500 hover:text-blue-700 transition-colors"
title="Оставить отзыв"
>
<MessageSquare size={20} />
</button>
)}
<button
onClick={() => handleDelete(entry.id)}
className="text-red-500 hover:text-red-700 transition-colors"
title="Удалить"
>
<Trash2 size={20} />
</button>
</div>
</div>
<div className="space-y-3">
{Object.entries(entry.data).map(([key, value]) => {
if (key === 'review' || key === 'reviewDate') return null;
const field = config.fields.find(f => f.name === key);
if (!field) return null;
return (
<div key={key}>
<p className="text-sm text-gray-500">{field.label}</p>
<p className="font-medium text-gray-900">{value}</p>
</div>
);
})}
</div>
{entry.rejectionReason && (
<div className="mt-4 p-3 bg-red-50 rounded-lg">
<p className="text-sm text-red-800">
<strong>Причина отклонения:</strong> {entry.rejectionReason}
</p>
</div>
)}
{entry.data.review && (
<div className="mt-4 p-3 bg-blue-50 rounded-lg">
<p className="text-sm text-blue-800">
<strong>Ваш отзыв:</strong> {entry.data.review}
</p>
</div>
)}
<div className="mt-4 pt-4 border-t border-gray-200 flex items-center justify-between text-sm text-gray-500">
<div className="flex items-center space-x-1">
<Calendar size={16} />
<span>
{format(new Date(entry.createdAt), 'dd MMM yyyy', { locale: ru })}
</span>
</div>
<div className="flex items-center space-x-1">
<Clock size={16} />
<span>
{format(new Date(entry.createdAt), 'HH:mm')}
</span>
</div>
</div>
</motion.div>
))}
</AnimatePresence>
</div>
)}
{/* Modal для отзыва */}
<AnimatePresence>
{showReviewModal && (
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center p-4 z-50"
onClick={() => setShowReviewModal(false)}
>
<motion.div
initial={{ scale: 0.9, opacity: 0 }}
animate={{ scale: 1, opacity: 1 }}
exit={{ scale: 0.9, opacity: 0 }}
className="bg-white rounded-xl p-6 max-w-md w-full"
onClick={(e) => e.stopPropagation()}
>
<h3 className="text-xl font-bold mb-4">Оставить отзыв</h3>
<textarea
value={review}
onChange={(e) => setReview(e.target.value)}
className="w-full p-3 border border-gray-300 rounded-lg resize-none focus:ring-2 focus:ring-primary focus:border-primary"
rows="4"
placeholder="Напишите ваш отзыв..."
/>
<div className="flex space-x-3 mt-4">
<button
onClick={handleReview}
className="btn-primary flex-1"
>
Отправить
</button>
<button
onClick={() => setShowReviewModal(false)}
className="flex-1 px-4 py-2 border border-gray-300 rounded-lg hover:bg-gray-50 transition-colors"
>
Отмена
</button>
</div>
</motion.div>
</motion.div>
)}
</AnimatePresence>
</div>
);
};
export default Dashboard;