Spaces:
Sleeping
Sleeping
Tristan Yu commited on
Commit Β·
03a1f85
1
Parent(s): 99c1fa1
Add subtitle submissions viewing feature - new UI panel below video player
Browse files
client/src/pages/WeeklyPractice.tsx
CHANGED
|
@@ -68,6 +68,16 @@ interface VideoInfo {
|
|
| 68 |
currentSegment: number;
|
| 69 |
}
|
| 70 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 71 |
const WeeklyPractice: React.FC = () => {
|
| 72 |
const [selectedWeek, setSelectedWeek] = useState<number>(() => {
|
| 73 |
const saved = localStorage.getItem('selectedWeeklyPracticeWeek');
|
|
@@ -114,6 +124,12 @@ const WeeklyPractice: React.FC = () => {
|
|
| 114 |
const [editingSegment, setEditingSegment] = useState<number | null>(null);
|
| 115 |
const [currentVideoTime, setCurrentVideoTime] = useState<number>(0);
|
| 116 |
const [currentDisplayedSubtitle, setCurrentDisplayedSubtitle] = useState<string>('');
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 117 |
|
| 118 |
const weeks = [1, 2, 3, 4, 5, 6];
|
| 119 |
|
|
@@ -155,6 +171,11 @@ const WeeklyPractice: React.FC = () => {
|
|
| 155 |
video.addEventListener('timeupdate', checkEndTime);
|
| 156 |
}
|
| 157 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 158 |
};
|
| 159 |
|
| 160 |
const parseTimeToSeconds = (timeString: string): number => {
|
|
@@ -320,6 +341,27 @@ const WeeklyPractice: React.FC = () => {
|
|
| 320 |
setTimeout(() => setSaveStatus('Save'), 2000);
|
| 321 |
}
|
| 322 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 323 |
// Clear the input field after saving
|
| 324 |
setTargetText('');
|
| 325 |
setCharacterCount(0);
|
|
@@ -453,6 +495,44 @@ const WeeklyPractice: React.FC = () => {
|
|
| 453 |
}
|
| 454 |
};
|
| 455 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 456 |
// Load subtitles when component mounts
|
| 457 |
React.useEffect(() => {
|
| 458 |
if (selectedWeek === 2) {
|
|
@@ -1284,6 +1364,98 @@ const WeeklyPractice: React.FC = () => {
|
|
| 1284 |
</div>
|
| 1285 |
</div>
|
| 1286 |
</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1287 |
</div>
|
| 1288 |
) : (
|
| 1289 |
/* Weekly Practice */
|
|
|
|
| 68 |
currentSegment: number;
|
| 69 |
}
|
| 70 |
|
| 71 |
+
interface SubtitleSubmission {
|
| 72 |
+
_id: string;
|
| 73 |
+
username: string;
|
| 74 |
+
chineseTranslation: string;
|
| 75 |
+
submissionDate: string;
|
| 76 |
+
isAnonymous: boolean;
|
| 77 |
+
status?: string;
|
| 78 |
+
notes?: string;
|
| 79 |
+
}
|
| 80 |
+
|
| 81 |
const WeeklyPractice: React.FC = () => {
|
| 82 |
const [selectedWeek, setSelectedWeek] = useState<number>(() => {
|
| 83 |
const saved = localStorage.getItem('selectedWeeklyPracticeWeek');
|
|
|
|
| 124 |
const [editingSegment, setEditingSegment] = useState<number | null>(null);
|
| 125 |
const [currentVideoTime, setCurrentVideoTime] = useState<number>(0);
|
| 126 |
const [currentDisplayedSubtitle, setCurrentDisplayedSubtitle] = useState<string>('');
|
| 127 |
+
|
| 128 |
+
// Submissions viewing state
|
| 129 |
+
const [showSubmissions, setShowSubmissions] = useState(false);
|
| 130 |
+
const [submissions, setSubmissions] = useState<SubtitleSubmission[]>([]);
|
| 131 |
+
const [loadingSubmissions, setLoadingSubmissions] = useState(false);
|
| 132 |
+
const [submissionCount, setSubmissionCount] = useState(0);
|
| 133 |
|
| 134 |
const weeks = [1, 2, 3, 4, 5, 6];
|
| 135 |
|
|
|
|
| 171 |
video.addEventListener('timeupdate', checkEndTime);
|
| 172 |
}
|
| 173 |
}
|
| 174 |
+
|
| 175 |
+
// Fetch submissions for this segment if submissions are shown
|
| 176 |
+
if (showSubmissions) {
|
| 177 |
+
fetchSubmissionsForSegment(segmentId);
|
| 178 |
+
}
|
| 179 |
};
|
| 180 |
|
| 181 |
const parseTimeToSeconds = (timeString: string): number => {
|
|
|
|
| 341 |
setTimeout(() => setSaveStatus('Save'), 2000);
|
| 342 |
}
|
| 343 |
|
| 344 |
+
// Also save as a submission
|
| 345 |
+
try {
|
| 346 |
+
const user = JSON.parse(localStorage.getItem('user') || '{}');
|
| 347 |
+
const submissionResponse = await api.post('/api/subtitle-submissions/submit', {
|
| 348 |
+
segmentId: currentSegment,
|
| 349 |
+
chineseTranslation: targetText,
|
| 350 |
+
isAnonymous: false,
|
| 351 |
+
weekNumber: 2
|
| 352 |
+
});
|
| 353 |
+
|
| 354 |
+
if (submissionResponse.data.success) {
|
| 355 |
+
console.log('β
Translation submitted successfully');
|
| 356 |
+
// Refresh submissions if they're currently shown
|
| 357 |
+
if (showSubmissions) {
|
| 358 |
+
fetchSubmissionsForSegment(currentSegment);
|
| 359 |
+
}
|
| 360 |
+
}
|
| 361 |
+
} catch (submissionError: any) {
|
| 362 |
+
console.warn('β οΈ Submission save failed:', submissionError.message);
|
| 363 |
+
}
|
| 364 |
+
|
| 365 |
// Clear the input field after saving
|
| 366 |
setTargetText('');
|
| 367 |
setCharacterCount(0);
|
|
|
|
| 495 |
}
|
| 496 |
};
|
| 497 |
|
| 498 |
+
// Fetch submissions for a specific segment
|
| 499 |
+
const fetchSubmissionsForSegment = async (segmentId: number) => {
|
| 500 |
+
try {
|
| 501 |
+
setLoadingSubmissions(true);
|
| 502 |
+
console.log('π§ Fetching submissions for segment:', segmentId);
|
| 503 |
+
|
| 504 |
+
const response = await api.get(`/api/subtitle-submissions/segment/${segmentId}?week=2`);
|
| 505 |
+
|
| 506 |
+
if (response.data.success) {
|
| 507 |
+
setSubmissions(response.data.data);
|
| 508 |
+
setSubmissionCount(response.data.count);
|
| 509 |
+
console.log('β
Fetched submissions:', response.data.count);
|
| 510 |
+
} else {
|
| 511 |
+
console.error('β Failed to fetch submissions:', response.data);
|
| 512 |
+
setSubmissions([]);
|
| 513 |
+
setSubmissionCount(0);
|
| 514 |
+
}
|
| 515 |
+
} catch (error) {
|
| 516 |
+
console.error('β Error fetching submissions:', error);
|
| 517 |
+
setSubmissions([]);
|
| 518 |
+
setSubmissionCount(0);
|
| 519 |
+
} finally {
|
| 520 |
+
setLoadingSubmissions(false);
|
| 521 |
+
}
|
| 522 |
+
};
|
| 523 |
+
|
| 524 |
+
// Toggle submissions panel
|
| 525 |
+
const toggleSubmissions = async () => {
|
| 526 |
+
if (!showSubmissions) {
|
| 527 |
+
// Show submissions and fetch data
|
| 528 |
+
setShowSubmissions(true);
|
| 529 |
+
await fetchSubmissionsForSegment(currentSegment);
|
| 530 |
+
} else {
|
| 531 |
+
// Hide submissions
|
| 532 |
+
setShowSubmissions(false);
|
| 533 |
+
}
|
| 534 |
+
};
|
| 535 |
+
|
| 536 |
// Load subtitles when component mounts
|
| 537 |
React.useEffect(() => {
|
| 538 |
if (selectedWeek === 2) {
|
|
|
|
| 1364 |
</div>
|
| 1365 |
</div>
|
| 1366 |
</div>
|
| 1367 |
+
|
| 1368 |
+
{/* Submissions Panel - Only show for Week 2 subtitling */}
|
| 1369 |
+
{selectedWeek === 2 && (
|
| 1370 |
+
<div className="mt-6">
|
| 1371 |
+
<div className="bg-white rounded-xl shadow-lg border border-gray-100 p-6">
|
| 1372 |
+
<div className="flex items-center justify-between mb-4">
|
| 1373 |
+
<h3 className="text-lg font-semibold text-gray-900">
|
| 1374 |
+
π₯ View Submissions for Segment {currentSegment}
|
| 1375 |
+
</h3>
|
| 1376 |
+
<button
|
| 1377 |
+
onClick={toggleSubmissions}
|
| 1378 |
+
className={`px-4 py-2 rounded-lg flex items-center space-x-2 transition-colors duration-200 ${
|
| 1379 |
+
showSubmissions
|
| 1380 |
+
? 'bg-gray-500 hover:bg-gray-600 text-white'
|
| 1381 |
+
: 'bg-purple-600 hover:bg-purple-700 text-white'
|
| 1382 |
+
}`}
|
| 1383 |
+
>
|
| 1384 |
+
<span>{showSubmissions ? 'Hide Submissions' : 'Show Submissions'}</span>
|
| 1385 |
+
<span>{showSubmissions ? 'βΌ' : 'βΆ'}</span>
|
| 1386 |
+
</button>
|
| 1387 |
+
</div>
|
| 1388 |
+
|
| 1389 |
+
{showSubmissions && (
|
| 1390 |
+
<div className="space-y-4">
|
| 1391 |
+
{loadingSubmissions ? (
|
| 1392 |
+
<div className="text-center py-8">
|
| 1393 |
+
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-purple-600 mx-auto mb-4"></div>
|
| 1394 |
+
<p className="text-gray-600">Loading submissions...</p>
|
| 1395 |
+
</div>
|
| 1396 |
+
) : submissions.length > 0 ? (
|
| 1397 |
+
<>
|
| 1398 |
+
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
| 1399 |
+
{submissions.map((submission) => (
|
| 1400 |
+
<div key={submission._id} className="bg-gray-50 rounded-lg p-4 border border-gray-200">
|
| 1401 |
+
<div className="flex items-center justify-between mb-2">
|
| 1402 |
+
<span className="font-medium text-gray-900">
|
| 1403 |
+
{submission.username}
|
| 1404 |
+
</span>
|
| 1405 |
+
<span className="text-xs text-gray-500">
|
| 1406 |
+
{new Date(submission.submissionDate).toLocaleString()}
|
| 1407 |
+
</span>
|
| 1408 |
+
</div>
|
| 1409 |
+
<div className="bg-white rounded p-3 border border-gray-200">
|
| 1410 |
+
<p className="text-gray-800 text-sm leading-relaxed">
|
| 1411 |
+
{submission.chineseTranslation}
|
| 1412 |
+
</p>
|
| 1413 |
+
</div>
|
| 1414 |
+
{submission.status && (
|
| 1415 |
+
<div className="mt-2">
|
| 1416 |
+
<span className={`inline-flex items-center px-2 py-1 rounded-full text-xs font-medium ${
|
| 1417 |
+
submission.status === 'approved' ? 'bg-green-100 text-green-800' :
|
| 1418 |
+
submission.status === 'reviewed' ? 'bg-blue-100 text-blue-800' :
|
| 1419 |
+
'bg-gray-100 text-gray-800'
|
| 1420 |
+
}`}>
|
| 1421 |
+
{submission.status}
|
| 1422 |
+
</span>
|
| 1423 |
+
</div>
|
| 1424 |
+
)}
|
| 1425 |
+
</div>
|
| 1426 |
+
))}
|
| 1427 |
+
</div>
|
| 1428 |
+
<div className="flex justify-between items-center pt-4 border-t border-gray-200">
|
| 1429 |
+
<span className="text-sm text-gray-600">
|
| 1430 |
+
{submissionCount} submission{submissionCount !== 1 ? 's' : ''} for this segment
|
| 1431 |
+
</span>
|
| 1432 |
+
<button
|
| 1433 |
+
onClick={() => {
|
| 1434 |
+
// Export functionality can be added here
|
| 1435 |
+
console.log('Export submissions for segment', currentSegment);
|
| 1436 |
+
}}
|
| 1437 |
+
className="px-3 py-1 text-sm bg-purple-100 text-purple-700 rounded hover:bg-purple-200 transition-colors"
|
| 1438 |
+
>
|
| 1439 |
+
Export This Segment
|
| 1440 |
+
</button>
|
| 1441 |
+
</div>
|
| 1442 |
+
</>
|
| 1443 |
+
) : (
|
| 1444 |
+
<div className="text-center py-8">
|
| 1445 |
+
<div className="bg-gray-100 rounded-full p-4 w-16 h-16 mx-auto mb-4 flex items-center justify-center">
|
| 1446 |
+
<span className="text-2xl">π</span>
|
| 1447 |
+
</div>
|
| 1448 |
+
<h4 className="text-lg font-medium text-gray-900 mb-2">No submissions yet</h4>
|
| 1449 |
+
<p className="text-gray-600">
|
| 1450 |
+
Be the first to submit a translation for this segment!
|
| 1451 |
+
</p>
|
| 1452 |
+
</div>
|
| 1453 |
+
)}
|
| 1454 |
+
</div>
|
| 1455 |
+
)}
|
| 1456 |
+
</div>
|
| 1457 |
+
</div>
|
| 1458 |
+
)}
|
| 1459 |
</div>
|
| 1460 |
) : (
|
| 1461 |
/* Weekly Practice */
|
client/src/services/api.ts
CHANGED
|
@@ -19,6 +19,7 @@ console.log('Environment variables:', {
|
|
| 19 |
});
|
| 20 |
console.log('Build timestamp:', new Date().toISOString()); // FORCE REBUILD - Video seeking and subtitle syncing
|
| 21 |
console.log('π FORCE REBUILD: Admin API routes fixed - should resolve 404 errors');
|
|
|
|
| 22 |
|
| 23 |
// Request interceptor to add auth token and user role
|
| 24 |
api.interceptors.request.use(
|
|
@@ -28,12 +29,17 @@ api.interceptors.request.use(
|
|
| 28 |
config.headers.Authorization = `Bearer ${token}`;
|
| 29 |
}
|
| 30 |
|
| 31 |
-
// Add user role to headers
|
| 32 |
const user = localStorage.getItem('user');
|
| 33 |
if (user) {
|
| 34 |
try {
|
| 35 |
const userData = JSON.parse(user);
|
| 36 |
config.headers['user-role'] = userData.role || 'visitor';
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 37 |
} catch (error) {
|
| 38 |
config.headers['user-role'] = 'visitor';
|
| 39 |
}
|
|
|
|
| 19 |
});
|
| 20 |
console.log('Build timestamp:', new Date().toISOString()); // FORCE REBUILD - Video seeking and subtitle syncing
|
| 21 |
console.log('π FORCE REBUILD: Admin API routes fixed - should resolve 404 errors');
|
| 22 |
+
console.log('π FORCE REBUILD: Subtitle submissions feature added - new UI and API endpoints');
|
| 23 |
|
| 24 |
// Request interceptor to add auth token and user role
|
| 25 |
api.interceptors.request.use(
|
|
|
|
| 29 |
config.headers.Authorization = `Bearer ${token}`;
|
| 30 |
}
|
| 31 |
|
| 32 |
+
// Add user role and info to headers
|
| 33 |
const user = localStorage.getItem('user');
|
| 34 |
if (user) {
|
| 35 |
try {
|
| 36 |
const userData = JSON.parse(user);
|
| 37 |
config.headers['user-role'] = userData.role || 'visitor';
|
| 38 |
+
config.headers['user-info'] = JSON.stringify({
|
| 39 |
+
_id: userData._id,
|
| 40 |
+
username: userData.username,
|
| 41 |
+
role: userData.role
|
| 42 |
+
});
|
| 43 |
} catch (error) {
|
| 44 |
config.headers['user-role'] = 'visitor';
|
| 45 |
}
|