Spaces:
Runtime error
Runtime error
| import React, { useState, useEffect } from 'react'; | |
| import { api } from '../utils/api.js'; | |
| export default function AdminPanel({ onProjectChange }) { | |
| const [token, setToken] = useState(localStorage.getItem('admin_token') || ''); | |
| const [loginData, setLoginData] = useState({ username: '', password: '' }); | |
| const [loginError, setLoginError] = useState(''); | |
| const [activeTab, setActiveTab] = useState('projects'); // 'projects' or 'messages' | |
| const [projects, setProjects] = useState([]); | |
| const [messages, setMessages] = useState([]); | |
| const [loading, setLoading] = useState(false); | |
| // Modal states | |
| const [projectModal, setProjectModal] = useState({ | |
| isOpen: false, | |
| mode: 'create', // 'create' or 'edit' | |
| data: { | |
| id: null, | |
| title: '', | |
| description: '', | |
| category: 'Web App', | |
| status: 'Active Development', | |
| tech_stack: '', | |
| github_url: '', | |
| demo_url: '', | |
| image_url: '' | |
| } | |
| }); | |
| const [messageModal, setMessageModal] = useState({ | |
| isOpen: false, | |
| data: null | |
| }); | |
| const [notification, setNotification] = useState({ type: '', text: '' }); | |
| // Load admin dashboard data when token changes | |
| useEffect(() => { | |
| if (token) { | |
| fetchDashboardData(); | |
| } | |
| }, [token]); | |
| const showNotification = (type, text) => { | |
| setNotification({ type, text }); | |
| setTimeout(() => setNotification({ type: '', text: '' }), 4000); | |
| }; | |
| const fetchDashboardData = async () => { | |
| setLoading(true); | |
| try { | |
| const projData = await api.get('/api/projects'); | |
| if (projData.success) setProjects(projData.data); | |
| const msgData = await api.get('/api/messages', token); | |
| if (msgData.success) { | |
| setMessages(msgData.data); | |
| } else if (msgData.message?.includes('token')) { | |
| handleLogout(); | |
| } | |
| } catch (error) { | |
| console.error('Fetch dashboard error:', error); | |
| showNotification('error', 'Lỗi tải dữ liệu quản trị.'); | |
| } finally { | |
| setLoading(false); | |
| } | |
| }; | |
| const handleLoginSubmit = async (e) => { | |
| e.preventDefault(); | |
| setLoginError(''); | |
| try { | |
| const data = await api.post('/api/auth/login', loginData); | |
| if (data.success && data.token) { | |
| localStorage.setItem('admin_token', data.token); | |
| setToken(data.token); | |
| setLoginData({ username: '', password: '' }); | |
| } else { | |
| setLoginError(data.message || 'Sai tài khoản hoặc mật khẩu.'); | |
| } | |
| } catch (error) { | |
| console.error('Login submit error:', error); | |
| setLoginError('Không kết nối được với server.'); | |
| } | |
| }; | |
| const handleLogout = () => { | |
| localStorage.removeItem('admin_token'); | |
| setToken(''); | |
| setProjects([]); | |
| setMessages([]); | |
| }; | |
| // --- PROJECT CRUD --- | |
| const openProjectModal = (mode, project = null) => { | |
| if (mode === 'edit' && project) { | |
| setProjectModal({ | |
| isOpen: true, | |
| mode: 'edit', | |
| data: { | |
| id: project.id, | |
| title: project.title, | |
| description: project.description, | |
| category: project.category, | |
| status: project.status, | |
| tech_stack: project.tech_stack, | |
| github_url: project.github_url || '', | |
| demo_url: project.demo_url || '', | |
| image_url: project.image_url || '' | |
| } | |
| }); | |
| } else { | |
| setProjectModal({ | |
| isOpen: true, | |
| mode: 'create', | |
| data: { | |
| id: null, | |
| title: '', | |
| description: '', | |
| category: 'Web App', | |
| status: 'Active Development', | |
| tech_stack: '', | |
| github_url: '', | |
| demo_url: '', | |
| image_url: '' | |
| } | |
| }); | |
| } | |
| }; | |
| const handleProjectFormChange = (e) => { | |
| setProjectModal({ | |
| ...projectModal, | |
| data: { | |
| ...projectModal.data, | |
| [e.target.name]: e.target.value | |
| } | |
| }); | |
| }; | |
| const handleProjectSubmit = async (e) => { | |
| e.preventDefault(); | |
| const { id, title, description, category, status, tech_stack, github_url, demo_url, image_url } = projectModal.data; | |
| if (!title || !description || !category || !status || !tech_stack) { | |
| showNotification('error', 'Vui lòng điền đủ các trường bắt buộc.'); | |
| return; | |
| } | |
| try { | |
| const body = { title, description, category, status, tech_stack, | |
| github_url: github_url || null, demo_url: demo_url || null, image_url: image_url || null }; | |
| const resData = projectModal.mode === 'create' | |
| ? await api.post('/api/projects', body, token) | |
| : await api.put(`/api/projects/${id}`, body, token); | |
| if (resData.success) { | |
| showNotification('success', projectModal.mode === 'create' ? 'Tạo dự án thành công!' : 'Cập nhật dự án thành công!'); | |
| setProjectModal({ ...projectModal, isOpen: false }); | |
| fetchDashboardData(); | |
| if (onProjectChange) onProjectChange(); | |
| } else { | |
| showNotification('error', resData.message || 'Không thể lưu dự án.'); | |
| } | |
| } catch (error) { | |
| console.error('Submit project error:', error); | |
| showNotification('error', 'Lỗi lưu thông tin lên server.'); | |
| } | |
| }; | |
| const handleDeleteProject = async (id, title) => { | |
| if (!window.confirm(`Bạn có chắc chắn muốn xoá dự án "${title}"?`)) return; | |
| try { | |
| const data = await api.delete(`/api/projects/${id}`, token); | |
| if (data.success) { | |
| showNotification('success', 'Xoá dự án thành công.'); | |
| fetchDashboardData(); | |
| if (onProjectChange) onProjectChange(); | |
| } else { | |
| showNotification('error', data.message || 'Lỗi khi xoá dự án.'); | |
| } | |
| } catch (error) { | |
| console.error('Delete project error:', error); | |
| showNotification('error', 'Lỗi kết nối xoá dự án.'); | |
| } | |
| }; | |
| // --- MESSAGES ACTIONS --- | |
| const handleViewMessage = async (message) => { | |
| setMessageModal({ isOpen: true, data: message }); | |
| if (message.read_status === 0) { | |
| try { | |
| const data = await api.put(`/api/messages/${message.id}`, { read_status: 1 }, token); | |
| if (data.success) { | |
| setMessages(prev => prev.map(m => m.id === message.id ? { ...m, read_status: 1 } : m)); | |
| } | |
| } catch (error) { | |
| console.error('Mark as read error:', error); | |
| } | |
| } | |
| }; | |
| const handleDeleteMessage = async (id) => { | |
| if (!window.confirm('Xoá thư liên hệ này?')) return; | |
| try { | |
| const data = await api.delete(`/api/messages/${id}`, token); | |
| if (data.success) { | |
| showNotification('success', 'Đã xoá thư.'); | |
| setMessages(prev => prev.filter(m => m.id !== id)); | |
| if (messageModal.isOpen && messageModal.data?.id === id) { | |
| setMessageModal({ isOpen: false, data: null }); | |
| } | |
| } else { | |
| showNotification('error', data.message || 'Không xoá được thư.'); | |
| } | |
| } catch (error) { | |
| console.error('Delete message error:', error); | |
| showNotification('error', 'Lỗi kết nối xoá thư.'); | |
| } | |
| }; | |
| // --- RENDER LOGIN FORM --- | |
| if (!token) { | |
| return ( | |
| <section className="section" style={{ minHeight: 'calc(100vh - 80px)', display: 'flex', alignItems: 'center' }}> | |
| <div className="container" style={{ maxWidth: '400px' }}> | |
| <div className="glass" style={{ padding: '2.5rem' }}> | |
| <h2 style={{ textAlign: 'center', marginBottom: '1.5rem', fontWeight: '800' }}>Admin Login</h2> | |
| {loginError && <div className="toast toast-error">{loginError}</div>} | |
| <form onSubmit={handleLoginSubmit}> | |
| <div className="form-group"> | |
| <label className="form-label" htmlFor="username">Tài Khoản</label> | |
| <input | |
| type="text" | |
| id="username" | |
| value={loginData.username} | |
| onChange={(e) => setLoginData({ ...loginData, username: e.target.value })} | |
| className="form-control" | |
| placeholder="admin" | |
| required | |
| /> | |
| </div> | |
| <div className="form-group"> | |
| <label className="form-label" htmlFor="password">Mật Khẩu</label> | |
| <input | |
| type="password" | |
| id="password" | |
| value={loginData.password} | |
| onChange={(e) => setLoginData({ ...loginData, password: e.target.value })} | |
| className="form-control" | |
| placeholder="••••••••" | |
| required | |
| /> | |
| </div> | |
| <button type="submit" className="btn btn-primary" style={{ width: '100%', marginTop: '1rem' }}> | |
| Đăng Nhập | |
| </button> | |
| </form> | |
| </div> | |
| </div> | |
| </section> | |
| ); | |
| } | |
| // --- RENDER ADMIN DASHBOARD --- | |
| return ( | |
| <section className="section" style={{ minHeight: 'calc(100vh - 80px)' }}> | |
| <div className="container"> | |
| {/* Toast notifications */} | |
| {notification.text && ( | |
| <div className={`toast toast-${notification.type}`} style={{ position: 'fixed', bottom: '2rem', right: '2rem', zIndex: 9999, marginBottom: 0 }}> | |
| {notification.text} | |
| </div> | |
| )} | |
| <div className="admin-header"> | |
| <div className="admin-title-area"> | |
| <h2>Bảng Quản Trị</h2> | |
| <span className="admin-badge">Admin Mode</span> | |
| </div> | |
| <button className="btn btn-secondary btn-sm" onClick={handleLogout}> | |
| Đăng Xuất | |
| </button> | |
| </div> | |
| {/* Tab Selection */} | |
| <div className="admin-tabs"> | |
| <button | |
| className={`admin-tab ${activeTab === 'projects' ? 'active' : ''}`} | |
| onClick={() => setActiveTab('projects')} | |
| > | |
| Quản Lý Sản Phẩm | |
| </button> | |
| <button | |
| className={`admin-tab ${activeTab === 'messages' ? 'active' : ''}`} | |
| onClick={() => setActiveTab('messages')} | |
| > | |
| Hộp Thư Liên Hệ ({messages.filter(m => m.read_status === 0).length}) | |
| </button> | |
| </div> | |
| {loading ? ( | |
| <div style={{ textAlign: 'center', padding: '3rem', color: 'var(--text-secondary)' }}> | |
| Đang tải dữ liệu hệ thống... | |
| </div> | |
| ) : ( | |
| <> | |
| {/* PROJECTS TAB CONTENTS */} | |
| {activeTab === 'projects' && ( | |
| <div> | |
| <div className="admin-list-actions"> | |
| <h3 style={{ fontSize: '1.25rem' }}>Danh sách dự án ({projects.length})</h3> | |
| <button className="btn btn-primary btn-sm" onClick={() => openProjectModal('create')}> | |
| + Thêm Sản Phẩm Mới | |
| </button> | |
| </div> | |
| <div className="glass admin-table-container"> | |
| <table className="admin-table"> | |
| <thead> | |
| <tr> | |
| <th>Tên sản phẩm</th> | |
| <th>Danh mục</th> | |
| <th>Trạng thái</th> | |
| <th>Công nghệ</th> | |
| <th style={{ width: '150px' }}>Hành động</th> | |
| </tr> | |
| </thead> | |
| <tbody> | |
| {projects.length === 0 ? ( | |
| <tr> | |
| <td colSpan="5" style={{ textAlign: 'center', color: 'var(--text-secondary)' }}> | |
| Chưa có sản phẩm nào được nhập. | |
| </td> | |
| </tr> | |
| ) : ( | |
| projects.map(proj => ( | |
| <tr key={proj.id}> | |
| <td style={{ fontWeight: '600' }}>{proj.title}</td> | |
| <td>{proj.category}</td> | |
| <td> | |
| <span className={`status-badge ${proj.status.toLowerCase().includes('active') || proj.status.toLowerCase().includes('phát triển') ? 'active-development' : 'completed'}`} style={{ fontSize: '0.7rem' }}> | |
| {proj.status} | |
| </span> | |
| </td> | |
| <td style={{ maxWidth: '250px', whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}> | |
| {proj.tech_stack} | |
| </td> | |
| <td className="admin-actions-cell"> | |
| <button className="btn btn-secondary btn-sm" onClick={() => openProjectModal('edit', proj)}> | |
| Sửa | |
| </button> | |
| <button className="btn btn-danger btn-sm" onClick={() => handleDeleteProject(proj.id, proj.title)}> | |
| Xoá | |
| </button> | |
| </td> | |
| </tr> | |
| )) | |
| )} | |
| </tbody> | |
| </table> | |
| </div> | |
| </div> | |
| )} | |
| {/* MESSAGES TAB CONTENTS */} | |
| {activeTab === 'messages' && ( | |
| <div> | |
| <h3 style={{ fontSize: '1.25rem', marginBottom: '1.5rem' }}>Thư đã nhận ({messages.length})</h3> | |
| <div className="glass admin-table-container"> | |
| <table className="admin-table"> | |
| <thead> | |
| <tr> | |
| <th>Người gửi</th> | |
| <th>Email</th> | |
| <th>Tiêu đề</th> | |
| <th>Thời gian</th> | |
| <th>Trạng thái</th> | |
| <th style={{ width: '150px' }}>Hành động</th> | |
| </tr> | |
| </thead> | |
| <tbody> | |
| {messages.length === 0 ? ( | |
| <tr> | |
| <td colSpan="6" style={{ textAlign: 'center', color: 'var(--text-secondary)' }}> | |
| Hộp thư của bạn hiện đang trống. | |
| </td> | |
| </tr> | |
| ) : ( | |
| messages.map(msg => ( | |
| <tr key={msg.id} style={msg.read_status === 0 ? { fontWeight: '700', background: 'rgba(6,182,212,0.02)' } : {}}> | |
| <td>{msg.name}</td> | |
| <td>{msg.email}</td> | |
| <td style={{ maxWidth: '250px', whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>{msg.subject}</td> | |
| <td>{new Date(msg.created_at).toLocaleString('vi-VN')}</td> | |
| <td> | |
| {msg.read_status === 0 ? ( | |
| <span style={{ color: 'var(--accent-cyan)' }}>Chưa đọc</span> | |
| ) : ( | |
| <span style={{ color: 'var(--text-muted)' }}>Đã đọc</span> | |
| )} | |
| </td> | |
| <td className="admin-actions-cell"> | |
| <button className="btn btn-secondary btn-sm" onClick={() => handleViewMessage(msg)}> | |
| Xem | |
| </button> | |
| <button className="btn btn-danger btn-sm" onClick={() => handleDeleteMessage(msg.id)}> | |
| Xoá | |
| </button> | |
| </td> | |
| </tr> | |
| )) | |
| )} | |
| </tbody> | |
| </table> | |
| </div> | |
| </div> | |
| )} | |
| </> | |
| )} | |
| {/* --- PROJECT EDIT/CREATE MODAL --- */} | |
| {projectModal.isOpen && ( | |
| <div className="modal-overlay"> | |
| <div className="glass modal-content"> | |
| <button className="modal-close" onClick={() => setProjectModal({ ...projectModal, isOpen: false })}> | |
| ✕ | |
| </button> | |
| <h3 className="modal-title"> | |
| {projectModal.mode === 'create' ? 'Thêm Sản Phẩm Mới' : 'Cập Nhật Sản Phẩm'} | |
| </h3> | |
| <form onSubmit={handleProjectSubmit}> | |
| <div className="form-group"> | |
| <label className="form-label">Tên sản phẩm *</label> | |
| <input | |
| type="text" | |
| name="title" | |
| value={projectModal.data.title} | |
| onChange={handleProjectFormChange} | |
| className="form-control" | |
| required | |
| /> | |
| </div> | |
| <div className="form-group"> | |
| <label className="form-label">Mô tả sản phẩm *</label> | |
| <textarea | |
| name="description" | |
| value={projectModal.data.description} | |
| onChange={handleProjectFormChange} | |
| className="form-control" | |
| required | |
| ></textarea> | |
| </div> | |
| <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '1rem' }}> | |
| <div className="form-group"> | |
| <label className="form-label">Danh mục *</label> | |
| <input | |
| type="text" | |
| name="category" | |
| value={projectModal.data.category} | |
| onChange={handleProjectFormChange} | |
| className="form-control" | |
| required | |
| /> | |
| </div> | |
| <div className="form-group"> | |
| <label className="form-label">Trạng thái *</label> | |
| <select | |
| name="status" | |
| value={projectModal.data.status} | |
| onChange={handleProjectFormChange} | |
| className="form-control" | |
| required | |
| > | |
| <option value="Active Development">Active Development</option> | |
| <option value="Completed">Completed</option> | |
| <option value="Archived">Archived</option> | |
| </select> | |
| </div> | |
| </div> | |
| <div className="form-group"> | |
| <label className="form-label">Công nghệ sử dụng * (ngăn cách bằng dấu phẩy)</label> | |
| <input | |
| type="text" | |
| name="tech_stack" | |
| value={projectModal.data.tech_stack} | |
| onChange={handleProjectFormChange} | |
| className="form-control" | |
| placeholder="React, Node.js, SQLite" | |
| required | |
| /> | |
| </div> | |
| <div className="form-group"> | |
| <label className="form-label">Hình ảnh minh hoạ (URL)</label> | |
| <input | |
| type="text" | |
| name="image_url" | |
| value={projectModal.data.image_url} | |
| onChange={handleProjectFormChange} | |
| className="form-control" | |
| placeholder="https://images.unsplash.com/..." | |
| /> | |
| </div> | |
| <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '1rem' }}> | |
| <div className="form-group"> | |
| <label className="form-label">GitHub Link (URL)</label> | |
| <input | |
| type="text" | |
| name="github_url" | |
| value={projectModal.data.github_url} | |
| onChange={handleProjectFormChange} | |
| className="form-control" | |
| placeholder="https://github.com/..." | |
| /> | |
| </div> | |
| <div className="form-group"> | |
| <label className="form-label">Live Demo (URL)</label> | |
| <input | |
| type="text" | |
| name="demo_url" | |
| value={projectModal.data.demo_url} | |
| onChange={handleProjectFormChange} | |
| className="form-control" | |
| placeholder="https://demo.lyvuha.com/..." | |
| /> | |
| </div> | |
| </div> | |
| <div style={{ display: 'flex', gap: '1rem', marginTop: '1.5rem', justifyContent: 'flex-end' }}> | |
| <button type="button" className="btn btn-secondary" onClick={() => setProjectModal({ ...projectModal, isOpen: false })}> | |
| Huỷ | |
| </button> | |
| <button type="submit" className="btn btn-primary"> | |
| Lưu Lại | |
| </button> | |
| </div> | |
| </form> | |
| </div> | |
| </div> | |
| )} | |
| {/* --- VIEW MESSAGE DETAIL MODAL --- */} | |
| {messageModal.isOpen && messageModal.data && ( | |
| <div className="modal-overlay"> | |
| <div className="glass modal-content"> | |
| <button className="modal-close" onClick={() => setMessageModal({ isOpen: false, data: null })}> | |
| ✕ | |
| </button> | |
| <h3 className="modal-title">Chi Tiết Thư</h3> | |
| <div className="message-detail-content"> | |
| <div className="message-meta"> | |
| <span><strong>Người gửi:</strong> {messageModal.data.name}</span> | |
| <span><strong>Email:</strong> {messageModal.data.email}</span> | |
| <span><strong>Tiêu đề:</strong> {messageModal.data.subject}</span> | |
| <span><strong>Gửi lúc:</strong> {new Date(messageModal.data.created_at).toLocaleString('vi-VN')}</span> | |
| </div> | |
| <div className="message-body"> | |
| {messageModal.data.content} | |
| </div> | |
| </div> | |
| <div style={{ display: 'flex', gap: '1rem', marginTop: '1.5rem', justifyContent: 'flex-end' }}> | |
| <button className="btn btn-danger" onClick={() => handleDeleteMessage(messageModal.data.id)}> | |
| Xoá Thư | |
| </button> | |
| <button className="btn btn-primary" onClick={() => setMessageModal({ isOpen: false, data: null })}> | |
| Đóng | |
| </button> | |
| </div> | |
| </div> | |
| </div> | |
| )} | |
| </div> | |
| </section> | |
| ); | |
| } | |