Spaces:
Sleeping
Sleeping
| import React, { useState, useEffect, useCallback } from 'react'; | |
| import { useNavigate } from 'react-router-dom'; | |
| import { api } from '../services/api'; | |
| import { | |
| AcademicCapIcon, | |
| DocumentTextIcon, | |
| CheckCircleIcon, | |
| ClockIcon, | |
| ArrowRightIcon, | |
| PencilIcon, | |
| XMarkIcon, | |
| CheckIcon, | |
| PlusIcon, | |
| TrashIcon | |
| } from '@heroicons/react/24/outline'; | |
| interface TutorialTask { | |
| _id: string; | |
| content: string; | |
| weekNumber: number; | |
| translationBrief?: string; | |
| imageUrl?: string; | |
| imageAlt?: string; | |
| } | |
| interface TutorialWeek { | |
| weekNumber: number; | |
| translationBrief?: string; | |
| tasks: TutorialTask[]; | |
| } | |
| interface UserSubmission { | |
| _id: string; | |
| transcreation: string; | |
| status: string; | |
| score: number; | |
| groupNumber?: number; | |
| isOwner?: boolean; | |
| userId?: { | |
| _id: string; | |
| username: string; | |
| }; | |
| voteCounts: { | |
| '1': number; | |
| '2': number; | |
| '3': number; | |
| }; | |
| } | |
| const TutorialTasks: React.FC = () => { | |
| const [selectedWeek, setSelectedWeek] = useState<number>(() => { | |
| const savedWeek = localStorage.getItem('selectedTutorialWeek'); | |
| return savedWeek ? parseInt(savedWeek) : 1; | |
| }); | |
| const [tutorialTasks, setTutorialTasks] = useState<TutorialTask[]>([]); | |
| const [tutorialWeek, setTutorialWeek] = useState<TutorialWeek | 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 [selectedGroups, setSelectedGroups] = useState<{[key: string]: number}>({}); | |
| const [expandedSections, setExpandedSections] = useState<{[key: string]: boolean}>({}); | |
| const [editingTask, setEditingTask] = useState<string | null>(null); | |
| const [editingBrief, setEditingBrief] = useState<{[key: number]: boolean}>({}); | |
| const [addingTask, setAddingTask] = useState<boolean>(false); | |
| const [editForm, setEditForm] = useState<{ | |
| content: string; | |
| translationBrief: string; | |
| imageUrl: string; | |
| imageAlt: string; | |
| }>({ | |
| content: '', | |
| translationBrief: '', | |
| imageUrl: '', | |
| imageAlt: '' | |
| }); | |
| const [selectedFile, setSelectedFile] = useState<File | null>(null); | |
| const [uploading, setUploading] = useState(false); | |
| const [saving, setSaving] = useState(false); | |
| const navigate = useNavigate(); | |
| const weeks = [1, 2, 3, 4, 5, 6]; | |
| const handleFileUpload = async (file: File): Promise<string> => { | |
| try { | |
| setUploading(true); | |
| // Convert file to data URL for display | |
| return new Promise((resolve, reject) => { | |
| const reader = new FileReader(); | |
| reader.onload = () => { | |
| const dataUrl = reader.result as string; | |
| console.log('File uploaded:', file.name, 'Size:', file.size); | |
| console.log('Generated data URL:', dataUrl.substring(0, 50) + '...'); | |
| resolve(dataUrl); | |
| }; | |
| reader.onerror = () => { | |
| console.error('Error reading file:', reader.error); | |
| reject(reader.error); | |
| }; | |
| reader.readAsDataURL(file); | |
| }); | |
| } catch (error) { | |
| console.error('Error uploading file:', error); | |
| throw error; | |
| } finally { | |
| setUploading(false); | |
| } | |
| }; | |
| const handleFileChange = (event: React.ChangeEvent<HTMLInputElement>) => { | |
| const file = event.target.files?.[0]; | |
| if (file) { | |
| setSelectedFile(file); | |
| } | |
| }; | |
| const toggleExpanded = (taskId: string) => { | |
| setExpandedSections(prev => ({ | |
| ...prev, | |
| [taskId]: !prev[taskId] | |
| })); | |
| }; | |
| const fetchUserSubmissions = useCallback(async (tasks: TutorialTask[]) => { | |
| try { | |
| const token = localStorage.getItem('token'); | |
| const user = localStorage.getItem('user'); | |
| if (!token || !user) { | |
| return; | |
| } | |
| const response = await api.get('/submissions/my-submissions'); | |
| if (response.data && response.data.submissions) { | |
| const data = response.data; | |
| const groupedSubmissions: {[key: string]: UserSubmission[]} = {}; | |
| // Initialize all tasks with empty arrays | |
| tasks.forEach(task => { | |
| groupedSubmissions[task._id] = []; | |
| }); | |
| // Then populate with actual submissions | |
| if (data.submissions && Array.isArray(data.submissions)) { | |
| data.submissions.forEach((submission: any) => { | |
| // Extract the actual ID from sourceTextId (could be string or object) | |
| const sourceTextId = submission.sourceTextId?._id || submission.sourceTextId; | |
| if (sourceTextId && groupedSubmissions[sourceTextId]) { | |
| groupedSubmissions[sourceTextId].push(submission); | |
| } | |
| }); | |
| } | |
| setUserSubmissions(groupedSubmissions); | |
| } | |
| } catch (error) { | |
| console.error('Error fetching user submissions:', error); | |
| } | |
| }, []); | |
| const fetchTutorialTasks = useCallback(async (showLoading = true) => { | |
| try { | |
| if (showLoading) { | |
| setLoading(true); | |
| } | |
| const token = localStorage.getItem('token'); | |
| const user = localStorage.getItem('user'); | |
| if (!token || !user) { | |
| navigate('/login'); | |
| return; | |
| } | |
| const response = await api.get(`/search/tutorial-tasks/${selectedWeek}`); | |
| if (response.data) { | |
| const tasks = response.data; | |
| console.log('Fetched tasks:', tasks); | |
| console.log('Tasks with images:', tasks.filter((task: TutorialTask) => task.imageUrl)); | |
| // Debug: Log each task's fields | |
| tasks.forEach((task: any, index: number) => { | |
| console.log(`Task ${index} fields:`, { | |
| _id: task._id, | |
| content: task.content, | |
| imageUrl: task.imageUrl, | |
| imageAlt: task.imageAlt, | |
| translationBrief: task.translationBrief, | |
| weekNumber: task.weekNumber, | |
| category: task.category | |
| }); | |
| console.log(`Task ${index} imageUrl:`, task.imageUrl); | |
| console.log(`Task ${index} translationBrief:`, task.translationBrief); | |
| }); | |
| setTutorialTasks(tasks); | |
| // Use translation brief from tasks or localStorage | |
| let translationBrief = null; | |
| if (tasks.length > 0) { | |
| translationBrief = tasks[0].translationBrief; | |
| console.log('Translation brief from task:', translationBrief); | |
| } else { | |
| // Check localStorage for brief if no tasks exist | |
| const briefKey = `translationBrief_week_${selectedWeek}`; | |
| translationBrief = localStorage.getItem(briefKey); | |
| console.log('Translation brief from localStorage:', translationBrief); | |
| console.log('localStorage key:', briefKey); | |
| } | |
| console.log('Final translation brief:', translationBrief); | |
| const tutorialWeekData: TutorialWeek = { | |
| weekNumber: selectedWeek, | |
| translationBrief: translationBrief, | |
| tasks: tasks | |
| }; | |
| setTutorialWeek(tutorialWeekData); | |
| await fetchUserSubmissions(tasks); | |
| } else { | |
| console.error('Failed to fetch tutorial tasks'); | |
| } | |
| } catch (error) { | |
| console.error('Error fetching tutorial tasks:', error); | |
| } finally { | |
| if (showLoading) { | |
| setLoading(false); | |
| } | |
| } | |
| }, [selectedWeek, fetchUserSubmissions, navigate]); | |
| useEffect(() => { | |
| const user = localStorage.getItem('user'); | |
| if (!user) { | |
| navigate('/login'); | |
| return; | |
| } | |
| fetchTutorialTasks(); | |
| }, [fetchTutorialTasks, navigate]); | |
| // Refresh submissions when user changes (after login/logout) | |
| useEffect(() => { | |
| const user = localStorage.getItem('user'); | |
| if (user && tutorialTasks.length > 0) { | |
| fetchUserSubmissions(tutorialTasks); | |
| } | |
| }, [tutorialTasks, fetchUserSubmissions]); | |
| const handleSubmitTranslation = async (taskId: string) => { | |
| if (!translationText[taskId]?.trim()) { | |
| return; | |
| } | |
| if (!selectedGroups[taskId]) { | |
| return; | |
| } | |
| try { | |
| setSubmitting({ ...submitting, [taskId]: true }); | |
| const user = JSON.parse(localStorage.getItem('user') || '{}'); | |
| const response = await api.post('/submissions', { | |
| sourceTextId: taskId, | |
| transcreation: translationText[taskId], | |
| groupNumber: selectedGroups[taskId], | |
| culturalAdaptations: [], | |
| username: user.name || 'Unknown' | |
| }); | |
| if (response.status >= 200 && response.status < 300) { | |
| const result = response.data; | |
| console.log('Submission created successfully:', result); | |
| setTranslationText({ ...translationText, [taskId]: '' }); | |
| setSelectedGroups({ ...selectedGroups, [taskId]: 0 }); | |
| await fetchUserSubmissions(tutorialTasks); | |
| } else { | |
| console.error('Failed to submit translation:', response.data); | |
| } | |
| } catch (error) { | |
| console.error('Error submitting translation:', error); | |
| } finally { | |
| setSubmitting({ ...submitting, [taskId]: false }); | |
| } | |
| }; | |
| const [editingSubmission, setEditingSubmission] = useState<{id: string, text: string} | null>(null); | |
| const [editSubmissionText, setEditSubmissionText] = useState(''); | |
| const handleEditSubmission = async (submissionId: string, currentText: string) => { | |
| setEditingSubmission({ id: submissionId, text: currentText }); | |
| setEditSubmissionText(currentText); | |
| }; | |
| const saveEditedSubmission = async () => { | |
| if (!editingSubmission || !editSubmissionText.trim()) return; | |
| try { | |
| const response = await api.put(`/submissions/${editingSubmission.id}`, { | |
| transcreation: editSubmissionText | |
| }); | |
| if (response.status >= 200 && response.status < 300) { | |
| setEditingSubmission(null); | |
| setEditSubmissionText(''); | |
| await fetchUserSubmissions(tutorialTasks); | |
| } else { | |
| console.error('Failed to update translation:', response.data); | |
| } | |
| } catch (error) { | |
| console.error('Error updating translation:', error); | |
| } | |
| }; | |
| const cancelEditSubmission = () => { | |
| setEditingSubmission(null); | |
| setEditSubmissionText(''); | |
| }; | |
| const handleDeleteSubmission = async (submissionId: string) => { | |
| try { | |
| const response = await api.delete(`/submissions/${submissionId}`); | |
| if (response.status === 200) { | |
| await fetchUserSubmissions(tutorialTasks); | |
| } else { | |
| } | |
| } catch (error) { | |
| console.error('Error deleting submission:', error); | |
| } | |
| }; | |
| const getStatusIcon = (status: string) => { | |
| switch (status) { | |
| case 'approved': | |
| return <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 = (task: TutorialTask) => { | |
| setEditingTask(task._id); | |
| setEditForm({ | |
| content: task.content, | |
| translationBrief: task.translationBrief || '', | |
| imageUrl: task.imageUrl || '', | |
| imageAlt: task.imageAlt || '' | |
| }); | |
| }; | |
| const startEditingBrief = () => { | |
| setEditingBrief(prev => ({ ...prev, [selectedWeek]: true })); | |
| setEditForm({ | |
| content: '', | |
| translationBrief: tutorialWeek?.translationBrief || '', | |
| imageUrl: '', | |
| imageAlt: '' | |
| }); | |
| }; | |
| const startAddingBrief = () => { | |
| setEditingBrief(prev => ({ ...prev, [selectedWeek]: true })); | |
| setEditForm({ | |
| content: '', | |
| translationBrief: '', | |
| imageUrl: '', | |
| imageAlt: '' | |
| }); | |
| }; | |
| const removeBrief = async () => { | |
| try { | |
| setSaving(true); | |
| const token = localStorage.getItem('token'); | |
| const user = JSON.parse(localStorage.getItem('user') || '{}'); | |
| // Check if user is admin | |
| if (user.role !== 'admin') { | |
| return; | |
| } | |
| const response = await api.put(`/auth/admin/tutorial-brief/${selectedWeek}`, { | |
| translationBrief: '', | |
| weekNumber: selectedWeek | |
| }); | |
| if (response.status >= 200 && response.status < 300) { | |
| await fetchTutorialTasks(); | |
| } else { | |
| console.error('Failed to remove translation brief:', response.data); | |
| } | |
| } catch (error) { | |
| console.error('Failed to remove translation brief:', error); | |
| } finally { | |
| setSaving(false); | |
| } | |
| }; | |
| const cancelEditing = () => { | |
| setEditingTask(null); | |
| setEditingBrief(prev => ({ ...prev, [selectedWeek]: false })); | |
| setEditForm({ | |
| content: '', | |
| translationBrief: '', | |
| imageUrl: '', | |
| imageAlt: '' | |
| }); | |
| setSelectedFile(null); | |
| }; | |
| const saveTask = async () => { | |
| if (!editingTask) return; | |
| try { | |
| setSaving(true); | |
| const token = localStorage.getItem('token'); | |
| const user = JSON.parse(localStorage.getItem('user') || '{}'); | |
| // Check if user is admin | |
| if (user.role !== 'admin') { | |
| return; | |
| } | |
| const updateData = { | |
| ...editForm, | |
| weekNumber: selectedWeek | |
| }; | |
| console.log('Saving task with data:', updateData); | |
| const response = await api.put(`/auth/admin/tutorial-tasks/${editingTask}`, updateData); | |
| if (response.status >= 200 && response.status < 300) { | |
| await fetchTutorialTasks(false); | |
| setEditingTask(null); | |
| } else { | |
| console.error('Failed to update tutorial task:', response.data); | |
| } | |
| } catch (error) { | |
| console.error('Failed to update tutorial task:', error); | |
| } finally { | |
| setSaving(false); | |
| } | |
| }; | |
| const saveBrief = async () => { | |
| try { | |
| setSaving(true); | |
| const user = JSON.parse(localStorage.getItem('user') || '{}'); | |
| // Check if user is admin | |
| if (user.role !== 'admin') { | |
| return; | |
| } | |
| console.log('Saving brief for week:', selectedWeek); | |
| console.log('Brief content:', editForm.translationBrief); | |
| // Save brief by creating or updating the first task of the week | |
| if (tutorialTasks.length > 0) { | |
| const firstTask = tutorialTasks[0]; | |
| console.log('Updating first task with brief:', firstTask._id); | |
| const response = await api.put(`/auth/admin/tutorial-tasks/${firstTask._id}`, { | |
| ...firstTask, | |
| translationBrief: editForm.translationBrief, | |
| weekNumber: selectedWeek | |
| }); | |
| if (response.status >= 200 && response.status < 300) { | |
| console.log('Brief saved successfully'); | |
| await fetchTutorialTasks(false); | |
| setEditingBrief(prev => ({ ...prev, [selectedWeek]: false })); | |
| } else { | |
| console.error('Failed to save brief:', response.data); | |
| } | |
| } else { | |
| // If no tasks exist, save the brief to localStorage | |
| console.log('No tasks available to save brief to - saving to localStorage'); | |
| const briefKey = `translationBrief_week_${selectedWeek}`; | |
| localStorage.setItem(briefKey, editForm.translationBrief); | |
| setEditingBrief(prev => ({ ...prev, [selectedWeek]: false })); | |
| } | |
| } catch (error) { | |
| console.error('Failed to update translation brief:', error); | |
| } finally { | |
| setSaving(false); | |
| } | |
| }; | |
| const startAddingTask = () => { | |
| setAddingTask(true); | |
| setEditForm({ | |
| content: '', | |
| translationBrief: '', | |
| imageUrl: '', | |
| imageAlt: '' | |
| }); | |
| }; | |
| const cancelAddingTask = () => { | |
| setAddingTask(false); | |
| setEditForm({ | |
| content: '', | |
| translationBrief: '', | |
| imageUrl: '', | |
| imageAlt: '' | |
| }); | |
| setSelectedFile(null); | |
| }; | |
| const saveNewTask = async () => { | |
| try { | |
| setSaving(true); | |
| const user = JSON.parse(localStorage.getItem('user') || '{}'); | |
| // Check if user is admin | |
| if (user.role !== 'admin') { | |
| return; | |
| } | |
| if (!editForm.content.trim()) { | |
| return; | |
| } | |
| console.log('Saving new task for week:', selectedWeek); | |
| console.log('Task content:', editForm.content); | |
| console.log('Image URL:', editForm.imageUrl); | |
| console.log('Image Alt:', editForm.imageAlt); | |
| const taskData = { | |
| title: `Week ${selectedWeek} Tutorial Task`, | |
| content: editForm.content, | |
| sourceLanguage: 'English', | |
| weekNumber: selectedWeek, | |
| category: 'tutorial', | |
| imageUrl: editForm.imageUrl || null, | |
| imageAlt: editForm.imageAlt || null | |
| }; | |
| console.log('Task data being sent:', JSON.stringify(taskData, null, 2)); | |
| console.log('Sending task data:', taskData); | |
| const response = await api.post('/auth/admin/tutorial-tasks', taskData); | |
| console.log('Task save response:', response.data); | |
| if (response.status >= 200 && response.status < 300) { | |
| console.log('Task saved successfully'); | |
| console.log('Saved task response:', response.data); | |
| console.log('Saved task response keys:', Object.keys(response.data || {})); | |
| console.log('Saved task response.task:', response.data?.task); | |
| console.log('Saved task response.task.imageUrl:', response.data?.task?.imageUrl); | |
| console.log('Saved task response.task.translationBrief:', response.data?.task?.translationBrief); | |
| await fetchTutorialTasks(false); | |
| setAddingTask(false); | |
| } else { | |
| console.error('Failed to add tutorial task:', response.data); | |
| } | |
| } catch (error) { | |
| console.error('Failed to add tutorial task:', error); | |
| } finally { | |
| setSaving(false); | |
| } | |
| }; | |
| const deleteTask = async (taskId: string) => { | |
| try { | |
| const token = localStorage.getItem('token'); | |
| const user = JSON.parse(localStorage.getItem('user') || '{}'); | |
| // Check if user is admin | |
| if (user.role !== 'admin') { | |
| return; | |
| } | |
| const response = await api.delete(`/auth/admin/tutorial-tasks/${taskId}`); | |
| if (response.status >= 200 && response.status < 300) { | |
| await fetchTutorialTasks(false); | |
| } else { | |
| console.error('Failed to delete tutorial task:', response.data); | |
| } | |
| } catch (error) { | |
| console.error('Failed to delete tutorial task:', error); | |
| } | |
| }; | |
| // Remove intrusive loading screen - just show content with loading state | |
| return ( | |
| <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"> | |
| <AcademicCapIcon className="h-8 w-8 text-indigo-900 mr-3" /> | |
| <h1 className="text-3xl font-bold text-gray-900">Tutorial Tasks</h1> | |
| </div> | |
| <p className="text-gray-600"> | |
| Complete weekly tutorial tasks with your group to practice collaborative translation skills. | |
| </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('selectedTutorialWeek', 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 */} | |
| {tutorialWeek && tutorialWeek.translationBrief ? ( | |
| <div className="bg-white rounded-lg p-6 mb-8 border border-gray-200 shadow-sm"> | |
| <div className="flex items-center justify-between mb-4"> | |
| <div className="flex items-center space-x-3"> | |
| <div className="bg-indigo-600 rounded-lg p-2"> | |
| <DocumentTextIcon className="h-5 w-5 text-white" /> | |
| </div> | |
| <h3 className="text-indigo-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-indigo-500 hover:bg-indigo-600 text-white px-3 py-1 rounded-md transition-colors duration-200 text-sm" | |
| > | |
| {saving ? ( | |
| <div className="animate-spin rounded-full h-4 w-4 border-b-2 border-white"></div> | |
| ) : ( | |
| <CheckIcon className="h-4 w-4" /> | |
| )} | |
| </button> | |
| <button | |
| onClick={cancelEditing} | |
| className="bg-gray-500 hover:bg-gray-600 text-white px-3 py-1 rounded-md transition-colors duration-200 text-sm" | |
| > | |
| <XMarkIcon className="h-4 w-4" /> | |
| </button> | |
| </> | |
| ) : ( | |
| <> | |
| <button | |
| onClick={startEditingBrief} | |
| className="bg-gray-100 hover:bg-gray-200 text-gray-700 px-3 py-1 rounded-md transition-colors duration-200 text-sm" | |
| > | |
| <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-md transition-colors duration-200 text-sm" | |
| > | |
| <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-gray-300 rounded-lg text-gray-900 leading-relaxed text-base bg-white focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500" | |
| rows={6} | |
| placeholder="Enter translation brief..." | |
| /> | |
| ) : ( | |
| <p className="text-gray-900 leading-relaxed text-lg font-smiley">{tutorialWeek.translationBrief}</p> | |
| )} | |
| <div className="mt-4 p-3 bg-indigo-50 rounded-lg"> | |
| <p className="text-indigo-900 text-sm"> | |
| <strong>Note:</strong> Tutorial tasks are completed as group submissions. Each group should collaborate to create a single translation per task. | |
| </p> | |
| </div> | |
| </div> | |
| ) : ( | |
| // Show add brief button when no brief exists | |
| JSON.parse(localStorage.getItem('user') || '{}').role === 'admin' && ( | |
| <div className="bg-white rounded-lg p-6 mb-8 border border-gray-200 border-dashed shadow-sm"> | |
| <div className="flex items-center justify-between mb-4"> | |
| <div className="flex items-center space-x-3"> | |
| <div className="bg-indigo-100 rounded-lg p-2"> | |
| <DocumentTextIcon className="h-5 w-5 text-indigo-900" /> | |
| </div> | |
| <h3 className="text-indigo-900 font-semibold text-xl">Translation Brief</h3> | |
| </div> | |
| <button | |
| onClick={startAddingBrief} | |
| className="bg-indigo-600 hover:bg-indigo-700 text-white px-6 py-3 rounded-lg font-medium flex items-center space-x-2 shadow-sm" | |
| > | |
| <PlusIcon className="h-5 w-5" /> | |
| <span className="font-medium">Add 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-gray-300 rounded-lg text-gray-900 leading-relaxed text-base bg-white focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500" | |
| rows={6} | |
| placeholder="Enter translation brief..." | |
| /> | |
| <div className="flex justify-end space-x-2"> | |
| <button | |
| onClick={saveBrief} | |
| disabled={saving} | |
| className="bg-indigo-600 hover:bg-indigo-700 text-white px-6 py-3 rounded-lg font-medium flex items-center space-x-2 shadow-sm" | |
| > | |
| {saving ? ( | |
| <div className="animate-spin rounded-full h-4 w-4 border-b-2 border-white"></div> | |
| ) : ( | |
| <> | |
| <CheckIcon className="h-5 w-5" /> | |
| <span className="font-medium">Save Brief</span> | |
| </> | |
| )} | |
| </button> | |
| <button | |
| onClick={cancelEditing} | |
| className="bg-gray-500 hover:bg-gray-600 text-white px-6 py-3 rounded-lg font-medium flex items-center space-x-2 shadow-sm" | |
| > | |
| <XMarkIcon className="h-5 w-5" /> | |
| <span className="font-medium">Cancel</span> | |
| </button> | |
| </div> | |
| </div> | |
| )} | |
| </div> | |
| ) | |
| )} | |
| {/* Tutorial Tasks */} | |
| <div className="space-y-6"> | |
| {/* Add New Tutorial Task Section */} | |
| {JSON.parse(localStorage.getItem('user') || '{}').role === 'admin' && ( | |
| <div className="mb-8"> | |
| {addingTask ? ( | |
| <div className="bg-white rounded-lg p-6 border border-gray-200 shadow-sm"> | |
| <div className="flex items-center space-x-3 mb-4"> | |
| <div className="bg-gray-100 rounded-lg p-2"> | |
| <PlusIcon className="h-4 w-4 text-gray-600" /> | |
| </div> | |
| <h4 className="text-gray-900 font-semibold text-lg">New Tutorial Task</h4> | |
| </div> | |
| <div className="space-y-4"> | |
| <div> | |
| <label className="block text-sm font-medium text-gray-700 mb-2"> | |
| Task Content * | |
| </label> | |
| <textarea | |
| value={editForm.content} | |
| onChange={(e) => setEditForm({ ...editForm, content: e.target.value })} | |
| className="w-full px-4 py-3 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500 bg-white" | |
| rows={4} | |
| placeholder="Enter tutorial task content..." | |
| /> | |
| </div> | |
| <div className="space-y-4"> | |
| <div className="grid grid-cols-1 md:grid-cols-2 gap-4"> | |
| <div> | |
| <label className="block text-sm font-medium text-gray-700 mb-2"> | |
| Image URL (Optional) | |
| </label> | |
| <input | |
| type="url" | |
| value={editForm.imageUrl} | |
| onChange={(e) => setEditForm({ ...editForm, imageUrl: e.target.value })} | |
| className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500" | |
| placeholder="https://example.com/image.jpg" | |
| /> | |
| </div> | |
| <div> | |
| <label className="block text-sm font-medium text-gray-700 mb-2"> | |
| Image Alt Text (Optional) | |
| </label> | |
| <input | |
| type="text" | |
| value={editForm.imageAlt} | |
| onChange={(e) => setEditForm({ ...editForm, imageAlt: e.target.value })} | |
| className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500" | |
| placeholder="Description of the image" | |
| /> | |
| </div> | |
| </div> | |
| {/* File Upload Section - Only for Week 2+ */} | |
| {selectedWeek >= 2 && ( | |
| <div className="border-t pt-4"> | |
| <label className="block text-sm font-medium text-gray-700 mb-2"> | |
| Upload Local Image (Optional) | |
| </label> | |
| <div className="space-y-2"> | |
| <input | |
| type="file" | |
| accept="image/*" | |
| onChange={handleFileChange} | |
| className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500" | |
| /> | |
| {selectedFile && ( | |
| <div className="flex items-center space-x-2"> | |
| <span className="text-sm text-gray-600">{selectedFile.name}</span> | |
| <button | |
| type="button" | |
| onClick={async () => { | |
| try { | |
| const imageUrl = await handleFileUpload(selectedFile); | |
| setEditForm({ ...editForm, imageUrl }); | |
| setSelectedFile(null); | |
| } catch (error) { | |
| console.error('Upload error:', error); | |
| } | |
| }} | |
| disabled={uploading} | |
| className="bg-green-500 hover:bg-green-600 disabled:bg-gray-400 text-white px-3 py-1 rounded text-sm" | |
| > | |
| {uploading ? 'Uploading...' : 'Upload'} | |
| </button> | |
| </div> | |
| )} | |
| </div> | |
| </div> | |
| )} | |
| </div> | |
| </div> | |
| <div className="flex justify-end space-x-2 mt-4"> | |
| <button | |
| onClick={saveNewTask} | |
| disabled={saving} | |
| className="bg-indigo-500 hover:bg-indigo-600 text-white px-4 py-2 rounded-lg transition-colors duration-200 flex items-center space-x-2 text-sm" | |
| > | |
| {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 Task</span> | |
| </> | |
| )} | |
| </button> | |
| <button | |
| onClick={cancelAddingTask} | |
| 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 text-sm" | |
| > | |
| <XMarkIcon className="h-4 w-4" /> | |
| <span>Cancel</span> | |
| </button> | |
| </div> | |
| </div> | |
| ) : ( | |
| <div className="bg-white rounded-lg p-6 border border-gray-200 border-dashed shadow-sm"> | |
| <div className="flex items-center justify-between"> | |
| <div className="flex items-center space-x-3"> | |
| <div className="bg-gray-100 rounded-lg p-2"> | |
| <PlusIcon className="h-5 w-5 text-gray-600" /> | |
| </div> | |
| <div> | |
| <h3 className="text-lg font-semibold text-indigo-900">Add New Tutorial Task</h3> | |
| <p className="text-gray-600 text-sm">Create a new tutorial task for Week {selectedWeek}</p> | |
| </div> | |
| </div> | |
| <button | |
| onClick={startAddingTask} | |
| className="bg-indigo-600 hover:bg-indigo-700 text-white px-6 py-3 rounded-lg font-medium flex items-center space-x-2 shadow-sm" | |
| > | |
| <PlusIcon className="h-5 w-5" /> | |
| <span className="font-medium">Add Task</span> | |
| </button> | |
| </div> | |
| </div> | |
| )} | |
| </div> | |
| )} | |
| {tutorialTasks.length === 0 && !addingTask ? ( | |
| <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 tutorial tasks available | |
| </h3> | |
| <p className="text-gray-600"> | |
| Tutorial tasks for Week {selectedWeek} haven't been set up yet. | |
| </p> | |
| </div> | |
| ) : ( | |
| tutorialTasks.map((task) => ( | |
| <div key={task._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-indigo-100 rounded-full p-2"> | |
| <DocumentTextIcon className="h-5 w-5 text-indigo-900" /> | |
| </div> | |
| <div> | |
| <h3 className="text-lg font-semibold text-gray-900">Source Text #{tutorialTasks.indexOf(task) + 1}</h3> | |
| </div> | |
| </div> | |
| {JSON.parse(localStorage.getItem('user') || '{}').role === 'admin' && ( | |
| <div className="flex items-center space-x-2"> | |
| {editingTask === task._id ? ( | |
| <> | |
| <button | |
| onClick={saveTask} | |
| 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(task)} | |
| className="bg-gray-100 hover:bg-gray-200 text-gray-700 px-3 py-1 rounded-md transition-colors duration-200 text-sm" | |
| > | |
| <PencilIcon className="h-4 w-4" /> | |
| </button> | |
| <button | |
| onClick={() => deleteTask(task._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 - Clean styling with image support */} | |
| <div className="bg-gradient-to-r from-indigo-50 to-blue-50 rounded-xl p-6 mb-6 border border-indigo-200"> | |
| {editingTask === task._id ? ( | |
| <div className="space-y-4"> | |
| <textarea | |
| value={editForm.content} | |
| onChange={(e) => setEditForm({...editForm, content: e.target.value})} | |
| className="w-full px-4 py-3 border border-indigo-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500 bg-white" | |
| rows={5} | |
| placeholder="Enter source text..." | |
| /> | |
| <div className="space-y-4"> | |
| <div className="grid grid-cols-1 md:grid-cols-2 gap-4"> | |
| <div> | |
| <label className="block text-sm font-medium text-gray-700 mb-2">Image URL</label> | |
| <input | |
| type="url" | |
| value={editForm.imageUrl} | |
| onChange={(e) => setEditForm({...editForm, imageUrl: e.target.value})} | |
| className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500" | |
| placeholder="https://example.com/image.jpg" | |
| /> | |
| </div> | |
| <div> | |
| <label className="block text-sm font-medium text-gray-700 mb-2">Image Alt Text</label> | |
| <input | |
| type="text" | |
| value={editForm.imageAlt} | |
| onChange={(e) => setEditForm({...editForm, imageAlt: e.target.value})} | |
| className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500" | |
| placeholder="Description of the image" | |
| /> | |
| </div> | |
| </div> | |
| {/* File Upload Section - Only for Week 2+ */} | |
| {selectedWeek >= 2 && ( | |
| <div className="border-t pt-4"> | |
| <label className="block text-sm font-medium text-gray-700 mb-2"> | |
| Upload Local Image (Optional) | |
| </label> | |
| <div className="space-y-2"> | |
| <input | |
| type="file" | |
| accept="image/*" | |
| onChange={handleFileChange} | |
| className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500" | |
| /> | |
| {selectedFile && ( | |
| <div className="flex items-center space-x-2"> | |
| <span className="text-sm text-gray-600">{selectedFile.name}</span> | |
| <button | |
| type="button" | |
| onClick={async () => { | |
| try { | |
| const imageUrl = await handleFileUpload(selectedFile); | |
| console.log('Uploaded image URL:', imageUrl); | |
| setEditForm({ ...editForm, imageUrl }); | |
| console.log('Updated editForm:', { ...editForm, imageUrl }); | |
| // Save the task with the new image URL | |
| if (editingTask) { | |
| console.log('Saving task with image URL:', imageUrl); | |
| const response = await api.put(`/auth/admin/tutorial-tasks/${editingTask}`, { | |
| ...editForm, | |
| imageUrl, | |
| weekNumber: selectedWeek | |
| }); | |
| console.log('Task save response:', response.data); | |
| if (response.status >= 200 && response.status < 300) { | |
| await fetchTutorialTasks(false); // Refresh tasks | |
| } | |
| } | |
| setSelectedFile(null); | |
| } catch (error) { | |
| console.error('Upload error:', error); | |
| } | |
| }} | |
| disabled={uploading} | |
| className="bg-green-500 hover:bg-green-600 disabled:bg-gray-400 text-white px-3 py-1 rounded text-sm" | |
| > | |
| {uploading ? 'Uploading...' : 'Upload'} | |
| </button> | |
| </div> | |
| )} | |
| </div> | |
| </div> | |
| )} | |
| </div> | |
| </div> | |
| ) : ( | |
| <div className="space-y-4"> | |
| {task.imageUrl ? ( | |
| // Side-by-side layout when image exists | |
| <div className="flex flex-col md:flex-row gap-6 items-start"> | |
| {/* Image on the left - 50% width */} | |
| <div className="w-full md:w-1/2 flex justify-center"> | |
| {task.imageUrl.startsWith('data:') ? ( | |
| // Show actual image if it's a data URL | |
| <div className="inline-block rounded-lg shadow-md overflow-hidden"> | |
| <img | |
| src={task.imageUrl} | |
| alt={task.imageAlt || 'Uploaded image'} | |
| className="w-full h-auto" | |
| style={{ height: '200px', width: 'auto', objectFit: 'contain' }} // Fixed height for consistency | |
| onError={(e) => { | |
| console.error('Image failed to load:', e); | |
| e.currentTarget.style.display = 'none'; | |
| }} | |
| /> | |
| {task.imageAlt && ( | |
| <div className="text-xs text-gray-500 mt-2 text-center">Alt: {task.imageAlt}</div> | |
| )} | |
| </div> | |
| ) : ( | |
| // Show placeholder if it's not a data URL | |
| <div className="inline-block rounded-lg shadow-md bg-green-500 text-white p-6 text-center"> | |
| <div className="text-3xl mb-2">📷</div> | |
| <div className="font-semibold">Image Uploaded</div> | |
| <div className="text-sm opacity-75">{task.imageUrl}</div> | |
| {task.imageAlt && ( | |
| <div className="text-xs opacity-50 mt-2">Alt: {task.imageAlt}</div> | |
| )} | |
| </div> | |
| )} | |
| </div> | |
| {/* Text on the right - 50% width */} | |
| <div className="w-full md:w-1/2"> | |
| <div className="text-indigo-900 leading-relaxed text-lg font-source-text whitespace-pre-wrap">{task.content}</div> | |
| </div> | |
| </div> | |
| ) : ( | |
| // Text only when no image | |
| <div> | |
| <div className="text-indigo-900 leading-relaxed text-lg font-source-text whitespace-pre-wrap">{task.content}</div> | |
| </div> | |
| )} | |
| {(() => { console.log('Task imageUrl check:', task._id, task.imageUrl, !!task.imageUrl); return null; })()} | |
| </div> | |
| )} | |
| </div> | |
| </div> | |
| {/* All Submissions for this Task */} | |
| {userSubmissions[task._id] && userSubmissions[task._id].length > 0 && ( | |
| <div className="bg-gradient-to-r from-white to-indigo-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-indigo-100 rounded-full p-1"> | |
| <CheckCircleIcon className="h-4 w-4 text-indigo-900" /> | |
| </div> | |
| <h4 className="text-stone-900 font-semibold text-lg">All Submissions ({userSubmissions[task._id].length})</h4> | |
| </div> | |
| <button | |
| onClick={() => toggleExpanded(task._id)} | |
| className="flex items-center space-x-1 text-indigo-900 hover:text-indigo-900 text-sm font-medium" | |
| > | |
| <span>{expandedSections[task._id] ? 'Collapse' : 'Expand'}</span> | |
| <svg | |
| className={`w-4 h-4 transition-transform duration-200 ${expandedSections[task._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[task._id] | |
| ? 'max-h-none overflow-visible' | |
| : 'max-h-0 overflow-hidden' | |
| }`}> | |
| {userSubmissions[task._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">Group:</span> | |
| <span className="bg-indigo-100 px-1.5 py-0.5 rounded-full text-indigo-900 text-xs"> | |
| {submission.groupNumber} | |
| </span> | |
| </div> | |
| <div className="flex items-center space-x-1"> | |
| <span className="font-medium">Votes:</span> | |
| <span className="bg-indigo-100 px-1.5 py-0.5 rounded-full text-indigo-900 text-xs"> | |
| {(submission.voteCounts?.['1'] || 0) + (submission.voteCounts?.['2'] || 0) + (submission.voteCounts?.['3'] || 0)} | |
| </span> | |
| </div> | |
| </div> | |
| <div className="flex items-center space-x-2 mt-2"> | |
| {submission.isOwner && ( | |
| <button | |
| onClick={() => handleEditSubmission(submission._id, submission.transcreation)} | |
| className="text-indigo-900 hover:text-indigo-900 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" | |
| > | |
| Delete | |
| </button> | |
| )} | |
| </div> | |
| </div> | |
| ))} | |
| </div> | |
| </div> | |
| )} | |
| {/* Translation Input (always show if user is logged in) */} | |
| {localStorage.getItem('token') && ( | |
| <div className="bg-white rounded-lg p-6 border border-gray-200 shadow-sm"> | |
| <div className="flex items-center space-x-3 mb-4"> | |
| <div className="bg-gray-100 rounded-lg p-2"> | |
| <DocumentTextIcon className="h-4 w-4 text-gray-600" /> | |
| </div> | |
| <h4 className="text-gray-900 font-semibold text-lg">Group Translation</h4> | |
| </div> | |
| {/* Group Selection */} | |
| <div className="mb-4"> | |
| <label className="block text-sm font-medium text-gray-700 mb-2"> | |
| Select Your Group * | |
| </label> | |
| <select | |
| value={selectedGroups[task._id] || ''} | |
| onChange={(e) => setSelectedGroups({ ...selectedGroups, [task._id]: parseInt(e.target.value) })} | |
| className="w-48 px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500 bg-white text-sm" | |
| required | |
| > | |
| <option value="">Choose your group...</option> | |
| {[1, 2, 3, 4, 5, 6, 7, 8].map((group) => ( | |
| <option key={group} value={group}> | |
| Group {group} | |
| </option> | |
| ))} | |
| </select> | |
| </div> | |
| <div className="mb-4"> | |
| <label className="block text-sm font-medium text-gray-700 mb-2"> | |
| Your Group's Translation * | |
| </label> | |
| <textarea | |
| value={translationText[task._id] || ''} | |
| onChange={(e) => setTranslationText({ ...translationText, [task._id]: e.target.value })} | |
| className="w-full px-4 py-3 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500 bg-white" | |
| rows={4} | |
| placeholder="Enter your group's translation here..." | |
| /> | |
| </div> | |
| <button | |
| onClick={() => handleSubmitTranslation(task._id)} | |
| disabled={submitting[task._id]} | |
| className="bg-indigo-500 hover:bg-indigo-600 disabled:bg-gray-400 text-white px-6 py-3 rounded-lg font-medium flex items-center justify-center transition-all duration-200" | |
| > | |
| {submitting[task._id] ? ( | |
| <> | |
| <div className="animate-spin rounded-full h-4 w-4 border-b-2 border-white mr-2"></div> | |
| Submitting... | |
| </> | |
| ) : ( | |
| <> | |
| Submit Group 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-indigo-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 tutorial task. | |
| </p> | |
| <button | |
| onClick={() => window.location.href = '/login'} | |
| className="bg-indigo-500 hover:bg-indigo-600 text-white px-6 py-3 rounded-lg font-medium flex items-center justify-center transition-all duration-200" | |
| > | |
| 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 TutorialTasks; |