Site Maintainer commited on
Commit
330bf8a
·
1 Parent(s): 439dceb

chore: sync root client pages from deploy/frontend to match live build

Browse files
client/src/pages/TutorialTasks.tsx CHANGED
@@ -123,14 +123,13 @@ const TutorialTasks: React.FC = () => {
123
  const [docs, setDocs] = useState<any[]>([]);
124
  const [urlInput, setUrlInput] = useState<string>('');
125
  const [errorMsg, setErrorMsg] = useState<string>('');
126
- const [replaceMode, setReplaceMode] = useState<boolean>(false);
127
  const [copiedLink, setCopiedLink] = useState<string>('');
128
  const isAdmin = (JSON.parse(localStorage.getItem('user') || '{}').role === 'admin');
129
 
130
  const CopySquaresIcon: React.FC<{ className?: string }> = ({ className }) => (
131
- <svg className={className || 'h-4 w-4'} viewBox="0 0 20 20" fill="currentColor" xmlns="http://www.w3.org/2000/svg">
132
- <rect x="6" y="6" width="10" height="10" rx="2" className="opacity-100"/>
133
- <rect x="2" y="2" width="10" height="10" rx="2" className="opacity-60"/>
134
  </svg>
135
  );
136
 
@@ -164,7 +163,6 @@ const TutorialTasks: React.FC = () => {
164
  await api.post('/api/docs/create', { weekNumber, groupNumber: group, docUrl: url });
165
  await loadDocs();
166
  setUrlInput('');
167
- setReplaceMode(false);
168
  } finally {
169
  setCreating(false);
170
  }
@@ -182,27 +180,25 @@ const TutorialTasks: React.FC = () => {
182
  <div>
183
  {/* Top control row */}
184
  {isAdmin && (
185
- <div className="flex items-center justify-between gap-4 mb-4">
186
- <div className="flex-1">
187
- <label className="text-sm text-gray-700 block mb-1">Group</label>
188
- <select
189
- value={group}
190
- onChange={(e) => {
191
- const g = parseInt(e.target.value);
192
- setGroup(g);
193
- localStorage.setItem(`tutorial_group_${weekNumber}`, String(g));
194
- }}
195
- className="w-full px-3 py-2 border rounded-md text-sm"
196
- >
197
- {[1,2,3,4,5,6,7,8].map(g => <option key={g} value={g}>Group {g}</option>)}
198
- </select>
199
- </div>
200
  </div>
201
  )}
202
 
203
  {/* Replace / Add link inline editor */}
204
  {isAdmin && (
205
- <div className="mb-4">
206
  <div className="flex items-center gap-2">
207
  <input
208
  type="url"
@@ -212,12 +208,6 @@ const TutorialTasks: React.FC = () => {
212
  className="flex-1 px-3 py-2 border border-gray-300 rounded-md text-sm"
213
  />
214
  <button onClick={createDoc} disabled={creating} className="px-4 py-2 bg-indigo-600 text-white rounded-md text-sm">{creating ? 'Saving…' : (current ? 'Save new link' : 'Add Doc Link')}</button>
215
- {current && (
216
- <button onClick={() => setReplaceMode(v => !v)} className="px-4 py-2 bg-gray-100 text-gray-700 rounded-md text-sm inline-flex items-center gap-2">
217
- <ArrowsRightLeftIcon className="h-4 w-4" />
218
- {replaceMode ? 'Cancel' : 'Replace'}
219
- </button>
220
- )}
221
  </div>
222
  {errorMsg && <div className="mt-1 text-xs text-red-600">{errorMsg}</div>}
223
  </div>
@@ -247,11 +237,11 @@ const TutorialTasks: React.FC = () => {
247
  <ArrowTopRightOnSquareIcon className="h-4 w-4" /> Open
248
  </a>
249
  <button onClick={() => copyLink(d.docUrl)} className="inline-flex items-center gap-1 text-indigo-700">
250
- <CopySquaresIcon className="h-4 w-4" /> Copy
251
  </button>
252
  {isAdmin && (
253
- <button onClick={() => { setGroup(d.groupNumber); setReplaceMode(true); }} className="inline-flex items-center gap-1 text-gray-700">
254
- <ArrowsRightLeftIcon className="h-4 w-4" /> Replace
255
  </button>
256
  )}
257
  </div>
 
123
  const [docs, setDocs] = useState<any[]>([]);
124
  const [urlInput, setUrlInput] = useState<string>('');
125
  const [errorMsg, setErrorMsg] = useState<string>('');
 
126
  const [copiedLink, setCopiedLink] = useState<string>('');
127
  const isAdmin = (JSON.parse(localStorage.getItem('user') || '{}').role === 'admin');
128
 
129
  const CopySquaresIcon: React.FC<{ className?: string }> = ({ className }) => (
130
+ <svg className={className || 'h-4 w-4'} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" xmlns="http://www.w3.org/2000/svg">
131
+ <rect x="8" y="8" width="12" height="12" rx="2"/>
132
+ <rect x="4" y="4" width="12" height="12" rx="2"/>
133
  </svg>
134
  );
135
 
 
163
  await api.post('/api/docs/create', { weekNumber, groupNumber: group, docUrl: url });
164
  await loadDocs();
165
  setUrlInput('');
 
166
  } finally {
167
  setCreating(false);
168
  }
 
180
  <div>
181
  {/* Top control row */}
182
  {isAdmin && (
183
+ <div className="mb-4 max-w-2xl">
184
+ <label className="text-sm text-gray-700 block mb-1">Group</label>
185
+ <select
186
+ value={group}
187
+ onChange={(e) => {
188
+ const g = parseInt(e.target.value);
189
+ setGroup(g);
190
+ localStorage.setItem(`tutorial_group_${weekNumber}`, String(g));
191
+ }}
192
+ className="w-full px-3 py-2 border rounded-md text-sm"
193
+ >
194
+ {[1,2,3,4,5,6,7,8].map(g => <option key={g} value={g}>Group {g}</option>)}
195
+ </select>
 
 
196
  </div>
197
  )}
198
 
199
  {/* Replace / Add link inline editor */}
200
  {isAdmin && (
201
+ <div className="mb-4 max-w-2xl">
202
  <div className="flex items-center gap-2">
203
  <input
204
  type="url"
 
208
  className="flex-1 px-3 py-2 border border-gray-300 rounded-md text-sm"
209
  />
210
  <button onClick={createDoc} disabled={creating} className="px-4 py-2 bg-indigo-600 text-white rounded-md text-sm">{creating ? 'Saving…' : (current ? 'Save new link' : 'Add Doc Link')}</button>
 
 
 
 
 
 
211
  </div>
212
  {errorMsg && <div className="mt-1 text-xs text-red-600">{errorMsg}</div>}
213
  </div>
 
237
  <ArrowTopRightOnSquareIcon className="h-4 w-4" /> Open
238
  </a>
239
  <button onClick={() => copyLink(d.docUrl)} className="inline-flex items-center gap-1 text-indigo-700">
240
+ <CopySquaresIcon className="h-4 w-4" /> {copiedLink === d.docUrl ? 'Copied' : 'Copy'}
241
  </button>
242
  {isAdmin && (
243
+ <button onClick={() => { setGroup(d.groupNumber); }} className="inline-flex items-center gap-1 text-gray-700">
244
+ <ArrowsRightLeftIcon className="h-4 w-4" /> Edit
245
  </button>
246
  )}
247
  </div>
client/src/pages/WeeklyPractice.tsx CHANGED
@@ -25,7 +25,7 @@ interface WeeklyPractice {
25
  imageUrl?: string;
26
  imageAlt?: string;
27
  imageSize?: number;
28
- imageAlignment?: 'left' | 'center' | 'right';
29
  }
30
 
31
  interface WeeklyPracticeWeek {
@@ -95,6 +95,71 @@ const WeeklyPractice: React.FC = () => {
95
  const [anonymousSubmissions, setAnonymousSubmissions] = useState<{[key: string]: boolean}>({});
96
  const [expandedSections, setExpandedSections] = useState<{[key: string]: boolean}>({});
97
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
98
  const [editingPractice, setEditingPractice] = useState<string | null>(null);
99
  const [editingBrief, setEditingBrief] = useState<{[key: number]: boolean}>({});
100
  const [addingPractice, setAddingPractice] = useState<boolean>(false);
@@ -114,7 +179,7 @@ const WeeklyPractice: React.FC = () => {
114
  imageUrl: string;
115
  imageAlt: string;
116
  imageSize: number;
117
- imageAlignment: 'left' | 'center' | 'right';
118
  }>({
119
  imageUrl: '',
120
  imageAlt: '',
@@ -123,6 +188,10 @@ const WeeklyPractice: React.FC = () => {
123
  });
124
  const [saving, setSaving] = useState(false);
125
  const [uploading, setUploading] = useState(false);
 
 
 
 
126
  const navigate = useNavigate();
127
 
128
  // Subtitling state for Week 2
@@ -150,32 +219,51 @@ const WeeklyPractice: React.FC = () => {
150
 
151
  const handleWeekChange = async (week: number) => {
152
  setIsWeekTransitioning(true);
153
-
154
- // Clear existing data first and update state
155
  setWeeklyPractice([]);
156
  setWeeklyPracticeWeek(null);
157
  setUserSubmissions({});
158
- setSelectedWeek(week);
159
  localStorage.setItem('selectedWeeklyPracticeWeek', week.toString());
160
-
161
- // Wait for actual content to load before ending animation
162
  try {
163
- // Fetch new week's data
164
- await fetchWeeklyPractice(false);
165
- // fetchUserSubmissions will be called automatically by useEffect
166
- // after weeklyPractice is updated
167
-
168
- // Wait longer for DOM to update with new content (especially for Week 2)
169
  const delay = week === 2 ? 400 : 200;
170
  await new Promise(resolve => setTimeout(resolve, delay));
171
  } catch (error) {
172
  console.error('Error loading week data:', error);
173
  } finally {
174
- // End transition after content is loaded and rendered
175
  setIsWeekTransitioning(false);
176
  }
177
  };
178
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
179
  // Subtitle data state - will be loaded from database
180
  const [subtitleSegments, setSubtitleSegments] = useState<SubtitleSegment[]>([]);
181
  const [loadingSubtitles, setLoadingSubtitles] = useState<boolean>(true);
@@ -728,11 +816,11 @@ const WeeklyPractice: React.FC = () => {
728
  groupedSubmissions[practice._id] = [];
729
  });
730
 
731
- // Then populate with actual submissions
732
  practice.forEach(practice => {
733
- const practiceSubmissions = data.submissions.filter((sub: any) =>
734
- sub.sourceTextId && sub.sourceTextId._id === practice._id
735
- );
736
  if (practiceSubmissions.length > 0) {
737
  groupedSubmissions[practice._id] = practiceSubmissions;
738
  }
@@ -745,6 +833,7 @@ const WeeklyPractice: React.FC = () => {
745
  }
746
  }, []);
747
 
 
748
  const fetchWeeklyPractice = useCallback(async (showLoading = true) => {
749
  try {
750
  if (showLoading) { setLoading(true); }
@@ -756,7 +845,14 @@ const WeeklyPractice: React.FC = () => {
756
 
757
  // Organize practices into week structure
758
  if (practices.length > 0) {
759
- const translationBrief = practices[0].translationBrief;
 
 
 
 
 
 
 
760
  const weeklyPracticeWeekData: WeeklyPracticeWeek = {
761
  weekNumber: selectedWeek,
762
  translationBrief: translationBrief,
@@ -764,7 +860,13 @@ const WeeklyPractice: React.FC = () => {
764
  };
765
  setWeeklyPracticeWeek(weeklyPracticeWeekData);
766
  } else {
767
- setWeeklyPracticeWeek(null);
 
 
 
 
 
 
768
  }
769
 
770
  await fetchUserSubmissions(practices);
@@ -778,6 +880,53 @@ const WeeklyPractice: React.FC = () => {
778
  }
779
  }, [selectedWeek, fetchUserSubmissions]);
780
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
781
  useEffect(() => {
782
  const user = localStorage.getItem('user');
783
  if (!user) {
@@ -785,7 +934,40 @@ const WeeklyPractice: React.FC = () => {
785
  return;
786
  }
787
  fetchWeeklyPractice();
788
- }, [fetchWeeklyPractice, navigate]);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
789
 
790
  // Listen for week reset events from page navigation
791
  useEffect(() => {
@@ -822,11 +1004,11 @@ const WeeklyPractice: React.FC = () => {
822
  const token = localStorage.getItem('token');
823
  const user = JSON.parse(localStorage.getItem('user') || '{}');
824
  const response = await api.post('/api/submissions', {
825
- sourceTextId: practiceId,
826
- transcreation: translationText[practiceId],
827
- culturalAdaptations: [],
828
- isAnonymous: anonymousSubmissions[practiceId] || false,
829
- username: user.name || 'Unknown'
830
  });
831
 
832
  if (response.status === 201 || response.status === 200) {
@@ -858,7 +1040,7 @@ const WeeklyPractice: React.FC = () => {
858
  try {
859
  const token = localStorage.getItem('token');
860
  const response = await api.put(`/api/submissions/${editingSubmission.id}`, {
861
- transcreation: editSubmissionText
862
  });
863
 
864
  if (response.status === 200) {
@@ -1030,8 +1212,11 @@ const WeeklyPractice: React.FC = () => {
1030
  });
1031
 
1032
  if (response.data) {
1033
- await fetchWeeklyPractice(false);
 
1034
  setEditingBrief(prev => ({ ...prev, [selectedWeek]: false }));
 
 
1035
  } else {
1036
  console.error('Failed to update translation brief');
1037
  }
@@ -1164,12 +1349,20 @@ const WeeklyPractice: React.FC = () => {
1164
  imageAlignment: imageForm.imageAlignment
1165
  };
1166
 
1167
- console.log('Saving new image with payload:', payload);
1168
 
1169
  const response = await api.post('/api/auth/admin/weekly-practice', payload);
1170
 
1171
  if (response.data) {
1172
  console.log('Image saved successfully:', response.data);
 
 
 
 
 
 
 
 
1173
  await fetchWeeklyPractice(false);
1174
  setAddingImage(false);
1175
  } else {
@@ -1192,10 +1385,18 @@ const WeeklyPractice: React.FC = () => {
1192
 
1193
  setSaving(true);
1194
  try {
 
 
 
 
 
1195
  const response = await api.delete(`/api/auth/admin/weekly-practice/${practiceId}`);
1196
 
1197
  if (response.data) {
1198
- await fetchWeeklyPractice(false);
 
 
 
1199
  } else {
1200
  console.error('Failed to delete weekly practice');
1201
  }
@@ -1251,7 +1452,7 @@ const WeeklyPractice: React.FC = () => {
1251
  </div>
1252
  )}
1253
 
1254
- {/* Translation Brief - Shown once at the top */}
1255
  {!isWeekTransitioning && weeklyPracticeWeek && weeklyPracticeWeek.translationBrief ? (
1256
  <div className="bg-gradient-to-r from-blue-50 to-indigo-50 rounded-xl p-8 mb-8 border border-blue-200">
1257
  <div className="flex items-center justify-between mb-4">
@@ -1303,15 +1504,23 @@ const WeeklyPractice: React.FC = () => {
1303
  )}
1304
  </div>
1305
  {editingBrief[selectedWeek] ? (
1306
- <textarea
1307
- value={editForm.translationBrief}
1308
- onChange={(e) => setEditForm({ ...editForm, translationBrief: e.target.value })}
1309
- className="w-full p-4 border border-blue-300 rounded-lg text-blue-800 leading-relaxed text-lg bg-white"
1310
- rows={6}
1311
- placeholder="Enter translation brief..."
1312
- />
 
 
 
 
 
 
 
 
1313
  ) : (
1314
- <p className="text-blue-800 leading-relaxed text-lg font-smiley">{weeklyPracticeWeek.translationBrief}</p>
1315
  )}
1316
  </div>
1317
  ) : (
@@ -1335,6 +1544,11 @@ const WeeklyPractice: React.FC = () => {
1335
  </div>
1336
  {editingBrief[selectedWeek] && (
1337
  <div className="space-y-4">
 
 
 
 
 
1338
  <textarea
1339
  value={editForm.translationBrief}
1340
  onChange={(e) => setEditForm({ ...editForm, translationBrief: e.target.value })}
@@ -1367,6 +1581,109 @@ const WeeklyPractice: React.FC = () => {
1367
  )
1368
  )}
1369
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1370
  {/* Special Subtitling Interface for Week 2 */}
1371
  {!isWeekTransitioning && selectedWeek === 2 ? (
1372
  <div className="grid grid-cols-1 lg:grid-cols-2 gap-8">
@@ -1698,13 +2015,19 @@ const WeeklyPractice: React.FC = () => {
1698
  <label className="block text-sm font-medium text-gray-700 mb-1">
1699
  Practice Content <span className="text-gray-500">(optional if image is provided)</span>
1700
  </label>
1701
- <textarea
1702
- value={editForm.content}
1703
- onChange={(e) => setEditForm({ ...editForm, content: e.target.value })}
1704
- 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"
1705
- rows={4}
 
 
 
 
 
 
1706
  placeholder="Enter weekly practice content (or upload an image below)..."
1707
- />
1708
  </div>
1709
  {selectedWeek >= 3 && (
1710
  <div className="space-y-3">
@@ -1861,6 +2184,7 @@ const WeeklyPractice: React.FC = () => {
1861
  <option value="left">Left</option>
1862
  <option value="center">Center</option>
1863
  <option value="right">Right</option>
 
1864
  </select>
1865
  </div>
1866
  </div>
@@ -1879,13 +2203,13 @@ const WeeklyPractice: React.FC = () => {
1879
  </div>
1880
  </div>
1881
  <div className="flex space-x-3">
1882
- <button
1883
- onClick={startAddingPractice}
1884
- 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"
1885
- >
1886
- <PlusIcon className="h-5 w-5" />
1887
- <span className="font-medium">Add Practice</span>
1888
- </button>
1889
  {selectedWeek >= 3 && (
1890
  <button
1891
  onClick={startAddingImage}
@@ -1905,18 +2229,18 @@ const WeeklyPractice: React.FC = () => {
1905
 
1906
  {!isWeekTransitioning && (
1907
  <>
1908
- {weeklyPractice.length === 0 && !addingPractice ? (
1909
- <div className="text-center py-12">
1910
- <DocumentTextIcon className="h-12 w-12 text-gray-400 mx-auto mb-4" />
1911
- <h3 className="text-lg font-medium text-gray-900 mb-2">
1912
- No practice examples available
1913
- </h3>
1914
- <p className="text-gray-600">
1915
- Practice examples for Week {selectedWeek} haven't been set up yet.
1916
- </p>
1917
- </div>
1918
- ) : (
1919
- weeklyPractice.map((practice) => (
1920
  <div key={practice._id} className="bg-white rounded-xl shadow-lg border border-gray-100 p-8 hover:shadow-xl transition-shadow duration-300">
1921
  <div className="mb-6">
1922
  <div className="flex items-center justify-between mb-4">
@@ -2034,74 +2358,54 @@ const WeeklyPractice: React.FC = () => {
2034
  ) : (
2035
  <div>
2036
  {practice.imageUrl ? (
2037
- // Check if this is an image-only practice (created via "Add Image" function)
2038
- practice.content === 'Image-based practice' ? (
2039
- // Image-only layout with dynamic sizing and alignment
2040
- <div className={`flex flex-col md:flex-row gap-6 items-start ${
2041
- practice.imageAlignment === 'left' ? 'md:flex-row' :
2042
- practice.imageAlignment === 'right' ? 'md:flex-row-reverse' :
2043
- 'md:flex-col'
2044
- }`}>
2045
- {/* Image section */}
2046
- <div className={`${
2047
- practice.imageAlignment === 'left' ? 'w-full md:w-1/2' :
2048
- practice.imageAlignment === 'right' ? 'w-full md:w-1/2' :
2049
- 'w-full'
2050
- } flex ${
2051
- practice.imageAlignment === 'left' ? 'justify-start' :
2052
- practice.imageAlignment === 'right' ? 'justify-end' :
2053
- 'justify-center'
2054
- }`}>
2055
  <div className="inline-block rounded-lg shadow-md overflow-hidden">
2056
- <img
2057
- src={practice.imageUrl}
2058
- alt={practice.imageAlt || 'Uploaded image'}
2059
- className="w-full h-auto"
2060
- style={{
2061
- height: `${practice.imageSize || 200}px`,
2062
- width: 'auto',
2063
- objectFit: 'contain'
2064
- }}
2065
- onError={(e) => {
2066
- console.error('Image failed to load:', e);
2067
- e.currentTarget.style.display = 'none';
2068
- }}
2069
- onLoad={() => {
2070
- console.log('🔧 Debug - Practice image loaded:', {
2071
- imageUrl: practice.imageUrl,
2072
- imageSize: practice.imageSize,
2073
- content: practice.content,
2074
- weekNumber: practice.weekNumber
2075
- });
2076
- }}
2077
- />
2078
- {practice.imageAlt && (
2079
- <div className="text-xs text-gray-500 mt-2 text-center">Alt: {practice.imageAlt}</div>
2080
- )}
 
 
 
 
 
 
2081
  </div>
2082
  </div>
2083
  </div>
2084
  ) : (
2085
- // Regular practice layout (original side-by-side for ALL weeks)
2086
  <div className="flex flex-col md:flex-row gap-6 items-start">
2087
- {/* Image on the left - 50% width */}
2088
  <div className="w-full md:w-1/2 flex justify-center">
2089
  {practice.imageUrl.startsWith('data:') ? (
2090
- // Show actual image if it's a data URL
2091
  <div className="inline-block rounded-lg shadow-md overflow-hidden">
2092
- <img
2093
- src={practice.imageUrl}
2094
- alt={practice.imageAlt || 'Uploaded image'}
2095
- className="w-full h-auto"
2096
- style={{ height: '200px', width: 'auto', objectFit: 'contain' }} // Fixed height for consistency
2097
- onError={(e) => {
2098
- console.error('Image failed to load:', e);
2099
- e.currentTarget.style.display = 'none';
2100
- }}
2101
- />
2102
  </div>
2103
  ) : (
2104
- // Show placeholder if it's not a data URL
2105
  <div className="inline-block rounded-lg shadow-md bg-green-500 text-white p-6 text-center">
2106
  <div className="text-3xl mb-2">📷</div>
2107
  <div className="font-semibold">Image Uploaded</div>
@@ -2109,7 +2413,6 @@ const WeeklyPractice: React.FC = () => {
2109
  </div>
2110
  )}
2111
  </div>
2112
- {/* Text on the right - 50% width */}
2113
  <div className="w-full md:w-1/2">
2114
  <p className="text-orange-800 leading-relaxed text-lg font-source-text whitespace-pre-wrap">{practice.content}</p>
2115
  </div>
@@ -2166,7 +2469,7 @@ const WeeklyPractice: React.FC = () => {
2166
  </div>
2167
  {getStatusIcon(submission.status)}
2168
  </div>
2169
- <p className="text-stone-800 leading-relaxed text-base mb-2 font-smiley">{submission.transcreation}</p>
2170
  <div className="flex items-center space-x-4 text-xs text-stone-700 mt-auto">
2171
  <div className="flex items-center space-x-1">
2172
  <span className="font-medium">By:</span>
@@ -2180,7 +2483,7 @@ const WeeklyPractice: React.FC = () => {
2180
  {(submission.voteCounts?.['1'] || 0) + (submission.voteCounts?.['2'] || 0) + (submission.voteCounts?.['3'] || 0)}
2181
  </span>
2182
  </div>
2183
- {submission.isOwner && (
2184
  <button
2185
  onClick={() => handleEditSubmission(submission._id, submission.transcreation)}
2186
  className="text-purple-600 hover:text-purple-800 text-sm font-medium"
@@ -2203,8 +2506,8 @@ const WeeklyPractice: React.FC = () => {
2203
  </div>
2204
  )}
2205
 
2206
- {/* Translation Input (only show if user is logged in and has no submission, but hide for image-only content) */}
2207
- {localStorage.getItem('token') && (!userSubmissions[practice._id] || userSubmissions[practice._id].length === 0) && practice.content !== 'Image-based practice' && (
2208
  <div className="bg-gradient-to-r from-purple-50 to-violet-50 rounded-xl p-6 border border-purple-200">
2209
  <div className="flex items-center space-x-2 mb-4">
2210
  <div className="bg-purple-100 rounded-full p-1">
@@ -2213,7 +2516,12 @@ const WeeklyPractice: React.FC = () => {
2213
  <h4 className="text-purple-900 font-semibold text-lg">Your Translation</h4>
2214
  </div>
2215
  <div className="mb-4">
2216
- <textarea
 
 
 
 
 
2217
  value={translationText[practice._id] || ''}
2218
  onChange={(e) => setTranslationText({ ...translationText, [practice._id]: e.target.value })}
2219
  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"
@@ -2233,17 +2541,14 @@ const WeeklyPractice: React.FC = () => {
2233
  })}
2234
  className="rounded border-gray-300 text-purple-600 focus:ring-purple-500"
2235
  />
2236
- <span className="text-purple-700 font-medium">Submit anonymously</span>
2237
  </label>
2238
- <p className="text-sm text-purple-600 mt-1">
2239
- Check this box to submit without showing your name
2240
- </p>
2241
  </div>
2242
 
2243
  <button
2244
  onClick={() => handleSubmitTranslation(practice._id)}
2245
  disabled={submitting[practice._id]}
2246
- 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"
2247
  >
2248
  {submitting[practice._id] ? (
2249
  <>
@@ -2253,7 +2558,6 @@ const WeeklyPractice: React.FC = () => {
2253
  ) : (
2254
  <>
2255
  Submit Translation
2256
- <ArrowRightIcon className="h-4 w-4 ml-2" />
2257
  </>
2258
  )}
2259
  </button>
@@ -2285,7 +2589,7 @@ const WeeklyPractice: React.FC = () => {
2285
  ))
2286
  )}
2287
  </>
2288
- )}
2289
  </div>
2290
  )}
2291
  </div>
 
25
  imageUrl?: string;
26
  imageAlt?: string;
27
  imageSize?: number;
28
+ imageAlignment?: 'left' | 'center' | 'right' | 'portrait-split';
29
  }
30
 
31
  interface WeeklyPracticeWeek {
 
95
  const [anonymousSubmissions, setAnonymousSubmissions] = useState<{[key: string]: boolean}>({});
96
  const [expandedSections, setExpandedSections] = useState<{[key: string]: boolean}>({});
97
 
98
+ // Basic inline formatting helpers for weeks 4–6 (bold/italic via simple markdown) + links
99
+ const renderFormatted = (text: string) => {
100
+ const escape = (s: string) => s.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
101
+ const html = escape(text)
102
+ .replace(/\[([^\]]+?)\]\((https?:\/\/[^\s)]+)\)/g, '<a class="text-purple-700 underline" href="$2" target="_blank" rel="noopener noreferrer">$1</a>')
103
+ .replace(/(^|[^=\"'\/:])(https?:\/\/[^\s<]+)/g, (m, p1, url) => `${p1}<a class=\"text-purple-700 underline\" href=\"${url}\" target=\"_blank\" rel=\"noopener noreferrer\">${url}</a>`)
104
+ .replace(/(^|[^=\"'\/:])(www\.[^\s<]+)/g, (m, p1, host) => `${p1}<a class=\"text-purple-700 underline\" href=\"https://${host}\" target=\"_blank\" rel=\"noopener noreferrer\">${host}</a>`)
105
+ .replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>')
106
+ .replace(/\*(.+?)\*/g, '<em>$1</em>')
107
+ .replace(/\n/g, '<br/>');
108
+ return <span dangerouslySetInnerHTML={{ __html: html }} />;
109
+ };
110
+
111
+ const applyInlineFormat = (
112
+ elementId: string,
113
+ current: string,
114
+ setValue: (v: string) => void,
115
+ wrapper: '**' | '*'
116
+ ) => {
117
+ const el = document.getElementById(elementId) as HTMLTextAreaElement | null;
118
+ if (!el) {
119
+ setValue(current + wrapper + wrapper);
120
+ return;
121
+ }
122
+ const start = el.selectionStart ?? current.length;
123
+ const end = el.selectionEnd ?? current.length;
124
+ const before = current.slice(0, start);
125
+ const selection = current.slice(start, end);
126
+ const after = current.slice(end);
127
+ const next = `${before}${wrapper}${selection}${wrapper}${after}`;
128
+ setValue(next);
129
+ setTimeout(() => {
130
+ try {
131
+ el.focus();
132
+ el.selectionStart = start + wrapper.length;
133
+ el.selectionEnd = end + wrapper.length;
134
+ } catch {}
135
+ }, 0);
136
+ };
137
+
138
+ const applyLinkFormat = (
139
+ elementId: string,
140
+ current: string,
141
+ setValue: (v: string) => void
142
+ ) => {
143
+ const urlInput = window.prompt('Enter URL (e.g., https://example.com):');
144
+ if (!urlInput) return;
145
+ let url = /^https?:\/\//i.test(urlInput) ? urlInput : `https://${urlInput}`;
146
+ url = url.replace(/["'>)\s]+$/g, '');
147
+ const el = document.getElementById(elementId) as HTMLTextAreaElement | null;
148
+ if (!el) {
149
+ setValue(`${current}[link](${url})`);
150
+ return;
151
+ }
152
+ const start = el.selectionStart ?? current.length;
153
+ const end = el.selectionEnd ?? current.length;
154
+ const before = current.slice(0, start);
155
+ const selection = current.slice(start, end) || 'link';
156
+ const after = current.slice(end);
157
+ setValue(`${before}[${selection}](${url})${after}`);
158
+ setTimeout(() => {
159
+ try { el.focus(); el.setSelectionRange(end, end); } catch {}
160
+ }, 0);
161
+ };
162
+
163
  const [editingPractice, setEditingPractice] = useState<string | null>(null);
164
  const [editingBrief, setEditingBrief] = useState<{[key: number]: boolean}>({});
165
  const [addingPractice, setAddingPractice] = useState<boolean>(false);
 
179
  imageUrl: string;
180
  imageAlt: string;
181
  imageSize: number;
182
+ imageAlignment: 'left' | 'center' | 'right' | 'portrait-split';
183
  }>({
184
  imageUrl: '',
185
  imageAlt: '',
 
188
  });
189
  const [saving, setSaving] = useState(false);
190
  const [uploading, setUploading] = useState(false);
191
+ const [uploadingSource, setUploadingSource] = useState(false);
192
+ const [uploadingTranslation, setUploadingTranslation] = useState(false);
193
+ const [weekFiles, setWeekFiles] = useState<{ source: any[]; translation: any[] }>({ source: [], translation: [] });
194
+ const [showAllTranslations, setShowAllTranslations] = useState(false);
195
  const navigate = useNavigate();
196
 
197
  // Subtitling state for Week 2
 
219
 
220
  const handleWeekChange = async (week: number) => {
221
  setIsWeekTransitioning(true);
222
+ // Clear existing data immediately
 
223
  setWeeklyPractice([]);
224
  setWeeklyPracticeWeek(null);
225
  setUserSubmissions({});
 
226
  localStorage.setItem('selectedWeeklyPracticeWeek', week.toString());
227
+ setSelectedWeek(week);
228
+
229
  try {
230
+ // Explicitly fetch for the target week to avoid stale selectedWeek race
231
+ await fetchWeeklyPracticeForWeek(week, false);
 
 
 
 
232
  const delay = week === 2 ? 400 : 200;
233
  await new Promise(resolve => setTimeout(resolve, delay));
234
  } catch (error) {
235
  console.error('Error loading week data:', error);
236
  } finally {
 
237
  setIsWeekTransitioning(false);
238
  }
239
  };
240
 
241
+ // Authenticated file download helper
242
+ const downloadWeekFile = async (fileId: string, fallbackName?: string) => {
243
+ try {
244
+ const response = await api.get(`/api/weekly-practice-files/${fileId}/download`, { responseType: 'blob' });
245
+ // Try to extract filename from Content-Disposition
246
+ const disposition = (response.headers?.['content-disposition'] as string) || '';
247
+ let fileName = fallbackName || 'download';
248
+ try {
249
+ const starMatch = disposition.match(/filename\*=UTF-8''([^;]+)/i);
250
+ const plainMatch = disposition.match(/filename="([^"\\]+)"/i);
251
+ if (starMatch && starMatch[1]) fileName = decodeURIComponent(starMatch[1]);
252
+ else if (plainMatch && plainMatch[1]) fileName = plainMatch[1];
253
+ } catch {}
254
+ const blobUrl = window.URL.createObjectURL(new Blob([response.data]));
255
+ const link = document.createElement('a');
256
+ link.href = blobUrl;
257
+ link.download = fileName;
258
+ document.body.appendChild(link);
259
+ link.click();
260
+ document.body.removeChild(link);
261
+ window.URL.revokeObjectURL(blobUrl);
262
+ } catch (e) {
263
+ console.error('Failed to download file', e);
264
+ }
265
+ };
266
+
267
  // Subtitle data state - will be loaded from database
268
  const [subtitleSegments, setSubtitleSegments] = useState<SubtitleSegment[]>([]);
269
  const [loadingSubtitles, setLoadingSubtitles] = useState<boolean>(true);
 
816
  groupedSubmissions[practice._id] = [];
817
  });
818
 
819
+ // Then populate with actual submissions and mark ownership for edit visibility after refresh/login
820
  practice.forEach(practice => {
821
+ const practiceSubmissions = data.submissions
822
+ .filter((sub: any) => sub.sourceTextId && sub.sourceTextId._id === practice._id)
823
+ .map((sub: any) => ({ ...sub, isOwner: true }));
824
  if (practiceSubmissions.length > 0) {
825
  groupedSubmissions[practice._id] = practiceSubmissions;
826
  }
 
833
  }
834
  }, []);
835
 
836
+ // Fetch for current selectedWeek
837
  const fetchWeeklyPractice = useCallback(async (showLoading = true) => {
838
  try {
839
  if (showLoading) { setLoading(true); }
 
845
 
846
  // Organize practices into week structure
847
  if (practices.length > 0) {
848
+ let translationBrief = practices[0].translationBrief;
849
+ // Fallback to explicit week brief API if missing
850
+ if (!translationBrief) {
851
+ try {
852
+ const br = await api.get(`/api/search/weekly-practice/${selectedWeek}/brief`);
853
+ translationBrief = br.data?.translationBrief || '';
854
+ } catch {}
855
+ }
856
  const weeklyPracticeWeekData: WeeklyPracticeWeek = {
857
  weekNumber: selectedWeek,
858
  translationBrief: translationBrief,
 
860
  };
861
  setWeeklyPracticeWeek(weeklyPracticeWeekData);
862
  } else {
863
+ // No practices: still fetch brief
864
+ try {
865
+ const br = await api.get(`/api/search/weekly-practice/${selectedWeek}/brief`);
866
+ setWeeklyPracticeWeek({ weekNumber: selectedWeek, translationBrief: br.data?.translationBrief || '', practices: [] });
867
+ } catch {
868
+ setWeeklyPracticeWeek(null);
869
+ }
870
  }
871
 
872
  await fetchUserSubmissions(practices);
 
880
  }
881
  }, [selectedWeek, fetchUserSubmissions]);
882
 
883
+ // Fetch files list for current week (source + translation)
884
+ const fetchWeekFiles = useCallback(async () => {
885
+ try {
886
+ const [src, trn] = await Promise.all([
887
+ api.get(`/api/weekly-practice-files/week/${selectedWeek}?type=source`),
888
+ api.get(`/api/weekly-practice-files/week/${selectedWeek}?type=translation`)
889
+ ]);
890
+ setWeekFiles({ source: src.data.files || [], translation: trn.data.files || [] });
891
+ } catch (e) {
892
+ console.error('Failed to load week files', e);
893
+ setWeekFiles({ source: [], translation: [] });
894
+ }
895
+ }, [selectedWeek]);
896
+
897
+ // Fetch for a specific week (avoids stale week race when changing weeks)
898
+ const fetchWeeklyPracticeForWeek = useCallback(async (week: number, showLoading = true) => {
899
+ try {
900
+ if (showLoading) { setLoading(true); }
901
+ const response = await api.get(`/api/search/weekly-practice/${week}`);
902
+
903
+ if (response.data) {
904
+ const practices = response.data;
905
+ setWeeklyPractice(practices);
906
+
907
+ if (practices.length > 0) {
908
+ const translationBrief = practices[0].translationBrief;
909
+ const weeklyPracticeWeekData: WeeklyPracticeWeek = {
910
+ weekNumber: week,
911
+ translationBrief: translationBrief,
912
+ practices: practices
913
+ };
914
+ setWeeklyPracticeWeek(weeklyPracticeWeekData);
915
+ } else {
916
+ setWeeklyPracticeWeek(null);
917
+ }
918
+
919
+ await fetchUserSubmissions(practices);
920
+ } else {
921
+ console.error('Failed to fetch weekly practice');
922
+ }
923
+ } catch (error) {
924
+ console.error('Error fetching weekly practice (explicit week):', error);
925
+ } finally {
926
+ if (showLoading) { setLoading(false); }
927
+ }
928
+ }, [fetchUserSubmissions]);
929
+
930
  useEffect(() => {
931
  const user = localStorage.getItem('user');
932
  if (!user) {
 
934
  return;
935
  }
936
  fetchWeeklyPractice();
937
+ fetchWeekFiles();
938
+ }, [fetchWeeklyPractice, fetchWeekFiles, navigate]);
939
+
940
+ // Upload handlers for week files
941
+ const uploadSourceFile = async (file: File, description?: string) => {
942
+ try {
943
+ setUploadingSource(true);
944
+ const form = new FormData();
945
+ form.append('file', file);
946
+ if (description) form.append('description', description);
947
+ await api.post(`/api/weekly-practice-files/week/${selectedWeek}/source`, form, {
948
+ headers: { 'Content-Type': 'multipart/form-data' }
949
+ });
950
+ await fetchWeekFiles();
951
+ } finally {
952
+ setUploadingSource(false);
953
+ }
954
+ };
955
+
956
+ const uploadTranslationFile = async (file: File, description?: string, sourceFileId?: string) => {
957
+ try {
958
+ setUploadingTranslation(true);
959
+ const form = new FormData();
960
+ form.append('file', file);
961
+ if (description) form.append('description', description);
962
+ if (sourceFileId) form.append('sourceFileId', sourceFileId);
963
+ await api.post(`/api/weekly-practice-files/week/${selectedWeek}/translation`, form, {
964
+ headers: { 'Content-Type': 'multipart/form-data' }
965
+ });
966
+ await fetchWeekFiles();
967
+ } finally {
968
+ setUploadingTranslation(false);
969
+ }
970
+ };
971
 
972
  // Listen for week reset events from page navigation
973
  useEffect(() => {
 
1004
  const token = localStorage.getItem('token');
1005
  const user = JSON.parse(localStorage.getItem('user') || '{}');
1006
  const response = await api.post('/api/submissions', {
1007
+ sourceTextId: practiceId,
1008
+ transcreation: translationText[practiceId],
1009
+ culturalAdaptations: [],
1010
+ isAnonymous: anonymousSubmissions[practiceId] || false,
1011
+ username: user.name || 'Unknown'
1012
  });
1013
 
1014
  if (response.status === 201 || response.status === 200) {
 
1040
  try {
1041
  const token = localStorage.getItem('token');
1042
  const response = await api.put(`/api/submissions/${editingSubmission.id}`, {
1043
+ transcreation: editSubmissionText
1044
  });
1045
 
1046
  if (response.status === 200) {
 
1212
  });
1213
 
1214
  if (response.data) {
1215
+ // Optimistic UI update
1216
+ setWeeklyPracticeWeek(prev => prev ? { ...prev, translationBrief: editForm.translationBrief } : prev);
1217
  setEditingBrief(prev => ({ ...prev, [selectedWeek]: false }));
1218
+ // Background refresh
1219
+ fetchWeeklyPractice(false);
1220
  } else {
1221
  console.error('Failed to update translation brief');
1222
  }
 
1349
  imageAlignment: imageForm.imageAlignment
1350
  };
1351
 
1352
+ console.log('Saving new image with payload:', payload);
1353
 
1354
  const response = await api.post('/api/auth/admin/weekly-practice', payload);
1355
 
1356
  if (response.data) {
1357
  console.log('Image saved successfully:', response.data);
1358
+ // Optimistic UI update: append new practice for current week (4–6 only)
1359
+ if (selectedWeek >= 4) {
1360
+ const newPractice = response.data.weeklyPractice || response.data.practice || null;
1361
+ if (newPractice) {
1362
+ setWeeklyPractice(prev => [...prev, newPractice]);
1363
+ setWeeklyPracticeWeek(prev => prev ? { ...prev, practices: [...prev.practices, newPractice] } : prev);
1364
+ }
1365
+ }
1366
  await fetchWeeklyPractice(false);
1367
  setAddingImage(false);
1368
  } else {
 
1385
 
1386
  setSaving(true);
1387
  try {
1388
+ if (String(practiceId).startsWith('wb-')) {
1389
+ console.warn('Skipping deletion of week-brief placeholder');
1390
+ setSaving(false);
1391
+ return;
1392
+ }
1393
  const response = await api.delete(`/api/auth/admin/weekly-practice/${practiceId}`);
1394
 
1395
  if (response.data) {
1396
+ setWeeklyPracticeWeek(prev => prev ? { ...prev, translationBrief: '' } : prev);
1397
+ const briefKey = `weeklyBrief_week_${selectedWeek}`;
1398
+ try { localStorage.removeItem(briefKey); } catch {}
1399
+ fetchWeeklyPractice(false);
1400
  } else {
1401
  console.error('Failed to delete weekly practice');
1402
  }
 
1452
  </div>
1453
  )}
1454
 
1455
+ {/* Translation Brief - Shown once at the top (standalone allowed) */}
1456
  {!isWeekTransitioning && weeklyPracticeWeek && weeklyPracticeWeek.translationBrief ? (
1457
  <div className="bg-gradient-to-r from-blue-50 to-indigo-50 rounded-xl p-8 mb-8 border border-blue-200">
1458
  <div className="flex items-center justify-between mb-4">
 
1504
  )}
1505
  </div>
1506
  {editingBrief[selectedWeek] ? (
1507
+ <div>
1508
+ <div className="flex items-center justify-end space-x-2 mb-2">
1509
+ <button onClick={() => applyInlineFormat('weekly-brief-input', editForm.translationBrief, v => setEditForm({ ...editForm, translationBrief: v }), '**')} className="px-2 py-1 text-xs bg-blue-100 text-blue-800 rounded">B</button>
1510
+ <button onClick={() => applyInlineFormat('weekly-brief-input', editForm.translationBrief, v => setEditForm({ ...editForm, translationBrief: v }), '*')} className="px-2 py-1 text-xs bg-blue-100 text-blue-800 rounded italic">I</button>
1511
+ <button onClick={() => applyLinkFormat('weekly-brief-input', editForm.translationBrief || '', v => setEditForm({ ...editForm, translationBrief: v }))} className="px-2 py-1 text-xs bg-blue-100 text-blue-800 rounded">Link</button>
1512
+ </div>
1513
+ <textarea
1514
+ id="weekly-brief-input"
1515
+ value={editForm.translationBrief}
1516
+ onChange={(e) => setEditForm({ ...editForm, translationBrief: e.target.value })}
1517
+ className="w-full p-4 border border-blue-300 rounded-lg text-blue-800 leading-relaxed text-lg bg-white"
1518
+ rows={6}
1519
+ placeholder="Enter translation brief..."
1520
+ />
1521
+ </div>
1522
  ) : (
1523
+ <div className="text-blue-800 leading-relaxed text-lg font-smiley whitespace-pre-wrap">{renderFormatted(weeklyPracticeWeek.translationBrief || '')}</div>
1524
  )}
1525
  </div>
1526
  ) : (
 
1544
  </div>
1545
  {editingBrief[selectedWeek] && (
1546
  <div className="space-y-4">
1547
+ <div className="flex items-center justify-end space-x-2">
1548
+ <button onClick={() => applyInlineFormat('weekly-brief-input', editForm.translationBrief, v => setEditForm({ ...editForm, translationBrief: v }), '**')} className="px-2 py-1 text-xs bg-blue-100 text-blue-800 rounded">B</button>
1549
+ <button onClick={() => applyInlineFormat('weekly-brief-input', editForm.translationBrief, v => setEditForm({ ...editForm, translationBrief: v }), '*')} className="px-2 py-1 text-xs bg-blue-100 text-blue-800 rounded italic">I</button>
1550
+ <button onClick={() => applyLinkFormat('weekly-brief-input', editForm.translationBrief || '', v => setEditForm({ ...editForm, translationBrief: v }))} className="px-2 py-1 text-xs bg-blue-100 text-blue-800 rounded">Link</button>
1551
+ </div>
1552
  <textarea
1553
  value={editForm.translationBrief}
1554
  onChange={(e) => setEditForm({ ...editForm, translationBrief: e.target.value })}
 
1581
  )
1582
  )}
1583
 
1584
+ {/* Week Files (Week 6 uploads) */}
1585
+ {(weekFiles.source.length > 0 || weekFiles.translation.length > 0 || (JSON.parse(localStorage.getItem('user') || '{}').role === 'admin')) && (
1586
+ <div className="bg-white rounded-xl shadow p-6 mb-8">
1587
+ <h3 className="text-lg font-semibold text-gray-900 mb-4">Week {selectedWeek} Files</h3>
1588
+ <div className="grid grid-cols-1 md:grid-cols-2 gap-6 items-start content-start">
1589
+ <div className="flex flex-col gap-2">
1590
+ <div className="flex items-center justify-between mb-2">
1591
+ <span className="font-medium text-gray-800">Source Files</span>
1592
+ {((JSON.parse(localStorage.getItem('user') || '{}').role || 'visitor') === 'admin') && (
1593
+ <label className="inline-flex items-center px-3 py-1.5 text-sm bg-indigo-600 text-white rounded cursor-pointer hover:bg-indigo-700">
1594
+ <input
1595
+ type="file"
1596
+ className="hidden"
1597
+ onChange={(e) => {
1598
+ const f = e.target.files && e.target.files[0];
1599
+ if (f) uploadSourceFile(f);
1600
+ }}
1601
+ />
1602
+ {uploadingSource ? 'Uploading…' : 'Upload'}
1603
+ </label>
1604
+ )}
1605
+ </div>
1606
+ <ul className="text-sm text-gray-700 space-y-1 max-h-96 overflow-auto divide-y">
1607
+ {weekFiles.source.length === 0 && (
1608
+ <li className="text-gray-500">No source files yet.</li>
1609
+ )}
1610
+ {weekFiles.source.map((f: any) => (
1611
+ <li key={f._id} className="flex items-center justify-between space-x-3 py-1">
1612
+ <div className="min-w-0 truncate font-medium" title={f.fileName}>{f.fileName}</div>
1613
+ <div className="flex items-center space-x-3 flex-shrink-0">
1614
+ <button className="text-indigo-700 hover:underline" onClick={() => downloadWeekFile(f._id, f.fileName)}>Download</button>
1615
+ {JSON.parse(localStorage.getItem('user') || '{}').role === 'admin' && (
1616
+ <button
1617
+ className="text-red-600 hover:underline"
1618
+ onClick={async () => {
1619
+ if (!window.confirm('Delete this file?')) return;
1620
+ await api.delete(`/api/weekly-practice-files/${f._id}`);
1621
+ await fetchWeekFiles();
1622
+ }}
1623
+ >
1624
+ Delete
1625
+ </button>
1626
+ )}
1627
+ </div>
1628
+ </li>
1629
+ ))}
1630
+ </ul>
1631
+ </div>
1632
+ <div className="flex flex-col gap-2">
1633
+ <div className="flex items-center justify-between mb-2">
1634
+ <span className="font-medium text-gray-800">Translation Files</span>
1635
+ <div className="flex items-center space-x-3">
1636
+ {weekFiles.translation.length > 10 && (
1637
+ <button
1638
+ className="text-xs text-gray-600 hover:text-gray-800 underline"
1639
+ onClick={() => setShowAllTranslations(v => !v)}
1640
+ >
1641
+ {showAllTranslations ? 'Show first 10' : `Show all (${weekFiles.translation.length})`}
1642
+ </button>
1643
+ )}
1644
+ <label className="inline-flex items-center px-3 py-1.5 text-sm bg-orange-600 text-white rounded cursor-pointer hover:bg-orange-700">
1645
+ <input
1646
+ type="file"
1647
+ className="hidden"
1648
+ onChange={(e) => {
1649
+ const f = e.target.files && e.target.files[0];
1650
+ if (f) uploadTranslationFile(f);
1651
+ }}
1652
+ />
1653
+ {uploadingTranslation ? 'Uploading…' : 'Upload'}
1654
+ </label>
1655
+ </div>
1656
+ </div>
1657
+ <ul className="text-sm text-gray-700 space-y-1 max-h-96 overflow-auto divide-y">
1658
+ {weekFiles.translation.length === 0 && (
1659
+ <li className="text-gray-500">Please upload your translated file here.</li>
1660
+ )}
1661
+ {(showAllTranslations ? weekFiles.translation : weekFiles.translation.slice(0, 10)).map((f: any) => (
1662
+ <li key={f._id} className="flex items-center justify-between space-x-3 py-1">
1663
+ <div className="min-w-0 truncate font-medium" title={f.fileName}>{f.fileName}</div>
1664
+ <div className="flex items-center space-x-3 flex-shrink-0">
1665
+ <button className="text-indigo-700 hover:underline" onClick={() => downloadWeekFile(f._id, f.fileName)}>Download</button>
1666
+ {JSON.parse(localStorage.getItem('user') || '{}').role === 'admin' && (
1667
+ <button
1668
+ className="text-red-600 hover:underline"
1669
+ onClick={async () => {
1670
+ if (!window.confirm('Delete this file?')) return;
1671
+ await api.delete(`/api/weekly-practice-files/${f._id}`);
1672
+ await fetchWeekFiles();
1673
+ }}
1674
+ >
1675
+ Delete
1676
+ </button>
1677
+ )}
1678
+ </div>
1679
+ </li>
1680
+ ))}
1681
+ </ul>
1682
+ </div>
1683
+ </div>
1684
+ </div>
1685
+ )}
1686
+
1687
  {/* Special Subtitling Interface for Week 2 */}
1688
  {!isWeekTransitioning && selectedWeek === 2 ? (
1689
  <div className="grid grid-cols-1 lg:grid-cols-2 gap-8">
 
2015
  <label className="block text-sm font-medium text-gray-700 mb-1">
2016
  Practice Content <span className="text-gray-500">(optional if image is provided)</span>
2017
  </label>
2018
+ <div className="flex items-center justify-end space-x-2 mb-2">
2019
+ <button onClick={() => applyInlineFormat('weekly-newpractice-input', editForm.content, v => setEditForm({ ...editForm, content: v }), '**')} className="px-2 py-1 text-xs bg-orange-100 text-orange-800 rounded">B</button>
2020
+ <button onClick={() => applyInlineFormat('weekly-newpractice-input', editForm.content, v => setEditForm({ ...editForm, content: v }), '*')} className="px-2 py-1 text-xs bg-orange-100 text-orange-800 rounded italic">I</button>
2021
+ <button onClick={() => applyLinkFormat('weekly-newpractice-input', editForm.content || '', v => setEditForm({ ...editForm, content: v }))} className="px-2 py-1 text-xs bg-orange-100 text-orange-800 rounded">Link</button>
2022
+ </div>
2023
+ <textarea
2024
+ id="weekly-newpractice-input"
2025
+ value={editForm.content}
2026
+ onChange={(e) => setEditForm({ ...editForm, content: e.target.value })}
2027
+ 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"
2028
+ rows={4}
2029
  placeholder="Enter weekly practice content (or upload an image below)..."
2030
+ />
2031
  </div>
2032
  {selectedWeek >= 3 && (
2033
  <div className="space-y-3">
 
2184
  <option value="left">Left</option>
2185
  <option value="center">Center</option>
2186
  <option value="right">Right</option>
2187
+ {selectedWeek >= 4 && <option value="portrait-split">Portrait Split (image left, text+input right)</option>}
2188
  </select>
2189
  </div>
2190
  </div>
 
2203
  </div>
2204
  </div>
2205
  <div className="flex space-x-3">
2206
+ <button
2207
+ onClick={startAddingPractice}
2208
+ 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"
2209
+ >
2210
+ <PlusIcon className="h-5 w-5" />
2211
+ <span className="font-medium">Add Practice</span>
2212
+ </button>
2213
  {selectedWeek >= 3 && (
2214
  <button
2215
  onClick={startAddingImage}
 
2229
 
2230
  {!isWeekTransitioning && (
2231
  <>
2232
+ {weeklyPractice.length === 0 && !addingPractice ? (
2233
+ <div className="text-center py-12">
2234
+ <DocumentTextIcon className="h-12 w-12 text-gray-400 mx-auto mb-4" />
2235
+ <h3 className="text-lg font-medium text-gray-900 mb-2">
2236
+ No practice examples available
2237
+ </h3>
2238
+ <p className="text-gray-600">
2239
+ Practice examples for Week {selectedWeek} haven't been set up yet.
2240
+ </p>
2241
+ </div>
2242
+ ) : (
2243
+ weeklyPractice.map((practice) => (
2244
  <div key={practice._id} className="bg-white rounded-xl shadow-lg border border-gray-100 p-8 hover:shadow-xl transition-shadow duration-300">
2245
  <div className="mb-6">
2246
  <div className="flex items-center justify-between mb-4">
 
2358
  ) : (
2359
  <div>
2360
  {practice.imageUrl ? (
2361
+ selectedWeek >= 4 && practice.imageAlignment === 'portrait-split' ? (
2362
+ // Portrait split layout
2363
+ <div className="grid grid-cols-1 md:grid-cols-2 gap-6 items-start">
2364
+ <div className="w-full flex justify-center">
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2365
  <div className="inline-block rounded-lg shadow-md overflow-hidden">
2366
+ <img src={practice.imageUrl} alt={practice.imageAlt || 'Uploaded image'} className="w-full h-auto" style={{ maxHeight: '520px', objectFit: 'contain' }} />
2367
+ </div>
2368
+ </div>
2369
+ <div className="w-full">
2370
+ <div className="bg-orange-50 rounded-lg p-4 mb-4 border border-orange-200">
2371
+ <h5 className="text-orange-900 font-semibold mb-2">Source Text (from image)</h5>
2372
+ <div className="text-orange-800 leading-relaxed text-lg font-source-text whitespace-pre-wrap">{practice.content}</div>
2373
+ </div>
2374
+ {localStorage.getItem('token') && (
2375
+ <div className="bg-white rounded-lg p-4 border border-purple-200">
2376
+ <h5 className="text-purple-900 font-semibold mb-2">Your Translation</h5>
2377
+ <div className="flex items-center justify-end space-x-2 mb-2">
2378
+ <button onClick={() => applyInlineFormat(`weekly-translation-${practice._id}`, translationText[practice._id] || '', v => setTranslationText({ ...translationText, [practice._id]: v }), '**')} className="px-2 py-1 text-xs bg-purple-100 text-purple-700 rounded">B</button>
2379
+ <button onClick={() => applyInlineFormat(`weekly-translation-${practice._id}`, translationText[practice._id] || '', v => setTranslationText({ ...translationText, [practice._id]: v }), '*')} className="px-2 py-1 text-xs bg-purple-100 text-purple-700 rounded italic">I</button>
2380
+ <button onClick={() => applyLinkFormat(`weekly-translation-${practice._id}`, translationText[practice._id] || '', v => setTranslationText({ ...translationText, [practice._id]: v }))} className="px-2 py-1 text-xs bg-purple-100 text-purple-700 rounded">Link</button>
2381
+ </div>
2382
+ <textarea id={`weekly-translation-${practice._id}`} 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..." />
2383
+ <div className="flex justify-end mt-2">
2384
+ <button onClick={() => handleSubmitTranslation(practice._id)} disabled={submitting[practice._id]} className="bg-purple-600 hover:bg-purple-700 disabled:bg-gray-400 text-white px-4 py-2 rounded-lg text-sm">{submitting[practice._id] ? 'Submitting...' : 'Submit Translation'}</button>
2385
+ </div>
2386
+ </div>
2387
+ )}
2388
+ </div>
2389
+ </div>
2390
+ ) : practice.content === 'Image-based practice' ? (
2391
+ // Image-only layout
2392
+ <div className={`flex flex-col md:flex-row gap-6 items-start ${practice.imageAlignment === 'left' ? 'md:flex-row' : practice.imageAlignment === 'right' ? 'md:flex-row-reverse' : 'md:flex-col'}`}>
2393
+ <div className={`${practice.imageAlignment === 'left' || practice.imageAlignment === 'right' ? 'w-full md:w-1/2' : 'w-full'} flex ${practice.imageAlignment === 'left' ? 'justify-start' : practice.imageAlignment === 'right' ? 'justify-end' : 'justify-center'}`}>
2394
+ <div className="inline-block rounded-lg shadow-md overflow-hidden">
2395
+ <img src={practice.imageUrl} alt={practice.imageAlt || 'Uploaded image'} className="h-auto" style={{ height: `${practice.imageSize || 200}px`, width: 'auto', maxWidth: '100%', objectFit: 'contain', imageRendering: 'auto' }} onError={(e) => { console.error('Error loading image:', e); (e.currentTarget as HTMLImageElement).style.display = 'none'; }} />
2396
+ {practice.imageAlt && (<div className="text-xs text-gray-500 mt-2 text-center">Alt: {practice.imageAlt}</div>)}
2397
  </div>
2398
  </div>
2399
  </div>
2400
  ) : (
2401
+ // Regular layout
2402
  <div className="flex flex-col md:flex-row gap-6 items-start">
 
2403
  <div className="w-full md:w-1/2 flex justify-center">
2404
  {practice.imageUrl.startsWith('data:') ? (
 
2405
  <div className="inline-block rounded-lg shadow-md overflow-hidden">
2406
+ <img src={practice.imageUrl} alt={practice.imageAlt || 'Uploaded image'} className="w-full h-auto" style={{ height: '200px', width: 'auto', objectFit: 'contain' }} onError={(e) => { console.error('Error loading image:', e); (e.currentTarget as HTMLImageElement).style.display = 'none'; }} />
 
 
 
 
 
 
 
 
 
2407
  </div>
2408
  ) : (
 
2409
  <div className="inline-block rounded-lg shadow-md bg-green-500 text-white p-6 text-center">
2410
  <div className="text-3xl mb-2">📷</div>
2411
  <div className="font-semibold">Image Uploaded</div>
 
2413
  </div>
2414
  )}
2415
  </div>
 
2416
  <div className="w-full md:w-1/2">
2417
  <p className="text-orange-800 leading-relaxed text-lg font-source-text whitespace-pre-wrap">{practice.content}</p>
2418
  </div>
 
2469
  </div>
2470
  {getStatusIcon(submission.status)}
2471
  </div>
2472
+ <p className="text-stone-800 leading-relaxed text-base mb-2 font-smiley whitespace-pre-wrap">{renderFormatted(submission.transcreation || '')}</p>
2473
  <div className="flex items-center space-x-4 text-xs text-stone-700 mt-auto">
2474
  <div className="flex items-center space-x-1">
2475
  <span className="font-medium">By:</span>
 
2483
  {(submission.voteCounts?.['1'] || 0) + (submission.voteCounts?.['2'] || 0) + (submission.voteCounts?.['3'] || 0)}
2484
  </span>
2485
  </div>
2486
+ {(submission.isOwner || (JSON.parse(localStorage.getItem('user') || '{}').role === 'admin')) && (
2487
  <button
2488
  onClick={() => handleEditSubmission(submission._id, submission.transcreation)}
2489
  className="text-purple-600 hover:text-purple-800 text-sm font-medium"
 
2506
  </div>
2507
  )}
2508
 
2509
+ {/* Translation Input (always show for logged-in users on weeks 3–6; keep hidden for image-only and portrait-split variants) */}
2510
+ {localStorage.getItem('token') && selectedWeek !== 2 && practice.content !== 'Image-based practice' && !(selectedWeek >= 4 && practice.imageAlignment === 'portrait-split') && (
2511
  <div className="bg-gradient-to-r from-purple-50 to-violet-50 rounded-xl p-6 border border-purple-200">
2512
  <div className="flex items-center space-x-2 mb-4">
2513
  <div className="bg-purple-100 rounded-full p-1">
 
2516
  <h4 className="text-purple-900 font-semibold text-lg">Your Translation</h4>
2517
  </div>
2518
  <div className="mb-4">
2519
+ <div className="flex items-center justify-end space-x-2 mb-2">
2520
+ <button onClick={() => applyInlineFormat(`weekly-translation-${practice._id}`, translationText[practice._id] || '', v => setTranslationText({ ...translationText, [practice._id]: v }), '**')} className="px-2 py-1 text-xs bg-purple-100 text-purple-700 rounded">B</button>
2521
+ <button onClick={() => applyInlineFormat(`weekly-translation-${practice._id}`, translationText[practice._id] || '', v => setTranslationText({ ...translationText, [practice._id]: v }), '*')} className="px-2 py-1 text-xs bg-purple-100 text-purple-700 rounded italic">I</button>
2522
+ <button onClick={() => applyLinkFormat(`weekly-translation-${practice._id}`, translationText[practice._id] || '', v => setTranslationText({ ...translationText, [practice._id]: v }))} className="px-2 py-1 text-xs bg-purple-100 text-purple-700 rounded">Link</button>
2523
+ </div>
2524
+ <textarea id={`weekly-translation-${practice._id}`}
2525
  value={translationText[practice._id] || ''}
2526
  onChange={(e) => setTranslationText({ ...translationText, [practice._id]: e.target.value })}
2527
  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"
 
2541
  })}
2542
  className="rounded border-gray-300 text-purple-600 focus:ring-purple-500"
2543
  />
2544
+ <span className="text-sm text-stone-700">Submit anonymously</span>
2545
  </label>
 
 
 
2546
  </div>
2547
 
2548
  <button
2549
  onClick={() => handleSubmitTranslation(practice._id)}
2550
  disabled={submitting[practice._id]}
2551
+ className="bg-purple-600 hover:bg-purple-700 disabled:bg-gray-400 text-white px-6 py-3 rounded-lg font-medium flex items-center justify-center transition-all duration-200"
2552
  >
2553
  {submitting[practice._id] ? (
2554
  <>
 
2558
  ) : (
2559
  <>
2560
  Submit Translation
 
2561
  </>
2562
  )}
2563
  </button>
 
2589
  ))
2590
  )}
2591
  </>
2592
+ )}
2593
  </div>
2594
  )}
2595
  </div>