transcreation-frontend / src /pages /TutorialTasks.tsx
Tristan Yu
Update src/pages/TutorialTasks.tsx with working version
14ab027
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;