UNPKG

create-ex-den-kit

Version:

Create exam projects with Ex-Den-Kit

298 lines (275 loc) 10.4 kB
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;