transcreation-frontend / src /pages /WeeklyPractice.tsx
Tristan Yu
WIP: sync workspace before rebase
3aad738
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<number>(() => {
const savedWeek = localStorage.getItem('selectedWeeklyPracticeWeek');
return savedWeek ? parseInt(savedWeek) : 1;
});
const [weeklyPractice, setWeeklyPractice] = useState<WeeklyPractice[]>([]);
const [weeklyPracticeWeek, setWeeklyPracticeWeek] = useState<WeeklyPracticeWeek | null>(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<string | null>(null);
const [editingBrief, setEditingBrief] = useState<{[key: number]: boolean}>({});
const [addingPractice, setAddingPractice] = useState<boolean>(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<HTMLVideoElement | null>(null);
const [currentSegment, setCurrentSegment] = useState<number>(() => {
const savedSegment = localStorage.getItem('currentSegment');
return savedSegment ? parseInt(savedSegment) : 3;
});
const [isPlaying, setIsPlaying] = useState<boolean>(false);
const [currentTime, setCurrentTime] = useState<string>('00:15');
const [totalTime, setTotalTime] = useState<string>('01:30');
const [subtitleText, setSubtitleText] = useState<string>('I\'m single-minded, I\'m deceptive, I\'m obsessive...');
const [targetText, setTargetText] = useState<string>('Soy determinado, soy astuto, soy obsesivo...');
const [characterCount, setCharacterCount] = useState<number>(47);
const [translations, setTranslations] = useState<{ [key: number]: string }>({});
const [previewText, setPreviewText] = useState<string>('');
const [isPreviewMode, setIsPreviewMode] = useState<boolean>(false);
const [savedSegments, setSavedSegments] = useState<Set<number>>(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<SubtitleSegment[]>(() => {
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<boolean>(false);
const [editingSegment, setEditingSegment] = useState<number | null>(null);
const [editStartTime, setEditStartTime] = useState<string>('');
const [editEndTime, setEditEndTime] = useState<string>('');
// 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 <CheckCircleIcon className="h-5 w-5 text-green-500" />;
case 'pending':
return <ClockIcon className="h-5 w-5 text-yellow-500" />;
default:
return <ClockIcon className="h-5 w-5 text-gray-500" />;
}
};
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 (
<div className="min-h-screen bg-gray-50 py-8">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="text-center">
<div className="animate-pulse">
<div className="h-8 w-64 bg-gray-200 rounded mx-auto mb-4"></div>
<div className="h-4 w-96 bg-gray-200 rounded mx-auto mb-2"></div>
<div className="h-4 w-80 bg-gray-200 rounded mx-auto"></div>
</div>
</div>
</div>
</div>
);
}
return (
<div className="min-h-screen bg-gray-50 py-8">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
{/* Header */}
<div className="mb-8">
<div className="flex items-center mb-4">
<BookOpenIcon className="h-8 w-8 text-indigo-600 mr-3" />
<h1 className="text-3xl font-bold text-gray-900">Weekly Practice</h1>
</div>
<p className="text-gray-600">
Practice your translation skills with weekly examples and cultural elements.
</p>
</div>
{/* Week Selector */}
<div className="mb-6">
<div className="flex space-x-2 overflow-x-auto pb-2">
{weeks.map((week) => (
<button
key={week}
onClick={() => {
setSelectedWeek(week);
localStorage.setItem('selectedWeeklyPracticeWeek', week.toString());
}}
className={`px-4 py-2 rounded-lg font-medium whitespace-nowrap ${
selectedWeek === week
? 'bg-indigo-600 text-white'
: 'bg-white text-gray-700 hover:bg-gray-50 border border-gray-300'
}`}
>
Week {week}
</button>
))}
</div>
</div>
{/* Translation Brief - Shown once at the top */}
{weeklyPracticeWeek && weeklyPracticeWeek.translationBrief ? (
<div className="bg-gradient-to-r from-blue-50 to-indigo-50 rounded-xl p-8 mb-8 border border-blue-200">
<div className="flex items-center justify-between mb-4">
<div className="flex items-center space-x-2">
<div className="bg-blue-100 rounded-full p-2">
<BookOpenIcon className="h-5 w-5 text-blue-600" />
</div>
<h3 className="text-blue-900 font-semibold text-xl">Translation Brief</h3>
</div>
{JSON.parse(localStorage.getItem('user') || '{}').role === 'admin' && (
<div className="flex items-center space-x-2">
{editingBrief[selectedWeek] ? (
<>
<button
onClick={saveBrief}
disabled={saving}
className="bg-green-100 hover:bg-green-200 text-green-700 px-3 py-1 rounded-lg transition-colors duration-200"
>
{saving ? (
<div className="animate-spin rounded-full h-4 w-4 border-b-2 border-green-600"></div>
) : (
<CheckIcon className="h-4 w-4" />
)}
</button>
<button
onClick={cancelEditing}
className="bg-red-100 hover:bg-red-200 text-red-700 px-3 py-1 rounded-lg transition-colors duration-200"
>
<XMarkIcon className="h-4 w-4" />
</button>
</>
) : (
<>
<button
onClick={startEditingBrief}
className="bg-blue-100 hover:bg-blue-200 text-blue-700 px-3 py-1 rounded-lg transition-colors duration-200"
>
<PencilIcon className="h-4 w-4" />
</button>
<button
onClick={() => removeBrief()}
className="bg-red-100 hover:bg-red-200 text-red-700 px-3 py-1 rounded-lg transition-colors duration-200"
>
<TrashIcon className="h-4 w-4" />
</button>
</>
)}
</div>
)}
</div>
{editingBrief[selectedWeek] ? (
<textarea
value={editForm.translationBrief}
onChange={(e) => setEditForm({ ...editForm, translationBrief: e.target.value })}
className="w-full p-4 border border-blue-300 rounded-lg text-blue-800 leading-relaxed text-lg bg-white"
rows={6}
placeholder="Enter translation brief..."
/>
) : (
<p className="text-blue-800 leading-relaxed text-lg font-smiley">{weeklyPracticeWeek.translationBrief}</p>
)}
</div>
) : (
// Show add brief button when no brief exists
JSON.parse(localStorage.getItem('user') || '{}').role === 'admin' && (
<div className="bg-gradient-to-r from-blue-50 to-indigo-50 rounded-xl p-8 mb-8 border border-blue-200 border-dashed">
<div className="flex items-center justify-between mb-4">
<div className="flex items-center space-x-2">
<div className="bg-blue-100 rounded-full p-2">
<BookOpenIcon className="h-5 w-5 text-blue-600" />
</div>
<h3 className="text-blue-900 font-semibold text-xl">Translation Brief</h3>
</div>
<button
onClick={startAddingBrief}
className="bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded-lg transition-colors duration-200 flex items-center space-x-2"
>
<PlusIcon className="h-4 w-4" />
<span>Add Translation Brief</span>
</button>
</div>
{editingBrief[selectedWeek] && (
<div className="space-y-4">
<textarea
value={editForm.translationBrief}
onChange={(e) => setEditForm({ ...editForm, translationBrief: e.target.value })}
className="w-full p-4 border border-blue-300 rounded-lg text-blue-800 leading-relaxed text-lg bg-white"
rows={6}
placeholder="Enter translation brief..."
/>
<div className="flex items-center space-x-2">
<button
onClick={saveBrief}
disabled={saving}
className="bg-green-600 hover:bg-green-700 text-white px-4 py-2 rounded-lg transition-colors duration-200"
>
{saving ? (
<div className="animate-spin rounded-full h-4 w-4 border-b-2 border-white"></div>
) : (
'Save Brief'
)}
</button>
<button
onClick={cancelEditing}
className="bg-gray-500 hover:bg-gray-600 text-white px-4 py-2 rounded-lg transition-colors duration-200"
>
Cancel
</button>
</div>
</div>
)}
</div>
)
)}
{/* Special Subtitling Interface for Week 2 */}
{selectedWeek === 2 ? (
<div className="grid grid-cols-1 lg:grid-cols-2 gap-8">
{/* Left Panel - Video Player */}
<div className="bg-white rounded-xl shadow-lg border border-gray-100 p-6">
<div className="mb-4">
<h3 className="text-xl font-bold text-gray-900 mb-2">{videoInfo.title}</h3>
</div>
{/* Video Player */}
<div className="bg-black rounded-lg aspect-video mb-4 relative">
<video
ref={setVideoRef}
src="/videos/nike-winning-isnt-for-everyone.mp4"
controls
onTimeUpdate={() => {
const currentTime = videoRef?.currentTime || 0;
setCurrentTime(formatTime(currentTime));
}}
onLoadedMetadata={() => {
if (videoRef) {
setTotalTime(formatTime(videoRef.duration));
}
}}
onPlay={() => setIsPlaying(true)}
onPause={() => setIsPlaying(false)}
className="w-full h-full"
/>
{/* Professional subtitle overlay - dynamically sized, precise timing */}
{(previewText || subtitleText) && (
<div className="absolute bottom-8 left-1/2 transform -translate-x-1/2 bg-black bg-opacity-70 text-white px-4 py-2 rounded-lg shadow-lg">
<p className="text-lg font-medium whitespace-nowrap">{previewText || subtitleText}</p>
</div>
)}
</div>
</div>
{/* Right Panel - Translation Workspace */}
<div className="bg-white rounded-xl shadow-lg border border-gray-100 p-6">
<h3 className="text-xl font-bold text-gray-900 mb-4">Translation Workspace</h3>
{/* Segment Navigation Grid */}
<div className="grid grid-cols-8 gap-2 mb-6">
{subtitleSegments.map((segment) => (
<button
key={segment.id}
onClick={() => handleSegmentClick(segment.id)}
className={`px-2 py-1 rounded text-xs ${getSegmentButtonClass(segment.id)}`}
>
Seg {segment.id}
</button>
))}
</div>
{/* Current Segment Details - Only show timecodes to admin users */}
<div className="bg-gray-100 rounded-lg p-4 mb-4">
<p className="text-gray-600">
Segment {currentSegment}
{JSON.parse(localStorage.getItem('user') || '{}').role === 'admin' && (
<>: {subtitleSegments[currentSegment - 1]?.startTime} – {subtitleSegments[currentSegment - 1]?.endTime} ({subtitleSegments[currentSegment - 1]?.duration})</>
)}
</p>
{/* Timecode Editing for Admin Users Only */}
{JSON.parse(localStorage.getItem('user') || '{}').role === 'admin' && (
<div className="mt-3 pt-3 border-t border-gray-300">
<button
onClick={() => startEditingTimecodes(currentSegment)}
className="bg-purple-500 hover:bg-purple-600 text-white px-3 py-1 rounded text-sm"
>
Edit Timecodes
</button>
<button
onClick={resetToDefaultTimecodes}
className="bg-red-500 hover:bg-red-600 text-white px-3 py-1 rounded text-sm ml-2"
>
Reset Default
</button>
</div>
)}
</div>
{/* Timecode Editing Modal for Admin Only */}
{editingTimecodes && editingSegment && JSON.parse(localStorage.getItem('user') || '{}').role === 'admin' && (
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
<div className="bg-white rounded-lg p-6 w-96">
<h3 className="text-lg font-bold mb-4">Edit Timecodes for Segment {editingSegment}</h3>
<div className="space-y-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Start Time (HH:MM:SS,mmm)</label>
<input
type="text"
value={editStartTime}
onChange={(e) => setEditStartTime(e.target.value)}
className="w-full px-3 py-2 border border-gray-300 rounded-md"
placeholder="00:00:00,000"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">End Time (HH:MM:SS,mmm)</label>
<input
type="text"
value={editEndTime}
onChange={(e) => setEditEndTime(e.target.value)}
className="w-full px-3 py-2 border border-gray-300 rounded-md"
placeholder="00:00:00,000"
/>
</div>
<div className="flex space-x-3">
<button
onClick={saveTimecodes}
className="bg-green-500 hover:bg-green-600 text-white px-4 py-2 rounded"
>
Save
</button>
<button
onClick={cancelEditingTimecodes}
className="bg-gray-500 hover:bg-gray-600 text-white px-4 py-2 rounded"
>
Cancel
</button>
</div>
</div>
</div>
</div>
)}
{/* Source Text */}
<div className="mb-4">
<label className="block text-sm font-medium text-gray-700 mb-2">Source Text</label>
<textarea
value={subtitleText}
readOnly
className="w-full px-3 py-2 border border-gray-300 rounded-md bg-gray-50 text-gray-700"
rows={2}
/>
</div>
{/* Target Text */}
<div className="mb-4">
<label className="block text-sm font-medium text-gray-700 mb-2">Target Text</label>
<textarea
value={targetText}
onChange={(e) => handleTargetTextChange(e.target.value)}
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-orange-500 focus:border-orange-500"
rows={2}
placeholder="Enter your translation..."
/>
<div className="flex justify-between items-center mt-2">
<span className="text-xs text-gray-500">Recommended: 2 lines max, 16 chars/line</span>
<span className="text-xs text-gray-500">{characterCount} characters</span>
</div>
</div>
{/* Action Buttons */}
<div className="flex space-x-3 mb-4">
<button
onClick={handleSaveAndNext}
className="bg-blue-500 hover:bg-blue-600 text-white px-4 py-2 rounded-lg transition-colors"
>
Save
</button>
<button
onClick={handlePreviewTranslation}
className={`px-4 py-2 rounded-lg flex items-center space-x-2 ${
isPreviewMode
? 'bg-green-500 hover:bg-green-600 text-white'
: 'bg-orange-500 hover:bg-orange-600 text-white'
}`}
>
<span>{isPreviewMode ? 'Show Original' : 'Show Translation'}</span>
</button>
</div>
{/* Translation Progress */}
<div className="mb-4">
<div className="flex justify-between items-center mb-2">
<span className="text-sm font-medium text-gray-700">Translation Progress</span>
<span className="text-sm text-gray-500">{savedSegments.size} of {subtitleSegments.length} segments completed</span>
</div>
<div className="w-full bg-gray-200 rounded-full h-2">
<div
className="bg-green-500 h-2 rounded-full transition-all duration-300"
style={{ width: `${(savedSegments.size / subtitleSegments.length) * 100}%` }}
></div>
</div>
</div>
</div>
</div>
) : (
/* Weekly Practice */
<div className="space-y-6">
{/* Add Practice Button for Admin */}
{JSON.parse(localStorage.getItem('user') || '{}').role === 'admin' && (
<div className="mb-6">
{addingPractice ? (
<div className="bg-white rounded-xl shadow-lg border border-gray-100 p-6 w-full">
<div className="flex items-center justify-between mb-4">
<div className="flex items-center space-x-2">
<div className="bg-orange-100 rounded-full p-2">
<PlusIcon className="h-5 w-5 text-orange-600" />
</div>
<h3 className="text-lg font-semibold text-gray-900">Add New Weekly Practice</h3>
</div>
<div className="flex items-center space-x-2">
<button
onClick={saveNewPractice}
disabled={saving}
className="bg-green-600 hover:bg-green-700 text-white px-4 py-2 rounded-lg transition-colors duration-200 flex items-center space-x-2"
>
{saving ? (
<div className="animate-spin rounded-full h-4 w-4 border-b-2 border-white"></div>
) : (
<>
<CheckIcon className="h-4 w-4" />
<span>Save Practice</span>
</>
)}
</button>
<button
onClick={cancelAddingPractice}
className="bg-gray-500 hover:bg-gray-600 text-white px-4 py-2 rounded-lg transition-colors duration-200 flex items-center space-x-2"
>
<XMarkIcon className="h-4 w-4" />
<span>Cancel</span>
</button>
</div>
</div>
<textarea
value={editForm.content}
onChange={(e) => setEditForm({ ...editForm, content: e.target.value })}
className="w-full p-4 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-orange-500 focus:border-orange-500"
rows={4}
placeholder="Enter weekly practice content..."
/>
</div>
) : (
<div className="bg-gradient-to-r from-orange-50 to-amber-50 rounded-xl p-6 border border-orange-200 border-dashed">
<div className="flex items-center justify-between">
<div className="flex items-center space-x-3">
<div className="bg-orange-100 rounded-full p-2">
<PlusIcon className="h-5 w-5 text-orange-600" />
</div>
<div>
<h3 className="text-lg font-semibold text-orange-900">Add New Weekly Practice</h3>
<p className="text-orange-700 text-sm">Create a new practice example for Week {selectedWeek}</p>
</div>
</div>
<button
onClick={startAddingPractice}
className="bg-orange-600 hover:bg-orange-700 text-white px-6 py-3 rounded-lg transition-colors duration-200 flex items-center space-x-2 shadow-lg hover:shadow-xl"
>
<PlusIcon className="h-5 w-5" />
<span className="font-medium">Add Practice</span>
</button>
</div>
</div>
)}
</div>
)}
{weeklyPractice.length === 0 && !addingPractice ? (
<div className="text-center py-12">
<DocumentTextIcon className="h-12 w-12 text-gray-400 mx-auto mb-4" />
<h3 className="text-lg font-medium text-gray-900 mb-2">
No practice examples available
</h3>
<p className="text-gray-600">
Practice examples for Week {selectedWeek} haven't been set up yet.
</p>
</div>
) : (
weeklyPractice.map((practice) => (
<div key={practice._id} className="bg-white rounded-xl shadow-lg border border-gray-100 p-8 hover:shadow-xl transition-shadow duration-300">
<div className="mb-6">
<div className="flex items-center justify-between mb-4">
<div className="flex items-center space-x-3">
<div className="bg-orange-100 rounded-full p-2">
<DocumentTextIcon className="h-5 w-5 text-orange-600" />
</div>
<div>
<h3 className="text-lg font-semibold text-gray-900">Source Text #{weeklyPractice.indexOf(practice) + 1}</h3>
</div>
</div>
{JSON.parse(localStorage.getItem('user') || '{}').role === 'admin' && (
<div className="flex items-center space-x-2">
{editingPractice === practice._id ? (
<>
<button
onClick={savePractice}
disabled={saving}
className="bg-green-100 hover:bg-green-200 text-green-700 px-3 py-1 rounded-lg transition-colors duration-200"
>
{saving ? (
<div className="animate-spin rounded-full h-4 w-4 border-b-2 border-green-600"></div>
) : (
<CheckIcon className="h-4 w-4" />
)}
</button>
<button
onClick={cancelEditing}
className="bg-red-100 hover:bg-red-200 text-red-700 px-3 py-1 rounded-lg transition-colors duration-200"
>
<XMarkIcon className="h-4 w-4" />
</button>
</>
) : (
<>
<button
onClick={() => startEditing(practice)}
className="bg-blue-100 hover:bg-blue-200 text-blue-700 px-3 py-1 rounded-lg transition-colors duration-200"
>
<PencilIcon className="h-4 w-4" />
</button>
<button
onClick={() => deletePractice(practice._id)}
className="bg-red-100 hover:bg-red-200 text-red-700 px-3 py-1 rounded-lg transition-colors duration-200"
>
<TrashIcon className="h-4 w-4" />
</button>
</>
)}
</div>
)}
</div>
{/* Content - Enhanced styling */}
<div className="bg-gradient-to-r from-orange-50 to-amber-50 rounded-xl p-6 mb-6 border border-orange-200">
{editingPractice === practice._id ? (
<textarea
value={editForm.content}
onChange={(e) => setEditForm({...editForm, content: e.target.value})}
className="w-full px-4 py-3 border border-orange-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-orange-500 focus:border-orange-500 bg-white"
rows={5}
placeholder="Enter source text..."
/>
) : (
<p className="text-orange-800 leading-relaxed text-lg font-source-text">{practice.content}</p>
)}
</div>
</div>
{/* All Submissions for this Practice */}
{userSubmissions[practice._id] && userSubmissions[practice._id].length > 0 && (
<div className="bg-gradient-to-r from-white to-orange-50 rounded-xl p-6 mb-6 border border-stone-200">
<div className="flex items-center justify-between mb-4">
<div className="flex items-center space-x-2">
<div className="bg-amber-100 rounded-full p-1">
<CheckCircleIcon className="h-4 w-4 text-amber-600" />
</div>
<h4 className="text-stone-900 font-semibold text-lg">All Submissions ({userSubmissions[practice._id].length})</h4>
</div>
<button
onClick={() => toggleExpanded(practice._id)}
className="flex items-center space-x-1 text-amber-700 hover:text-amber-800 text-sm font-medium"
>
<span>{expandedSections[practice._id] ? 'Collapse' : 'Expand'}</span>
<svg
className={`w-4 h-4 transition-transform duration-200 ${expandedSections[practice._id] ? 'rotate-180' : ''}`}
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
</svg>
</button>
</div>
<div className={`grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-3 transition-all duration-300 ${
expandedSections[practice._id]
? 'max-h-none overflow-visible'
: 'max-h-0 overflow-hidden'
}`}>
{userSubmissions[practice._id].map((submission, index) => (
<div key={submission._id} className="bg-white rounded-lg p-3 border border-stone-200 flex flex-col justify-between h-full">
<div className="flex items-center justify-between mb-2">
<div className="flex items-center space-x-2">
{submission.isOwner && (
<span className="inline-block bg-purple-100 text-purple-800 text-xs px-1.5 py-0.5 rounded-full">
Your Submission
</span>
)}
</div>
{getStatusIcon(submission.status)}
</div>
<p className="text-stone-800 leading-relaxed text-base mb-2 font-smiley">{submission.transcreation}</p>
<div className="flex items-center space-x-4 text-xs text-stone-700 mt-auto">
<div className="flex items-center space-x-1">
<span className="font-medium">By:</span>
<span className="bg-amber-100 px-1.5 py-0.5 rounded-full text-amber-800 text-xs">
{submission.userId?.username || 'Unknown'}
</span>
</div>
<div className="flex items-center space-x-1">
<span className="font-medium">Votes:</span>
<span className="bg-amber-100 px-1.5 py-0.5 rounded-full text-xs">
{(submission.voteCounts?.['1'] || 0) + (submission.voteCounts?.['2'] || 0) + (submission.voteCounts?.['3'] || 0)}
</span>
</div>
{submission.isOwner && (
<button
onClick={() => handleEditSubmission(submission._id, submission.transcreation)}
className="text-purple-600 hover:text-purple-800 text-sm font-medium"
>
Edit
</button>
)}
{JSON.parse(localStorage.getItem('user') || '{}').role === 'admin' && (
<button
onClick={() => handleDeleteSubmission(submission._id)}
className="text-red-600 hover:text-red-800 text-sm font-medium ml-2"
>
Delete
</button>
)}
</div>
</div>
))}
</div>
</div>
)}
{/* Translation Input (only show if user is logged in and has no submission) */}
{(() => {
const hasToken = localStorage.getItem('token');
const allSubs = userSubmissions[practice._id] || [];
const currentUser = JSON.parse(localStorage.getItem('user') || '{}');
const currentUserSubs = allSubs.filter(sub => sub.userId?._id === currentUser._id);
return hasToken && currentUserSubs.length === 0;
})() && (
<div className="bg-gradient-to-r from-purple-50 to-violet-50 rounded-xl p-6 border border-purple-200">
<div className="flex items-center space-x-2 mb-4">
<div className="bg-purple-100 rounded-full p-1">
<DocumentTextIcon className="h-4 w-4 text-purple-600" />
</div>
<h4 className="text-purple-900 font-semibold text-lg">Your Translation</h4>
</div>
<div className="mb-4">
<textarea
value={translationText[practice._id] || ''}
onChange={(e) => setTranslationText({ ...translationText, [practice._id]: e.target.value })}
className="w-full px-4 py-3 border border-purple-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-purple-500 focus:border-purple-500 bg-white"
rows={4}
placeholder="Enter your translation here..."
/>
</div>
<div className="mb-4">
<label className="flex items-center space-x-2 cursor-pointer">
<input
type="checkbox"
checked={anonymousSubmissions[practice._id] || false}
onChange={(e) => setAnonymousSubmissions({
...anonymousSubmissions,
[practice._id]: e.target.checked
})}
className="rounded border-gray-300 text-purple-600 focus:ring-purple-500"
/>
<span className="text-purple-700 font-medium">Submit anonymously</span>
</label>
<p className="text-sm text-purple-600 mt-1">
Check this box to submit without showing your name
</p>
</div>
<button
onClick={() => handleSubmitTranslation(practice._id)}
disabled={submitting[practice._id]}
className="bg-gradient-to-r from-purple-600 to-violet-600 hover:from-purple-700 hover:to-violet-700 disabled:from-gray-400 disabled:to-gray-500 text-white px-6 py-3 rounded-lg font-medium flex items-center justify-center transition-all duration-200 transform hover:scale-105"
>
{submitting[practice._id] ? (
<>
<div className="animate-spin rounded-full h-4 w-4 border-b-2 border-white mr-2"></div>
Submitting...
</>
) : (
<>
Submit Translation
<ArrowRightIcon className="h-4 w-4 ml-2" />
</>
)}
</button>
</div>
)}
{/* Show login message for visitors */}
{!localStorage.getItem('token') && (
<div className="bg-gradient-to-r from-gray-50 to-blue-50 rounded-xl p-6 border border-gray-200">
<div className="flex items-center space-x-2 mb-4">
<div className="bg-gray-100 rounded-full p-1">
<DocumentTextIcon className="h-4 w-4 text-gray-600" />
</div>
<h4 className="text-gray-900 font-semibold text-lg">Login Required</h4>
</div>
<p className="text-gray-700 mb-4">
Please log in to submit translations for this weekly practice.
</p>
<button
onClick={() => window.location.href = '/login'}
className="bg-blue-600 hover:bg-blue-700 text-white px-6 py-3 rounded-lg font-medium flex items-center justify-center transition-all duration-200 transform hover:scale-105"
>
Go to Login
<ArrowRightIcon className="h-4 w-4 ml-2" />
</button>
</div>
)}
</div>
))
)}
</div>
)}
</div>
{/* Edit Submission Modal */}
{editingSubmission && (
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
<div className="bg-white rounded-lg p-6 w-full max-w-2xl mx-4">
<div className="flex items-center justify-between mb-4">
<h3 className="text-lg font-semibold text-gray-900">Edit Translation</h3>
<button
onClick={cancelEditSubmission}
className="text-gray-400 hover:text-gray-600"
>
<XMarkIcon className="h-6 w-6" />
</button>
</div>
<div className="mb-4">
<textarea
value={editSubmissionText}
onChange={(e) => setEditSubmissionText(e.target.value)}
className="w-full px-4 py-3 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-purple-500 focus:border-purple-500"
rows={6}
placeholder="Enter your translation..."
/>
</div>
<div className="flex justify-end space-x-3">
<button
onClick={cancelEditSubmission}
className="px-4 py-2 text-gray-600 hover:text-gray-800 border border-gray-300 rounded-lg"
>
Cancel
</button>
<button
onClick={saveEditedSubmission}
className="px-4 py-2 bg-purple-600 text-white rounded-lg hover:bg-purple-700"
>
Save Changes
</button>
</div>
</div>
</div>
)}
</div>
);
};
export default WeeklyPractice;