import React, { useState, useEffect, useCallback } from 'react'; import { useNavigate } from 'react-router-dom'; import { api } from '../services/api'; import { BookOpenIcon, DocumentTextIcon, CheckCircleIcon, ClockIcon, ArrowRightIcon, PencilIcon, XMarkIcon, CheckIcon, PlusIcon, TrashIcon, PlayIcon, PauseIcon, ArrowLeftIcon } from '@heroicons/react/24/outline'; interface WeeklyPractice { _id: string; content: string; weekNumber: number; translationBrief?: string; imageUrl?: string; imageAlt?: string; imageSize?: number; imageAlignment?: 'left' | 'center' | 'right' | 'portrait-split'; } interface WeeklyPracticeWeek { weekNumber: number; translationBrief?: string; practices: WeeklyPractice[]; } interface UserSubmission { _id: string; transcreation: string; status: string; score: number; isOwner?: boolean; userId?: { _id: string; username: string; }; voteCounts: { '1': number; '2': number; '3': number; }; isAnonymous?: boolean; } // Subtitling interfaces interface SubtitleSegment { id: number; startTime: string; endTime: string; duration: string; sourceText: string; targetText?: string; isCurrent?: boolean; } interface VideoInfo { title: string; duration: string; totalSegments: number; currentSegment: number; } interface SubtitleSubmission { _id: string; username: string; chineseTranslation: string; submissionDate: string; isAnonymous: boolean; status?: string; notes?: string; } const WeeklyPractice: React.FC = () => { const [selectedWeek, setSelectedWeek] = useState(() => { const saved = localStorage.getItem('selectedWeeklyPracticeWeek'); return saved ? parseInt(saved) : 1; }); const [isWeekTransitioning, setIsWeekTransitioning] = useState(false); const [weeklyPractice, setWeeklyPractice] = useState([]); const [weeklyPracticeWeek, setWeeklyPracticeWeek] = useState(null); const [isWeekHidden, setIsWeekHidden] = useState(false); const [userSubmissions, setUserSubmissions] = useState<{[key: string]: UserSubmission[]}>({}); const [loading, setLoading] = useState(true); const [submitting, setSubmitting] = useState<{[key: string]: boolean}>({}); const [translationText, setTranslationText] = useState<{[key: string]: string}>({}); const [anonymousSubmissions, setAnonymousSubmissions] = useState<{[key: string]: boolean}>({}); const [expandedSections, setExpandedSections] = useState<{[key: string]: boolean}>({}); // Basic inline formatting helpers for weeks 4–6 (bold/italic via simple markdown) + links const renderFormatted = (text: string) => { const escape = (s: string) => s.replace(/&/g, '&').replace(//g, '>'); const html = escape(text) .replace(/\[([^\]]+?)\]\((https?:\/\/[^\s)]+)\)/g, '$1') .replace(/(^|[^=\"'\/:])(https?:\/\/[^\s<]+)/g, (m, p1, url) => `${p1}${url}`) .replace(/(^|[^=\"'\/:])(www\.[^\s<]+)/g, (m, p1, host) => `${p1}${host}`) .replace(/\*\*(.+?)\*\*/g, '$1') .replace(/\*(.+?)\*/g, '$1') .replace(/\n/g, '
'); return ; }; 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 applyLinkFormat = ( elementId: string, current: string, setValue: (v: string) => void ) => { const urlInput = window.prompt('Enter URL (e.g., https://example.com):'); if (!urlInput) return; 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}`); setTimeout(() => { try { el.focus(); el.setSelectionRange(end, end); } catch {} }, 0); }; // Fetch visibility for current week (admin use only) useEffect(() => { const loadVisibility = async () => { try { const base = (((api.defaults as any)?.baseURL as string) || '').replace(/\/$/, ''); const resp = await fetch(`${base}/api/admin/weeks/weekly-practice/${selectedWeek}/visibility`, { headers: { 'Authorization': localStorage.getItem('token') ? `Bearer ${localStorage.getItem('token')}` : '', 'user-role': 'admin' } }); const json = await resp.json().catch(() => ({})); setIsWeekHidden(!!json?.week?.hidden); } catch (e) { /* noop */ } }; loadVisibility(); }, [selectedWeek]); const [editingPractice, setEditingPractice] = useState(null); const [editingBrief, setEditingBrief] = useState<{[key: number]: boolean}>({}); const [addingPractice, setAddingPractice] = 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 [saving, setSaving] = useState(false); const [uploading, setUploading] = useState(false); const [uploadingSource, setUploadingSource] = useState(false); const [uploadingTranslation, setUploadingTranslation] = useState(false); const [weekFiles, setWeekFiles] = useState<{ source: any[]; translation: any[] }>({ source: [], translation: [] }); const [showAllTranslations, setShowAllTranslations] = useState(false); const navigate = useNavigate(); // Subtitling state for Week 2 const [currentSegment, setCurrentSegment] = useState(1); const [isPlaying, setIsPlaying] = useState(false); const [currentTime, setCurrentTime] = useState('00:00'); const [totalTime, setTotalTime] = useState('01:26'); const [subtitleText, setSubtitleText] = useState('Am I a bad person?'); const [targetText, setTargetText] = useState(''); const [characterCount, setCharacterCount] = useState(0); const [showTranslatedSubtitles, setShowTranslatedSubtitles] = useState(false); const [subtitleTranslations, setSubtitleTranslations] = useState<{[key: number]: string}>({}); const [editingTimeCodes, setEditingTimeCodes] = useState(false); const [editingSegment, setEditingSegment] = useState(null); const [currentVideoTime, setCurrentVideoTime] = useState(0); const [currentDisplayedSubtitle, setCurrentDisplayedSubtitle] = useState(''); // Submissions viewing state const [showSubmissions, setShowSubmissions] = useState(false); const [submissions, setSubmissions] = useState([]); const [loadingSubmissions, setLoadingSubmissions] = useState(false); const [submissionCount, setSubmissionCount] = useState(0); const weeks = [1, 2, 3, 4, 5]; const isAdmin = (() => { try { const viewMode = (localStorage.getItem('viewMode')||'auto'); const role = JSON.parse(localStorage.getItem('user')||'{}').role; return (viewMode !== 'student') && role === 'admin'; } catch { return false; } })(); const handleWeekChange = async (week: number) => { setIsWeekTransitioning(true); // Clear existing data immediately setWeeklyPractice([]); setWeeklyPracticeWeek(null); setUserSubmissions({}); localStorage.setItem('selectedWeeklyPracticeWeek', week.toString()); setSelectedWeek(week); try { // Explicitly fetch for the target week to avoid stale selectedWeek race await fetchWeeklyPracticeForWeek(week, false); const delay = week === 2 ? 400 : 200; await new Promise(resolve => setTimeout(resolve, delay)); } catch (error) { console.error('Error loading week data:', error); } finally { setIsWeekTransitioning(false); } }; // Authenticated file download helper const downloadWeekFile = async (fileId: string, fallbackName?: string) => { try { const response = await api.get(`/api/weekly-practice-files/${fileId}/download`, { responseType: 'blob' }); // Try to extract filename from Content-Disposition const disposition = (response.headers?.['content-disposition'] as string) || ''; let fileName = fallbackName || 'download'; try { const starMatch = disposition.match(/filename\*=UTF-8''([^;]+)/i); const plainMatch = disposition.match(/filename="([^"\\]+)"/i); if (starMatch && starMatch[1]) fileName = decodeURIComponent(starMatch[1]); else if (plainMatch && plainMatch[1]) fileName = plainMatch[1]; } catch {} const blobUrl = window.URL.createObjectURL(new Blob([response.data])); const link = document.createElement('a'); link.href = blobUrl; link.download = fileName; document.body.appendChild(link); link.click(); document.body.removeChild(link); window.URL.revokeObjectURL(blobUrl); } catch (e) { console.error('Failed to download file', e); } }; // Subtitle data state - will be loaded from database const [subtitleSegments, setSubtitleSegments] = useState([]); const [loadingSubtitles, setLoadingSubtitles] = useState(true); const videoInfo: VideoInfo = { title: 'Nike \'Winning Isn\'t for Everyone\'', duration: '1:26', totalSegments: 26, currentSegment: 1 }; // Subtitling functions const handleSegmentClick = (segmentId: number) => { setCurrentSegment(segmentId); const segment = subtitleSegments.find(s => s.id === segmentId); if (segment) { setSubtitleText(segment.sourceText); setTargetText(subtitleTranslations[segmentId] || ''); setCharacterCount(subtitleTranslations[segmentId]?.length || 0); // Seek video to segment start time and set end time const video = document.getElementById('nike-video') as HTMLVideoElement; if (video) { const startTime = parseTimeToSeconds(segment.startTime); const endTime = parseTimeToSeconds(segment.endTime); video.currentTime = startTime; video.play(); // Stop video at segment end const checkEndTime = () => { if (video.currentTime >= endTime) { video.pause(); video.removeEventListener('timeupdate', checkEndTime); } }; video.addEventListener('timeupdate', checkEndTime); } } // Fetch submissions for this segment if submissions are shown if (showSubmissions) { fetchSubmissionsForSegment(segmentId); } }; const parseTimeToSeconds = (timeString: string): number => { // Parse time format like "00:00:03,000" to seconds const parts = timeString.split(':'); const secondsAndMs = parts[2].split(','); const seconds = parseInt(secondsAndMs[0]); const milliseconds = parseInt(secondsAndMs[1] || '0'); const minutes = parseInt(parts[1]); const hours = parseInt(parts[0]); return hours * 3600 + minutes * 60 + seconds + milliseconds / 1000; }; const formatSubtitleForDisplay = (text: string): string => { // Format subtitle text for on-screen display with 60-character limit per line const maxCharsPerLine = 60; // First, replace any \n characters with spaces to normalize the text const normalizedText = text.replace(/\n/g, ' '); // If normalized text is 42 characters or less, keep it on a single line if (normalizedText.length <= maxCharsPerLine) { return normalizedText; } // For longer text, wrap at word boundaries const words = normalizedText.split(' '); const lines: string[] = []; let currentLine = ''; words.forEach(word => { // If adding this word would exceed the limit if ((currentLine + ' ' + word).length > maxCharsPerLine) { // If current line is not empty, save it and start a new line if (currentLine) { lines.push(currentLine); currentLine = word; } else { // If current line is empty and word is too long, split the word if (word.length > maxCharsPerLine) { // Split long word if necessary const firstPart = word.substring(0, maxCharsPerLine); const secondPart = word.substring(maxCharsPerLine); lines.push(firstPart); currentLine = secondPart; } else { currentLine = word; } } } else { // Add word to current line currentLine += (currentLine ? ' ' : '') + word; } }); if (currentLine) { lines.push(currentLine); } return lines.join('\n'); }; const updateSubtitleDisplay = (currentTime: number) => { console.log('πŸ”§ Updating subtitle display at time:', currentTime); console.log('πŸ”§ Available segments:', subtitleSegments.length); // Find the current segment with 0.5s buffer const currentSegment = subtitleSegments.find(segment => { const startTime = parseTimeToSeconds(segment.startTime); const endTime = parseTimeToSeconds(segment.endTime); // Add 0.5s buffer to keep subtitle visible longer const isInRange = currentTime >= startTime && currentTime <= (endTime + 0.5); console.log(`πŸ”§ Segment ${segment.id}: ${startTime}s - ${endTime + 0.5}s, in range: ${isInRange}`); return isInRange; }); if (currentSegment) { let subtitleText = currentSegment.sourceText; if (showTranslatedSubtitles && subtitleTranslations[currentSegment.id]) { subtitleText = subtitleTranslations[currentSegment.id]; } // Format the subtitle text for proper display const formattedText = formatSubtitleForDisplay(subtitleText); console.log('πŸ”§ Setting subtitle text:', formattedText); setCurrentDisplayedSubtitle(formattedText); } else { // Clear subtitle when no segment is found console.log('πŸ”§ No segment found for current time, clearing subtitle'); setCurrentDisplayedSubtitle(''); } }; // Video time tracking effect React.useEffect(() => { const video = document.getElementById('nike-video') as HTMLVideoElement; if (!video) { console.log('πŸ”§ Video element not found'); return; } console.log('πŸ”§ Setting up video time tracking'); console.log('πŸ”§ Current subtitle segments:', subtitleSegments.length); const handleTimeUpdate = () => { const currentTime = video.currentTime; setCurrentVideoTime(currentTime); updateSubtitleDisplay(currentTime); }; video.addEventListener('timeupdate', handleTimeUpdate); return () => video.removeEventListener('timeupdate', handleTimeUpdate); }, [showTranslatedSubtitles, subtitleTranslations, subtitleSegments]); const handlePlayPause = () => { setIsPlaying(!isPlaying); }; const handleTargetTextChange = (text: string) => { setTargetText(text); setCharacterCount(text.length); }; const handleSaveAndNext = () => { // Save current translation console.log('Saving translation for segment:', currentSegment, targetText); // Move to next segment if (currentSegment < subtitleSegments.length) { handleSegmentClick(currentSegment + 1); } }; const handlePrevious = () => { if (currentSegment > 1) { handleSegmentClick(currentSegment - 1); } }; const handlePreviewTranslation = () => { // Toggle between original and translated subtitles const newShowTranslated = !showTranslatedSubtitles; setShowTranslatedSubtitles(newShowTranslated); // Immediately update the displayed subtitle if (currentSegment > 0 && subtitleSegments[currentSegment - 1]) { const segment = subtitleSegments[currentSegment - 1]; let subtitleText = segment.sourceText; if (newShowTranslated && subtitleTranslations[currentSegment]) { subtitleText = subtitleTranslations[currentSegment]; } // Format the subtitle text for proper display const formattedText = formatSubtitleForDisplay(subtitleText); setCurrentDisplayedSubtitle(formattedText); console.log('πŸ”„ Preview toggle:', newShowTranslated ? 'Translated' : 'Original', formattedText); } }; const [saveStatus, setSaveStatus] = useState('Save'); const handleSaveTranslation = async () => { try { setSaveStatus('Saving...'); // Save to local state immediately setSubtitleTranslations(prev => ({ ...prev, [currentSegment]: targetText })); console.log('πŸ’Ύ Saving translation for segment:', currentSegment, targetText); // Try to save to database try { const response = await api.put(`/api/subtitles/translate/${currentSegment}`, { chineseTranslation: targetText }); if (response.data.success) { console.log('βœ… Translation saved to database successfully'); setSaveStatus('Saved!'); setTimeout(() => setSaveStatus('Save'), 2000); } } catch (dbError: any) { console.warn('⚠️ Database save failed, but local changes applied:', dbError.message); setSaveStatus('Saved'); setTimeout(() => setSaveStatus('Save'), 2000); } // Also save as a submission try { const user = JSON.parse(localStorage.getItem('user') || '{}'); console.log('πŸ”§ Submitting translation as submission for user:', user.username); // Make sure we have a valid username const username = user.username || user.name || 'Unknown User'; console.log('πŸ”§ Using username for submission:', username); const submissionResponse = await api.post('/api/subtitle-submissions/submit', { segmentId: currentSegment, chineseTranslation: targetText, isAnonymous: false, weekNumber: 2, username: username }); if (submissionResponse.data.success) { console.log('βœ… Translation submitted successfully'); // Refresh submissions if they're currently shown if (showSubmissions) { await fetchSubmissionsForSegment(currentSegment); } } } catch (submissionError: any) { console.error('❌ Submission save failed:', submissionError.response?.data || submissionError.message); } // Keep the translation in the input field after saving // setTargetText(''); // Removed - keep translation visible // setCharacterCount(0); // Removed - keep character count } catch (error: any) { console.error('❌ Error saving translation:', error); setSaveStatus('Error'); setTimeout(() => setSaveStatus('Save'), 2000); } }; const handleEditTimeCode = (segmentId: number) => { setEditingSegment(segmentId); }; const handleSaveTimeCode = async (segmentId: number, startTime: string, endTime: string) => { try { // Check if user is admin (for UI purposes) const user = JSON.parse(localStorage.getItem('user') || '{}'); console.log('πŸ”§ Debug - User:', user); if (user.role !== 'admin') { console.error('❌ User is not admin'); return; } // Calculate duration const startSeconds = parseTimeToSeconds(startTime); const endSeconds = parseTimeToSeconds(endTime); const durationMs = (endSeconds - startSeconds) * 1000; const duration = `${Math.floor(durationMs / 1000)}s ${durationMs % 1000}ms`; console.log('πŸ”§ Updating time codes for segment:', segmentId); console.log('πŸ”§ New times:', { startTime, endTime, duration }); // Update local state immediately for UI responsiveness setSubtitleSegments(prev => prev.map(segment => segment.id === segmentId ? { ...segment, startTime, endTime, duration } : segment )); // Try to update database try { const response = await api.put(`/api/subtitles/update/${segmentId}`, { startTime, endTime, duration, reason: 'Time code update' }); if (response.data.success) { console.log('βœ… Time codes updated in database successfully'); } } catch (dbError: any) { console.warn('⚠️ Database update failed, but local changes applied:', dbError.message); } } catch (error: any) { console.error('❌ Error updating time codes:', error); } setEditingSegment(null); }; // Fetch subtitles from database const fetchSubtitles = async () => { try { setLoadingSubtitles(true); console.log('πŸ”§ Fetching subtitles from database...'); const response = await api.get('/api/subtitles/all'); console.log('πŸ”§ Subtitle response:', response.data); if (response.data.success) { const subtitles = response.data.data.map((subtitle: any) => ({ id: subtitle.segmentId, startTime: subtitle.startTime, endTime: subtitle.endTime, duration: subtitle.duration, sourceText: subtitle.englishText })); console.log('πŸ”§ Processed subtitles:', subtitles.length); setSubtitleSegments(subtitles); // Load existing translations from database const translations: { [key: number]: string } = {}; response.data.data.forEach((subtitle: any) => { if (subtitle.chineseTranslation) { translations[subtitle.segmentId] = subtitle.chineseTranslation; } }); console.log('πŸ”§ Loaded translations:', Object.keys(translations).length); setSubtitleTranslations(translations); } else { console.error('❌ Subtitle fetch failed:', response.data); } } catch (error) { console.error('❌ Error fetching subtitles:', error); // Fallback to hardcoded data if database fails console.log('πŸ”§ Using fallback subtitle data...'); const fallbackSubtitles = [ { id: 1, startTime: '00:00:00,640', endTime: '00:00:02,400', duration: '1s 760ms', sourceText: 'Am I a bad person?' }, { id: 2, startTime: '00:00:06,320', endTime: '00:00:07,860', duration: '1s 540ms', sourceText: 'Tell me. Am I?' }, { id: 3, startTime: '00:00:08,480', endTime: '00:00:09,740', duration: '1s 260ms', sourceText: 'I\'m single minded.' }, { id: 4, startTime: '00:00:10,570', endTime: '00:00:11,780', duration: '1s 210ms', sourceText: 'I\'m deceptive.' }, { id: 5, startTime: '00:00:12,050', endTime: '00:00:13,490', duration: '1s 440ms', sourceText: 'I\'m obsessive.' }, { id: 6, startTime: '00:00:13,780', endTime: '00:00:14,910', duration: '1s 130ms', sourceText: 'I\'m selfish.' }, { id: 7, startTime: '00:00:15,120', endTime: '00:00:17,200', duration: '2s 80ms', sourceText: 'Does that make me a bad person?' }, { id: 8, startTime: '00:00:18,010', endTime: '00:00:19,660', duration: '1s 650ms', sourceText: 'Am I a bad person?' }, { id: 9, startTime: '00:00:20,870', endTime: '00:00:21,870', duration: '1s 0ms', sourceText: 'Am I?' }, { id: 10, startTime: '00:00:23,120', endTime: '00:00:24,390', duration: '1s 270ms', sourceText: 'I have no empathy.' }, { id: 11, startTime: '00:00:25,540', endTime: '00:00:27,170', duration: '1s 630ms', sourceText: 'I don\'t respect you.' }, { id: 12, startTime: '00:00:28,550', endTime: '00:00:29,880', duration: '1s 330ms', sourceText: 'I\'m never satisfied.' }, { id: 13, startTime: '00:00:30,440', endTime: '00:00:33,180', duration: '2s 740ms', sourceText: 'I have an obsession with power.' }, { id: 14, startTime: '00:00:37,850', endTime: '00:00:38,950', duration: '1s 100ms', sourceText: 'I\'m irrational.' }, { id: 15, startTime: '00:00:39,930', endTime: '00:00:41,520', duration: '1s 590ms', sourceText: 'I have zero remorse.' }, { id: 16, startTime: '00:00:41,770', endTime: '00:00:43,900', duration: '2s 130ms', sourceText: 'I have no sense of compassion.' }, { id: 17, startTime: '00:00:44,480', endTime: '00:00:46,650', duration: '2s 170ms', sourceText: 'I\'m delusional. I\'m maniacal.' }, { id: 18, startTime: '00:00:46,960', endTime: '00:00:48,980', duration: '2s 20ms', sourceText: 'You think I\'m a bad person?' }, { id: 19, startTime: '00:00:49,320', endTime: '00:00:52,700', duration: '3s 380ms', sourceText: 'Tell me. Tell me. Tell me.\nTell me. Am I?' }, { id: 20, startTime: '00:00:52,990', endTime: '00:00:55,136', duration: '2s 146ms', sourceText: 'I think I\'m better than everyone else.' }, { id: 21, startTime: '00:00:55,170', endTime: '00:00:57,820', duration: '2s 650ms', sourceText: 'I want to take what\'s yours\nand never give it back.' }, { id: 22, startTime: '00:00:57,840', endTime: '00:01:00,640', duration: '2s 800ms', sourceText: 'What\'s mine is mine\nand what\'s yours is mine.' }, { id: 23, startTime: '00:01:06,920', endTime: '00:01:08,290', duration: '1s 370ms', sourceText: 'Am I a bad person?' }, { id: 24, startTime: '00:01:08,840', endTime: '00:01:10,420', duration: '1s 580ms', sourceText: 'Tell me. Am I?' }, { id: 25, startTime: '00:01:21,500', endTime: '00:01:23,650', duration: '2s 150ms', sourceText: 'Does that make me a bad person?' }, { id: 26, startTime: '00:01:25,060', endTime: '00:01:26,900', duration: '1s 840ms', sourceText: 'Tell me. Does it?' }, ]; setSubtitleSegments(fallbackSubtitles); } finally { setLoadingSubtitles(false); } }; // Fetch submissions for a specific segment const fetchSubmissionsForSegment = async (segmentId: number) => { try { setLoadingSubmissions(true); console.log('πŸ”§ Fetching submissions for segment:', segmentId); // Add timeout to prevent long loading states const timeoutPromise = new Promise((_, reject) => setTimeout(() => reject(new Error('Request timeout')), 5000) ); const response = await Promise.race([ api.get(`/api/subtitle-submissions/segment/${segmentId}?week=2`), timeoutPromise ]) as any; if (response.data.success) { setSubmissions(response.data.data); setSubmissionCount(response.data.count); console.log('βœ… Fetched submissions:', response.data.count); } else { console.error('❌ Failed to fetch submissions:', response.data); setSubmissions([]); setSubmissionCount(0); } } catch (error) { console.error('❌ Error fetching submissions:', error); setSubmissions([]); setSubmissionCount(0); } finally { setLoadingSubmissions(false); } }; // Toggle submissions panel const toggleSubmissions = async () => { if (!showSubmissions) { // Show submissions and fetch data setShowSubmissions(true); await fetchSubmissionsForSegment(currentSegment); } else { // Hide submissions setShowSubmissions(false); } }; // Delete subtitle submission (admin only) const handleDeleteSubtitleSubmission = async (submissionId: string) => { try { console.log('πŸ—‘οΈ Deleting subtitle submission:', submissionId); // Show loading state on the delete button const deleteButton = document.querySelector(`[data-submission-id="${submissionId}"]`) as HTMLButtonElement; if (deleteButton) { deleteButton.innerHTML = '⏳'; deleteButton.disabled = true; } const response = await api.delete(`/api/subtitle-submissions/${submissionId}`); if (response.data.success) { console.log('βœ… Subtitle submission deleted successfully'); // Refresh submissions if they're currently shown if (showSubmissions) { await fetchSubmissionsForSegment(currentSegment); } } } catch (error: any) { console.error('❌ Error deleting subtitle submission:', error.response?.data || error.message); } finally { // Restore delete button const deleteButton = document.querySelector(`[data-submission-id="${submissionId}"]`) as HTMLButtonElement; if (deleteButton) { deleteButton.innerHTML = 'πŸ—‘οΈ'; deleteButton.disabled = false; } } }; // Load subtitles when component mounts React.useEffect(() => { if (selectedWeek === 2) { fetchSubtitles(); } }, [selectedWeek]); // Debug: Log subtitle segments when they change React.useEffect(() => { console.log('πŸ”§ Subtitle segments updated:', subtitleSegments.length); if (subtitleSegments.length > 0) { console.log('πŸ”§ First segment:', subtitleSegments[0]); } }, [subtitleSegments]); const getSegmentStatus = (segmentId: number) => { if (segmentId === currentSegment) return 'current'; return 'pending'; }; const getSegmentButtonClass = (segmentId: number) => { const status = getSegmentStatus(segmentId); const isSaved = subtitleTranslations[segmentId]; switch (status) { case 'current': return 'bg-pink-600 text-white'; default: return isSaved ? 'bg-pink-100 text-pink-700 border border-pink-300' : 'bg-gray-100 text-gray-700 border border-gray-300'; } }; const handleFileUpload = async (file: File): Promise => { try { setUploading(true); 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 toggleExpanded = (practiceId: string) => { setExpandedSections(prev => ({ ...prev, [practiceId]: !prev[practiceId] })); }; const fetchUserSubmissions = useCallback(async (practice: WeeklyPractice[]) => { try { const response = await api.get('/api/submissions/my-submissions'); if (response.data) { const data = response.data; const groupedSubmissions: {[key: string]: UserSubmission[]} = {}; // Initialize all practices with empty arrays practice.forEach(practice => { groupedSubmissions[practice._id] = []; }); // Then populate with actual submissions and mark ownership for edit visibility after refresh/login practice.forEach(practice => { const practiceSubmissions = data.submissions .filter((sub: any) => sub.sourceTextId && sub.sourceTextId._id === practice._id) .map((sub: any) => ({ ...sub, isOwner: true })); if (practiceSubmissions.length > 0) { groupedSubmissions[practice._id] = practiceSubmissions; } }); setUserSubmissions(groupedSubmissions); } } catch (error) { console.error('Error fetching user submissions:', error); } }, []); // Fetch for current selectedWeek const fetchWeeklyPractice = useCallback(async (showLoading = true) => { try { if (showLoading) { setLoading(true); } const response = await api.get(`/api/search/weekly-practice/${selectedWeek}`); if (response.data) { const practices = response.data; setWeeklyPractice(practices); // Organize practices into week structure if (practices.length > 0) { let translationBrief = practices[0].translationBrief; // Fallback to explicit week brief API if missing if (!translationBrief) { try { const br = await api.get(`/api/search/weekly-practice/${selectedWeek}/brief`); translationBrief = br.data?.translationBrief || ''; } catch {} } const weeklyPracticeWeekData: WeeklyPracticeWeek = { weekNumber: selectedWeek, translationBrief: translationBrief, practices: practices }; setWeeklyPracticeWeek(weeklyPracticeWeekData); } else { // No practices: still fetch brief, but do not surface brief for students when hidden (server already returns empty when hidden) try { const br = await api.get(`/api/search/weekly-practice/${selectedWeek}/brief`); const viewMode = (localStorage.getItem('viewMode')||'auto'); const role = (JSON.parse(localStorage.getItem('user')||'{}').role); const isAdminView = (viewMode !== 'student') && role === 'admin'; setWeeklyPracticeWeek({ weekNumber: selectedWeek, translationBrief: isAdminView ? (br.data?.translationBrief || '') : (br.data?.translationBrief || ''), practices: [] }); } catch { setWeeklyPracticeWeek(null); } } await fetchUserSubmissions(practices); } else { console.error('Failed to fetch weekly practice'); } } catch (error) { console.error('Error fetching weekly practice:', error); } finally { if (showLoading) { setLoading(false); } } }, [selectedWeek, fetchUserSubmissions]); // Fetch files list for current week (source + translation) const fetchWeekFiles = useCallback(async () => { try { const [src, trn] = await Promise.all([ api.get(`/api/weekly-practice-files/week/${selectedWeek}?type=source`), api.get(`/api/weekly-practice-files/week/${selectedWeek}?type=translation`) ]); setWeekFiles({ source: src.data.files || [], translation: trn.data.files || [] }); } catch (e) { console.error('Failed to load week files', e); setWeekFiles({ source: [], translation: [] }); } }, [selectedWeek]); // Fetch for a specific week (avoids stale week race when changing weeks) const fetchWeeklyPracticeForWeek = useCallback(async (week: number, showLoading = true) => { try { if (showLoading) { setLoading(true); } const response = await api.get(`/api/search/weekly-practice/${week}`); if (response.data) { const practices = response.data; setWeeklyPractice(practices); if (practices.length > 0) { const translationBrief = practices[0].translationBrief; const weeklyPracticeWeekData: WeeklyPracticeWeek = { weekNumber: week, translationBrief: translationBrief, practices: practices }; setWeeklyPracticeWeek(weeklyPracticeWeekData); } else { setWeeklyPracticeWeek(null); } await fetchUserSubmissions(practices); } else { console.error('Failed to fetch weekly practice'); } } catch (error) { console.error('Error fetching weekly practice (explicit week):', error); } finally { if (showLoading) { setLoading(false); } } }, [fetchUserSubmissions]); useEffect(() => { const user = localStorage.getItem('user'); if (!user) { navigate('/login'); return; } fetchWeeklyPractice(); fetchWeekFiles(); }, [fetchWeeklyPractice, fetchWeekFiles, navigate]); // Upload handlers for week files const uploadSourceFile = async (file: File, description?: string) => { try { setUploadingSource(true); const form = new FormData(); form.append('file', file); if (description) form.append('description', description); await api.post(`/api/weekly-practice-files/week/${selectedWeek}/source`, form, { headers: { 'Content-Type': 'multipart/form-data' } }); await fetchWeekFiles(); } finally { setUploadingSource(false); } }; const uploadTranslationFile = async (file: File, description?: string, sourceFileId?: string) => { try { setUploadingTranslation(true); const form = new FormData(); form.append('file', file); if (description) form.append('description', description); if (sourceFileId) form.append('sourceFileId', sourceFileId); await api.post(`/api/weekly-practice-files/week/${selectedWeek}/translation`, form, { headers: { 'Content-Type': 'multipart/form-data' } }); await fetchWeekFiles(); } finally { setUploadingTranslation(false); } }; // Listen for week reset events from page navigation useEffect(() => { const handleWeekReset = (event: CustomEvent) => { if (event.detail.page === 'weekly-practice') { console.log('Week reset event received for weekly practice'); setSelectedWeek(event.detail.week); localStorage.setItem('selectedWeeklyPracticeWeek', 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 && weeklyPractice.length > 0) { fetchUserSubmissions(weeklyPractice); } }, [weeklyPractice, fetchUserSubmissions]); const handleSubmitTranslation = async (practiceId: string) => { if (!translationText[practiceId]?.trim()) { alert('Please provide a translation'); return; } try { setSubmitting({ ...submitting, [practiceId]: true }); const token = localStorage.getItem('token'); const user = JSON.parse(localStorage.getItem('user') || '{}'); const response = await api.post('/api/submissions', { sourceTextId: practiceId, transcreation: translationText[practiceId], culturalAdaptations: [], isAnonymous: anonymousSubmissions[practiceId] || false, username: user.name || 'Unknown' }); if (response.status === 201 || response.status === 200) { console.log('βœ… Translation submitted successfully'); setTranslationText({ ...translationText, [practiceId]: '' }); await fetchUserSubmissions(weeklyPractice); } else { console.error('❌ Submission failed:', response.data); } } catch (error) { console.error('Error submitting translation:', error); } finally { setSubmitting({ ...submitting, [practiceId]: 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 token = localStorage.getItem('token'); const response = await api.put(`/api/submissions/${editingSubmission.id}`, { transcreation: editSubmissionText }); if (response.status === 200) { console.log('βœ… Translation updated successfully'); setEditingSubmission(null); setEditSubmissionText(''); await fetchUserSubmissions(weeklyPractice); } else { console.error('❌ Update failed:', 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(weeklyPractice); } 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 = (practice: WeeklyPractice) => { setEditingPractice(practice._id); setEditForm({ content: practice.content, translationBrief: '', imageUrl: practice.imageUrl || '', imageAlt: practice.imageAlt || '' }); }; const startEditingBrief = () => { setEditingBrief(prev => ({ ...prev, [selectedWeek]: true })); setEditForm({ content: '', translationBrief: weeklyPracticeWeek?.translationBrief || '', imageUrl: '', imageAlt: '' }); }; const startAddingBrief = () => { setEditingBrief(prev => ({ ...prev, [selectedWeek]: true })); setEditForm({ content: '', translationBrief: '', imageUrl: '', imageAlt: '' }); }; const removeBrief = async () => { try { setSaving(true); 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/weekly-brief/${selectedWeek}`, { translationBrief: '', weekNumber: selectedWeek }); if (response.data) { await fetchWeeklyPractice(false); } else { console.error('Failed to remove translation brief'); } } catch (error) { console.error('Failed to remove translation brief:', error); } finally { setSaving(false); } }; const cancelEditing = () => { setEditingPractice(null); setEditingBrief(prev => ({ ...prev, [selectedWeek]: false })); setEditForm({ content: '', translationBrief: '', imageUrl: '', imageAlt: '' }); }; const savePractice = async () => { if (!editingPractice) return; try { setSaving(true); const user = JSON.parse(localStorage.getItem('user') || '{}'); // Check if user is admin if (user.role !== 'admin') { return; } const payload = { ...editForm, weekNumber: selectedWeek, imageUrl: editForm.imageUrl || null, imageAlt: editForm.imageAlt || null }; console.log('Saving practice with payload:', payload); const response = await api.put(`/api/auth/admin/weekly-practice/${editingPractice}`, payload); if (response.data) { console.log('Practice updated successfully:', response.data); await fetchWeeklyPractice(false); setEditingPractice(null); } else { console.error('Failed to update practice'); } } catch (error) { console.error('Failed to update weekly practice:', 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; } const response = await api.put(`/api/auth/admin/weekly-brief/${selectedWeek}`, { translationBrief: editForm.translationBrief, weekNumber: selectedWeek }); if (response.data) { // Optimistic UI update setWeeklyPracticeWeek(prev => prev ? { ...prev, translationBrief: editForm.translationBrief } : prev); setEditingBrief(prev => ({ ...prev, [selectedWeek]: false })); // Background refresh fetchWeeklyPractice(false); } else { console.error('Failed to update translation brief'); } } catch (error) { console.error('Failed to update translation brief:', error); } finally { setSaving(false); } }; const startAddingPractice = () => { setAddingPractice(true); setEditForm({ content: '', translationBrief: '', imageUrl: '', imageAlt: '' }); }; const cancelAddingPractice = () => { setAddingPractice(false); setEditForm({ content: '', translationBrief: '', imageUrl: '', imageAlt: '' }); }; const startAddingImage = () => { setAddingImage(true); setImageForm({ imageUrl: '', imageAlt: '', imageSize: 200, imageAlignment: 'center' }); }; const cancelAddingImage = () => { setAddingImage(false); setImageForm({ imageUrl: '', imageAlt: '', imageSize: 200, imageAlignment: 'center' }); }; const saveNewPractice = 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 image, but not both empty console.log('πŸ”§ Validation check:', { content: editForm.content.trim(), imageUrl: editForm.imageUrl.trim(), contentLength: editForm.content.trim().length, imageUrlLength: editForm.imageUrl.trim().length }); if (!editForm.content.trim() && !editForm.imageUrl.trim()) { console.log('❌ Both content and image are empty'); return; } const payload = { title: `Week ${selectedWeek} Weekly Practice`, content: editForm.content.trim() || (editForm.imageUrl.trim() ? 'Image-based practice' : ''), sourceLanguage: 'English', weekNumber: selectedWeek, category: 'weekly-practice', imageUrl: editForm.imageUrl.trim() || null, imageAlt: editForm.imageAlt.trim() || null, // Add imageSize and imageAlignment for image-only content ...(editForm.imageUrl.trim() && !editForm.content.trim() && { imageSize: 200 }), ...(editForm.imageUrl.trim() && !editForm.content.trim() && { imageAlignment: 'center' }) }; console.log('πŸ”§ Saving new practice with payload:', payload); console.log('πŸ”§ Week number:', selectedWeek); console.log('πŸ”§ Image URL length:', editForm.imageUrl.trim().length); const response = await api.post('/api/auth/admin/weekly-practice', payload); if (response.data) { console.log('Practice saved successfully:', response.data); await fetchWeeklyPractice(false); setAddingPractice(false); } else { console.error('Failed to save practice'); } } catch (error) { console.error('Failed to add weekly practice:', 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 Practice`, content: '', // Empty content for image-only practice sourceLanguage: 'English', weekNumber: selectedWeek, category: 'weekly-practice', imageUrl: imageForm.imageUrl.trim(), imageAlt: imageForm.imageAlt.trim() || null, imageSize: imageForm.imageSize, imageAlignment: imageForm.imageAlignment }; console.log('Saving new image with payload:', payload); const response = await api.post('/api/auth/admin/weekly-practice', payload); if (response.data) { console.log('Image saved successfully:', response.data); // Optimistic UI update: append new practice for current week (4–6 only) if (selectedWeek >= 4) { const newPractice = response.data.weeklyPractice || response.data.practice || null; if (newPractice) { setWeeklyPractice(prev => [...prev, newPractice]); setWeeklyPracticeWeek(prev => prev ? { ...prev, practices: [...prev.practices, newPractice] } : prev); } } await fetchWeeklyPractice(false); setAddingImage(false); } else { console.error('Failed to save image'); } } catch (error) { console.error('Failed to add image practice:', error); } finally { setSaving(false); } }; const deletePractice = async (practiceId: string) => { const user = JSON.parse(localStorage.getItem('user') || '{}'); // Check if user is admin if (user.role !== 'admin') { return; } setSaving(true); try { if (String(practiceId).startsWith('wb-')) { console.warn('Skipping deletion of week-brief placeholder'); setSaving(false); return; } const response = await api.delete(`/api/auth/admin/weekly-practice/${practiceId}`); if (response.data) { setWeeklyPracticeWeek(prev => prev ? { ...prev, translationBrief: '' } : prev); const briefKey = `weeklyBrief_week_${selectedWeek}`; try { localStorage.removeItem(briefKey); } catch {} fetchWeeklyPractice(false); } else { console.error('Failed to delete weekly practice'); } } catch (error) { console.error('Failed to delete weekly practice:', error); } finally { setSaving(false); } }; return (
{/* Header */}
Weekly Practice

Weekly Practice

Practice your translation skills with weekly examples and cultural elements.

{/* Week Selector */}
{weeks.map((week) => { const isActive = selectedWeek === week; return ( ); })}
{isAdmin && (
Visibility {isWeekHidden ? 'Hidden' : 'Shown'}
)}
{/* Week Transition Loading Spinner */} {isWeekTransitioning && (
Loading...
)} {/* Translation Brief - Shown once at the top (hidden for students when week is hidden) */} {!isWeekTransitioning && weeklyPracticeWeek && weeklyPracticeWeek.translationBrief && (isAdmin || !isWeekHidden) ? (

Translation Brief

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