| import React, { useState, useEffect, useCallback } from 'react'; |
| import { Link, useNavigate } from 'react-router-dom'; |
| import { api } from '../services/api'; |
| import { |
| UsersIcon, |
| DocumentTextIcon, |
| ChartBarIcon, |
| CogIcon, |
| UserGroupIcon, |
| AcademicCapIcon, |
| ShieldCheckIcon |
| } from '@heroicons/react/24/outline'; |
|
|
| interface User { |
| name: string; |
| email: string; |
| role?: string; |
| displayName?: string; |
| online?: boolean; |
| } |
|
|
| interface SystemStats { |
| totalUsers: number; |
| practiceExamples: number; |
| totalSubmissions: number; |
| activeSessions: number; |
| } |
|
|
| interface PracticeExample { |
| _id: string; |
| title: string; |
| content: string; |
| sourceLanguage: string; |
| sourceCulture: string; |
| culturalElements: any[]; |
| difficulty: string; |
| createdAt: string; |
| } |
|
|
| interface TutorialTask { |
| _id: string; |
| title: string; |
| content: string; |
| sourceLanguage: string; |
| sourceCulture: string; |
| weekNumber: number; |
| difficulty: string; |
| culturalElements: any[]; |
| translationBrief?: string; |
| createdAt: string; |
| } |
|
|
| interface WeeklyPractice { |
| _id: string; |
| title: string; |
| content: string; |
| sourceLanguage: string; |
| sourceCulture: string; |
| weekNumber: number; |
| difficulty: string; |
| culturalElements: any[]; |
| translationBrief?: string; |
| createdAt: string; |
| } |
|
|
| const Manage: React.FC = () => { |
| const [user, setUser] = useState<User | null>(null); |
| const [loading, setLoading] = useState(true); |
| const [viewMode, setViewMode] = useState<'admin' | 'student' | 'auto'>(() => { |
| try { return (localStorage.getItem('viewMode') as any) || 'auto'; } catch { return 'auto'; } |
| }); |
| const [stats, setStats] = useState<SystemStats | null>(null); |
| const [statsLoading, setStatsLoading] = useState(true); |
| const [examples, setExamples] = useState<PracticeExample[]>([]); |
| const [examplesLoading, setExamplesLoading] = useState(false); |
| const [tutorialTasks, setTutorialTasks] = useState<TutorialTask[]>([]); |
| const [tutorialTasksLoading, setTutorialTasksLoading] = useState(false); |
| const [weeklyPractice, setWeeklyPractice] = useState<WeeklyPractice[]>([]); |
| const [weeklyPracticeLoading, setWeeklyPracticeLoading] = useState(false); |
| const [users, setUsers] = useState<User[]>([]); |
| const [usersLoading, setUsersLoading] = useState(false); |
| const [loginSummary, setLoginSummary] = useState<any[]>([]); |
| const [loadingSummary, setLoadingSummary] = useState(false); |
| const [summaryRole, setSummaryRole] = useState<string>('all'); |
| const [summaryRange, setSummaryRange] = useState<number>(7*24*60*60*1000); |
| const [showAllSummary, setShowAllSummary] = useState<boolean>(false); |
| const [allLoginSummary, setAllLoginSummary] = useState<any[]>([]); |
| const fetchLoginSummary = useCallback(async (rangeMs: number, role: string) => { |
| setLoadingSummary(true); |
| try { |
| const resp = await api.get(`/api/auth/admin/login-summary?sinceMs=${rangeMs}&role=${role}`); |
| const sessions = resp.data?.sessions || []; |
| setAllLoginSummary(sessions); |
| const filtered = role === 'all' ? sessions : sessions.filter((s: any) => s.role === role); |
| setLoginSummary(filtered); |
| } catch (e) { |
| setLoginSummary([]); |
| } finally { |
| setLoadingSummary(false); |
| } |
| }, [setLoadingSummary, setLoginSummary]); |
| |
| useEffect(() => { |
| const handler = (e: any) => { |
| const mode = e?.detail; |
| if (mode === 'admin' || mode === 'student') setViewMode(mode); |
| }; |
| window.addEventListener('view-mode-change', handler as any); |
| return () => window.removeEventListener('view-mode-change', handler as any); |
| }, []); |
| const [showAddUser, setShowAddUser] = useState(false); |
| const [showAddExample, setShowAddExample] = useState(false); |
| const [showAddTutorialTask, setShowAddTutorialTask] = useState(false); |
| const [showAddWeeklyPractice, setShowAddWeeklyPractice] = useState(false); |
| const [showAddTranslationBrief, setShowAddTranslationBrief] = useState(false); |
| const [editingUser, setEditingUser] = useState<User | null>(null); |
| const [editingExample, setEditingExample] = useState<PracticeExample | null>(null); |
| const [editingTutorialTask, setEditingTutorialTask] = useState<TutorialTask | null>(null); |
| const [editingWeeklyPractice, setEditingWeeklyPractice] = useState<WeeklyPractice | null>(null); |
| const [newUser, setNewUser] = useState({ name: '', displayName: '', email: '', role: 'student' }); |
| const [newExample, setNewExample] = useState({ |
| title: '', |
| content: '', |
| sourceLanguage: 'English', |
| sourceCulture: 'American', |
| difficulty: 'intermediate' |
| }); |
| const [newTutorialTask, setNewTutorialTask] = useState({ |
| title: '', |
| content: '', |
| sourceLanguage: 'English', |
| sourceCulture: 'American', |
| weekNumber: 1, |
| difficulty: 'intermediate' |
| }); |
| const [newWeeklyPractice, setNewWeeklyPractice] = useState({ |
| title: '', |
| content: '', |
| sourceLanguage: 'English', |
| sourceCulture: 'American', |
| weekNumber: 1, |
| difficulty: 'intermediate' |
| }); |
| const [newTranslationBrief, setNewTranslationBrief] = useState({ |
| weekNumber: 1, |
| translationBrief: '', |
| type: 'tutorial' |
| }); |
| const navigate = useNavigate(); |
|
|
| useEffect(() => { |
| const userData = localStorage.getItem('user'); |
| if (userData) { |
| const user = JSON.parse(userData); |
| setUser(user); |
| |
| |
| if (user.role !== 'admin') { |
| navigate('/dashboard'); |
| return; |
| } |
| } else { |
| navigate('/login'); |
| return; |
| } |
| setLoading(false); |
| }, [navigate]); |
|
|
| const fetchAdminStats = useCallback(async () => { |
| try { |
| setStatsLoading(true); |
| const response = await api.get('/api/auth/admin/stats'); |
| setStats(response.data.stats); |
| } catch (error) { |
| console.error('Failed to fetch admin stats:', error); |
| } finally { |
| setStatsLoading(false); |
| } |
| }, []); |
|
|
| const fetchPracticeExamples = useCallback(async () => { |
| try { |
| setExamplesLoading(true); |
| const response = await api.get('/api/auth/admin/practice-examples'); |
| setExamples(response.data.examples); |
| } catch (error) { |
| console.error('Failed to fetch practice examples:', error); |
| } finally { |
| setExamplesLoading(false); |
| } |
| }, []); |
|
|
| const fetchUsers = useCallback(async () => { |
| try { |
| setUsersLoading(true); |
| const response = await api.get('/api/auth/admin/users'); |
| setUsers(response.data.users); |
| } catch (error) { |
| console.error('Failed to fetch users:', error); |
| } finally { |
| setUsersLoading(false); |
| } |
| }, []); |
|
|
| const fetchTutorialTasks = useCallback(async () => { |
| try { |
| setTutorialTasksLoading(true); |
| const response = await api.get('/api/auth/admin/tutorial-tasks'); |
| setTutorialTasks(response.data.tutorialTasks); |
| } catch (error) { |
| console.error('Failed to fetch tutorial tasks:', error); |
| } finally { |
| setTutorialTasksLoading(false); |
| } |
| }, []); |
|
|
| const fetchWeeklyPractice = useCallback(async () => { |
| try { |
| setWeeklyPracticeLoading(true); |
| const response = await api.get('/api/auth/admin/weekly-practice'); |
| setWeeklyPractice(response.data.weeklyPractice); |
| } catch (error) { |
| console.error('Failed to fetch weekly practice:', error); |
| } finally { |
| setWeeklyPracticeLoading(false); |
| } |
| }, []); |
|
|
| useEffect(() => { |
| if (user?.role === 'admin') { |
| fetchAdminStats(); |
| fetchPracticeExamples(); |
| fetchTutorialTasks(); |
| fetchWeeklyPractice(); |
| fetchUsers(); |
| (async () => { |
| try { |
| setLoadingSummary(true); |
| const resp = await api.get(`/api/auth/admin/login-summary?sinceMs=${summaryRange}&role=${summaryRole}`); |
| setLoginSummary(resp.data?.sessions || []); |
| } catch (e) { |
| setLoginSummary([]); |
| } finally { |
| setLoadingSummary(false); |
| } |
| })(); |
| } |
| }, [user, fetchAdminStats, fetchPracticeExamples, fetchTutorialTasks, fetchWeeklyPractice, fetchUsers, summaryRange, summaryRole]); |
|
|
| const addUser = async () => { |
| try { |
| const response = await api.post('/api/auth/admin/users', newUser); |
| setNewUser({ name: '', displayName: '', email: '', role: 'student' }); |
| setShowAddUser(false); |
| await fetchUsers(); |
| alert('User added successfully!'); |
| } catch (error) { |
| console.error('Failed to add user:', error); |
| alert('Failed to add user'); |
| } |
| }; |
|
|
| const updateUser = async (email: string, updates: Partial<User>) => { |
| try { |
| await api.put(`/api/auth/admin/users/${email}`, updates); |
| setEditingUser(null); |
| await fetchUsers(); |
| |
| try { |
| const cur = localStorage.getItem('user'); |
| if (cur) { |
| const curObj = JSON.parse(cur); |
| if (curObj?.email === email) { |
| const next = { ...curObj, ...(updates.name ? { name: updates.name } : {}), ...(updates.displayName !== undefined ? { displayName: updates.displayName } : {}) }; |
| localStorage.setItem('user', JSON.stringify(next)); |
| } |
| } |
| } catch {} |
| alert('User updated successfully!'); |
| } catch (error) { |
| console.error('Failed to update user:', error); |
| alert('Failed to update user'); |
| } |
| }; |
|
|
| const deleteUser = async (email: string) => { |
| if (!window.confirm('Are you sure you want to delete this user?')) return; |
| |
| try { |
| await api.delete(`/api/auth/admin/users/${email}`); |
| await fetchUsers(); |
| alert('User deleted successfully!'); |
| } catch (error) { |
| console.error('Failed to delete user:', error); |
| alert('Failed to delete user'); |
| } |
| }; |
|
|
| const addExample = async () => { |
| try { |
| await api.post('/api/auth/admin/practice-examples', newExample); |
| setNewExample({ |
| title: '', |
| content: '', |
| sourceLanguage: 'English', |
| sourceCulture: 'American', |
| difficulty: 'intermediate' |
| }); |
| setShowAddExample(false); |
| await fetchPracticeExamples(); |
| alert('Example added successfully!'); |
| } catch (error) { |
| console.error('Failed to add example:', error); |
| alert('Failed to add example'); |
| } |
| }; |
|
|
| const updateExample = async (id: string, updates: Partial<PracticeExample>) => { |
| try { |
| await api.put(`/api/auth/admin/practice-examples/${id}`, updates); |
| setEditingExample(null); |
| await fetchPracticeExamples(); |
| alert('Example updated successfully!'); |
| } catch (error) { |
| console.error('Failed to update example:', error); |
| alert('Failed to update example'); |
| } |
| }; |
|
|
| const deleteExample = async (id: string) => { |
| if (!window.confirm('Are you sure you want to delete this example?')) return; |
| |
| try { |
| await api.delete(`/api/auth/admin/practice-examples/${id}`); |
| await fetchPracticeExamples(); |
| alert('Example deleted successfully!'); |
| } catch (error) { |
| console.error('Failed to delete example:', error); |
| alert('Failed to delete example'); |
| } |
| }; |
|
|
| |
| const addTutorialTask = async () => { |
| try { |
| await api.post('/api/auth/admin/tutorial-tasks', newTutorialTask); |
| setNewTutorialTask({ |
| title: '', |
| content: '', |
| sourceLanguage: 'English', |
| sourceCulture: 'American', |
| weekNumber: 1, |
| difficulty: 'intermediate' |
| }); |
| setShowAddTutorialTask(false); |
| await fetchTutorialTasks(); |
| alert('Tutorial task added successfully!'); |
| } catch (error) { |
| console.error('Failed to add tutorial task:', error); |
| alert('Failed to add tutorial task'); |
| } |
| }; |
|
|
| const updateTutorialTask = async (id: string, updates: Partial<TutorialTask>) => { |
| try { |
| await api.put(`/api/auth/admin/tutorial-tasks/${id}`, updates); |
| setEditingTutorialTask(null); |
| await fetchTutorialTasks(); |
| alert('Tutorial task updated successfully!'); |
| } catch (error) { |
| console.error('Failed to update tutorial task:', error); |
| alert('Failed to update tutorial task'); |
| } |
| }; |
|
|
| const deleteTutorialTask = async (id: string) => { |
| if (!window.confirm('Are you sure you want to delete this tutorial task?')) return; |
| |
| try { |
| await api.delete(`/api/auth/admin/tutorial-tasks/${id}`); |
| await fetchTutorialTasks(); |
| alert('Tutorial task deleted successfully!'); |
| } catch (error) { |
| console.error('Failed to delete tutorial task:', error); |
| alert('Failed to delete tutorial task'); |
| } |
| }; |
|
|
| |
| const addWeeklyPractice = async () => { |
| try { |
| await api.post('/api/auth/admin/weekly-practice', newWeeklyPractice); |
| setNewWeeklyPractice({ |
| title: '', |
| content: '', |
| sourceLanguage: 'English', |
| sourceCulture: 'American', |
| weekNumber: 1, |
| difficulty: 'intermediate' |
| }); |
| setShowAddWeeklyPractice(false); |
| await fetchWeeklyPractice(); |
| alert('Weekly practice added successfully!'); |
| } catch (error) { |
| console.error('Failed to add weekly practice:', error); |
| alert('Failed to add weekly practice'); |
| } |
| }; |
|
|
| const updateWeeklyPractice = async (id: string, updates: Partial<WeeklyPractice>) => { |
| try { |
| await api.put(`/api/auth/admin/weekly-practice/${id}`, updates); |
| setEditingWeeklyPractice(null); |
| await fetchWeeklyPractice(); |
| alert('Weekly practice updated successfully!'); |
| } catch (error) { |
| console.error('Failed to update weekly practice:', error); |
| alert('Failed to update weekly practice'); |
| } |
| }; |
|
|
| const deleteWeeklyPractice = async (id: string) => { |
| if (!window.confirm('Are you sure you want to delete this weekly practice?')) return; |
| |
| try { |
| await api.delete(`/api/auth/admin/weekly-practice/${id}`); |
| await fetchWeeklyPractice(); |
| alert('Weekly practice deleted successfully!'); |
| } catch (error) { |
| console.error('Failed to delete weekly practice:', error); |
| alert('Failed to delete weekly practice'); |
| } |
| }; |
|
|
| const addTranslationBrief = async () => { |
| try { |
| await api.post('/api/auth/admin/translation-brief', newTranslationBrief); |
| setShowAddTranslationBrief(false); |
| setNewTranslationBrief({ |
| weekNumber: 1, |
| translationBrief: '', |
| type: 'tutorial' |
| }); |
| alert('Translation brief added successfully!'); |
| } catch (error) { |
| console.error('Failed to add translation brief:', error); |
| alert('Failed to add translation brief'); |
| } |
| }; |
|
|
| const handleLogout = () => { |
| localStorage.removeItem('token'); |
| localStorage.removeItem('user'); |
| window.location.href = '/'; |
| }; |
|
|
| if (loading) { |
| return ( |
| <div className="min-h-screen flex items-center justify-center"> |
| <div className="animate-spin rounded-full h-12 w-12 border-b-2 border-indigo-600"></div> |
| </div> |
| ); |
| } |
|
|
| if (!user || user.role !== 'admin') { |
| return null; |
| } |
|
|
| return ( |
| <div className="px-4 sm:px-6 lg:px-8"> |
| <div className="mb-8"> |
| <div className="flex justify-between items-center"> |
| <div> |
| <div className="flex items-center"> |
| <img src="/icons/manage.svg" alt="Manage" className="h-8 w-8 mr-3" /> |
| <h1 className="text-2xl font-bold text-gray-900">Manage</h1> |
| </div> |
| <p className="mt-2 text-gray-600">Admin panel for system management</p> |
| </div> |
| <div className="flex items-center space-x-4"> |
| <span className="text-sm text-gray-500"> |
| Admin • {user.email} |
| </span> |
| <div className="flex items-center bg-gray-100 rounded-md p-1 text-xs"> |
| <button |
| onClick={() => { |
| try { localStorage.setItem('viewMode', 'admin'); } catch {} |
| window.dispatchEvent(new CustomEvent('view-mode-change', { detail: 'admin' } as any)); |
| setViewMode('admin'); |
| }} |
| className={`px-2 py-1 rounded-sm font-medium ${viewMode === 'admin' ? 'bg-white text-gray-900 ring-1 ring-gray-300' : 'text-gray-700 hover:bg-white'}`} |
| > |
| Admin view |
| </button> |
| <button |
| onClick={() => { |
| try { localStorage.setItem('viewMode', 'student'); } catch {} |
| window.dispatchEvent(new CustomEvent('view-mode-change', { detail: 'student' } as any)); |
| setViewMode('student'); |
| }} |
| className={`px-2 py-1 rounded-sm font-medium ${viewMode === 'student' ? 'bg-white text-gray-900 ring-1 ring-gray-300' : 'text-gray-700 hover:bg-white'}`} |
| > |
| Student view |
| </button> |
| </div> |
| <button |
| onClick={handleLogout} |
| className="bg-red-600 hover:bg-red-700 text-white px-4 py-2 rounded-md text-sm font-medium" |
| > |
| Logout |
| </button> |
| </div> |
| </div> |
| </div> |
| |
| {/* Login Summary removed as requested */} |
| |
| {/* Admin Management Sections */} |
| <div className="grid grid-cols-1 gap-6 mb-8"> |
| {/* User Management */} |
| <div className="bg-white rounded-lg shadow p-6"> |
| <div className="flex items-center mb-4"> |
| <img src="/icons/user management.png" alt="User Management" className="mr-3" style={{ width: '2.4rem', height: '2.4rem' }} /> |
| <h2 className="text-lg font-medium text-gray-900">User Management</h2> |
| </div> |
| <p className="text-gray-600 mb-4"> |
| Manage student accounts, roles, and permissions. |
| </p> |
| <div className="space-y-2 mb-4"> |
| <div className="flex items-center text-sm text-gray-500"> |
| <div className="w-2 h-2 bg-purple-600 rounded-full mr-3"></div> |
| {usersLoading ? 'Loading users...' : `${users.length} registered users`} |
| </div> |
| <div className="flex items-center text-sm text-gray-500"> |
| <div className="w-2 h-2 bg-purple-600 rounded-full mr-3"></div> |
| {users.filter(u => u.role === 'admin').length} admin users |
| </div> |
| <div className="flex items-center text-sm text-gray-500"> |
| <div className="w-2 h-2 bg-purple-600 rounded-full mr-3"></div> |
| {users.filter(u => u.role === 'student').length} student users |
| </div> |
| </div> |
| <div className="space-y-2"> |
| <button |
| onClick={() => setShowAddUser(!showAddUser)} |
| className="bg-purple-600 hover:bg-purple-700 text-white px-4 py-2 rounded-md text-sm font-medium" |
| > |
| {showAddUser ? 'Cancel' : 'Add User'} |
| </button> |
| <button |
| onClick={fetchUsers} |
| className="bg-gray-600 hover:bg-gray-700 text-white px-4 py-2 rounded-md text-sm font-medium ml-2" |
| > |
| Refresh |
| </button> |
| </div> |
| |
| {/* Add User Form */} |
| {showAddUser && ( |
| <div className="mt-4 p-4 bg-gray-50 rounded-md"> |
| <h4 className="text-sm font-medium text-gray-900 mb-3">Add New User:</h4> |
| <div className="space-y-3"> |
| <input |
| type="text" |
| placeholder="Name" |
| value={newUser.name} |
| onChange={(e) => setNewUser({...newUser, name: e.target.value})} |
| className="w-full px-3 py-2 border border-gray-300 rounded-md" |
| /> |
| <input |
| type="text" |
| placeholder="Display Name (optional)" |
| value={newUser.displayName} |
| onChange={(e) => setNewUser({...newUser, displayName: e.target.value})} |
| className="w-full px-3 py-2 border border-gray-300 rounded-md" |
| /> |
| <input |
| type="email" |
| placeholder="Email" |
| value={newUser.email} |
| onChange={(e) => setNewUser({...newUser, email: e.target.value})} |
| className="w-full px-3 py-2 border border-gray-300 rounded-md" |
| /> |
| <select |
| value={newUser.role} |
| onChange={(e) => setNewUser({...newUser, role: e.target.value})} |
| className="w-full px-3 py-2 border border-gray-300 rounded-md" |
| > |
| <option value="student">Student</option> |
| <option value="admin">Admin</option> |
| </select> |
| <button |
| onClick={addUser} |
| className="bg-green-600 hover:bg-green-700 text-white px-4 py-2 rounded-md text-sm font-medium" |
| > |
| Add User |
| </button> |
| </div> |
| </div> |
| )} |
| |
| {/* Users List */} |
| {users.length > 0 && ( |
| <div className="mt-4 max-h-60 overflow-y-auto"> |
| <h4 className="text-sm font-medium text-gray-900 mb-2">Registered Users:</h4> |
| <div className="space-y-2"> |
| {users.map((user) => ( |
| <div key={user.email} className="bg-gray-50 p-3 rounded-md"> |
| <div className="flex justify-between items-center"> |
| <div className="flex-1"> |
| <p className="text-sm font-medium text-gray-900 flex items-center"> |
| {user.online && ( |
| <span className="inline-block w-2 h-2 bg-green-500 rounded-full mr-2" aria-label="online" /> |
| )} |
| {user.name} |
| </p> |
| <p className="text-xs text-gray-600">{user.email}</p> |
| </div> |
| <div className="flex items-center space-x-2"> |
| <span className={`text-xs px-2 py-1 rounded ${ |
| user.role === 'admin' |
| ? 'bg-red-100 text-red-800' |
| : 'bg-green-100 text-green-800' |
| }`}> |
| {user.role} |
| </span> |
| <button |
| onClick={() => setEditingUser(user)} |
| className="text-blue-600 hover:text-blue-800 text-xs" |
| > |
| Edit |
| </button> |
| {user.email !== 'hongchang.yu@monash.edu' && ( |
| <button |
| onClick={() => deleteUser(user.email)} |
| className="text-red-600 hover:text-red-800 text-xs" |
| > |
| Delete |
| </button> |
| )} |
| </div> |
| </div> |
| </div> |
| ))} |
| </div> |
| </div> |
| )} |
| |
| {/* Edit User Modal */} |
| {editingUser && ( |
| <div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50"> |
| <div className="bg-white p-6 rounded-lg max-w-md w-full mx-4"> |
| <h3 className="text-lg font-medium text-gray-900 mb-4">Edit User</h3> |
| <div className="space-y-3"> |
| <input |
| type="text" |
| placeholder="Name" |
| value={editingUser.name} |
| onChange={(e) => setEditingUser({...editingUser, name: e.target.value})} |
| className="w-full px-3 py-2 border border-gray-300 rounded-md" |
| /> |
| <input |
| type="text" |
| placeholder="Preferred name (on-screen)" |
| value={editingUser.displayName || ''} |
| onChange={(e) => setEditingUser({...editingUser, displayName: e.target.value})} |
| className="w-full px-3 py-2 border border-gray-300 rounded-md" |
| /> |
| <input |
| type="email" |
| placeholder="Email" |
| value={editingUser.email} |
| disabled |
| className="w-full px-3 py-2 border border-gray-300 rounded-md bg-gray-100" |
| /> |
| <select |
| value={editingUser.role} |
| onChange={(e) => setEditingUser({...editingUser, role: e.target.value})} |
| className="w-full px-3 py-2 border border-gray-300 rounded-md" |
| > |
| <option value="student">Student</option> |
| <option value="admin">Admin</option> |
| </select> |
| <div className="flex space-x-2"> |
| <button |
| onClick={() => updateUser(editingUser.email, editingUser)} |
| className="bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded-md text-sm font-medium" |
| > |
| Update |
| </button> |
| <button |
| onClick={() => setEditingUser(null)} |
| className="bg-gray-600 hover:bg-gray-700 text-white px-4 py-2 rounded-md text-sm font-medium" |
| > |
| Cancel |
| </button> |
| </div> |
| </div> |
| </div> |
| </div> |
| )} |
| </div> |
| |
| {/* Content Management removed */} |
| |
| {/* Tutorial Tasks Management */} |
| <div className="bg-white rounded-lg shadow p-6 mb-6"> |
| <div className="flex items-center mb-4"> |
| <img src="/icons/Tutorial Tasks Management.png" alt="Tutorial Tasks Management" className="mr-3" style={{ width: '2.4rem', height: '2.4rem' }} /> |
| <h2 className="text-lg font-medium text-gray-900">Tutorial Tasks Management</h2> |
| </div> |
| <p className="text-gray-600 mb-4"> |
| Manage tutorial tasks for each week. |
| </p> |
| <div className="space-y-2 mb-4"> |
| <div className="flex items-center text-sm text-gray-500"> |
| <div className="w-2 h-2 bg-green-600 rounded-full mr-3"></div> |
| {tutorialTasksLoading ? 'Loading tutorial tasks...' : `${tutorialTasks.length} tutorial tasks`} |
| </div> |
| <div className="flex items-center text-sm text-gray-500"> |
| <div className="w-2 h-2 bg-green-600 rounded-full mr-3"></div> |
| Edit existing tutorial tasks |
| </div> |
| </div> |
| <div className="space-y-2"> |
| <button |
| onClick={() => setShowAddTutorialTask(!showAddTutorialTask)} |
| className="bg-green-600 hover:bg-green-700 text-white px-4 py-2 rounded-md text-sm font-medium" |
| > |
| {showAddTutorialTask ? 'Cancel' : 'Add Tutorial Task'} |
| </button> |
| <button |
| onClick={() => setShowAddTranslationBrief(!showAddTranslationBrief)} |
| className="bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded-md text-sm font-medium ml-2" |
| > |
| {showAddTranslationBrief ? 'Cancel' : 'Add Translation Brief'} |
| </button> |
| <button |
| onClick={fetchTutorialTasks} |
| className="bg-gray-600 hover:bg-gray-700 text-white px-4 py-2 rounded-md text-sm font-medium ml-2" |
| > |
| Refresh |
| </button> |
| </div> |
| |
| {/* Tutorial Tasks List */} |
| {tutorialTasks.length > 0 && ( |
| <div className="mt-4 max-h-60 overflow-y-auto"> |
| <h4 className="text-sm font-medium text-gray-900 mb-2">Current Tutorial Tasks:</h4> |
| <div className="space-y-2"> |
| {tutorialTasks.map((task) => ( |
| <div key={task._id} className="bg-gray-50 p-3 rounded-md"> |
| <div className="flex justify-between items-start"> |
| <div className="flex-1"> |
| <p className="text-sm font-medium text-gray-900">{task.title}</p> |
| <p className="text-xs text-gray-600 mt-1 line-clamp-2">{task.content}</p> |
| <div className="flex items-center mt-1 space-x-2"> |
| <span className="text-xs bg-green-100 text-green-800 px-2 py-1 rounded"> |
| Week {task.weekNumber} |
| </span> |
| <span className="text-xs bg-blue-100 text-blue-800 px-2 py-1 rounded"> |
| {task.sourceLanguage} |
| </span> |
| <span className="text-xs bg-purple-100 text-purple-800 px-2 py-1 rounded"> |
| {task.difficulty} |
| </span> |
| </div> |
| </div> |
| <div className="flex items-center space-x-2 ml-2"> |
| <button |
| onClick={() => setEditingTutorialTask(task)} |
| className="text-blue-600 hover:text-blue-800 text-xs" |
| > |
| Edit |
| </button> |
| <button |
| onClick={() => deleteTutorialTask(task._id)} |
| className="text-red-600 hover:text-red-800 text-xs" |
| > |
| Delete |
| </button> |
| </div> |
| </div> |
| </div> |
| ))} |
| </div> |
| </div> |
| )} |
| |
| {/* Add Tutorial Task Form */} |
| {showAddTutorialTask && ( |
| <div className="mt-4 p-4 bg-gray-50 rounded-md"> |
| <h4 className="text-sm font-medium text-gray-900 mb-3">Add New Tutorial Task:</h4> |
| <div className="space-y-3"> |
| <input |
| type="text" |
| placeholder="Title" |
| value={newTutorialTask.title} |
| onChange={(e) => setNewTutorialTask({...newTutorialTask, title: e.target.value})} |
| className="w-full px-3 py-2 border border-gray-300 rounded-md" |
| /> |
| <textarea |
| placeholder="Content" |
| value={newTutorialTask.content} |
| onChange={(e) => setNewTutorialTask({...newTutorialTask, content: e.target.value})} |
| className="w-full px-3 py-2 border border-gray-300 rounded-md" |
| rows={3} |
| /> |
| <div className="grid grid-cols-3 gap-2"> |
| <select |
| value={newTutorialTask.sourceLanguage} |
| onChange={(e) => setNewTutorialTask({...newTutorialTask, sourceLanguage: e.target.value})} |
| className="px-3 py-2 border border-gray-300 rounded-md" |
| > |
| <option value="English">English</option> |
| <option value="Chinese">Chinese</option> |
| </select> |
| <select |
| value={newTutorialTask.weekNumber} |
| onChange={(e) => setNewTutorialTask({...newTutorialTask, weekNumber: parseInt(e.target.value)})} |
| className="px-3 py-2 border border-gray-300 rounded-md" |
| > |
| {[1, 2, 3, 4, 5, 6].map(week => ( |
| <option key={week} value={week}>Week {week}</option> |
| ))} |
| </select> |
| <select |
| value={newTutorialTask.difficulty} |
| onChange={(e) => setNewTutorialTask({...newTutorialTask, difficulty: e.target.value})} |
| className="px-3 py-2 border border-gray-300 rounded-md" |
| > |
| <option value="beginner">Beginner</option> |
| <option value="intermediate">Intermediate</option> |
| <option value="advanced">Advanced</option> |
| </select> |
| </div> |
| <button |
| onClick={addTutorialTask} |
| className="bg-green-600 hover:bg-green-700 text-white px-4 py-2 rounded-md text-sm font-medium" |
| > |
| Add Tutorial Task |
| </button> |
| </div> |
| </div> |
| )} |
| |
| {/* Add Translation Brief Form */} |
| {showAddTranslationBrief && ( |
| <div className="mt-4 p-4 bg-gray-50 rounded-md"> |
| <h4 className="text-sm font-medium text-gray-900 mb-3">Add Translation Brief:</h4> |
| <div className="space-y-3"> |
| <select |
| value={newTranslationBrief.type} |
| onChange={(e) => setNewTranslationBrief({...newTranslationBrief, type: e.target.value})} |
| className="w-full px-3 py-2 border border-gray-300 rounded-md" |
| > |
| <option value="tutorial">Tutorial Tasks</option> |
| <option value="weekly-practice">Weekly Practice</option> |
| </select> |
| <select |
| value={newTranslationBrief.weekNumber} |
| onChange={(e) => setNewTranslationBrief({...newTranslationBrief, weekNumber: parseInt(e.target.value)})} |
| className="w-full px-3 py-2 border border-gray-300 rounded-md" |
| > |
| {[1, 2, 3, 4, 5, 6].map(week => ( |
| <option key={week} value={week}>Week {week}</option> |
| ))} |
| </select> |
| <textarea |
| placeholder="Translation Brief" |
| value={newTranslationBrief.translationBrief} |
| onChange={(e) => setNewTranslationBrief({...newTranslationBrief, translationBrief: e.target.value})} |
| className="w-full px-3 py-2 border border-gray-300 rounded-md" |
| rows={4} |
| /> |
| <button |
| onClick={addTranslationBrief} |
| className="bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded-md text-sm font-medium" |
| > |
| Add Translation Brief |
| </button> |
| </div> |
| </div> |
| )} |
| |
| {/* Edit Tutorial Task Modal */} |
| {editingTutorialTask && ( |
| <div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50"> |
| <div className="bg-white p-6 rounded-lg max-w-md w-full mx-4 max-h-96 overflow-y-auto"> |
| <h3 className="text-lg font-medium text-gray-900 mb-4">Edit Tutorial Task</h3> |
| <div className="space-y-3"> |
| <input |
| type="text" |
| placeholder="Title" |
| value={editingTutorialTask.title} |
| onChange={(e) => setEditingTutorialTask({...editingTutorialTask, title: e.target.value})} |
| className="w-full px-3 py-2 border border-gray-300 rounded-md" |
| /> |
| <textarea |
| placeholder="Content" |
| value={editingTutorialTask.content} |
| onChange={(e) => setEditingTutorialTask({...editingTutorialTask, content: e.target.value})} |
| className="w-full px-3 py-2 border border-gray-300 rounded-md" |
| rows={3} |
| /> |
| <div className="grid grid-cols-3 gap-2"> |
| <select |
| value={editingTutorialTask.sourceLanguage} |
| onChange={(e) => setEditingTutorialTask({...editingTutorialTask, sourceLanguage: e.target.value})} |
| className="px-3 py-2 border border-gray-300 rounded-md" |
| > |
| <option value="English">English</option> |
| <option value="Chinese">Chinese</option> |
| </select> |
| <select |
| value={editingTutorialTask.weekNumber} |
| onChange={(e) => setEditingTutorialTask({...editingTutorialTask, weekNumber: parseInt(e.target.value)})} |
| className="px-3 py-2 border border-gray-300 rounded-md" |
| > |
| {[1, 2, 3, 4, 5, 6].map(week => ( |
| <option key={week} value={week}>Week {week}</option> |
| ))} |
| </select> |
| <select |
| value={editingTutorialTask.difficulty} |
| onChange={(e) => setEditingTutorialTask({...editingTutorialTask, difficulty: e.target.value})} |
| className="px-3 py-2 border border-gray-300 rounded-md" |
| > |
| <option value="beginner">Beginner</option> |
| <option value="intermediate">Intermediate</option> |
| <option value="advanced">Advanced</option> |
| </select> |
| </div> |
| <div className="flex space-x-2"> |
| <button |
| onClick={() => updateTutorialTask(editingTutorialTask._id, editingTutorialTask)} |
| className="bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded-md text-sm font-medium" |
| > |
| Update |
| </button> |
| <button |
| onClick={() => setEditingTutorialTask(null)} |
| className="bg-gray-600 hover:bg-gray-700 text-white px-4 py-2 rounded-md text-sm font-medium" |
| > |
| Cancel |
| </button> |
| </div> |
| </div> |
| </div> |
| </div> |
| )} |
| </div> |
| |
| {/* Weekly Practice Management */} |
| <div className="bg-white rounded-lg shadow p-6 mb-6"> |
| <div className="flex items-center mb-4"> |
| <img src="/icons/Weekly Practice Management.png" alt="Weekly Practice Management" className="mr-3" style={{ width: '2.4rem', height: '2.4rem' }} /> |
| <h2 className="text-lg font-medium text-gray-900">Weekly Practice Management</h2> |
| </div> |
| <p className="text-gray-600 mb-4"> |
| Manage weekly practice tasks for each week. |
| </p> |
| <div className="space-y-2 mb-4"> |
| <div className="flex items-center text-sm text-gray-500"> |
| <div className="w-2 h-2 bg-purple-600 rounded-full mr-3"></div> |
| {weeklyPracticeLoading ? 'Loading weekly practice...' : `${weeklyPractice.length} weekly practice tasks`} |
| </div> |
| <div className="flex items-center text-sm text-gray-500"> |
| <div className="w-2 h-2 bg-purple-600 rounded-full mr-3"></div> |
| Edit existing weekly practice tasks |
| </div> |
| </div> |
| <div className="space-y-2"> |
| <button |
| onClick={() => setShowAddWeeklyPractice(!showAddWeeklyPractice)} |
| className="bg-purple-600 hover:bg-purple-700 text-white px-4 py-2 rounded-md text-sm font-medium" |
| > |
| {showAddWeeklyPractice ? 'Cancel' : 'Add Weekly Practice'} |
| </button> |
| <button |
| onClick={() => setShowAddTranslationBrief(!showAddTranslationBrief)} |
| className="bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded-md text-sm font-medium ml-2" |
| > |
| {showAddTranslationBrief ? 'Cancel' : 'Add Translation Brief'} |
| </button> |
| <button |
| onClick={fetchWeeklyPractice} |
| className="bg-gray-600 hover:bg-gray-700 text-white px-4 py-2 rounded-md text-sm font-medium ml-2" |
| > |
| Refresh |
| </button> |
| </div> |
| |
| {/* Weekly Practice List */} |
| {weeklyPractice.length > 0 && ( |
| <div className="mt-4 max-h-60 overflow-y-auto"> |
| <h4 className="text-sm font-medium text-gray-900 mb-2">Current Weekly Practice:</h4> |
| <div className="space-y-2"> |
| {weeklyPractice.map((practice) => ( |
| <div key={practice._id} className="bg-gray-50 p-3 rounded-md"> |
| <div className="flex justify-between items-start"> |
| <div className="flex-1"> |
| <p className="text-sm font-medium text-gray-900">{practice.title}</p> |
| <p className="text-xs text-gray-600 mt-1 line-clamp-2">{practice.content}</p> |
| <div className="flex items-center mt-1 space-x-2"> |
| <span className="text-xs bg-purple-100 text-purple-800 px-2 py-1 rounded"> |
| Week {practice.weekNumber} |
| </span> |
| <span className="text-xs bg-blue-100 text-blue-800 px-2 py-1 rounded"> |
| {practice.sourceLanguage} |
| </span> |
| <span className="text-xs bg-orange-100 text-orange-800 px-2 py-1 rounded"> |
| {practice.difficulty} |
| </span> |
| </div> |
| </div> |
| <div className="flex items-center space-x-2 ml-2"> |
| <button |
| onClick={() => setEditingWeeklyPractice(practice)} |
| className="text-blue-600 hover:text-blue-800 text-xs" |
| > |
| Edit |
| </button> |
| <button |
| onClick={() => deleteWeeklyPractice(practice._id)} |
| className="text-red-600 hover:text-red-800 text-xs" |
| > |
| Delete |
| </button> |
| </div> |
| </div> |
| </div> |
| ))} |
| </div> |
| </div> |
| )} |
| |
| {/* Add Weekly Practice Form */} |
| {showAddWeeklyPractice && ( |
| <div className="mt-4 p-4 bg-gray-50 rounded-md"> |
| <h4 className="text-sm font-medium text-gray-900 mb-3">Add New Weekly Practice:</h4> |
| <div className="space-y-3"> |
| <input |
| type="text" |
| placeholder="Title" |
| value={newWeeklyPractice.title} |
| onChange={(e) => setNewWeeklyPractice({...newWeeklyPractice, title: e.target.value})} |
| className="w-full px-3 py-2 border border-gray-300 rounded-md" |
| /> |
| <textarea |
| placeholder="Content" |
| value={newWeeklyPractice.content} |
| onChange={(e) => setNewWeeklyPractice({...newWeeklyPractice, content: e.target.value})} |
| className="w-full px-3 py-2 border border-gray-300 rounded-md" |
| rows={3} |
| /> |
| <div className="grid grid-cols-3 gap-2"> |
| <select |
| value={newWeeklyPractice.sourceLanguage} |
| onChange={(e) => setNewWeeklyPractice({...newWeeklyPractice, sourceLanguage: e.target.value})} |
| className="px-3 py-2 border border-gray-300 rounded-md" |
| > |
| <option value="English">English</option> |
| <option value="Chinese">Chinese</option> |
| </select> |
| <select |
| value={newWeeklyPractice.weekNumber} |
| onChange={(e) => setNewWeeklyPractice({...newWeeklyPractice, weekNumber: parseInt(e.target.value)})} |
| className="px-3 py-2 border border-gray-300 rounded-md" |
| > |
| {[1, 2, 3, 4, 5, 6].map(week => ( |
| <option key={week} value={week}>Week {week}</option> |
| ))} |
| </select> |
| <select |
| value={newWeeklyPractice.difficulty} |
| onChange={(e) => setNewWeeklyPractice({...newWeeklyPractice, difficulty: e.target.value})} |
| className="px-3 py-2 border border-gray-300 rounded-md" |
| > |
| <option value="beginner">Beginner</option> |
| <option value="intermediate">Intermediate</option> |
| <option value="advanced">Advanced</option> |
| </select> |
| </div> |
| <button |
| onClick={addWeeklyPractice} |
| className="bg-purple-600 hover:bg-purple-700 text-white px-4 py-2 rounded-md text-sm font-medium" |
| > |
| Add Weekly Practice |
| </button> |
| </div> |
| </div> |
| )} |
| |
| {/* Edit Weekly Practice Modal */} |
| {editingWeeklyPractice && ( |
| <div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50"> |
| <div className="bg-white p-6 rounded-lg max-w-md w-full mx-4 max-h-96 overflow-y-auto"> |
| <h3 className="text-lg font-medium text-gray-900 mb-4">Edit Weekly Practice</h3> |
| <div className="space-y-3"> |
| <input |
| type="text" |
| placeholder="Title" |
| value={editingWeeklyPractice.title} |
| onChange={(e) => setEditingWeeklyPractice({...editingWeeklyPractice, title: e.target.value})} |
| className="w-full px-3 py-2 border border-gray-300 rounded-md" |
| /> |
| <textarea |
| placeholder="Content" |
| value={editingWeeklyPractice.content} |
| onChange={(e) => setEditingWeeklyPractice({...editingWeeklyPractice, content: e.target.value})} |
| className="w-full px-3 py-2 border border-gray-300 rounded-md" |
| rows={3} |
| /> |
| <div className="grid grid-cols-3 gap-2"> |
| <select |
| value={editingWeeklyPractice.sourceLanguage} |
| onChange={(e) => setEditingWeeklyPractice({...editingWeeklyPractice, sourceLanguage: e.target.value})} |
| className="px-3 py-2 border border-gray-300 rounded-md" |
| > |
| <option value="English">English</option> |
| <option value="Chinese">Chinese</option> |
| </select> |
| <select |
| value={editingWeeklyPractice.weekNumber} |
| onChange={(e) => setEditingWeeklyPractice({...editingWeeklyPractice, weekNumber: parseInt(e.target.value)})} |
| className="px-3 py-2 border border-gray-300 rounded-md" |
| > |
| {[1, 2, 3, 4, 5, 6].map(week => ( |
| <option key={week} value={week}>Week {week}</option> |
| ))} |
| </select> |
| <select |
| value={editingWeeklyPractice.difficulty} |
| onChange={(e) => setEditingWeeklyPractice({...editingWeeklyPractice, difficulty: e.target.value})} |
| className="px-3 py-2 border border-gray-300 rounded-md" |
| > |
| <option value="beginner">Beginner</option> |
| <option value="intermediate">Intermediate</option> |
| <option value="advanced">Advanced</option> |
| </select> |
| </div> |
| <div className="flex space-x-2"> |
| <button |
| onClick={() => updateWeeklyPractice(editingWeeklyPractice._id, editingWeeklyPractice)} |
| className="bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded-md text-sm font-medium" |
| > |
| Update |
| </button> |
| <button |
| onClick={() => setEditingWeeklyPractice(null)} |
| className="bg-gray-600 hover:bg-gray-700 text-white px-4 py-2 rounded-md text-sm font-medium" |
| > |
| Cancel |
| </button> |
| </div> |
| </div> |
| </div> |
| </div> |
| )} |
| </div> |
| |
| </div> |
| |
| {/* Quick Stats */} |
| <div className="bg-white rounded-lg shadow p-6"> |
| <div className="flex items-center mb-4"> |
| <img src="/icons/Quick Stats.png" alt="Quick Stats" className="mr-3" style={{ width: '2.4rem', height: '2.4rem' }} /> |
| <h2 className="text-lg font-medium text-gray-900">Quick Stats</h2> |
| </div> |
| {statsLoading ? ( |
| <div className="grid grid-cols-1 md:grid-cols-4 gap-4"> |
| {[1, 2, 3, 4].map((i) => ( |
| <div key={i} className="bg-gray-50 p-4 rounded-lg animate-pulse"> |
| <div className="flex items-center"> |
| <div className="h-6 w-6 bg-gray-300 rounded mr-2"></div> |
| <div> |
| <div className="h-4 bg-gray-300 rounded w-20 mb-2"></div> |
| <div className="h-6 bg-gray-300 rounded w-8"></div> |
| </div> |
| </div> |
| </div> |
| ))} |
| </div> |
| ) : ( |
| <div className="grid grid-cols-1 md:grid-cols-4 gap-4"> |
| <div className="bg-purple-50 p-4 rounded-lg"> |
| <div className="flex items-center"> |
| <UserGroupIcon className="h-6 w-6 text-purple-600 mr-2" /> |
| <div> |
| <p className="text-sm text-purple-600">Total Users</p> |
| <p className="text-2xl font-bold text-purple-900">{stats?.totalUsers || 0}</p> |
| </div> |
| </div> |
| </div> |
| <div className="bg-blue-50 p-4 rounded-lg"> |
| <div className="flex items-center"> |
| <AcademicCapIcon className="h-6 w-6 text-blue-600 mr-2" /> |
| <div> |
| <p className="text-sm text-blue-600">Practice Examples</p> |
| <p className="text-2xl font-bold text-blue-900">{stats?.practiceExamples || 0}</p> |
| </div> |
| </div> |
| </div> |
| <div className="bg-green-50 p-4 rounded-lg"> |
| <div className="flex items-center"> |
| <DocumentTextIcon className="h-6 w-6 text-green-600 mr-2" /> |
| <div> |
| <p className="text-sm text-green-600">Submissions</p> |
| <p className="text-2xl font-bold text-green-900">{stats?.totalSubmissions || 0}</p> |
| </div> |
| </div> |
| </div> |
| <div className="bg-orange-50 p-4 rounded-lg"> |
| <div className="flex items-center"> |
| <ShieldCheckIcon className="h-6 w-6 text-orange-600 mr-2" /> |
| <div> |
| <p className="text-sm text-orange-600">Active Sessions</p> |
| <p className="text-2xl font-bold text-orange-900">{stats?.activeSessions || 0}</p> |
| </div> |
| </div> |
| </div> |
| </div> |
| )} |
| </div> |
| |
| <div className="mt-6"> |
| <Link |
| to="/dashboard" |
| className="bg-gray-600 hover:bg-gray-700 text-white px-4 py-2 rounded-md text-sm font-medium transition-colors duration-200" |
| > |
| Back to Dashboard |
| </Link> |
| </div> |
| </div> |
| ); |
| }; |
|
|
| export default Manage; |