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, ArrowRightIcon as ArrowRightIconSolid } from '@heroicons/react/24/outline'; interface WeeklyPractice { _id: string; content: string; weekNumber: number; translationBrief?: string; } 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; } // New interfaces for subtitling 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; } const WeeklyPractice: React.FC = () => { const [selectedWeek, setSelectedWeek] = useState(() => { const savedWeek = localStorage.getItem('selectedWeeklyPracticeWeek'); return savedWeek ? parseInt(savedWeek) : 1; }); const [weeklyPractice, setWeeklyPractice] = useState([]); const [weeklyPracticeWeek, setWeeklyPracticeWeek] = useState(null); 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}>({}); const [editingPractice, setEditingPractice] = useState(null); const [editingBrief, setEditingBrief] = useState<{[key: number]: boolean}>({}); const [addingPractice, setAddingPractice] = useState(false); const [editForm, setEditForm] = useState<{ content: string; translationBrief: string; }>({ content: '', translationBrief: '' }); const [saving, setSaving] = useState(false); const navigate = useNavigate(); // HTML5 video with direct file access for precise control (like Amara) const [videoRef, setVideoRef] = useState(null); const [currentSegment, setCurrentSegment] = useState(() => { const savedSegment = localStorage.getItem('currentSegment'); return savedSegment ? parseInt(savedSegment) : 3; }); const [isPlaying, setIsPlaying] = useState(false); const [currentTime, setCurrentTime] = useState('00:15'); const [totalTime, setTotalTime] = useState('01:30'); const [subtitleText, setSubtitleText] = useState('I\'m single-minded, I\'m deceptive, I\'m obsessive...'); const [targetText, setTargetText] = useState('Soy determinado, soy astuto, soy obsesivo...'); const [characterCount, setCharacterCount] = useState(47); const [translations, setTranslations] = useState<{ [key: number]: string }>({}); const [previewText, setPreviewText] = useState(''); const [isPreviewMode, setIsPreviewMode] = useState(false); const [savedSegments, setSavedSegments] = useState>(new Set()); const weeks = [1, 2, 3, 4, 5, 6]; const videoInfo: VideoInfo = { title: 'Nike \'Winning Isn\'t for Everyone\'', duration: '1:26', totalSegments: 26, currentSegment: 3 }; // DEFINITIVE CORRECT TIMECODES - Cross-platform compatible const defaultSegments: SubtitleSegment[] = [ { id: 1, startTime: '00:00:00,640', endTime: '00:00:02,600', duration: '1s 960ms', sourceText: 'Am I a bad person?' }, { id: 2, startTime: '00:00:06,320', endTime: '00:00:08,000', duration: '1s 679.9999999999998ms', sourceText: 'Tell me. Am I?' }, { id: 3, startTime: '00:00:08,480', endTime: '00:00:10,200', duration: '1s 719.9999999999989ms', sourceText: 'I\'m single minded.', isCurrent: true }, { id: 4, startTime: '00:00:10,570', endTime: '00:00:12,100', duration: '1s 529.9999999999993ms', sourceText: 'I\'m deceptive.' }, { id: 5, startTime: '00:00:12,200', endTime: '00:00:13,690', duration: '1s 490.0000000000002ms', sourceText: 'I\'m obsessive.' }, { id: 6, startTime: '00:00:13,900', endTime: '00:00:15,210', duration: '1s 310.00000000000045ms', sourceText: 'I\'m selfish.' }, { id: 7, startTime: '00:00:15,220', endTime: '00:00:17,800', duration: '2s 580ms', sourceText: 'Does that make me a bad person?' }, { id: 8, startTime: '00:00:18,010', endTime: '00:00:19,990', duration: '1s 979.9999999999968ms', sourceText: 'Am I a bad person?' }, { id: 9, startTime: '00:00:20,870', endTime: '00:00:21,700', duration: '0s 829.9999999999983ms', sourceText: 'Am I?' }, { id: 10, startTime: '00:00:23,120', endTime: '00:00:24,700', duration: '1s 579.9999999999982ms', sourceText: 'I have no empathy.' }, { id: 11, startTime: '00:00:25,540', endTime: '00:00:27,650', duration: '2s 109.99999999999955ms', sourceText: 'I don\'t respect you.' }, { id: 12, startTime: '00:00:28,550', endTime: '00:00:30,500', duration: '1s 949.9999999999993ms', sourceText: 'I\'m never satisfied.' }, { id: 13, startTime: '00:00:30,500', endTime: '00:00:33,500', duration: '3s 0ms', sourceText: 'I have an obsession with power.' }, { id: 14, startTime: '00:00:38,000', endTime: '00:00:39,300', duration: '1s 299.99999999999727ms', sourceText: 'I\'m irrational.' }, { id: 15, startTime: '00:00:39,900', endTime: '00:00:41,700', duration: '1s 800.0000000000043ms', sourceText: 'I have zero remorse.' }, { id: 16, startTime: '00:00:42,000', endTime: '00:00:44,200', duration: '2s 200.00000000000273ms', sourceText: 'I have no sense of compassion.' }, { id: 17, startTime: '00:00:44,480', endTime: '00:00:46,950', duration: '2s 470.0000000000059ms', sourceText: 'I\'m delusional. I\'m maniacal.' }, { id: 18, startTime: '00:00:46,960', endTime: '00:00:49,319', duration: '2s 359.0000000000018ms', sourceText: 'You think I\'m a bad person?' }, { id: 19, startTime: '00:00:49,320', endTime: '00:00:52,900', duration: '3s 579.9999999999982ms', sourceText: 'Tell me. Tell me. Tell me.\nTell me. Am I?' }, { id: 20, startTime: '00:00:52,990', endTime: '00:00:55,299', duration: '2s 308.99999999999727ms', sourceText: 'I think I\'m better than everyone else.' }, { id: 21, startTime: '00:00:55,300', endTime: '00:00:57,899', duration: '2s 599.0000000000036ms', sourceText: 'I want to take what\'s yours\nand never give it back.' }, { id: 22, startTime: '00:00:57,900', endTime: '00:01:01,200', duration: '3s 300.0000000000041ms', sourceText: 'What\'s mine is mine\nand what\'s yours is mine.' }, { id: 23, startTime: '00:01:06,920', endTime: '00:01:08,830', duration: '1s 909.9999999999966ms', sourceText: 'Am I a bad person?' }, { id: 24, startTime: '00:01:08,840', endTime: '00:01:11,120', duration: '2s 280.0000000000009ms', sourceText: 'Tell me. Am I?' }, { id: 25, startTime: '00:01:21,500', endTime: '00:01:24,000', duration: '2s 500ms', sourceText: 'Does that make me a bad person?' }, { id: 26, startTime: '00:01:25,060', endTime: '00:01:27,100', duration: '2s 39.99999999999204ms', sourceText: 'Tell me. Does it?' }, ]; const [subtitleSegments, setSubtitleSegments] = useState(() => { const saved = localStorage.getItem('subtitleSegments'); return saved ? JSON.parse(saved) : defaultSegments; }); // Load subtitle data from localStorage (since backend endpoints don't exist yet) const loadSubtitleDataFromDatabase = async () => { try { // For now, use localStorage as the backend endpoints don't exist const savedSegments = localStorage.getItem('subtitleSegments'); if (savedSegments) { const segments = JSON.parse(savedSegments); setSubtitleSegments(segments); console.log('Subtitle data loaded from localStorage'); } else { // Use default segments if nothing saved setSubtitleSegments(defaultSegments); localStorage.setItem('subtitleSegments', JSON.stringify(defaultSegments)); console.log('Using default subtitle segments'); } } catch (error) { console.error('Failed to load subtitle data:', error); // Fallback to default segments setSubtitleSegments(defaultSegments); } }; // Load translations from localStorage (since backend doesn't have week2-segment submissions) const loadTranslationsFromDatabase = async () => { try { // For now, load translations from localStorage since the backend doesn't have // submissions with week2-segment IDs const savedTranslations = localStorage.getItem('week2-translations'); if (savedTranslations) { const translations = JSON.parse(savedTranslations); setTranslations(translations); // Update saved segments based on translations const savedSegmentsSet = new Set(Object.keys(translations).map(Number)); setSavedSegments(savedSegmentsSet); console.log('Translations loaded from localStorage:', translations); } } catch (error) { console.error('Failed to load translations:', error); // Don't throw error, just log it and continue } }; // Fetch user submissions (simplified for Week 2 focus) const fetchUserSubmissions = useCallback(async (practice: WeeklyPractice[]) => { try { const user = localStorage.getItem('user'); const token = localStorage.getItem('token'); if (!token || !user) { return; } // For Week 2, we don't need to fetch submissions since we're using localStorage // This function is kept for compatibility but doesn't do anything for Week 2 console.log('fetchUserSubmissions called but not needed for Week 2'); } catch (error) { console.error('Error fetching user submissions:', error); } }, []); // Fetch weekly practice data const fetchWeeklyPractice = useCallback(async () => { try { const response = await api.get(`/source-texts/weekly-practice/week/${selectedWeek}`); if (response.data) { setWeeklyPractice(response.data.practices || []); setWeeklyPracticeWeek(response.data); console.log('Weekly practice data loaded:', response.data); } } catch (error) { console.error('Error fetching weekly practice:', error); // Set empty arrays as fallback setWeeklyPractice([]); setWeeklyPracticeWeek(null); } }, [selectedWeek]); // Load data on component mount useEffect(() => { const loadData = async () => { await loadSubtitleDataFromDatabase(); await loadTranslationsFromDatabase(); await fetchWeeklyPractice(); setLoading(false); }; loadData(); }, [fetchWeeklyPractice]); // Load current segment data after translations are loaded useEffect(() => { if (!loading && currentSegment) { const segment = subtitleSegments.find(s => s.id === currentSegment); if (segment) { setSubtitleText(segment.sourceText); const existingTranslation = translations[currentSegment] || ''; setTargetText(existingTranslation); setCharacterCount(existingTranslation.length); // Update video subtitle overlay based on preview mode if (isPreviewMode && existingTranslation) { setPreviewText(existingTranslation); } else { setPreviewText(segment.sourceText); } } } }, [loading, currentSegment, subtitleSegments, translations, isPreviewMode]); // Handle segment click functionality const handleSegmentClick = (segmentId: number) => { setCurrentSegment(segmentId); localStorage.setItem('currentSegment', segmentId.toString()); const segment = subtitleSegments.find(s => s.id === segmentId); if (segment && videoRef) { const startTime = parseTimeToSeconds(segment.startTime); const endTime = parseTimeToSeconds(segment.endTime); console.log('Playing segment:', segmentId, 'from', startTime, 'to', endTime, 'duration:', endTime - startTime); // Seek to start of segment and play videoRef.currentTime = startTime; videoRef.play(); // Set up a timeout to pause at the end of the segment const segmentDuration = (endTime - startTime) * 1000; // Convert to milliseconds console.log('Segment duration in ms:', segmentDuration); setTimeout(() => { if (videoRef && !videoRef.paused) { videoRef.pause(); console.log('Segment ended, video paused at:', videoRef.currentTime); } }, segmentDuration); setCurrentTime(segment.startTime); setSubtitleText(segment.sourceText); // Preserve existing translation or load from saved translations const existingTranslation = translations[segmentId] || ''; setTargetText(existingTranslation); setCharacterCount(existingTranslation.length); // Update video subtitle overlay based on preview mode if (isPreviewMode && existingTranslation) { setPreviewText(existingTranslation); } else { setPreviewText(segment.sourceText); } } }; // Handle save and next functionality const handleSaveAndNext = async () => { // Save current translation to state const updatedTranslations = { ...translations, [currentSegment]: targetText }; setTranslations(updatedTranslations); // Save to localStorage for persistence localStorage.setItem('week2-translations', JSON.stringify(updatedTranslations)); // Track saved segments for progress setSavedSegments(prev => { const newSet = new Set(prev); newSet.add(currentSegment); return newSet; }); console.log('Translation saved for segment:', currentSegment); // Move to next segment if (currentSegment < subtitleSegments.length) { handleSegmentClick(currentSegment + 1); } }; // Handle preview translation functionality const handlePreviewTranslation = () => { // Toggle preview mode setIsPreviewMode(!isPreviewMode); if (!isPreviewMode) { // Enable preview mode - show translation setPreviewText(translations[currentSegment] || targetText); } else { // Disable preview mode - show source text setPreviewText(''); } console.log('Preview mode toggled:', !isPreviewMode); }; // Handle target text changes const handleTargetTextChange = (text: string) => { setTargetText(text); setCharacterCount(text.length); }; // Get segment status for styling const getSegmentStatus = (segmentId: number) => { if (savedSegments.has(segmentId)) return 'completed'; if (segmentId === currentSegment) return 'current'; return 'pending'; }; const getSegmentButtonClass = (segmentId: number) => { const status = getSegmentStatus(segmentId); switch (status) { case 'completed': return 'bg-green-500 text-white'; case 'current': return 'bg-orange-500 text-white'; default: return 'bg-gray-300 text-gray-700'; } }; // Save timecodes to localStorage (since backend endpoints don't exist) const saveTimecodesToDatabase = async (updatedSegments: SubtitleSegment[]) => { try { // For now, just save to localStorage since backend endpoints don't exist localStorage.setItem('subtitleSegments', JSON.stringify(updatedSegments)); console.log('Timecodes saved to localStorage'); } catch (error) { console.error('Failed to save timecodes:', error); } }; // Save source texts to localStorage (since backend endpoints don't exist) const saveSourceTextsToDatabase = async () => { try { // For now, just save to localStorage since backend endpoints don't exist const sourceTextsData = { weekNumber: 2, sourceTexts: subtitleSegments.map(segment => ({ segmentId: segment.id, sourceText: segment.sourceText, startTime: segment.startTime, endTime: segment.endTime })), createdAt: new Date().toISOString() }; localStorage.setItem('sourceTexts', JSON.stringify(sourceTextsData)); console.log('Source texts saved to localStorage'); } catch (error) { console.error('Failed to save source texts:', error); } }; const saveTimecodes = async () => { if (editingSegment) { // Validate time format const timeFormat = /^\d{2}:\d{2}:\d{2},\d{3}$/; if (!timeFormat.test(editStartTime) || !timeFormat.test(editEndTime)) { alert('Please use the correct time format: HH:MM:SS,mmm'); return; } // Calculate new duration const startSeconds = parseTimeToSeconds(editStartTime); const endSeconds = parseTimeToSeconds(editEndTime); // Validate start < end if (startSeconds >= endSeconds) { alert('Start time must be before end time'); return; } // Check for time clashes with other segments const hasClash = subtitleSegments.some(segment => { if (segment.id === editingSegment) return false; const segmentStart = parseTimeToSeconds(segment.startTime); const segmentEnd = parseTimeToSeconds(segment.endTime); // Check if new segment overlaps with existing segment return (startSeconds < segmentEnd && endSeconds > segmentStart); }); if (hasClash) { alert('Time clash detected! This segment overlaps with another segment. Please adjust the timecodes.'); return; } const durationMs = (endSeconds - startSeconds) * 1000; const duration = `${Math.floor(durationMs / 1000)}s ${durationMs % 1000}ms`; // Update the subtitle segment with new timecodes const updatedSegments = subtitleSegments.map(segment => segment.id === editingSegment ? { ...segment, startTime: editStartTime, endTime: editEndTime, duration } : segment ); // Update state with new timecodes setSubtitleSegments(updatedSegments); localStorage.setItem('subtitleSegments', JSON.stringify(updatedSegments)); // Save to MongoDB await saveTimecodesToDatabase(updatedSegments); await saveSourceTextsToDatabase(); console.log('Updated timecodes for segment:', editingSegment, 'to:', editStartTime, '-', editEndTime, 'duration:', duration); setEditingTimecodes(false); setEditingSegment(null); setEditStartTime(''); setEditEndTime(''); } }; const cancelEditingTimecodes = () => { setEditingTimecodes(false); setEditingSegment(null); setEditStartTime(''); setEditEndTime(''); }; const resetToDefaultTimecodes = () => { if (window.confirm('Are you sure you want to reset all timecodes to default? This will clear all custom changes.')) { setSubtitleSegments(defaultSegments); localStorage.removeItem('subtitleSegments'); console.log('Reset to default timecodes'); } }; // Update subtitle overlay based on current segment const updateSubtitleOverlay = (segmentId: number) => { const segment = subtitleSegments.find(s => s.id === segmentId); if (segment) { setSubtitleText(segment.sourceText); setTargetText(translations[segmentId] || ''); setCharacterCount(translations[segmentId]?.length || 0); // Update video subtitle overlay based on preview mode if (isPreviewMode && translations[segmentId]) { setPreviewText(translations[segmentId]); } else { setPreviewText(''); } } }; // Auto-subtitle sync: Update subtitle overlay when preview mode changes useEffect(() => { const segment = subtitleSegments.find(s => s.id === currentSegment); if (segment) { if (isPreviewMode && translations[currentSegment]) { setPreviewText(translations[currentSegment]); } else { setPreviewText(segment.sourceText); } } }, [isPreviewMode, currentSegment, translations, subtitleSegments]); // Helper function to parse time to seconds const parseTimeToSeconds = (timeString: string): number => { const parts = timeString.split(':'); const hours = parseInt(parts[0]); const minutes = parseInt(parts[1]); const seconds = parseFloat(parts[2].replace(',', '.')); return hours * 3600 + minutes * 60 + seconds; }; const formatTime = (seconds: number) => { const mins = Math.floor(seconds / 60); const secs = Math.floor(seconds % 60); return `${mins.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`; }; // Refresh submissions when user changes (after login/logout) useEffect(() => { const user = localStorage.getItem('user'); if (user && weeklyPractice.length > 0) { fetchUserSubmissions(weeklyPractice); } }, [weeklyPractice, fetchUserSubmissions]); // Additional effect to handle user switching useEffect(() => { const handleUserChange = () => { const user = localStorage.getItem('user'); if (user && weeklyPractice.length > 0) { fetchUserSubmissions(weeklyPractice); } }; // Listen for storage changes (when user logs in/out) window.addEventListener('storage', handleUserChange); // Also check on focus (when user switches tabs/windows) window.addEventListener('focus', handleUserChange); // Also check when the page becomes visible const handleVisibilityChange = () => { if (!document.hidden) { handleUserChange(); } }; document.addEventListener('visibilitychange', handleVisibilityChange); return () => { window.removeEventListener('storage', handleUserChange); window.removeEventListener('focus', handleUserChange); document.removeEventListener('visibilitychange', handleVisibilityChange); }; }, [weeklyPractice, fetchUserSubmissions]); const handleRefreshSubmissions = async () => { if (weeklyPractice.length > 0) { await fetchUserSubmissions(weeklyPractice); } }; const handleSubmitTranslation = async (practiceId: string) => { if (!translationText[practiceId]?.trim()) { alert('Please provide a translation'); return; } try { setSubmitting({ ...submitting, [practiceId]: true }); const user = JSON.parse(localStorage.getItem('user') || '{}'); const response = await api.post('/submissions', { sourceTextId: practiceId, transcreation: translationText[practiceId], culturalAdaptations: [], isAnonymous: anonymousSubmissions[practiceId] || false, username: user.name || 'Unknown' }); if (response.status >= 200 && response.status < 300) { setTranslationText({ ...translationText, [practiceId]: '' }); await fetchUserSubmissions(weeklyPractice); } else { console.error('Failed to submit translation:', 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(''); // Timecode editing for admin users const [editingTimecodes, setEditingTimecodes] = useState(false); const [editingSegment, setEditingSegment] = useState(null); const [editStartTime, setEditStartTime] = useState(''); const [editEndTime, setEditEndTime] = useState(''); // Start editing timecodes for admin users const startEditingTimecodes = (segmentId: number) => { const segment = subtitleSegments.find(s => s.id === segmentId); if (segment) { setEditingSegment(segmentId); setEditStartTime(segment.startTime); setEditEndTime(segment.endTime); setEditingTimecodes(true); } }; 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(`/submissions/${editingSubmission.id}`, { transcreation: editSubmissionText }); if (response.status >= 200 && response.status < 300) { setEditingSubmission(null); setEditSubmissionText(''); await fetchUserSubmissions(weeklyPractice); } 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(`/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: '' }); }; const startEditingBrief = () => { setEditingBrief(prev => ({ ...prev, [selectedWeek]: true })); setEditForm({ content: '', translationBrief: weeklyPracticeWeek?.translationBrief || '' }); }; const startAddingBrief = () => { setEditingBrief(prev => ({ ...prev, [selectedWeek]: true })); setEditForm({ content: '', translationBrief: '' }); }; 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(`/auth/admin/weekly-brief/${selectedWeek}`, { translationBrief: '', weekNumber: selectedWeek }); if (response.status >= 200 && response.status < 300) { await fetchWeeklyPractice(); } else { console.error('Failed to remove translation brief:', response.data); } } catch (error) { console.error('Failed to remove translation brief:', error); } finally { setSaving(false); } }; const toggleExpanded = (practiceId: string) => { setExpandedSections(prev => ({ ...prev, [practiceId]: !prev[practiceId] })); }; const cancelEditing = () => { setEditingPractice(null); setEditingBrief(prev => ({ ...prev, [selectedWeek]: false })); setEditForm({ content: '', translationBrief: '' }); }; const savePractice = async () => { if (!editingPractice) 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 response = await api.put(`/auth/admin/weekly-practice/${editingPractice}`, { ...editForm, weekNumber: selectedWeek }); if (response.status >= 200 && response.status < 300) { await fetchWeeklyPractice(); setEditingPractice(null); } else { console.error('Failed to update weekly practice:', response.data); } } catch (error) { console.error('Failed to update weekly practice:', error); } finally { setSaving(false); } }; const saveBrief = 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(`/auth/admin/weekly-brief/${selectedWeek}`, { translationBrief: editForm.translationBrief, weekNumber: selectedWeek }); if (response.status >= 200 && response.status < 300) { await fetchWeeklyPractice(); setEditingBrief(prev => ({ ...prev, [selectedWeek]: false })); } else { console.error('Failed to update translation brief:', response.data); } } catch (error) { console.error('Failed to update translation brief:', error); } finally { setSaving(false); } }; const startAddingPractice = () => { setAddingPractice(true); setEditForm({ content: '', translationBrief: '' }); }; const cancelAddingPractice = () => { setAddingPractice(false); setEditForm({ content: '', translationBrief: '' }); }; const saveNewPractice = 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; } // 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 }; const response = await api.post('/auth/admin/weekly-practice', payload); if (response.status >= 200 && response.status < 300) { await fetchWeeklyPractice(); setAddingPractice(false); } else { console.error('Failed to add weekly practice:', response.data); } } catch (error) { console.error('Failed to add weekly practice:', error); } finally { setSaving(false); } }; const deletePractice = async (practiceId: string) => { const user = JSON.parse(localStorage.getItem('user') || '{}'); const token = localStorage.getItem('token'); // Check if user is admin if (user.role !== 'admin') { return; } setSaving(true); try { const response = await api.delete(`/auth/admin/weekly-practice/${practiceId}`); if (response.status >= 200 && response.status < 300) { await fetchWeeklyPractice(); } else { console.error('Failed to delete weekly practice:', response.data); } } catch (error) { console.error('Failed to delete weekly practice:', error); } finally { setSaving(false); } }; if (loading) { return (
); } return (
{/* Header */}

Weekly Practice

Practice your translation skills with weekly examples and cultural elements.

{/* Week Selector */}
{weeks.map((week) => ( ))}
{/* Translation Brief - Shown once at the top */} {weeklyPracticeWeek && weeklyPracticeWeek.translationBrief ? (

Translation Brief

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