Cong123779's picture
feat: add favicon, OG image, dual DB adapter (Supabase+SQLite), api utility
7c46a3a verified
Raw
History Blame Contribute Delete
23.1 kB
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>
);
}