Upload folder using huggingface_hub
Browse files
client/src/components/SubtitlingModule.tsx
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import React from 'react';
|
| 2 |
+
|
| 3 |
+
export interface SubtitleSegment {
|
| 4 |
+
id: number;
|
| 5 |
+
startTime: string;
|
| 6 |
+
endTime: string;
|
| 7 |
+
duration: string;
|
| 8 |
+
sourceText: string;
|
| 9 |
+
targetText?: string;
|
| 10 |
+
isCurrent?: boolean;
|
| 11 |
+
}
|
| 12 |
+
|
| 13 |
+
export interface VideoInfo {
|
| 14 |
+
title: string;
|
| 15 |
+
duration: string;
|
| 16 |
+
totalSegments: number;
|
| 17 |
+
currentSegment: number;
|
| 18 |
+
}
|
| 19 |
+
|
| 20 |
+
interface SubtitlingModuleProps {
|
| 21 |
+
videoInfo: VideoInfo;
|
| 22 |
+
subtitleSegments: SubtitleSegment[];
|
| 23 |
+
currentDisplayedSubtitle?: string;
|
| 24 |
+
onSegmentClick?: (id: number) => void;
|
| 25 |
+
getSegmentButtonClass?: (id: number) => string;
|
| 26 |
+
onPlayPause?: () => void;
|
| 27 |
+
isPlaying?: boolean;
|
| 28 |
+
onSaveSegment?: (id: number, text: string) => void;
|
| 29 |
+
}
|
| 30 |
+
|
| 31 |
+
/**
|
| 32 |
+
* Reusable subtitling interface template for future weeks.
|
| 33 |
+
* Not currently rendered in any page; kept for reuse.
|
| 34 |
+
*/
|
| 35 |
+
const SubtitlingModule: React.FC<SubtitlingModuleProps> = ({
|
| 36 |
+
videoInfo,
|
| 37 |
+
subtitleSegments,
|
| 38 |
+
currentDisplayedSubtitle,
|
| 39 |
+
onSegmentClick,
|
| 40 |
+
getSegmentButtonClass,
|
| 41 |
+
onPlayPause,
|
| 42 |
+
isPlaying,
|
| 43 |
+
}) => {
|
| 44 |
+
return (
|
| 45 |
+
<div className="grid grid-cols-1 lg:grid-cols-2 gap-8">
|
| 46 |
+
<div className="bg-white rounded-xl shadow-lg border border-gray-100 p-6">
|
| 47 |
+
<div className="mb-4">
|
| 48 |
+
<h3 className="text-xl font-bold text-gray-900 mb-2">{videoInfo.title}</h3>
|
| 49 |
+
</div>
|
| 50 |
+
<div className="bg-black rounded-lg aspect-video overflow-hidden relative">
|
| 51 |
+
<div className="absolute inset-0 flex items-center justify-center text-white/70 text-sm">Video Placeholder</div>
|
| 52 |
+
{currentDisplayedSubtitle && (
|
| 53 |
+
<div className="absolute bottom-8 left-1/2 transform -translate-x-1/2 z-10">
|
| 54 |
+
<div className="bg-black bg-opacity-80 text-white px-6 py-3 rounded-lg text-center max-w-lg">
|
| 55 |
+
<p className={`text-base font-medium leading-relaxed tracking-wide ${currentDisplayedSubtitle.length <= 42 ? 'whitespace-nowrap' : 'whitespace-pre-line'}`}>
|
| 56 |
+
{currentDisplayedSubtitle}
|
| 57 |
+
</p>
|
| 58 |
+
</div>
|
| 59 |
+
</div>
|
| 60 |
+
)}
|
| 61 |
+
</div>
|
| 62 |
+
</div>
|
| 63 |
+
|
| 64 |
+
<div className="bg-white rounded-xl shadow-lg border border-gray-100 p-6">
|
| 65 |
+
<h3 className="text-xl font-bold text-gray-900 mb-4">Translation Workspace</h3>
|
| 66 |
+
<div className="grid grid-cols-9 gap-1 mb-6">
|
| 67 |
+
{subtitleSegments.map((segment) => (
|
| 68 |
+
<button
|
| 69 |
+
key={segment.id}
|
| 70 |
+
onClick={() => onSegmentClick && onSegmentClick(segment.id)}
|
| 71 |
+
className={`px-1 py-1 rounded text-xs w-full ${getSegmentButtonClass ? getSegmentButtonClass(segment.id) : ''}`}
|
| 72 |
+
>
|
| 73 |
+
{segment.id}
|
| 74 |
+
</button>
|
| 75 |
+
))}
|
| 76 |
+
</div>
|
| 77 |
+
<div className="flex items-center gap-2 mb-4">
|
| 78 |
+
<button onClick={onPlayPause} className="px-3 py-1.5 text-sm rounded-md border border-gray-300">
|
| 79 |
+
{isPlaying ? 'Pause' : 'Play'}
|
| 80 |
+
</button>
|
| 81 |
+
<div className="text-sm text-gray-600">{videoInfo.currentSegment}/{videoInfo.totalSegments}</div>
|
| 82 |
+
</div>
|
| 83 |
+
<div className="space-y-4">
|
| 84 |
+
{subtitleSegments.map(seg => (
|
| 85 |
+
<div key={seg.id} className="border border-gray-200 rounded-lg p-3">
|
| 86 |
+
<div className="text-xs text-gray-500 mb-1">{seg.startTime} → {seg.endTime} ({seg.duration})</div>
|
| 87 |
+
<div className="text-sm text-gray-800 mb-2">{seg.sourceText}</div>
|
| 88 |
+
<textarea className="w-full border border-gray-300 rounded p-2 text-sm" defaultValue={seg.targetText || ''} rows={2} />
|
| 89 |
+
</div>
|
| 90 |
+
))}
|
| 91 |
+
</div>
|
| 92 |
+
</div>
|
| 93 |
+
</div>
|
| 94 |
+
);
|
| 95 |
+
};
|
| 96 |
+
|
| 97 |
+
export default SubtitlingModule;
|
| 98 |
+
|
| 99 |
+
|
client/src/pages/WeeklyPractice.tsx
CHANGED
|
@@ -1746,287 +1746,292 @@ const WeeklyPractice: React.FC = () => {
|
|
| 1746 |
|
| 1747 |
{/* Special Subtitling Interface for Week 2 (hidden from students when week is hidden) */}
|
| 1748 |
{!isWeekTransitioning && selectedWeek === 2 && (isAdmin || !isWeekHidden) ? (
|
| 1749 |
-
<div className="
|
| 1750 |
-
|
| 1751 |
-
|
| 1752 |
-
<div className="mb-
|
| 1753 |
-
<
|
| 1754 |
-
|
| 1755 |
-
|
| 1756 |
-
{/* Video Player */}
|
| 1757 |
-
<div className="bg-black rounded-lg aspect-video overflow-hidden relative">
|
| 1758 |
-
<video
|
| 1759 |
-
className="w-full h-full"
|
| 1760 |
-
controls
|
| 1761 |
-
preload="metadata"
|
| 1762 |
-
id="nike-video"
|
| 1763 |
-
onError={(e) => console.error('Video loading error:', e)}
|
| 1764 |
-
onLoadStart={() => console.log('Video loading started')}
|
| 1765 |
-
onCanPlay={() => console.log('Video can play')}
|
| 1766 |
-
>
|
| 1767 |
-
<source src="https://huggingface.co/spaces/linguabot/transcreation-frontend/resolve/main/public/videos/nike-winning-isnt-for-everyone.mp4" type="video/mp4" />
|
| 1768 |
-
Your browser does not support the video tag.
|
| 1769 |
-
</video>
|
| 1770 |
-
|
| 1771 |
-
{/* On-screen subtitles */}
|
| 1772 |
-
{currentDisplayedSubtitle && (
|
| 1773 |
-
<div className="absolute bottom-8 left-1/2 transform -translate-x-1/2 z-10">
|
| 1774 |
-
<div className="bg-black bg-opacity-80 text-white px-6 py-3 rounded-lg text-center max-w-lg">
|
| 1775 |
-
<p className={`text-base font-medium leading-relaxed tracking-wide ${currentDisplayedSubtitle.length <= 42 ? 'whitespace-nowrap' : 'whitespace-pre-line'}`} style={{ fontFamily: 'system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif' }}>
|
| 1776 |
-
{formatSubtitleForDisplay(currentDisplayedSubtitle)}
|
| 1777 |
-
</p>
|
| 1778 |
-
</div>
|
| 1779 |
-
</div>
|
| 1780 |
-
)}
|
| 1781 |
</div>
|
| 1782 |
-
|
| 1783 |
-
|
| 1784 |
-
|
| 1785 |
-
|
| 1786 |
-
<h3 className="text-xl font-bold text-gray-900 mb-4">Translation Workspace</h3>
|
| 1787 |
-
|
| 1788 |
-
{/* Segment Navigation Grid */}
|
| 1789 |
-
{loadingSubtitles ? (
|
| 1790 |
-
<div className="grid grid-cols-9 gap-1 mb-6">
|
| 1791 |
-
{Array.from({ length: 26 }, (_, i) => (
|
| 1792 |
-
<div key={i} className="px-1 py-1 rounded text-xs w-full bg-gray-200 animate-pulse">
|
| 1793 |
-
{i + 1}
|
| 1794 |
-
</div>
|
| 1795 |
-
))}
|
| 1796 |
-
</div>
|
| 1797 |
-
) : (
|
| 1798 |
-
<div className="grid grid-cols-9 gap-1 mb-6">
|
| 1799 |
-
{subtitleSegments.map((segment) => (
|
| 1800 |
-
<button
|
| 1801 |
-
key={segment.id}
|
| 1802 |
-
onClick={() => handleSegmentClick(segment.id)}
|
| 1803 |
-
className={`px-1 py-1 rounded text-xs w-full ${getSegmentButtonClass(segment.id)}`}
|
| 1804 |
-
>
|
| 1805 |
-
{segment.id}
|
| 1806 |
-
</button>
|
| 1807 |
-
))}
|
| 1808 |
-
</div>
|
| 1809 |
-
)}
|
| 1810 |
-
|
| 1811 |
-
{/* Admin Time Code Editor */}
|
| 1812 |
-
{editingSegment && (((localStorage.getItem('viewMode')||'auto') === 'student') ? false : (JSON.parse(localStorage.getItem('user') || '{}').role === 'admin')) && (
|
| 1813 |
-
<div className="bg-indigo-50 border border-indigo-200 rounded-lg p-4 mb-4">
|
| 1814 |
-
<h4 className="text-sm font-medium text-indigo-800 mb-2">Edit Time Codes for Segment {editingSegment}</h4>
|
| 1815 |
-
<div className="grid grid-cols-2 gap-2">
|
| 1816 |
-
<div>
|
| 1817 |
-
<label className="block text-xs text-indigo-700 mb-1">Start Time</label>
|
| 1818 |
-
<input
|
| 1819 |
-
type="text"
|
| 1820 |
-
id="startTimeInput"
|
| 1821 |
-
defaultValue={subtitleSegments[editingSegment - 1]?.startTime}
|
| 1822 |
-
className="w-full px-2 py-1 text-xs border border-indigo-300 rounded"
|
| 1823 |
-
placeholder="00:00:00,000"
|
| 1824 |
-
/>
|
| 1825 |
-
</div>
|
| 1826 |
-
<div>
|
| 1827 |
-
<label className="block text-xs text-indigo-700 mb-1">End Time</label>
|
| 1828 |
-
<input
|
| 1829 |
-
type="text"
|
| 1830 |
-
id="endTimeInput"
|
| 1831 |
-
defaultValue={subtitleSegments[editingSegment - 1]?.endTime}
|
| 1832 |
-
className="w-full px-2 py-1 text-xs border border-indigo-300 rounded"
|
| 1833 |
-
placeholder="00:00:00,000"
|
| 1834 |
-
/>
|
| 1835 |
-
</div>
|
| 1836 |
-
</div>
|
| 1837 |
-
<div className="flex space-x-2 mt-2">
|
| 1838 |
-
<button
|
| 1839 |
-
onClick={() => setEditingSegment(null)}
|
| 1840 |
-
className="px-2 py-1 text-xs bg-gray-500 text-white rounded"
|
| 1841 |
-
>
|
| 1842 |
-
Cancel
|
| 1843 |
-
</button>
|
| 1844 |
-
<button
|
| 1845 |
-
onClick={() => {
|
| 1846 |
-
const startInput = document.getElementById('startTimeInput') as HTMLInputElement;
|
| 1847 |
-
const endInput = document.getElementById('endTimeInput') as HTMLInputElement;
|
| 1848 |
-
if (startInput && endInput && startInput.value && endInput.value) {
|
| 1849 |
-
handleSaveTimeCode(editingSegment, startInput.value, endInput.value);
|
| 1850 |
-
}
|
| 1851 |
-
}}
|
| 1852 |
-
className="px-2 py-1 text-xs bg-indigo-600 text-white rounded"
|
| 1853 |
-
>
|
| 1854 |
-
Save
|
| 1855 |
-
</button>
|
| 1856 |
-
</div>
|
| 1857 |
-
</div>
|
| 1858 |
-
)}
|
| 1859 |
-
|
| 1860 |
-
{/* Current Segment Details */}
|
| 1861 |
-
<div className="bg-gray-50 rounded-lg p-4 mb-4 border border-gray-200">
|
| 1862 |
-
<div className="flex items-center justify-between">
|
| 1863 |
-
<p className="text-gray-800">
|
| 1864 |
-
Segment {currentSegment}: {subtitleSegments[currentSegment - 1]?.startTime} – {subtitleSegments[currentSegment - 1]?.endTime} ({subtitleSegments[currentSegment - 1]?.duration})
|
| 1865 |
-
</p>
|
| 1866 |
-
{((localStorage.getItem('viewMode')||'auto') === 'student') ? false : (JSON.parse(localStorage.getItem('user') || '{}').role === 'admin') && (
|
| 1867 |
-
<button
|
| 1868 |
-
onClick={() => handleEditTimeCode(currentSegment)}
|
| 1869 |
-
className="bg-indigo-600 hover:bg-indigo-700 text-white px-2 py-1 rounded text-xs flex items-center space-x-1"
|
| 1870 |
-
title="Edit time codes"
|
| 1871 |
-
>
|
| 1872 |
-
<PencilIcon className="w-3 h-3" />
|
| 1873 |
-
<span>Edit</span>
|
| 1874 |
-
</button>
|
| 1875 |
-
)}
|
| 1876 |
-
</div>
|
| 1877 |
-
</div>
|
| 1878 |
-
|
| 1879 |
-
{/* Source Text */}
|
| 1880 |
-
<div className="mb-4">
|
| 1881 |
-
<label className="block text-sm font-medium text-gray-700 mb-2">Source Text</label>
|
| 1882 |
-
<textarea
|
| 1883 |
-
value={subtitleText}
|
| 1884 |
-
readOnly
|
| 1885 |
-
className="w-full px-3 py-2 border border-gray-300 rounded-md bg-gray-50 text-gray-800"
|
| 1886 |
-
rows={2}
|
| 1887 |
-
/>
|
| 1888 |
-
</div>
|
| 1889 |
-
|
| 1890 |
-
{/* Target Text */}
|
| 1891 |
-
<div className="mb-4">
|
| 1892 |
-
<label className="block text-sm font-medium text-gray-700 mb-2">Target Text</label>
|
| 1893 |
-
<textarea
|
| 1894 |
-
value={targetText}
|
| 1895 |
-
onChange={(e) => handleTargetTextChange(e.target.value)}
|
| 1896 |
-
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"
|
| 1897 |
-
rows={2}
|
| 1898 |
-
placeholder="Enter your translation..."
|
| 1899 |
-
/>
|
| 1900 |
-
<div className="flex justify-between items-center mt-2">
|
| 1901 |
-
<span className="text-xs text-gray-600">Recommended: 2 lines max, 16 chars/line (Netflix standard)</span>
|
| 1902 |
-
<span className="text-xs text-gray-600">{characterCount} characters</span>
|
| 1903 |
-
</div>
|
| 1904 |
-
</div>
|
| 1905 |
-
|
| 1906 |
-
{/* Action Buttons */}
|
| 1907 |
-
<div className="flex space-x-3 mb-4">
|
| 1908 |
-
<button
|
| 1909 |
-
onClick={handleSaveTranslation}
|
| 1910 |
-
className={`px-4 py-2 rounded-lg ${
|
| 1911 |
-
saveStatus === 'Saved!' ? 'bg-green-600 hover:bg-green-700' :
|
| 1912 |
-
saveStatus === 'Saved Locally' ? 'bg-yellow-600 hover:bg-yellow-700' :
|
| 1913 |
-
saveStatus === 'Error' ? 'bg-red-600 hover:bg-red-700' :
|
| 1914 |
-
'bg-indigo-600 hover:bg-indigo-700'
|
| 1915 |
-
} text-white`}
|
| 1916 |
-
>
|
| 1917 |
-
{saveStatus}
|
| 1918 |
-
</button>
|
| 1919 |
-
<button
|
| 1920 |
-
onClick={handlePreviewTranslation}
|
| 1921 |
-
className={`px-4 py-2 rounded-lg flex items-center space-x-2 ${
|
| 1922 |
-
showTranslatedSubtitles
|
| 1923 |
-
? 'bg-green-500 hover:bg-green-600 text-white'
|
| 1924 |
-
: 'bg-gray-500 hover:bg-gray-600 text-white'
|
| 1925 |
-
}`}
|
| 1926 |
-
>
|
| 1927 |
-
<span>{showTranslatedSubtitles ? 'Show Original' : 'Show Translation'}</span>
|
| 1928 |
-
</button>
|
| 1929 |
-
</div>
|
| 1930 |
-
|
| 1931 |
-
{/* Translation Progress */}
|
| 1932 |
-
<div className="mb-4">
|
| 1933 |
-
<div className="flex justify-between items-center mb-2">
|
| 1934 |
-
<span className="text-sm font-medium text-gray-700">Translation Progress</span>
|
| 1935 |
-
<span className="text-sm text-gray-600">{Object.keys(subtitleTranslations).length} of {subtitleSegments.length} segments completed</span>
|
| 1936 |
-
</div>
|
| 1937 |
-
<div className="w-full bg-gray-200 rounded-full h-2">
|
| 1938 |
-
<div
|
| 1939 |
-
className="bg-green-500 h-2 rounded-full transition-all duration-300"
|
| 1940 |
-
style={{ width: `${(Object.keys(subtitleTranslations).length / subtitleSegments.length) * 100}%` }}
|
| 1941 |
-
></div>
|
| 1942 |
-
</div>
|
| 1943 |
-
</div>
|
| 1944 |
-
</div>
|
| 1945 |
-
|
| 1946 |
-
{/* Submissions Panel - Only show for Week 2 subtitling */}
|
| 1947 |
-
{selectedWeek === 2 && (
|
| 1948 |
-
<div className="mt-6 col-span-2">
|
| 1949 |
-
<div className="bg-white rounded-xl shadow-lg border border-gray-100 p-6">
|
| 1950 |
-
<div className="flex items-center justify-between mb-4">
|
| 1951 |
-
<h3 className="text-lg font-semibold text-gray-900">
|
| 1952 |
-
👥 View Submissions for Segment {currentSegment}
|
| 1953 |
-
</h3>
|
| 1954 |
-
<button
|
| 1955 |
-
onClick={toggleSubmissions}
|
| 1956 |
-
className={`px-4 py-2 rounded-lg flex items-center space-x-2 transition-colors duration-200 ${
|
| 1957 |
-
showSubmissions
|
| 1958 |
-
? 'bg-gray-500 hover:bg-gray-600 text-white'
|
| 1959 |
-
: 'bg-purple-600 hover:bg-purple-700 text-white'
|
| 1960 |
-
}`}
|
| 1961 |
-
>
|
| 1962 |
-
<span>{showSubmissions ? 'Hide Submissions' : 'Show Submissions'}</span>
|
| 1963 |
-
<span>{showSubmissions ? '▼' : '▶'}</span>
|
| 1964 |
-
</button>
|
| 1965 |
-
</div>
|
| 1966 |
-
|
| 1967 |
-
{showSubmissions && (
|
| 1968 |
-
<div className="space-y-4">
|
| 1969 |
-
{loadingSubmissions ? (
|
| 1970 |
-
<div className="text-center py-8">
|
| 1971 |
-
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-purple-600 mx-auto mb-4"></div>
|
| 1972 |
-
<p className="text-gray-600">Loading submissions...</p>
|
| 1973 |
-
</div>
|
| 1974 |
-
) : submissions.length > 0 ? (
|
| 1975 |
-
<>
|
| 1976 |
-
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-4">
|
| 1977 |
-
{submissions.map((submission) => (
|
| 1978 |
-
<div key={submission._id} className="bg-gray-50 rounded-lg p-4 border border-gray-200">
|
| 1979 |
-
<div className="flex items-center justify-between mb-2">
|
| 1980 |
-
<span className="font-medium text-gray-900">
|
| 1981 |
-
{submission.username}
|
| 1982 |
-
</span>
|
| 1983 |
-
<div className="flex items-center space-x-2">
|
| 1984 |
-
<span className="text-xs text-gray-500">
|
| 1985 |
-
{new Date(submission.submissionDate).toLocaleString()}
|
| 1986 |
-
</span>
|
| 1987 |
-
{((localStorage.getItem('viewMode')||'auto') === 'student') ? false : (JSON.parse(localStorage.getItem('user') || '{}').role === 'admin') && (
|
| 1988 |
-
<button
|
| 1989 |
-
onClick={() => handleDeleteSubtitleSubmission(submission._id)}
|
| 1990 |
-
className="text-red-500 hover:text-red-700 text-xs"
|
| 1991 |
-
title="Delete submission"
|
| 1992 |
-
data-submission-id={submission._id}
|
| 1993 |
-
>
|
| 1994 |
-
🗑️
|
| 1995 |
-
</button>
|
| 1996 |
-
)}
|
| 1997 |
-
</div>
|
| 1998 |
-
</div>
|
| 1999 |
-
<div className="bg-white rounded p-3 border border-gray-200">
|
| 2000 |
-
<p className="text-gray-800 text-sm leading-relaxed">
|
| 2001 |
-
{submission.chineseTranslation}
|
| 2002 |
-
</p>
|
| 2003 |
-
</div>
|
| 2004 |
-
</div>
|
| 2005 |
-
))}
|
| 2006 |
-
</div>
|
| 2007 |
-
<div className="flex justify-between items-center pt-4 border-t border-gray-200">
|
| 2008 |
-
<span className="text-sm text-gray-600">
|
| 2009 |
-
{submissionCount} submission{submissionCount !== 1 ? 's' : ''} for this segment
|
| 2010 |
-
</span>
|
| 2011 |
-
</div>
|
| 2012 |
-
</>
|
| 2013 |
-
) : (
|
| 2014 |
-
<div className="text-center py-8">
|
| 2015 |
-
<div className="bg-gray-100 rounded-full p-4 w-16 h-16 mx-auto mb-4 flex items-center justify-center">
|
| 2016 |
-
<span className="text-2xl">📝</span>
|
| 2017 |
-
</div>
|
| 2018 |
-
<h4 className="text-lg font-medium text-gray-900 mb-2">No submissions yet</h4>
|
| 2019 |
-
<p className="text-gray-600">
|
| 2020 |
-
Be the first to submit a translation for this segment!
|
| 2021 |
-
</p>
|
| 2022 |
-
</div>
|
| 2023 |
-
)}
|
| 2024 |
-
</div>
|
| 2025 |
-
)}
|
| 2026 |
-
</div>
|
| 2027 |
</div>
|
|
|
|
|
|
|
| 2028 |
)}
|
| 2029 |
</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2030 |
) : (
|
| 2031 |
/* Weekly Practice */
|
| 2032 |
<div className="space-y-6">
|
|
|
|
| 1746 |
|
| 1747 |
{/* Special Subtitling Interface for Week 2 (hidden from students when week is hidden) */}
|
| 1748 |
{!isWeekTransitioning && selectedWeek === 2 && (isAdmin || !isWeekHidden) ? (
|
| 1749 |
+
<div className="bg-white rounded-xl shadow p-6 mb-8">
|
| 1750 |
+
<h3 className="text-lg font-semibold text-gray-900 mb-4">Week 2 Practice</h3>
|
| 1751 |
+
{weeklyPracticeWeek?.translationBrief ? (
|
| 1752 |
+
<div className="bg-ui-panel rounded-xl p-6 mb-6 border border-ui-border">
|
| 1753 |
+
<h4 className="text-ui-text font-semibold text-base mb-2">Translation Brief</h4>
|
| 1754 |
+
<div className="text-ui-text leading-relaxed whitespace-pre-wrap">{renderFormatted(weeklyPracticeWeek.translationBrief)}</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1755 |
</div>
|
| 1756 |
+
) : null}
|
| 1757 |
+
{weeklyPractice.length > 0 ? weeklyPractice.map((practice) => (
|
| 1758 |
+
<div key={practice._id} className="bg-white rounded-lg border border-gray-200 p-4 mb-4">
|
| 1759 |
+
<div className="text-gray-900 whitespace-pre-wrap mb-2">{renderFormatted(practice.content)}</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1760 |
</div>
|
| 1761 |
+
)) : (
|
| 1762 |
+
<div className="text-gray-600">No practice items for Week 2 yet.</div>
|
| 1763 |
)}
|
| 1764 |
</div>
|
| 1765 |
+
) : null}
|
| 1766 |
+
{false && (
|
| 1767 |
+
<div className="bg-white rounded-xl shadow-lg border border-gray-100 p-6">
|
| 1768 |
+
<div className="mb-4">
|
| 1769 |
+
<h3 className="text-xl font-bold text-gray-900 mb-2">{videoInfo.title}</h3>
|
| 1770 |
+
</div>
|
| 1771 |
+
|
| 1772 |
+
{/* Video Player */}
|
| 1773 |
+
<div className="bg-black rounded-lg aspect-video overflow-hidden relative">
|
| 1774 |
+
<video
|
| 1775 |
+
className="w-full h-full"
|
| 1776 |
+
controls
|
| 1777 |
+
preload="metadata"
|
| 1778 |
+
id="nike-video"
|
| 1779 |
+
onError={(e) => console.error('Video loading error:', e)}
|
| 1780 |
+
onLoadStart={() => console.log('Video loading started')}
|
| 1781 |
+
onCanPlay={() => console.log('Video can play')}
|
| 1782 |
+
>
|
| 1783 |
+
<source src="https://huggingface.co/spaces/linguabot/transcreation-frontend/resolve/main/public/videos/nike-winning-isnt-for-everyone.mp4" type="video/mp4" />
|
| 1784 |
+
Your browser does not support the video tag.
|
| 1785 |
+
</video>
|
| 1786 |
+
|
| 1787 |
+
{/* On-screen subtitles */}
|
| 1788 |
+
{currentDisplayedSubtitle && (
|
| 1789 |
+
<div className="absolute bottom-8 left-1/2 transform -translate-x-1/2 z-10">
|
| 1790 |
+
<div className="bg-black bg-opacity-80 text-white px-6 py-3 rounded-lg text-center max-w-lg">
|
| 1791 |
+
<p className={`text-base font-medium leading-relaxed tracking-wide ${currentDisplayedSubtitle.length <= 42 ? 'whitespace-nowrap' : 'whitespace-pre-line'}`} style={{ fontFamily: 'system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif' }}>
|
| 1792 |
+
{formatSubtitleForDisplay(currentDisplayedSubtitle)}
|
| 1793 |
+
</p>
|
| 1794 |
+
</div>
|
| 1795 |
+
</div>
|
| 1796 |
+
)}
|
| 1797 |
+
</div>
|
| 1798 |
+
</div>
|
| 1799 |
+
|
| 1800 |
+
{/* Right Panel - Translation Workspace */}
|
| 1801 |
+
<div className="bg-white rounded-xl shadow-lg border border-gray-100 p-6">
|
| 1802 |
+
<h3 className="text-xl font-bold text-gray-900 mb-4">Translation Workspace</h3>
|
| 1803 |
+
|
| 1804 |
+
{/* Segment Navigation Grid */}
|
| 1805 |
+
{loadingSubtitles ? (
|
| 1806 |
+
<div className="grid grid-cols-9 gap-1 mb-6">
|
| 1807 |
+
{Array.from({ length: 26 }, (_, i) => (
|
| 1808 |
+
<div key={i} className="px-1 py-1 rounded text-xs w-full bg-gray-200 animate-pulse">
|
| 1809 |
+
{i + 1}
|
| 1810 |
+
</div>
|
| 1811 |
+
))}
|
| 1812 |
+
</div>
|
| 1813 |
+
) : (
|
| 1814 |
+
<div className="grid grid-cols-9 gap-1 mb-6">
|
| 1815 |
+
{subtitleSegments.map((segment) => (
|
| 1816 |
+
<button
|
| 1817 |
+
key={segment.id}
|
| 1818 |
+
onClick={() => handleSegmentClick(segment.id)}
|
| 1819 |
+
className={`px-1 py-1 rounded text-xs w-full ${getSegmentButtonClass(segment.id)}`}
|
| 1820 |
+
>
|
| 1821 |
+
{segment.id}
|
| 1822 |
+
</button>
|
| 1823 |
+
))}
|
| 1824 |
+
</div>
|
| 1825 |
+
)}
|
| 1826 |
+
|
| 1827 |
+
{/* Admin Time Code Editor */}
|
| 1828 |
+
{editingSegment && (((localStorage.getItem('viewMode')||'auto') === 'student') ? false : (JSON.parse(localStorage.getItem('user') || '{}').role === 'admin')) && (
|
| 1829 |
+
<div className="bg-indigo-50 border border-indigo-200 rounded-lg p-4 mb-4">
|
| 1830 |
+
<h4 className="text-sm font-medium text-indigo-800 mb-2">Edit Time Codes for Segment {editingSegment}</h4>
|
| 1831 |
+
<div className="grid grid-cols-2 gap-2">
|
| 1832 |
+
<div>
|
| 1833 |
+
<label className="block text-xs text-indigo-700 mb-1">Start Time</label>
|
| 1834 |
+
<input
|
| 1835 |
+
type="text"
|
| 1836 |
+
id="startTimeInput"
|
| 1837 |
+
defaultValue={subtitleSegments[editingSegment - 1]?.startTime}
|
| 1838 |
+
className="w-full px-2 py-1 text-xs border border-indigo-300 rounded"
|
| 1839 |
+
placeholder="00:00:00,000"
|
| 1840 |
+
/>
|
| 1841 |
+
</div>
|
| 1842 |
+
<div>
|
| 1843 |
+
<label className="block text-xs text-indigo-700 mb-1">End Time</label>
|
| 1844 |
+
<input
|
| 1845 |
+
type="text"
|
| 1846 |
+
id="endTimeInput"
|
| 1847 |
+
defaultValue={subtitleSegments[editingSegment - 1]?.endTime}
|
| 1848 |
+
className="w-full px-2 py-1 text-xs border border-indigo-300 rounded"
|
| 1849 |
+
placeholder="00:00:00,000"
|
| 1850 |
+
/>
|
| 1851 |
+
</div>
|
| 1852 |
+
</div>
|
| 1853 |
+
<div className="flex space-x-2 mt-2">
|
| 1854 |
+
<button
|
| 1855 |
+
onClick={() => setEditingSegment(null)}
|
| 1856 |
+
className="px-2 py-1 text-xs bg-gray-500 text-white rounded"
|
| 1857 |
+
>
|
| 1858 |
+
Cancel
|
| 1859 |
+
</button>
|
| 1860 |
+
<button
|
| 1861 |
+
onClick={() => {
|
| 1862 |
+
const startInput = document.getElementById('startTimeInput') as HTMLInputElement;
|
| 1863 |
+
const endInput = document.getElementById('endTimeInput') as HTMLInputElement;
|
| 1864 |
+
if (startInput && endInput && startInput.value && endInput.value) {
|
| 1865 |
+
handleSaveTimeCode(editingSegment, startInput.value, endInput.value);
|
| 1866 |
+
}
|
| 1867 |
+
}}
|
| 1868 |
+
className="px-2 py-1 text-xs bg-indigo-600 text-white rounded"
|
| 1869 |
+
>
|
| 1870 |
+
Save
|
| 1871 |
+
</button>
|
| 1872 |
+
</div>
|
| 1873 |
+
</div>
|
| 1874 |
+
)}
|
| 1875 |
+
|
| 1876 |
+
{/* Current Segment Details */}
|
| 1877 |
+
<div className="bg-gray-50 rounded-lg p-4 mb-4 border border-gray-200">
|
| 1878 |
+
<div className="flex items-center justify-between">
|
| 1879 |
+
<p className="text-gray-800">
|
| 1880 |
+
Segment {currentSegment}: {subtitleSegments[currentSegment - 1]?.startTime} – {subtitleSegments[currentSegment - 1]?.endTime} ({subtitleSegments[currentSegment - 1]?.duration})
|
| 1881 |
+
</p>
|
| 1882 |
+
{((localStorage.getItem('viewMode')||'auto') === 'student') ? false : (JSON.parse(localStorage.getItem('user') || '{}').role === 'admin') && (
|
| 1883 |
+
<button
|
| 1884 |
+
onClick={() => handleEditTimeCode(currentSegment)}
|
| 1885 |
+
className="bg-indigo-600 hover:bg-indigo-700 text-white px-2 py-1 rounded text-xs flex items-center space-x-1"
|
| 1886 |
+
title="Edit time codes"
|
| 1887 |
+
>
|
| 1888 |
+
<PencilIcon className="w-3 h-3" />
|
| 1889 |
+
<span>Edit</span>
|
| 1890 |
+
</button>
|
| 1891 |
+
)}
|
| 1892 |
+
</div>
|
| 1893 |
+
</div>
|
| 1894 |
+
|
| 1895 |
+
{/* Source Text */}
|
| 1896 |
+
<div className="mb-4">
|
| 1897 |
+
<label className="block text-sm font-medium text-gray-700 mb-2">Source Text</label>
|
| 1898 |
+
<textarea
|
| 1899 |
+
value={subtitleText}
|
| 1900 |
+
readOnly
|
| 1901 |
+
className="w-full px-3 py-2 border border-gray-300 rounded-md bg-gray-50 text-gray-800"
|
| 1902 |
+
rows={2}
|
| 1903 |
+
/>
|
| 1904 |
+
</div>
|
| 1905 |
+
|
| 1906 |
+
{/* Target Text */}
|
| 1907 |
+
<div className="mb-4">
|
| 1908 |
+
<label className="block text-sm font-medium text-gray-700 mb-2">Target Text</label>
|
| 1909 |
+
<textarea
|
| 1910 |
+
value={targetText}
|
| 1911 |
+
onChange={(e) => handleTargetTextChange(e.target.value)}
|
| 1912 |
+
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"
|
| 1913 |
+
rows={2}
|
| 1914 |
+
placeholder="Enter your translation..."
|
| 1915 |
+
/>
|
| 1916 |
+
<div className="flex justify-between items-center mt-2">
|
| 1917 |
+
<span className="text-xs text-gray-600">Recommended: 2 lines max, 16 chars/line (Netflix standard)</span>
|
| 1918 |
+
<span className="text-xs text-gray-600">{characterCount} characters</span>
|
| 1919 |
+
</div>
|
| 1920 |
+
</div>
|
| 1921 |
+
|
| 1922 |
+
{/* Action Buttons */}
|
| 1923 |
+
<div className="flex space-x-3 mb-4">
|
| 1924 |
+
<button
|
| 1925 |
+
onClick={handleSaveTranslation}
|
| 1926 |
+
className={`px-4 py-2 rounded-lg ${
|
| 1927 |
+
saveStatus === 'Saved!' ? 'bg-green-600 hover:bg-green-700' :
|
| 1928 |
+
saveStatus === 'Saved Locally' ? 'bg-yellow-600 hover:bg-yellow-700' :
|
| 1929 |
+
saveStatus === 'Error' ? 'bg-red-600 hover:bg-red-700' :
|
| 1930 |
+
'bg-indigo-600 hover:bg-indigo-700'
|
| 1931 |
+
} text-white`}
|
| 1932 |
+
>
|
| 1933 |
+
{saveStatus}
|
| 1934 |
+
</button>
|
| 1935 |
+
<button
|
| 1936 |
+
onClick={handlePreviewTranslation}
|
| 1937 |
+
className={`px-4 py-2 rounded-lg flex items-center space-x-2 ${
|
| 1938 |
+
showTranslatedSubtitles
|
| 1939 |
+
? 'bg-green-500 hover:bg-green-600 text-white'
|
| 1940 |
+
: 'bg-gray-500 hover:bg-gray-600 text-white'
|
| 1941 |
+
}`}
|
| 1942 |
+
>
|
| 1943 |
+
<span>{showTranslatedSubtitles ? 'Show Original' : 'Show Translation'}</span>
|
| 1944 |
+
</button>
|
| 1945 |
+
</div>
|
| 1946 |
+
|
| 1947 |
+
{/* Translation Progress */}
|
| 1948 |
+
<div className="mb-4">
|
| 1949 |
+
<div className="flex justify-between items-center mb-2">
|
| 1950 |
+
<span className="text-sm font-medium text-gray-700">Translation Progress</span>
|
| 1951 |
+
<span className="text-sm text-gray-600">{Object.keys(subtitleTranslations).length} of {subtitleSegments.length} segments completed</span>
|
| 1952 |
+
</div>
|
| 1953 |
+
<div className="w-full bg-gray-200 rounded-full h-2">
|
| 1954 |
+
<div
|
| 1955 |
+
className="bg-green-500 h-2 rounded-full transition-all duration-300"
|
| 1956 |
+
style={{ width: `${(Object.keys(subtitleTranslations).length / subtitleSegments.length) * 100}%` }}
|
| 1957 |
+
></div>
|
| 1958 |
+
</div>
|
| 1959 |
+
</div>
|
| 1960 |
+
</div>
|
| 1961 |
+
|
| 1962 |
+
{/* Submissions Panel - Only show for Week 2 subtitling */}
|
| 1963 |
+
- {selectedWeek === 2 && (
|
| 1964 |
+
+ {false && selectedWeek === 2 && (
|
| 1965 |
+
<div className="mt-6 col-span-2">
|
| 1966 |
+
<div className="bg-white rounded-xl shadow-lg border border-gray-100 p-6">
|
| 1967 |
+
<div className="flex items-center justify-between mb-4">
|
| 1968 |
+
<h3 className="text-lg font-semibold text-gray-900">
|
| 1969 |
+
👥 View Submissions for Segment {currentSegment}
|
| 1970 |
+
</h3>
|
| 1971 |
+
<button
|
| 1972 |
+
onClick={toggleSubmissions}
|
| 1973 |
+
className={`px-4 py-2 rounded-lg flex items-center space-x-2 transition-colors duration-200 ${
|
| 1974 |
+
showSubmissions
|
| 1975 |
+
? 'bg-gray-500 hover:bg-gray-600 text-white'
|
| 1976 |
+
: 'bg-purple-600 hover:bg-purple-700 text-white'
|
| 1977 |
+
}`}
|
| 1978 |
+
>
|
| 1979 |
+
<span>{showSubmissions ? 'Hide Submissions' : 'Show Submissions'}</span>
|
| 1980 |
+
<span>{showSubmissions ? '▼' : '▶'}</span>
|
| 1981 |
+
</button>
|
| 1982 |
+
</div>
|
| 1983 |
+
|
| 1984 |
+
{showSubmissions && (
|
| 1985 |
+
<div className="space-y-4">
|
| 1986 |
+
{loadingSubmissions ? (
|
| 1987 |
+
<div className="text-center py-8">
|
| 1988 |
+
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-purple-600 mx-auto mb-4"></div>
|
| 1989 |
+
<p className="text-gray-600">Loading submissions...</p>
|
| 1990 |
+
</div>
|
| 1991 |
+
) : submissions.length > 0 ? (
|
| 1992 |
+
<>
|
| 1993 |
+
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-4">
|
| 1994 |
+
{submissions.map((submission) => (
|
| 1995 |
+
<div key={submission._id} className="bg-gray-50 rounded-lg p-4 border border-gray-200">
|
| 1996 |
+
<div className="flex items-center justify-between mb-2">
|
| 1997 |
+
<span className="font-medium text-gray-900">
|
| 1998 |
+
{submission.username}
|
| 1999 |
+
</span>
|
| 2000 |
+
<div className="flex items-center space-x-2">
|
| 2001 |
+
<span className="text-xs text-gray-500">
|
| 2002 |
+
{new Date(submission.submissionDate).toLocaleString()}
|
| 2003 |
+
</span>
|
| 2004 |
+
{((localStorage.getItem('viewMode')||'auto') === 'student') ? false : (JSON.parse(localStorage.getItem('user') || '{}').role === 'admin') && (
|
| 2005 |
+
<button
|
| 2006 |
+
className="px-2 py-1 text-xs bg-red-500 text-white rounded hover:bg-red-600"
|
| 2007 |
+
onClick={async () => {
|
| 2008 |
+
try {
|
| 2009 |
+
await api.delete(`/api/subtitles/${submission._id}`);
|
| 2010 |
+
await fetchUserSubmissions(weeklyPractice);
|
| 2011 |
+
} catch (e) {
|
| 2012 |
+
console.error('Delete subtitle submission failed', e);
|
| 2013 |
+
}
|
| 2014 |
+
}}
|
| 2015 |
+
>
|
| 2016 |
+
Delete
|
| 2017 |
+
</button>
|
| 2018 |
+
)}
|
| 2019 |
+
</div>
|
| 2020 |
+
</div>
|
| 2021 |
+
<div className="text-sm text-gray-800 whitespace-pre-wrap">{submission.chineseTranslation}</div>
|
| 2022 |
+
</div>
|
| 2023 |
+
))}
|
| 2024 |
+
</div>
|
| 2025 |
+
</>
|
| 2026 |
+
) : (
|
| 2027 |
+
<div className="text-gray-600">No submissions yet.</div>
|
| 2028 |
+
)}
|
| 2029 |
+
</div>
|
| 2030 |
+
)}
|
| 2031 |
+
</div>
|
| 2032 |
+
</div>
|
| 2033 |
+
)}
|
| 2034 |
+
</div>
|
| 2035 |
) : (
|
| 2036 |
/* Weekly Practice */
|
| 2037 |
<div className="space-y-6">
|