import React, { useState, useEffect, useCallback } from 'react'; import { useNavigate } from 'react-router-dom'; import { api } from '../services/api'; import { AcademicCapIcon, DocumentTextIcon, CheckCircleIcon, ClockIcon, ArrowRightIcon, PencilIcon, XMarkIcon, CheckIcon, PlusIcon, TrashIcon, ArrowTopRightOnSquareIcon, ArrowsRightLeftIcon } from '@heroicons/react/24/outline'; import ReactDOM from 'react-dom'; interface TutorialTask { _id: string; content: string; weekNumber: number; translationBrief?: string; imageUrl?: string; imageAlt?: string; imageSize?: number; imageAlignment?: 'left' | 'center' | 'right' | 'portrait-split'; position?: number; } interface TutorialWeek { weekNumber: number; translationBrief?: string; tasks: TutorialTask[]; } interface UserSubmission { _id: string; transcreation: string; status: string; score: number; groupNumber?: number; isOwner?: boolean; userId?: { _id: string; username: string; }; voteCounts: { '1': number; '2': number; '3': number; }; } const TutorialTasks: React.FC = () => { const [selectedWeek, setSelectedWeek] = useState(() => { const savedWeek = localStorage.getItem('selectedTutorialWeek'); return savedWeek ? parseInt(savedWeek) : 1; }); const [isWeekTransitioning, setIsWeekTransitioning] = useState(false); const [tutorialTasks, setTutorialTasks] = useState([]); const [tutorialWeek, setTutorialWeek] = useState(null); const [userSubmissions, setUserSubmissions] = useState<{[key: string]: UserSubmission[]}>({}); const [sourceHeights, setSourceHeights] = useState<{[key: string]: number}>({}); // Move a task up or down by normalizing positions for the current visible list (weeks 4–6 only) const moveTask = async (taskId: string, direction: 'up' | 'down') => { try { const isAdmin = JSON.parse(localStorage.getItem('user') || '{}').role === 'admin'; if (!isAdmin || selectedWeek < 4) return; // Build ordered list for the current week from what is rendered const current = tutorialTasks.filter(t => t.weekNumber === selectedWeek); const index = current.findIndex(t => t._id === taskId); if (index === -1) return; const targetIndex = direction === 'up' ? index - 1 : index + 1; if (targetIndex < 0 || targetIndex >= current.length) return; // Normalize positions to 0..n-1 based on current screen order const normalized = current.map((t, i) => ({ id: t._id, position: i })); // Swap the two entries (calculate new positions first) const posA = normalized[index].position; const posB = normalized[targetIndex].position; normalized[index].position = posB; normalized[targetIndex].position = posA; // Optimistic UI update: swap in local state immediately for smoother UX const prevState = tutorialTasks; setTutorialTasks((prev) => { const next = [...prev]; // find actual indices in full list and swap their relative order by updating their position fields const aId = normalized[index].id; const bId = normalized[targetIndex].id; return next.map(item => { if (item._id === aId) return { ...item, position: posB } as any; if (item._id === bId) return { ...item, position: posA } as any; return item; }); }); // Send both updates in parallel; if either fails, revert then refetch await Promise.all([ api.put(`/api/auth/admin/tutorial-tasks/${normalized[index].id}/position`, { position: posB }), api.put(`/api/auth/admin/tutorial-tasks/${normalized[targetIndex].id}/position`, { position: posA }) ]); // Light refresh to ensure list order is consistent with server fetchTutorialTasks(false); } catch (error) { console.error('Reorder failed', error); } }; const [loading, setLoading] = useState(true); const [submitting, setSubmitting] = useState<{[key: string]: boolean}>({}); const [translationText, setTranslationText] = useState<{[key: string]: string}>({}); const [selectedGroups, setSelectedGroups] = useState<{[key: string]: number}>({}); const [expandedSections, setExpandedSections] = useState<{[key: string]: boolean}>({}); const GroupDocSection: React.FC<{ weekNumber: number }> = ({ weekNumber }) => { const [group, setGroup] = useState(() => { const saved = localStorage.getItem(`tutorial_group_${weekNumber}`); return saved ? parseInt(saved) : 1; }); const [creating, setCreating] = useState(false); const [docs, setDocs] = useState([]); const [urlInput, setUrlInput] = useState(''); const [errorMsg, setErrorMsg] = useState(''); const [copiedLink, setCopiedLink] = useState(''); const isAdmin = (JSON.parse(localStorage.getItem('user') || '{}').role === 'admin'); const CopySquaresIcon: React.FC<{ className?: string }> = ({ className }) => ( ); const loadDocs = useCallback(async () => { try { const resp = await api.get(`/api/docs/list?weekNumber=${weekNumber}`); setDocs(resp.data?.docs || []); } catch (e) { setDocs([]); } }, [weekNumber]); useEffect(() => { loadDocs(); }, [loadDocs]); const current = docs.find(d => d.groupNumber === group); const createDoc = async () => { try { setCreating(true); setErrorMsg(''); const url = urlInput.trim(); if (!url) { setErrorMsg('Please paste a Google Doc link.'); return; } const isValid = /docs\.google\.com\/document\/d\//.test(url); if (!isValid) { setErrorMsg('Provide a valid Google Doc link (docs.google.com/document/d/...).'); return; } await api.post('/api/docs/create', { weekNumber, groupNumber: group, docUrl: url }); await loadDocs(); setUrlInput(''); } finally { setCreating(false); } }; const copyLink = async (link: string) => { try { await navigator.clipboard.writeText(link); // Persist until refresh: do not clear after timeout setCopiedLink(link); } catch {} }; return (
{/* Top control row */} {isAdmin && (
)} {/* Replace / Add link inline editor */} {isAdmin && (
{ setUrlInput(e.target.value); setErrorMsg(''); }} placeholder="Paste Google Doc link (docs.google.com/document/d/...)" className="flex-1 px-3 py-2 border border-gray-300 rounded-md text-sm" />
{errorMsg &&
{errorMsg}
}
)} {/* All groups table */}
All Groups
{docs.length === 0 && ( )} {docs.map(d => ( ))}
Group Actions
No group docs yet.
Group {d.groupNumber}
Open {isAdmin && ( )}
); }; // Basic inline formatting helpers (bold/italic via simple markdown) for weeks 4–6 const renderFormatted = (text: string) => { const escape = (s: string) => s.replace(/&/g, '&').replace(//g, '>'); // Auto-linker: supports [label](url), plain URLs, and www.* without touching existing href attributes const html = escape(text) // Markdown-style links: [label](https://example.com) .replace(/\[([^\]]+?)\]\((https?:\/\/[^\s)]+)\)/g, '$1') // Plain URLs with protocol, avoid matching inside attributes (require a non-attribute preceding char) .replace(/(^|[^=\"'\/:])(https?:\/\/[^\s<]+)/g, (m, p1, url) => `${p1}${url}`) // www.* domains (prepend https://), also avoid attributes .replace(/(^|[^=\"'\/:])(www\.[^\s<]+)/g, (m, p1, host) => `${p1}${host}`) .replace(/\*\*(.+?)\*\*/g, '$1') .replace(/\*(.+?)\*/g, '$1') .replace(/\n/g, '
'); return ; }; const applyLinkFormat = ( elementId: string, current: string, setValue: (v: string) => void ) => { const urlInput = window.prompt('Enter URL (e.g., https://example.com):'); if (!urlInput) return; // Sanitize URL: ensure protocol, and strip accidental trailing quotes/attributes pasted from elsewhere let url = /^https?:\/\//i.test(urlInput) ? urlInput : `https://${urlInput}`; url = url.replace(/["'>)\s]+$/g, ''); const el = document.getElementById(elementId) as HTMLTextAreaElement | null; if (!el) { setValue(`${current}[link](${url})`); return; } const start = el.selectionStart ?? current.length; const end = el.selectionEnd ?? current.length; const before = current.slice(0, start); const selection = current.slice(start, end) || 'link'; const after = current.slice(end); setValue(`${before}[${selection}](${url})${after}`); // Restore focus and selection setTimeout(() => { el.focus(); const newPos = before.length + selection.length + 4 + url.length + 2; // rough caret placement try { el.setSelectionRange(newPos, newPos); } catch {} }, 0); }; const applyInlineFormat = ( elementId: string, current: string, setValue: (v: string) => void, wrapper: '**' | '*' ) => { const el = document.getElementById(elementId) as HTMLTextAreaElement | null; if (!el) { setValue(current + wrapper + wrapper); return; } const start = el.selectionStart ?? current.length; const end = el.selectionEnd ?? current.length; const before = current.slice(0, start); const selection = current.slice(start, end); const after = current.slice(end); const next = `${before}${wrapper}${selection}${wrapper}${after}`; setValue(next); setTimeout(() => { try { el.focus(); el.selectionStart = start + wrapper.length; el.selectionEnd = end + wrapper.length; } catch {} }, 0); }; const [editingTask, setEditingTask] = useState(null); const [editingBrief, setEditingBrief] = useState<{[key: number]: boolean}>({}); const [addingTask, setAddingTask] = useState(false); const [addingImage, setAddingImage] = useState(false); const [editForm, setEditForm] = useState<{ content: string; translationBrief: string; imageUrl: string; imageAlt: string; }>({ content: '', translationBrief: '', imageUrl: '', imageAlt: '' }); const [imageForm, setImageForm] = useState<{ imageUrl: string; imageAlt: string; imageSize: number; imageAlignment: 'left' | 'center' | 'right' | 'portrait-split'; }>({ imageUrl: '', imageAlt: '', imageSize: 200, imageAlignment: 'center' }); const [selectedFile, setSelectedFile] = useState(null); const [uploading, setUploading] = useState(false); const [saving, setSaving] = useState(false); const navigate = useNavigate(); const weeks = [1, 2, 3, 4, 5, 6]; const handleWeekChange = async (week: number) => { setIsWeekTransitioning(true); // Clear existing data first setTutorialTasks([]); setTutorialWeek(null); setUserSubmissions({}); // Update state and localStorage setSelectedWeek(week); localStorage.setItem('selectedTutorialWeek', week.toString()); // Force a small delay to ensure state is updated await new Promise(resolve => setTimeout(resolve, 50)); // Wait for actual content to load before ending animation try { // Fetch new week's data with the updated selectedWeek const response = await api.get(`/api/search/tutorial-tasks/${week}`); if (response.data) { const tasks = response.data; console.log('Fetched tasks for week', week, ':', tasks); // Ensure tasks are sorted by title const sortedTasks = tasks.sort((a: any, b: any) => { const aNum = parseInt(a.title?.match(/ST (\d+)/)?.[1] || '0'); const bNum = parseInt(b.title?.match(/ST (\d+)/)?.[1] || '0'); return aNum - bNum; }); setTutorialTasks(sortedTasks); // Use translation brief from tasks or localStorage let translationBrief = null; if (tasks.length > 0) { translationBrief = tasks[0].translationBrief; } else { const briefKey = `translationBrief_week_${week}`; translationBrief = localStorage.getItem(briefKey); } const tutorialWeekData: TutorialWeek = { weekNumber: week, translationBrief: translationBrief, tasks: tasks }; setTutorialWeek(tutorialWeekData); // Fetch user submissions for the new tasks await fetchUserSubmissions(tasks); } // Wait longer for DOM to update with new content (especially for Week 2) const delay = week === 2 ? 400 : 200; await new Promise(resolve => setTimeout(resolve, delay)); } catch (error) { console.error('Error loading week data:', error); } finally { // End transition after content is loaded and rendered setIsWeekTransitioning(false); } }; const handleFileUpload = async (file: File): Promise => { try { setUploading(true); // Convert file to data URL for display return new Promise((resolve, reject) => { const reader = new FileReader(); reader.onload = () => { const dataUrl = reader.result as string; console.log('File uploaded:', file.name, 'Size:', file.size); console.log('Generated data URL:', dataUrl.substring(0, 50) + '...'); resolve(dataUrl); }; reader.onerror = () => { console.error('Error reading file:', reader.error); reject(reader.error); }; reader.readAsDataURL(file); }); } catch (error) { console.error('Error uploading file:', error); throw error; } finally { setUploading(false); } }; const handleFileChange = (event: React.ChangeEvent) => { const file = event.target.files?.[0]; if (file) { setSelectedFile(file); } }; const toggleExpanded = (taskId: string) => { setExpandedSections(prev => ({ ...prev, [taskId]: !prev[taskId] })); }; const fetchUserSubmissions = useCallback(async (tasks: TutorialTask[]) => { try { const token = localStorage.getItem('token'); const user = localStorage.getItem('user'); if (!token || !user) { return; } const response = await api.get('/api/submissions/my-submissions'); if (response.data && response.data.submissions) { const data = response.data; const groupedSubmissions: {[key: string]: UserSubmission[]} = {}; // Initialize all tasks with empty arrays tasks.forEach(task => { groupedSubmissions[task._id] = []; }); // Then populate with actual submissions and mark ownership for edit visibility after refresh/login if (data.submissions && Array.isArray(data.submissions)) { data.submissions.forEach((submission: any) => { const sourceTextId = submission.sourceTextId?._id || submission.sourceTextId; if (sourceTextId && groupedSubmissions[sourceTextId]) { groupedSubmissions[sourceTextId].push({ ...submission, isOwner: true }); } }); } setUserSubmissions(groupedSubmissions); } } catch (error) { console.error('Error fetching user submissions:', error); } }, []); const fetchTutorialTasks = useCallback(async (showLoading = true) => { try { if (showLoading) { setLoading(true); } const token = localStorage.getItem('token'); const user = localStorage.getItem('user'); if (!token || !user) { navigate('/login'); return; } const response = await api.get(`/api/search/tutorial-tasks/${selectedWeek}`); if (response.data) { const tasks = response.data; console.log('Fetched tasks:', tasks); console.log('Tasks with images:', tasks.filter((task: TutorialTask) => task.imageUrl)); // Debug: Log each task's fields tasks.forEach((task: any, index: number) => { console.log(`Task ${index} fields:`, { _id: task._id, content: task.content, imageUrl: task.imageUrl, imageAlt: task.imageAlt, translationBrief: task.translationBrief, weekNumber: task.weekNumber, category: task.category }); console.log(`Task ${index} imageUrl:`, task.imageUrl); console.log(`Task ${index} translationBrief:`, task.translationBrief); }); // Ensure tasks are sorted by title to maintain correct order (Tutorial ST 1, Tutorial ST 2, etc.) const sortedTasks = tasks.sort((a: any, b: any) => { const aNum = parseInt(a.title?.match(/ST (\d+)/)?.[1] || '0'); const bNum = parseInt(b.title?.match(/ST (\d+)/)?.[1] || '0'); return aNum - bNum; }); setTutorialTasks(sortedTasks); // Use translation brief from tasks or localStorage let translationBrief = null; if (tasks.length > 0) { translationBrief = tasks[0].translationBrief; console.log('Translation brief from task:', translationBrief); } else { // Check localStorage for brief if no tasks exist const briefKey = `translationBrief_week_${selectedWeek}`; translationBrief = localStorage.getItem(briefKey); console.log('Translation brief from localStorage:', translationBrief); console.log('localStorage key:', briefKey); } console.log('Final translation brief:', translationBrief); const tutorialWeekData: TutorialWeek = { weekNumber: selectedWeek, translationBrief: translationBrief, tasks: tasks }; setTutorialWeek(tutorialWeekData); await fetchUserSubmissions(tasks); } else { console.error('Failed to fetch tutorial tasks'); } } catch (error) { console.error('Error fetching tutorial tasks:', error); } finally { if (showLoading) { setLoading(false); } } }, [selectedWeek, fetchUserSubmissions, navigate]); useEffect(() => { const user = localStorage.getItem('user'); if (!user) { navigate('/login'); return; } fetchTutorialTasks(); }, [fetchTutorialTasks, navigate]); // Listen for week reset events from page navigation useEffect(() => { const handleWeekReset = (event: CustomEvent) => { if (event.detail.page === 'tutorial-tasks') { console.log('Week reset event received for tutorial tasks'); setSelectedWeek(event.detail.week); localStorage.setItem('selectedTutorialWeek', event.detail.week.toString()); } }; window.addEventListener('weekReset', handleWeekReset as EventListener); return () => { window.removeEventListener('weekReset', handleWeekReset as EventListener); }; }, []); // Refresh submissions when user changes (after login/logout) useEffect(() => { const user = localStorage.getItem('user'); if (user && tutorialTasks.length > 0) { fetchUserSubmissions(tutorialTasks); } }, [tutorialTasks, fetchUserSubmissions]); const handleSubmitTranslation = async (taskId: string) => { if (!translationText[taskId]?.trim()) { return; } if (!selectedGroups[taskId]) { return; } try { setSubmitting({ ...submitting, [taskId]: true }); const user = JSON.parse(localStorage.getItem('user') || '{}'); const response = await api.post('/api/submissions', { sourceTextId: taskId, transcreation: translationText[taskId], groupNumber: selectedGroups[taskId], culturalAdaptations: [], username: user.name || 'Unknown' }); if (response.status >= 200 && response.status < 300) { const result = response.data; console.log('Submission created successfully:', result); setTranslationText({ ...translationText, [taskId]: '' }); setSelectedGroups({ ...selectedGroups, [taskId]: 0 }); await fetchUserSubmissions(tutorialTasks); } else { console.error('Failed to submit translation:', response.data); } } catch (error) { console.error('Error submitting translation:', error); } finally { setSubmitting({ ...submitting, [taskId]: false }); } }; const [editingSubmission, setEditingSubmission] = useState<{id: string, text: string} | null>(null); const [editSubmissionText, setEditSubmissionText] = useState(''); const handleEditSubmission = async (submissionId: string, currentText: string) => { setEditingSubmission({ id: submissionId, text: currentText }); setEditSubmissionText(currentText); }; const saveEditedSubmission = async () => { if (!editingSubmission || !editSubmissionText.trim()) return; try { const response = await api.put(`/api/submissions/${editingSubmission.id}`, { transcreation: editSubmissionText }); if (response.status >= 200 && response.status < 300) { setEditingSubmission(null); setEditSubmissionText(''); await fetchUserSubmissions(tutorialTasks); } else { console.error('Failed to update translation:', response.data); } } catch (error) { console.error('Error updating translation:', error); } }; const cancelEditSubmission = () => { setEditingSubmission(null); setEditSubmissionText(''); }; const handleDeleteSubmission = async (submissionId: string) => { try { const response = await api.delete(`/api/submissions/${submissionId}`); if (response.status === 200) { await fetchUserSubmissions(tutorialTasks); } else { } } catch (error) { console.error('Error deleting submission:', error); } }; const getStatusIcon = (status: string) => { switch (status) { case 'approved': return ; case 'pending': return ; default: return ; } }; const startEditing = (task: TutorialTask) => { setEditingTask(task._id); setEditForm({ content: task.content, translationBrief: task.translationBrief || '', imageUrl: task.imageUrl || '', imageAlt: task.imageAlt || '' }); }; const startEditingBrief = () => { setEditingBrief(prev => ({ ...prev, [selectedWeek]: true })); setEditForm({ content: '', translationBrief: tutorialWeek?.translationBrief || '', imageUrl: '', imageAlt: '' }); }; const startAddingBrief = () => { setEditingBrief(prev => ({ ...prev, [selectedWeek]: true })); setEditForm({ content: '', translationBrief: '', imageUrl: '', imageAlt: '' }); }; const removeBrief = async () => { try { setSaving(true); const token = localStorage.getItem('token'); const user = JSON.parse(localStorage.getItem('user') || '{}'); // Check if user is admin if (user.role !== 'admin') { return; } const response = await api.put(`/api/auth/admin/tutorial-brief/${selectedWeek}`, { translationBrief: '', weekNumber: selectedWeek }); if (response.status >= 200 && response.status < 300) { const briefKey = `translationBrief_week_${selectedWeek}`; localStorage.removeItem(briefKey); setEditForm((prev) => ({ ...prev, translationBrief: '' })); await fetchTutorialTasks(); } else { console.error('Failed to remove translation brief:', response.data); } } catch (error) { console.error('Failed to remove translation brief:', error); } finally { setSaving(false); } }; const cancelEditing = () => { setEditingTask(null); setEditingBrief(prev => ({ ...prev, [selectedWeek]: false })); setEditForm({ content: '', translationBrief: '', imageUrl: '', imageAlt: '' }); setSelectedFile(null); }; const saveTask = async () => { if (!editingTask) return; try { setSaving(true); const token = localStorage.getItem('token'); const user = JSON.parse(localStorage.getItem('user') || '{}'); // Check if user is admin if (user.role !== 'admin') { return; } const updateData = { ...editForm, weekNumber: selectedWeek }; console.log('Saving task with data:', updateData); const response = await api.put(`/api/auth/admin/tutorial-tasks/${editingTask}`, updateData); if (response.status >= 200 && response.status < 300) { await fetchTutorialTasks(false); setEditingTask(null); } else { console.error('Failed to update tutorial task:', response.data); } } catch (error) { console.error('Failed to update tutorial task:', error); } finally { setSaving(false); } }; const saveBrief = async () => { try { setSaving(true); const user = JSON.parse(localStorage.getItem('user') || '{}'); // Check if user is admin if (user.role !== 'admin') { return; } console.log('Saving brief for week:', selectedWeek); console.log('Brief content:', editForm.translationBrief); // Save brief by creating or updating the first task of the week if (tutorialTasks.length > 0) { const firstTask = tutorialTasks[0]; console.log('Updating first task with brief:', firstTask._id); const response = await api.put(`/api/auth/admin/tutorial-tasks/${firstTask._id}`, { ...firstTask, translationBrief: editForm.translationBrief, weekNumber: selectedWeek }); if (response.status >= 200 && response.status < 300) { console.log('Brief saved successfully'); // Optimistic UI update const briefKey = `translationBrief_week_${selectedWeek}`; localStorage.setItem(briefKey, editForm.translationBrief); setTutorialWeek(prev => prev ? { ...prev, translationBrief: editForm.translationBrief } : prev); setEditingBrief(prev => ({ ...prev, [selectedWeek]: false })); // Background refresh fetchTutorialTasks(false); } else { console.error('Failed to save brief:', response.data); } } else { // If no tasks exist, save the brief to localStorage console.log('No tasks available to save brief to - saving to localStorage'); const briefKey = `translationBrief_week_${selectedWeek}`; localStorage.setItem(briefKey, editForm.translationBrief); setEditingBrief(prev => ({ ...prev, [selectedWeek]: false })); } } catch (error) { console.error('Failed to update translation brief:', error); } finally { setSaving(false); } }; const startAddingTask = () => { setAddingTask(true); setEditForm({ content: '', translationBrief: '', imageUrl: '', imageAlt: '' }); }; const cancelAddingTask = () => { setAddingTask(false); setEditForm({ content: '', translationBrief: '', imageUrl: '', imageAlt: '' }); setSelectedFile(null); }; const startAddingImage = () => { setAddingImage(true); setImageForm({ imageUrl: '', imageAlt: '', imageSize: 200, imageAlignment: 'center' }); }; const cancelAddingImage = () => { setAddingImage(false); setImageForm({ imageUrl: '', imageAlt: '', imageSize: 200, imageAlignment: 'center' }); }; const saveNewTask = async () => { try { setSaving(true); const user = JSON.parse(localStorage.getItem('user') || '{}'); // Check if user is admin if (user.role !== 'admin') { return; } // Allow either content or imageUrl, but not both empty if (!editForm.content.trim() && !editForm.imageUrl.trim()) { return; } console.log('Saving new task for week:', selectedWeek); console.log('Task content:', editForm.content); console.log('Image URL:', editForm.imageUrl); console.log('Image Alt:', editForm.imageAlt); const taskData = { title: `Week ${selectedWeek} Tutorial Task`, content: editForm.content, sourceLanguage: 'English', weekNumber: selectedWeek, category: 'tutorial', imageUrl: editForm.imageUrl || null, imageAlt: editForm.imageAlt || null, // Add imageSize and imageAlignment for image-only content ...(editForm.imageUrl && !editForm.content.trim() && { imageSize: 200 }), ...(editForm.imageUrl && !editForm.content.trim() && { imageAlignment: 'center' }) }; console.log('Task data being sent:', JSON.stringify(taskData, null, 2)); console.log('Sending task data:', taskData); const response = await api.post('/api/auth/admin/tutorial-tasks', taskData); console.log('Task save response:', response.data); if (response.status >= 200 && response.status < 300) { console.log('Task saved successfully'); console.log('Saved task response:', response.data); console.log('Saved task response keys:', Object.keys(response.data || {})); console.log('Saved task response.task:', response.data?.task); console.log('Saved task response.task.imageUrl:', response.data?.task?.imageUrl); console.log('Saved task response.task.translationBrief:', response.data?.task?.translationBrief); await fetchTutorialTasks(false); setAddingTask(false); } else { console.error('Failed to add tutorial task:', response.data); } } catch (error) { console.error('Failed to add tutorial task:', error); } finally { setSaving(false); } }; const saveNewImage = async () => { try { setSaving(true); const user = JSON.parse(localStorage.getItem('user') || '{}'); // Check if user is admin if (user.role !== 'admin') { return; } if (!imageForm.imageUrl.trim()) { return; } const payload = { title: `Week ${selectedWeek} Image Task`, content: '', // Empty content for image-only task sourceLanguage: 'English', weekNumber: selectedWeek, category: 'tutorial', imageUrl: imageForm.imageUrl.trim(), imageAlt: imageForm.imageAlt.trim() || null, imageSize: imageForm.imageSize, imageAlignment: imageForm.imageAlignment }; console.log('Saving new image task with payload:', payload); const response = await api.post('/api/auth/admin/tutorial-tasks', payload); if (response.data) { console.log('Image task saved successfully:', response.data); await fetchTutorialTasks(false); setAddingImage(false); } else { console.error('Failed to save image task'); } } catch (error) { console.error('Failed to add image task:', error); } finally { setSaving(false); } }; const deleteTask = async (taskId: string) => { try { const token = localStorage.getItem('token'); const user = JSON.parse(localStorage.getItem('user') || '{}'); // Check if user is admin if (user.role !== 'admin') { return; } const response = await api.delete(`/api/auth/admin/tutorial-tasks/${taskId}`); if (response.status >= 200 && response.status < 300) { await fetchTutorialTasks(false); } else { console.error('Failed to delete tutorial task:', response.data); } } catch (error) { console.error('Failed to delete tutorial task:', error); } }; // Remove intrusive loading screen - just show content with loading state return (
{/* Header */}

Tutorial Tasks

Complete weekly tutorial tasks with your group to practice collaborative translation skills.

{/* Week Selector */}
{weeks.map((week) => ( ))}
{/* Week Transition Loading Spinner */} {isWeekTransitioning && (
Loading...
)} {!isWeekTransitioning && ( <> {/* Translation Brief - Shown once at the top */} {tutorialWeek && tutorialWeek.translationBrief ? (

Translation Brief

{JSON.parse(localStorage.getItem('user') || '{}').role === 'admin' && (
{editingBrief[selectedWeek] ? ( <> ) : ( <> )}
)}
{editingBrief[selectedWeek] ? (