linguabot commited on
Commit
da819ac
·
verified ·
1 Parent(s): 26d0140

Upload folder using huggingface_hub

Browse files
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. .gitattributes +2 -35
  2. .gitignore +62 -0
  3. Dockerfile +35 -0
  4. PROTECTION_SYSTEM_GUIDE.md +168 -0
  5. README.md +70 -7
  6. SECURITY_ENHANCEMENT_PLAN.md +433 -0
  7. backup-system.js +153 -0
  8. backup-version-control.js +364 -0
  9. backups/backup-2025-08-04T06-24-14-836Z.json +617 -0
  10. backups/complete-backup-2025-08-10T07-59-20-407Z/code/backend/index.js +252 -0
  11. backups/complete-backup-2025-08-10T07-59-20-407Z/code/backend/models/SourceText.js +75 -0
  12. backups/complete-backup-2025-08-10T07-59-20-407Z/code/backend/models/Subtitle.js +168 -0
  13. backups/complete-backup-2025-08-10T07-59-20-407Z/code/backend/models/SubtitleSubmission.js +102 -0
  14. backups/complete-backup-2025-08-10T07-59-20-407Z/code/backend/routes/auth.js +354 -0
  15. backups/complete-backup-2025-08-10T07-59-20-407Z/code/backend/routes/subtitleSubmissions.js +287 -0
  16. backups/complete-backup-2025-08-10T07-59-20-407Z/code/backend/routes/subtitles.js +343 -0
  17. backups/complete-backup-2025-08-10T07-59-20-407Z/code/backend/seed-atlas-subtitles.js +87 -0
  18. backups/complete-backup-2025-08-10T07-59-20-407Z/code/backend/seed-subtitle-submissions.js +178 -0
  19. backups/complete-backup-2025-08-10T07-59-20-407Z/code/frontend/Layout.tsx +191 -0
  20. backups/complete-backup-2025-08-10T07-59-20-407Z/code/frontend/TutorialTasks.tsx +1724 -0
  21. backups/complete-backup-2025-08-10T07-59-20-407Z/code/frontend/WeeklyPractice.tsx +0 -0
  22. backups/complete-backup-2025-08-10T07-59-20-407Z/code/frontend/api.ts +91 -0
  23. backups/complete-backup-2025-08-10T07-59-20-407Z/manifest.json +49 -0
  24. backups/complete-backup-2025-08-10T07-59-20-407Z/sourcetexts.json +0 -0
  25. backups/complete-backup-2025-08-10T07-59-20-407Z/submissions.json +1361 -0
  26. backups/complete-backup-2025-08-10T07-59-20-407Z/subtitles.json +608 -0
  27. backups/complete-backup-2025-08-10T07-59-20-407Z/subtitlesubmissions.json +1 -0
  28. backups/complete-backup-2025-08-10T07-59-20-407Z/users.json +24 -0
  29. backups/releases/release-2025-08-10T10-33-12-791Z/backend-a2c8b99.tar.gz +3 -0
  30. backups/releases/release-2025-08-10T10-33-12-791Z/db/sourcetexts.json +449 -0
  31. backups/releases/release-2025-08-10T10-33-12-791Z/db/submissions.json +56 -0
  32. backups/releases/release-2025-08-10T10-33-12-791Z/db/subtitles.json +392 -0
  33. backups/releases/release-2025-08-10T10-33-12-791Z/db/subtitlesubmissions.json +1 -0
  34. backups/releases/release-2025-08-10T10-33-12-791Z/db/users.json +24 -0
  35. backups/releases/release-2025-08-10T10-33-12-791Z/frontend-6d6d7c2.tar.gz +3 -0
  36. backups/releases/release-2025-08-10T10-33-12-791Z/manifest.json +26 -0
  37. comprehensive-backup.js +350 -0
  38. create-complete-backup.js +192 -0
  39. create-release-bundle.js +121 -0
  40. create-working-backup.js +80 -0
  41. cron-setup-guide.js +200 -0
  42. enhanced-protection-system.js +270 -0
  43. implement-security-enhancements.js +315 -0
  44. index.js +289 -0
  45. lock-subtitles.js +107 -0
  46. lock-week1-week2-tasks.js +244 -0
  47. middleware/auth.js +38 -0
  48. models/AccessSession.js +18 -0
  49. models/GroupDoc.js +17 -0
  50. models/Link.js +13 -0
.gitattributes CHANGED
@@ -1,35 +1,2 @@
1
- *.7z filter=lfs diff=lfs merge=lfs -text
2
- *.arrow filter=lfs diff=lfs merge=lfs -text
3
- *.bin filter=lfs diff=lfs merge=lfs -text
4
- *.bz2 filter=lfs diff=lfs merge=lfs -text
5
- *.ckpt filter=lfs diff=lfs merge=lfs -text
6
- *.ftz filter=lfs diff=lfs merge=lfs -text
7
- *.gz filter=lfs diff=lfs merge=lfs -text
8
- *.h5 filter=lfs diff=lfs merge=lfs -text
9
- *.joblib filter=lfs diff=lfs merge=lfs -text
10
- *.lfs.* filter=lfs diff=lfs merge=lfs -text
11
- *.mlmodel filter=lfs diff=lfs merge=lfs -text
12
- *.model filter=lfs diff=lfs merge=lfs -text
13
- *.msgpack filter=lfs diff=lfs merge=lfs -text
14
- *.npy filter=lfs diff=lfs merge=lfs -text
15
- *.npz filter=lfs diff=lfs merge=lfs -text
16
- *.onnx filter=lfs diff=lfs merge=lfs -text
17
- *.ot filter=lfs diff=lfs merge=lfs -text
18
- *.parquet filter=lfs diff=lfs merge=lfs -text
19
- *.pb filter=lfs diff=lfs merge=lfs -text
20
- *.pickle filter=lfs diff=lfs merge=lfs -text
21
- *.pkl filter=lfs diff=lfs merge=lfs -text
22
- *.pt filter=lfs diff=lfs merge=lfs -text
23
- *.pth filter=lfs diff=lfs merge=lfs -text
24
- *.rar filter=lfs diff=lfs merge=lfs -text
25
- *.safetensors filter=lfs diff=lfs merge=lfs -text
26
- saved_model/**/* filter=lfs diff=lfs merge=lfs -text
27
- *.tar.* filter=lfs diff=lfs merge=lfs -text
28
- *.tar filter=lfs diff=lfs merge=lfs -text
29
- *.tflite filter=lfs diff=lfs merge=lfs -text
30
- *.tgz filter=lfs diff=lfs merge=lfs -text
31
- *.wasm filter=lfs diff=lfs merge=lfs -text
32
- *.xz filter=lfs diff=lfs merge=lfs -text
33
- *.zip filter=lfs diff=lfs merge=lfs -text
34
- *.zst filter=lfs diff=lfs merge=lfs -text
35
- *tfevents* filter=lfs diff=lfs merge=lfs -text
 
1
+ backups/releases/release-2025-08-10T10-33-12-791Z/backend-a2c8b99.tar.gz filter=lfs diff=lfs merge=lfs -text
2
+ backups/releases/release-2025-08-10T10-33-12-791Z/frontend-6d6d7c2.tar.gz filter=lfs diff=lfs merge=lfs -text
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
.gitignore ADDED
@@ -0,0 +1,62 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Dependencies
2
+ node_modules/
3
+ npm-debug.log*
4
+ yarn-debug.log*
5
+ yarn-error.log*
6
+
7
+ # Environment variables
8
+ .env
9
+ .env.local
10
+ .env.development.local
11
+ .env.test.local
12
+ .env.production.local
13
+
14
+ # Logs
15
+ logs/
16
+ *.log
17
+
18
+ # Runtime data
19
+ pids/
20
+ *.pid
21
+ *.seed
22
+ *.pid.lock
23
+
24
+ # Coverage directory used by tools like istanbul
25
+ coverage/
26
+
27
+ # nyc test coverage
28
+ .nyc_output
29
+
30
+ # Dependency directories
31
+ node_modules/
32
+ jspm_packages/
33
+
34
+ # Optional npm cache directory
35
+ .npm
36
+
37
+ # Optional REPL history
38
+ .node_repl_history
39
+
40
+ # Output of 'npm pack'
41
+ *.tgz
42
+
43
+ # Yarn Integrity file
44
+ .yarn-integrity
45
+
46
+ # dotenv environment variables file
47
+ .env
48
+
49
+ # IDE files
50
+ .vscode/
51
+ .idea/
52
+ *.swp
53
+ *.swo
54
+
55
+ # OS generated files
56
+ .DS_Store
57
+ .DS_Store?
58
+ ._*
59
+ .Spotlight-V100
60
+ .Trashes
61
+ ehthumbs.db
62
+ Thumbs.db
Dockerfile ADDED
@@ -0,0 +1,35 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Use Node.js 18 Alpine for smaller image size
2
+ FROM node:18-alpine
3
+
4
+ # Set working directory
5
+ WORKDIR /app
6
+
7
+ # Copy package files
8
+ COPY package*.json ./
9
+
10
+ # Install dependencies
11
+ RUN npm install --only=production
12
+
13
+ # Copy server source code
14
+ COPY . ./
15
+
16
+ # Create a non-root user
17
+ RUN addgroup -g 1001 -S nodejs
18
+ RUN adduser -S nodejs -u 1001
19
+
20
+ # Change ownership of the app directory
21
+ RUN chown -R nodejs:nodejs /app
22
+ USER nodejs
23
+
24
+ # Expose port
25
+ EXPOSE 7860
26
+
27
+ # Health check with longer start period and more retries
28
+ HEALTHCHECK --interval=60s --timeout=10s --start-period=120s --retries=5 \
29
+ CMD curl -f http://localhost:7860/api/health || exit 1
30
+
31
+ # Start the application
32
+ # rebuild trigger 2025-09-01T14:18:50Z
33
+ CMD npm start
34
+
35
+ # rebuild trigger 2025-09-02T03:17:52Z
PROTECTION_SYSTEM_GUIDE.md ADDED
@@ -0,0 +1,168 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # 🔒 PROTECTION SYSTEM GUIDE
2
+
3
+ ## Overview
4
+
5
+ The protection system prevents accidental modification of critical tutorial tasks (Week 1 and Week 2) by implementing a locking mechanism that requires special authorization to modify protected content.
6
+
7
+ ## 🛡️ Current Protection Status
8
+
9
+ - **Week 1**: 3 tutorial tasks + 6 weekly practice tasks LOCKED
10
+ - **Week 2**: 12 tutorial tasks LOCKED
11
+ - **Total Protected**: 21 tasks
12
+ - **Unlock Key**: `UNLOCK_WEEK1_WEEK2_2024`
13
+
14
+ ## 📋 Protected Tasks
15
+
16
+ ### Week 1 (9 tasks)
17
+ **Tutorial Tasks (3):**
18
+ 1. Tutorial Task 1 - Gulangyu Stone
19
+ 2. Tutorial Task 2 - Gulangyu Geology
20
+ 3. Tutorial Task 3 - Opium War History
21
+
22
+ **Weekly Practice Tasks (6):**
23
+ 1. Chinese Pun 1
24
+ 2. Chinese Pun 2
25
+ 3. Chinese Pun 3
26
+ 4. English Joke 1
27
+ 5. English Joke 2
28
+ 6. English Joke 3
29
+
30
+ ### Week 2 (12 tasks)
31
+ 1. Tutorial ST 1 - Week 2
32
+ 2. Tutorial ST 2 - Week 2
33
+ 3. Tutorial ST 3 - Week 2
34
+ 4. Tutorial ST 4 - Week 2
35
+ 5. Tutorial ST 5 - Week 2
36
+ 6. Tutorial ST 6 - Week 2
37
+ 7. Tutorial ST 7 - Week 2
38
+ 8. Tutorial ST 8 - Week 2
39
+ 9. Tutorial ST 9 - Week 2
40
+ 10. Tutorial ST 10 - Week 2
41
+ 11. Tutorial ST 11 - Week 2
42
+ 12. Tutorial ST 12 - Week 2
43
+
44
+ ## 🔧 Available Scripts
45
+
46
+ ### 1. Check Protection Status
47
+ ```bash
48
+ node show-protection-status.js
49
+ ```
50
+ Shows current protection status of all tutorial tasks.
51
+
52
+ ### 2. Test Protection System
53
+ ```bash
54
+ node enhanced-protection-system.js
55
+ ```
56
+ Tests the protection system to ensure it's working correctly.
57
+
58
+ ### 3. Lock Tasks
59
+ ```bash
60
+ node lock-week1-week2-tasks.js
61
+ ```
62
+ Locks Week 1 and Week 2 tutorial tasks (already done).
63
+
64
+ ## 🚫 What's Protected
65
+
66
+ Protected tasks **CANNOT** be:
67
+ - ✅ Updated (content, images, etc.)
68
+ - ✅ Deleted
69
+ - ✅ Modified in any way
70
+
71
+ ## 🔓 How to Unlock a Protected Task
72
+
73
+ ### Step 1: Get the Task ID
74
+ ```bash
75
+ node show-protection-status.js
76
+ ```
77
+ Find the task ID from the output.
78
+
79
+ ### Step 2: Use the Unlock Function
80
+ ```javascript
81
+ const { unlockProtectedTask } = require('./enhanced-protection-system.js');
82
+
83
+ // Unlock a specific task
84
+ await unlockProtectedTask('TASK_ID_HERE', 'UNLOCK_WEEK1_WEEK2_2024');
85
+ ```
86
+
87
+ ### Step 3: Make Your Changes
88
+ Once unlocked, you can modify the task normally.
89
+
90
+ ### Step 4: Re-lock (Optional)
91
+ After making changes, you can re-lock the task:
92
+ ```bash
93
+ node lock-week1-week2-tasks.js
94
+ ```
95
+
96
+ ## ✅ Safe Operations
97
+
98
+ ### For Non-Protected Tasks
99
+ Use these functions for safe operations:
100
+
101
+ ```javascript
102
+ const { safeUpdateTask, safeDeleteTask } = require('./enhanced-protection-system.js');
103
+
104
+ // Safe update
105
+ await safeUpdateTask(taskId, { content: 'New content' }, 'Update reason');
106
+
107
+ // Safe delete
108
+ await safeDeleteTask(taskId, 'Delete reason');
109
+ ```
110
+
111
+ ### For Protected Tasks
112
+ 1. **Unlock first**: Use `unlockProtectedTask(taskId, key)`
113
+ 2. **Make changes**: Update normally
114
+ 3. **Re-lock**: Run the locking script again
115
+
116
+ ## 🚨 Important Notes
117
+
118
+ ### Protection Features
119
+ - ✅ **Automatic Protection**: Week 1 and Week 2 tasks are automatically protected
120
+ - ✅ **Clear Messages**: System provides clear feedback when operations are blocked
121
+ - ✅ **Modification History**: All changes are tracked with timestamps and reasons
122
+ - ✅ **Special Key Required**: Unlock key prevents accidental unlocking
123
+
124
+ ### Safety Measures
125
+ - ✅ **No Automatic Unlocking**: Tasks stay locked until manually unlocked
126
+ - ✅ **Key Verification**: Invalid keys are rejected
127
+ - ✅ **History Tracking**: All modifications are logged
128
+ - ✅ **Safe Functions**: Use `safeUpdateTask()` and `safeDeleteTask()` for protection
129
+
130
+ ## 🔍 Troubleshooting
131
+
132
+ ### Task Won't Update
133
+ **Problem**: Getting "CANNOT UPDATE: Task is PROTECTED" error
134
+ **Solution**:
135
+ 1. Unlock the task: `unlockProtectedTask(taskId, 'UNLOCK_WEEK1_WEEK2_2024')`
136
+ 2. Make your changes
137
+ 3. Re-lock if needed
138
+
139
+ ### Can't Find Task ID
140
+ **Problem**: Don't know the task ID
141
+ **Solution**: Run `node show-protection-status.js` to see all task IDs
142
+
143
+ ### Invalid Unlock Key
144
+ **Problem**: Getting "Invalid unlock key" error
145
+ **Solution**: Use the correct key: `UNLOCK_WEEK1_WEEK2_2024`
146
+
147
+ ## 📝 Best Practices
148
+
149
+ 1. **Always check protection status** before making changes
150
+ 2. **Use safe functions** (`safeUpdateTask`, `safeDeleteTask`) when possible
151
+ 3. **Document your changes** with clear reasons
152
+ 4. **Re-lock tasks** after making necessary changes
153
+ 5. **Keep the unlock key secure** and don't share it publicly
154
+
155
+ ## 🔐 Security Notes
156
+
157
+ - The unlock key is currently hardcoded for simplicity
158
+ - In production, consider using environment variables or a more secure method
159
+ - The protection system prevents accidental changes but doesn't replace proper backup procedures
160
+ - Always backup your database before making significant changes
161
+
162
+ ## 📞 Support
163
+
164
+ If you encounter issues with the protection system:
165
+ 1. Check the protection status first
166
+ 2. Verify you're using the correct task ID
167
+ 3. Ensure you're using the correct unlock key
168
+ 4. Check the modification history for recent changes
README.md CHANGED
@@ -1,12 +1,75 @@
1
  ---
2
- title: TransHub Backend
3
- emoji: 📈
4
- colorFrom: pink
5
- colorTo: green
6
  sdk: docker
7
  pinned: false
8
- license: mit
9
- short_description: Online collaborative translation platform (backend)
10
  ---
11
 
12
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  ---
2
+ title: Transcreation Backend
3
+ emoji: 🔧
4
+ colorFrom: blue
5
+ colorTo: indigo
6
  sdk: docker
7
  pinned: false
 
 
8
  ---
9
 
10
+ # Cultural Shift Sandbox - Backend API
11
+
12
+ A Node.js/Express backend API for the Cultural Shift Sandbox, a web-based tool for postgraduate translation students to practice transcreation and intercultural mediation.
13
+
14
+ ## Features
15
+
16
+ - **User Authentication**: Login and registration system
17
+ - **Tutorial Tasks**: Weekly tutorial assignments with translation briefs
18
+ - **Weekly Practice**: Practice exercises with cultural mediation tasks
19
+ - **Submission Management**: Create and manage translation submissions
20
+ - **Voting System**: Vote on peer submissions
21
+ - **Admin Panel**: Manage content and users (admin only)
22
+ - **Search Functionality**: Search through source texts and submissions
23
+
24
+ ## Tech Stack
25
+
26
+ - **Node.js 18** with Express
27
+ - **MongoDB** with Mongoose ODM
28
+ - **JWT Authentication** (simplified token system)
29
+ - **Rate Limiting** for API protection
30
+ - **CORS** enabled for frontend communication
31
+ - **Docker** for containerization
32
+
33
+ ## Environment Variables
34
+
35
+ Set these in your Space settings:
36
+
37
+ - `MONGODB_URI`: Your MongoDB Atlas connection string
38
+ - `NODE_ENV`: `production`
39
+ - `PORT`: `5000` (default)
40
+
41
+ ## API Endpoints
42
+
43
+ ### Authentication
44
+ - `POST /api/auth/register` - User registration
45
+ - `POST /api/auth/login` - User login
46
+ - `GET /api/auth/profile` - Get user profile
47
+
48
+ ### Source Texts
49
+ - `GET /api/source-texts/tutorial-tasks` - Get tutorial tasks
50
+ - `GET /api/source-texts/weekly-practice` - Get weekly practice texts
51
+ - `POST /api/source-texts` - Create new source text (admin)
52
+
53
+ ### Submissions
54
+ - `GET /api/submissions/voteable` - Get submissions for voting
55
+ - `GET /api/submissions/my-submissions` - Get user's submissions
56
+ - `POST /api/submissions` - Create new submission
57
+ - `POST /api/submissions/:id/vote` - Vote on submission
58
+
59
+ ### Search
60
+ - `GET /api/search` - Search source texts and submissions
61
+
62
+ ### Health Check
63
+ - `GET /api/health` - API health status
64
+ - `GET /health` - Simple health check
65
+
66
+ ## Development
67
+
68
+ This Space serves the backend API for the Cultural Shift Sandbox application. The frontend should be deployed separately and configured to communicate with this API.
69
+
70
+ ---
71
+
72
+ Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
73
+ Build trigger: 2025-08-28T12:42:29Z
74
+
75
+ Build trigger: 2025-09-01T14:21:53Z
SECURITY_ENHANCEMENT_PLAN.md ADDED
@@ -0,0 +1,433 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # 🔒 Security Enhancement Plan for Transcreation Sandbox
2
+
3
+ ## **1. Enhanced Authentication & Authorization**
4
+
5
+ ### **Current State:**
6
+ - Simple token-based auth with `user_` and `visitor_` prefixes
7
+ - Basic role-based access (admin/visitor)
8
+ - No session management or token expiration
9
+
10
+ ### **Recommended Improvements:**
11
+
12
+ #### **A. JWT Implementation**
13
+ ```javascript
14
+ // Enhanced JWT with proper expiration and refresh tokens
15
+ const jwt = require('jsonwebtoken');
16
+ const refreshTokens = new Set();
17
+
18
+ const generateTokens = (user) => {
19
+ const accessToken = jwt.sign(
20
+ { userId: user._id, role: user.role },
21
+ process.env.JWT_SECRET,
22
+ { expiresIn: '15m' }
23
+ );
24
+
25
+ const refreshToken = jwt.sign(
26
+ { userId: user._id },
27
+ process.env.JWT_REFRESH_SECRET,
28
+ { expiresIn: '7d' }
29
+ );
30
+
31
+ refreshTokens.add(refreshToken);
32
+ return { accessToken, refreshToken };
33
+ };
34
+ ```
35
+
36
+ #### **B. Role-Based Access Control (RBAC)**
37
+ ```javascript
38
+ // Enhanced middleware with granular permissions
39
+ const requireRole = (roles) => {
40
+ return (req, res, next) => {
41
+ if (!req.user || !roles.includes(req.user.role)) {
42
+ return res.status(403).json({
43
+ success: false,
44
+ message: 'Insufficient permissions'
45
+ });
46
+ }
47
+ next();
48
+ };
49
+ };
50
+
51
+ // Usage: requireRole(['admin', 'moderator'])
52
+ ```
53
+
54
+ #### **C. API Rate Limiting Enhancement**
55
+ ```javascript
56
+ // Per-user rate limiting
57
+ const userRateLimit = rateLimit({
58
+ windowMs: 15 * 60 * 1000,
59
+ max: (req) => {
60
+ if (req.user?.role === 'admin') return 1000;
61
+ if (req.user?.role === 'moderator') return 500;
62
+ return 100; // visitors
63
+ },
64
+ keyGenerator: (req) => req.user?.id || req.ip,
65
+ message: 'Too many requests from this user'
66
+ });
67
+ ```
68
+
69
+ ### **2. Data Protection & Encryption**
70
+
71
+ #### **A. Database Encryption**
72
+ ```javascript
73
+ // MongoDB Atlas already provides encryption at rest
74
+ // Additional field-level encryption for sensitive data
75
+ const crypto = require('crypto');
76
+
77
+ const encryptField = (text) => {
78
+ const cipher = crypto.createCipher('aes-256-cbc', process.env.ENCRYPTION_KEY);
79
+ let encrypted = cipher.update(text, 'utf8', 'hex');
80
+ encrypted += cipher.final('hex');
81
+ return encrypted;
82
+ };
83
+
84
+ const decryptField = (encryptedText) => {
85
+ const decipher = crypto.createDecipher('aes-256-cbc', process.env.ENCRYPTION_KEY);
86
+ let decrypted = decipher.update(encryptedText, 'hex', 'utf8');
87
+ decrypted += decipher.final('utf8');
88
+ return decrypted;
89
+ };
90
+ ```
91
+
92
+ #### **B. Input Validation & Sanitization**
93
+ ```javascript
94
+ // Enhanced input validation
95
+ const Joi = require('joi');
96
+
97
+ const subtitleSchema = Joi.object({
98
+ segmentId: Joi.number().integer().min(1).max(100).required(),
99
+ startTime: Joi.string().pattern(/^\d{2}:\d{2}:\d{2},\d{3}$/).required(),
100
+ endTime: Joi.string().pattern(/^\d{2}:\d{2}:\d{2},\d{3}$/).required(),
101
+ englishText: Joi.string().max(500).required(),
102
+ chineseTranslation: Joi.string().max(500).optional()
103
+ });
104
+
105
+ const validateSubtitle = (data) => {
106
+ return subtitleSchema.validate(data);
107
+ };
108
+ ```
109
+
110
+ ### **3. Content Protection System**
111
+
112
+ #### **A. Enhanced Protection with Checksums**
113
+ ```javascript
114
+ // Add checksums to detect unauthorized changes
115
+ const crypto = require('crypto');
116
+
117
+ const generateChecksum = (content) => {
118
+ return crypto.createHash('sha256').update(content).digest('hex');
119
+ };
120
+
121
+ // Enhanced Subtitle Schema
122
+ const subtitleSchema = new mongoose.Schema({
123
+ // ... existing fields ...
124
+ contentChecksum: { type: String, required: true },
125
+ lastVerified: { type: Date, default: Date.now },
126
+ verificationHistory: [{
127
+ timestamp: Date,
128
+ checksum: String,
129
+ verifiedBy: String,
130
+ status: String
131
+ }]
132
+ });
133
+
134
+ // Verification method
135
+ subtitleSchema.methods.verifyIntegrity = function() {
136
+ const currentChecksum = generateChecksum(this.englishText + this.startTime + this.endTime);
137
+ return currentChecksum === this.contentChecksum;
138
+ };
139
+ ```
140
+
141
+ #### **B. Watermarking System**
142
+ ```javascript
143
+ // Add invisible watermarks to detect unauthorized copying
144
+ const addWatermark = (text, userId) => {
145
+ const watermark = Buffer.from(userId).toString('base64').slice(0, 8);
146
+ return text + '\u200B' + watermark; // Zero-width space + watermark
147
+ };
148
+
149
+ const extractWatermark = (text) => {
150
+ const parts = text.split('\u200B');
151
+ return parts.length > 1 ? parts[1] : null;
152
+ };
153
+ ```
154
+
155
+ ### **4. API Security**
156
+
157
+ #### **A. Request Validation**
158
+ ```javascript
159
+ // Enhanced request validation middleware
160
+ const validateRequest = (schema) => {
161
+ return (req, res, next) => {
162
+ const { error } = schema.validate(req.body);
163
+ if (error) {
164
+ return res.status(400).json({
165
+ success: false,
166
+ message: 'Invalid request data',
167
+ details: error.details
168
+ });
169
+ }
170
+ next();
171
+ };
172
+ };
173
+ ```
174
+
175
+ #### **B. CORS Configuration**
176
+ ```javascript
177
+ // Strict CORS configuration
178
+ const corsOptions = {
179
+ origin: [
180
+ 'https://linguabot-transcreation-frontend.hf.space',
181
+ 'https://linguabot-transcreation-backend.hf.space'
182
+ ],
183
+ credentials: true,
184
+ methods: ['GET', 'POST', 'PUT', 'DELETE'],
185
+ allowedHeaders: ['Content-Type', 'Authorization', 'user-role'],
186
+ maxAge: 86400 // 24 hours
187
+ };
188
+
189
+ app.use(cors(corsOptions));
190
+ ```
191
+
192
+ ## **2. Backup & Version Control Strategy**
193
+
194
+ ### **A. Database Backup System**
195
+
196
+ #### **Automated MongoDB Atlas Backups**
197
+ ```javascript
198
+ // Enhanced backup system
199
+ const backupSystem = {
200
+ // Daily automated backups (MongoDB Atlas handles this)
201
+ // Manual backup triggers
202
+ async createManualBackup() {
203
+ const timestamp = new Date().toISOString();
204
+ const backupName = `manual-backup-${timestamp}`;
205
+
206
+ // Export all collections
207
+ const collections = ['subtitles', 'sourcetexts', 'submissions', 'users'];
208
+ const backupData = {};
209
+
210
+ for (const collection of collections) {
211
+ const data = await mongoose.connection.db.collection(collection).find({}).toArray();
212
+ backupData[collection] = data;
213
+ }
214
+
215
+ // Save to backup storage
216
+ await this.saveBackup(backupName, backupData);
217
+ return backupName;
218
+ },
219
+
220
+ async restoreFromBackup(backupName) {
221
+ const backupData = await this.loadBackup(backupName);
222
+
223
+ for (const [collection, data] of Object.entries(backupData)) {
224
+ await mongoose.connection.db.collection(collection).deleteMany({});
225
+ if (data.length > 0) {
226
+ await mongoose.connection.db.collection(collection).insertMany(data);
227
+ }
228
+ }
229
+ }
230
+ };
231
+ ```
232
+
233
+ #### **B. Git-Based Version Control for Content**
234
+
235
+ ```javascript
236
+ // Content version control system
237
+ const gitVersionControl = {
238
+ async commitContentChanges(collection, documentId, changes, userId) {
239
+ const commitMessage = `Update ${collection} ${documentId} by ${userId}`;
240
+ const timestamp = new Date().toISOString();
241
+
242
+ // Create git commit for content changes
243
+ const gitData = {
244
+ collection,
245
+ documentId,
246
+ changes,
247
+ timestamp,
248
+ userId,
249
+ commitHash: await this.createGitCommit(commitMessage, changes)
250
+ };
251
+
252
+ // Store in version control collection
253
+ await mongoose.connection.db.collection('versionControl').insertOne(gitData);
254
+ return gitData;
255
+ },
256
+
257
+ async getContentHistory(documentId) {
258
+ return await mongoose.connection.db.collection('versionControl')
259
+ .find({ documentId })
260
+ .sort({ timestamp: -1 })
261
+ .toArray();
262
+ }
263
+ };
264
+ ```
265
+
266
+ ### **C. Frontend State Management**
267
+
268
+ #### **Redux/Zustand for State Persistence**
269
+ ```javascript
270
+ // Enhanced state management with persistence
271
+ import { create } from 'zustand';
272
+ import { persist } from 'zustand/middleware';
273
+
274
+ const useAppStore = create(
275
+ persist(
276
+ (set, get) => ({
277
+ // User state
278
+ user: null,
279
+ isAuthenticated: false,
280
+
281
+ // Content state
282
+ subtitles: [],
283
+ sourceTexts: [],
284
+ submissions: [],
285
+
286
+ // Protection state
287
+ protectedContent: new Set(),
288
+
289
+ // Actions
290
+ setUser: (user) => set({ user, isAuthenticated: !!user }),
291
+ updateSubtitles: (subtitles) => set({ subtitles }),
292
+ markProtected: (contentId) => set((state) => ({
293
+ protectedContent: new Set([...state.protectedContent, contentId])
294
+ }))
295
+ }),
296
+ {
297
+ name: 'transcreation-sandbox-storage',
298
+ partialize: (state) => ({
299
+ user: state.user,
300
+ isAuthenticated: state.isAuthenticated
301
+ })
302
+ }
303
+ )
304
+ );
305
+ ```
306
+
307
+ ## **3. Monitoring & Alerting**
308
+
309
+ ### **A. Security Monitoring**
310
+ ```javascript
311
+ // Security event logging
312
+ const securityLogger = {
313
+ logSecurityEvent(event, details) {
314
+ const logEntry = {
315
+ timestamp: new Date(),
316
+ event,
317
+ details,
318
+ ip: req.ip,
319
+ userAgent: req.get('User-Agent'),
320
+ userId: req.user?.id
321
+ };
322
+
323
+ // Log to security collection
324
+ mongoose.connection.db.collection('securityLogs').insertOne(logEntry);
325
+
326
+ // Alert on suspicious activities
327
+ if (this.isSuspiciousActivity(event, details)) {
328
+ this.sendSecurityAlert(logEntry);
329
+ }
330
+ },
331
+
332
+ isSuspiciousActivity(event, details) {
333
+ const suspiciousPatterns = [
334
+ 'multiple_failed_logins',
335
+ 'unauthorized_access_attempt',
336
+ 'data_export_attempt',
337
+ 'bulk_deletion_attempt'
338
+ ];
339
+
340
+ return suspiciousPatterns.some(pattern => event.includes(pattern));
341
+ }
342
+ };
343
+ ```
344
+
345
+ ### **B. Performance Monitoring**
346
+ ```javascript
347
+ // Performance monitoring middleware
348
+ const performanceMonitor = (req, res, next) => {
349
+ const start = Date.now();
350
+
351
+ res.on('finish', () => {
352
+ const duration = Date.now() - start;
353
+ const logEntry = {
354
+ timestamp: new Date(),
355
+ method: req.method,
356
+ path: req.path,
357
+ statusCode: res.statusCode,
358
+ duration,
359
+ userId: req.user?.id
360
+ };
361
+
362
+ // Log slow requests
363
+ if (duration > 1000) {
364
+ console.warn('Slow request detected:', logEntry);
365
+ }
366
+
367
+ // Store in performance logs
368
+ mongoose.connection.db.collection('performanceLogs').insertOne(logEntry);
369
+ });
370
+
371
+ next();
372
+ };
373
+ ```
374
+
375
+ ## **4. Implementation Priority**
376
+
377
+ ### **Phase 1 (Immediate - 1-2 weeks)**
378
+ 1. ✅ Enhanced rate limiting (already implemented)
379
+ 2. ✅ Input validation and sanitization
380
+ 3. ✅ Content protection with checksums
381
+ 4. ✅ Automated backup verification
382
+
383
+ ### **Phase 2 (Short-term - 2-4 weeks)**
384
+ 1. JWT implementation with refresh tokens
385
+ 2. Enhanced RBAC system
386
+ 3. Security monitoring and alerting
387
+ 4. Git-based version control for content
388
+
389
+ ### **Phase 3 (Medium-term - 1-2 months)**
390
+ 1. Field-level encryption for sensitive data
391
+ 2. Advanced watermarking system
392
+ 3. Comprehensive audit logging
393
+ 4. Automated security testing
394
+
395
+ ## **5. Recommended Tools & Services**
396
+
397
+ ### **Security Tools:**
398
+ - **Helmet.js**: Security headers
399
+ - **Joi**: Input validation
400
+ - **Rate-limiter-flexible**: Advanced rate limiting
401
+ - **Winston**: Structured logging
402
+
403
+ ### **Monitoring Tools:**
404
+ - **MongoDB Atlas**: Built-in monitoring
405
+ - **Sentry**: Error tracking
406
+ - **LogRocket**: User session replay
407
+ - **DataDog**: Application performance monitoring
408
+
409
+ ### **Backup Services:**
410
+ - **MongoDB Atlas**: Automated backups
411
+ - **AWS S3**: Additional backup storage
412
+ - **GitHub**: Code and content version control
413
+
414
+ ## **6. Security Checklist**
415
+
416
+ ### **✅ Implemented:**
417
+ - Basic authentication
418
+ - Content protection flags
419
+ - Rate limiting
420
+ - CORS configuration
421
+
422
+ ### **🔄 In Progress:**
423
+ - Enhanced input validation
424
+ - Security monitoring
425
+
426
+ ### **📋 To Implement:**
427
+ - JWT with refresh tokens
428
+ - Field-level encryption
429
+ - Comprehensive audit logging
430
+ - Automated security testing
431
+ - Advanced watermarking
432
+
433
+ This comprehensive security plan will significantly enhance the protection of your transcreation sandbox while maintaining usability for legitimate users.
backup-system.js ADDED
@@ -0,0 +1,153 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ const mongoose = require('mongoose');
2
+ const fs = require('fs').promises;
3
+ const path = require('path');
4
+ const Submission = require('./models/Submission');
5
+ const SourceText = require('./models/SourceText');
6
+ const User = require('./models/User');
7
+ const MONGODB_URI = 'mongodb+srv://nothingyu:wSg3lbO1PkHiRMq9@sandbox.ecysggv.mongodb.net/?retryWrites=true&w=majority&appName=sandbox';
8
+
9
+ class BackupSystem {
10
+ constructor() {
11
+ this.backupDir = path.join(__dirname, 'backups');
12
+ this.ensureBackupDir();
13
+ }
14
+
15
+ async ensureBackupDir() {
16
+ try {
17
+ await fs.mkdir(this.backupDir, { recursive: true });
18
+ } catch (error) {
19
+ console.error('Failed to create backup directory:', error);
20
+ }
21
+ }
22
+
23
+ async createBackup() {
24
+ try {
25
+ console.log('🔄 Creating database backup...');
26
+ await mongoose.connect(MONGODB_URI);
27
+
28
+ const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
29
+ const backupData = {
30
+ timestamp,
31
+ collections: {}
32
+ };
33
+
34
+ // Backup Users
35
+ console.log('📋 Backing up users...');
36
+ const users = await User.find({});
37
+ backupData.collections.users = users.map(user => user.toObject());
38
+
39
+ // Backup SourceTexts
40
+ console.log('📚 Backing up source texts...');
41
+ const sourceTexts = await SourceText.find({});
42
+ backupData.collections.sourceTexts = sourceTexts.map(text => text.toObject());
43
+
44
+ // Backup Submissions
45
+ console.log('📝 Backing up submissions...');
46
+ const submissions = await Submission.find({});
47
+ backupData.collections.submissions = submissions.map(sub => sub.toObject());
48
+
49
+ // Save backup to file
50
+ const backupFile = path.join(this.backupDir, `backup-${timestamp}.json`);
51
+ await fs.writeFile(backupFile, JSON.stringify(backupData, null, 2));
52
+
53
+ console.log(`✅ Backup created: ${backupFile}`);
54
+ console.log(`📊 Backup summary:`);
55
+ console.log(` - Users: ${users.length}`);
56
+ console.log(` - Source Texts: ${sourceTexts.length}`);
57
+ console.log(` - Submissions: ${submissions.length}`);
58
+
59
+ return backupFile;
60
+ } catch (error) {
61
+ console.error('❌ Backup failed:', error);
62
+ throw error;
63
+ } finally {
64
+ await mongoose.connection.close();
65
+ }
66
+ }
67
+
68
+ async restoreBackup(backupFile) {
69
+ try {
70
+ console.log(`🔄 Restoring from backup: ${backupFile}`);
71
+ await mongoose.connect(MONGODB_URI);
72
+
73
+ const backupData = JSON.parse(await fs.readFile(backupFile, 'utf8'));
74
+
75
+ // Clear existing data
76
+ console.log('🗑️ Clearing existing data...');
77
+ await User.deleteMany({});
78
+ await SourceText.deleteMany({});
79
+ await Submission.deleteMany({});
80
+
81
+ // Restore Users
82
+ console.log('👥 Restoring users...');
83
+ if (backupData.collections.users) {
84
+ await User.insertMany(backupData.collections.users);
85
+ }
86
+
87
+ // Restore SourceTexts
88
+ console.log('📚 Restoring source texts...');
89
+ if (backupData.collections.sourceTexts) {
90
+ await SourceText.insertMany(backupData.collections.sourceTexts);
91
+ }
92
+
93
+ // Restore Submissions
94
+ console.log('📝 Restoring submissions...');
95
+ if (backupData.collections.submissions) {
96
+ await Submission.insertMany(backupData.collections.submissions);
97
+ }
98
+
99
+ console.log('✅ Backup restored successfully!');
100
+ } catch (error) {
101
+ console.error('❌ Restore failed:', error);
102
+ throw error;
103
+ } finally {
104
+ await mongoose.connection.close();
105
+ }
106
+ }
107
+
108
+ async listBackups() {
109
+ try {
110
+ const files = await fs.readdir(this.backupDir);
111
+ const backups = files.filter(file => file.startsWith('backup-') && file.endsWith('.json'));
112
+
113
+ console.log('📋 Available backups:');
114
+ backups.forEach(backup => {
115
+ console.log(` - ${backup}`);
116
+ });
117
+
118
+ return backups;
119
+ } catch (error) {
120
+ console.error('❌ Failed to list backups:', error);
121
+ return [];
122
+ }
123
+ }
124
+
125
+ async scheduleBackup() {
126
+ // Create backup every 24 hours
127
+ setInterval(async () => {
128
+ try {
129
+ await this.createBackup();
130
+ console.log('✅ Scheduled backup completed');
131
+ } catch (error) {
132
+ console.error('❌ Scheduled backup failed:', error);
133
+ }
134
+ }, 24 * 60 * 60 * 1000); // 24 hours
135
+ }
136
+ }
137
+
138
+ // Export for use in other files
139
+ module.exports = BackupSystem;
140
+
141
+ // If run directly, create a backup
142
+ if (require.main === module) {
143
+ const backupSystem = new BackupSystem();
144
+ backupSystem.createBackup()
145
+ .then(() => {
146
+ console.log('🎉 Backup completed successfully!');
147
+ process.exit(0);
148
+ })
149
+ .catch(error => {
150
+ console.error('💥 Backup failed:', error);
151
+ process.exit(1);
152
+ });
153
+ }
backup-version-control.js ADDED
@@ -0,0 +1,364 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ const mongoose = require('mongoose');
2
+ const fs = require('fs').promises;
3
+ const path = require('path');
4
+
5
+ // Atlas MongoDB connection string
6
+ const MONGODB_URI = 'mongodb+srv://nothingyu:wSg3lbO1PkHiRMq9@sandbox.ecysggv.mongodb.net/test?retryWrites=true&w=majority&appName=sandbox';
7
+
8
+ // Connect to MongoDB Atlas
9
+ const connectDB = async () => {
10
+ try {
11
+ await mongoose.connect(MONGODB_URI);
12
+ console.log('✅ Connected to MongoDB Atlas');
13
+ } catch (error) {
14
+ console.error('❌ MongoDB connection error:', error);
15
+ process.exit(1);
16
+ }
17
+ };
18
+
19
+ // Backup and Version Control System
20
+ const backupVersionControl = {
21
+ // Create comprehensive backup
22
+ async createBackup(customBackupName = null) {
23
+ try {
24
+ const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
25
+ const backupName = customBackupName || `comprehensive-backup-${timestamp}`;
26
+
27
+ console.log(`💾 Creating comprehensive backup: ${backupName}`);
28
+
29
+ const collections = ['subtitles', 'sourcetexts', 'submissions', 'users', 'securitylogs'];
30
+ const backupData = {
31
+ metadata: {
32
+ backupName,
33
+ timestamp: new Date(),
34
+ collections: collections,
35
+ totalRecords: 0,
36
+ version: '1.0',
37
+ createdBy: 'system'
38
+ },
39
+ data: {}
40
+ };
41
+
42
+ // Export all collections
43
+ for (const collection of collections) {
44
+ try {
45
+ const data = await mongoose.connection.db.collection(collection).find({}).toArray();
46
+ backupData.data[collection] = data;
47
+ backupData.metadata.totalRecords += data.length;
48
+ console.log(` 📦 Exported ${data.length} records from ${collection}`);
49
+ } catch (error) {
50
+ console.warn(` ⚠️ Could not export ${collection}:`, error.message);
51
+ }
52
+ }
53
+
54
+ // Save backup to file system
55
+ const backupDir = path.join(__dirname, 'backups');
56
+ await fs.mkdir(backupDir, { recursive: true });
57
+
58
+ const backupPath = path.join(backupDir, `${backupName}.json`);
59
+ await fs.writeFile(backupPath, JSON.stringify(backupData, null, 2));
60
+
61
+ // Create backup record in database
62
+ const backupRecord = {
63
+ backupName,
64
+ timestamp: new Date(),
65
+ collections: collections,
66
+ totalRecords: backupData.metadata.totalRecords,
67
+ filePath: backupPath,
68
+ status: 'created',
69
+ createdBy: 'system'
70
+ };
71
+
72
+ // Save to backup collection
73
+ const backupCollection = mongoose.connection.db.collection('backups');
74
+ await backupCollection.insertOne(backupRecord);
75
+
76
+ console.log(`✅ Backup created successfully: ${backupName}`);
77
+ console.log(`📊 Total records: ${backupData.metadata.totalRecords}`);
78
+ console.log(`💾 File saved: ${backupPath}`);
79
+
80
+ return backupName;
81
+
82
+ } catch (error) {
83
+ console.error('❌ Error creating backup:', error);
84
+ throw error;
85
+ }
86
+ },
87
+
88
+ // Restore from backup
89
+ async restoreFromBackup(backupName) {
90
+ try {
91
+ console.log(`🔄 Restoring from backup: ${backupName}`);
92
+
93
+ // Load backup file
94
+ const backupPath = path.join(__dirname, 'backups', `${backupName}.json`);
95
+ const backupData = JSON.parse(await fs.readFile(backupPath, 'utf8'));
96
+
97
+ console.log(`📊 Backup metadata:`, backupData.metadata);
98
+
99
+ // Confirm restoration
100
+ console.log('⚠️ This will overwrite existing data. Are you sure? (y/N)');
101
+ // In a real implementation, you'd get user confirmation here
102
+
103
+ // Restore each collection
104
+ for (const [collection, data] of Object.entries(backupData.data)) {
105
+ try {
106
+ // Clear existing data
107
+ await mongoose.connection.db.collection(collection).deleteMany({});
108
+
109
+ // Insert backup data
110
+ if (data.length > 0) {
111
+ await mongoose.connection.db.collection(collection).insertMany(data);
112
+ }
113
+
114
+ console.log(` ✅ Restored ${data.length} records to ${collection}`);
115
+ } catch (error) {
116
+ console.error(` ❌ Error restoring ${collection}:`, error.message);
117
+ }
118
+ }
119
+
120
+ console.log(`✅ Restoration completed: ${backupName}`);
121
+
122
+ } catch (error) {
123
+ console.error('❌ Error restoring from backup:', error);
124
+ throw error;
125
+ }
126
+ },
127
+
128
+ // List available backups
129
+ async listBackups() {
130
+ try {
131
+ console.log('📋 Available backups:');
132
+
133
+ // List from database
134
+ const backupCollection = mongoose.connection.db.collection('backups');
135
+ const dbBackups = await backupCollection.find({}).sort({ timestamp: -1 }).toArray();
136
+
137
+ if (dbBackups.length === 0) {
138
+ console.log(' No backups found in database');
139
+ } else {
140
+ console.log(' Database backups:');
141
+ dbBackups.forEach(backup => {
142
+ console.log(` 📦 ${backup.backupName} (${backup.totalRecords} records, ${new Date(backup.timestamp).toLocaleString()})`);
143
+ });
144
+ }
145
+
146
+ // List from file system
147
+ const backupDir = path.join(__dirname, 'backups');
148
+ try {
149
+ const files = await fs.readdir(backupDir);
150
+ const backupFiles = files.filter(file => file.endsWith('.json'));
151
+
152
+ if (backupFiles.length > 0) {
153
+ console.log(' File system backups:');
154
+ for (const file of backupFiles) {
155
+ const filePath = path.join(backupDir, file);
156
+ const stats = await fs.stat(filePath);
157
+ console.log(` 💾 ${file} (${(stats.size / 1024).toFixed(2)} KB, ${stats.mtime.toLocaleString()})`);
158
+ }
159
+ }
160
+ } catch (error) {
161
+ console.log(' No backup directory found');
162
+ }
163
+
164
+ } catch (error) {
165
+ console.error('❌ Error listing backups:', error);
166
+ }
167
+ },
168
+
169
+ // Version control for content changes
170
+ async createVersionControl() {
171
+ try {
172
+ console.log('📝 Creating version control system...');
173
+
174
+ const versionControlSchema = new mongoose.Schema({
175
+ documentId: { type: String, required: true },
176
+ collection: { type: String, required: true },
177
+ version: { type: Number, required: true },
178
+ changes: mongoose.Schema.Types.Mixed,
179
+ timestamp: { type: Date, default: Date.now },
180
+ userId: String,
181
+ commitMessage: String,
182
+ previousVersion: Number,
183
+ checksum: String
184
+ });
185
+
186
+ const VersionControl = mongoose.model('VersionControl', versionControlSchema);
187
+
188
+ // Create indexes for efficient querying
189
+ await VersionControl.createIndexes();
190
+
191
+ console.log('✅ Version control system created');
192
+
193
+ return VersionControl;
194
+
195
+ } catch (error) {
196
+ console.error('❌ Error creating version control:', error);
197
+ }
198
+ },
199
+
200
+ // Track content changes
201
+ async trackChange(collection, documentId, changes, userId, commitMessage) {
202
+ try {
203
+ const VersionControl = mongoose.model('VersionControl');
204
+
205
+ // Get current version
206
+ const latestVersion = await VersionControl.findOne({
207
+ documentId,
208
+ collection
209
+ }).sort({ version: -1 });
210
+
211
+ const newVersion = (latestVersion?.version || 0) + 1;
212
+
213
+ // Create version record
214
+ await VersionControl.create({
215
+ documentId,
216
+ collection,
217
+ version: newVersion,
218
+ changes,
219
+ userId,
220
+ commitMessage,
221
+ previousVersion: latestVersion?.version || null,
222
+ timestamp: new Date()
223
+ });
224
+
225
+ console.log(`📝 Version ${newVersion} created for ${collection}/${documentId}`);
226
+
227
+ } catch (error) {
228
+ console.error('❌ Error tracking change:', error);
229
+ }
230
+ },
231
+
232
+ // Get content history
233
+ async getContentHistory(collection, documentId) {
234
+ try {
235
+ const VersionControl = mongoose.model('VersionControl');
236
+
237
+ const history = await VersionControl.find({
238
+ documentId,
239
+ collection
240
+ }).sort({ version: -1 });
241
+
242
+ console.log(`📋 Version history for ${collection}/${documentId}:`);
243
+ history.forEach(version => {
244
+ console.log(` v${version.version} (${new Date(version.timestamp).toLocaleString()}) - ${version.commitMessage}`);
245
+ });
246
+
247
+ return history;
248
+
249
+ } catch (error) {
250
+ console.error('❌ Error getting content history:', error);
251
+ }
252
+ },
253
+
254
+ // Automated backup scheduling
255
+ async scheduleBackups() {
256
+ try {
257
+ console.log('⏰ Setting up automated backup scheduling...');
258
+
259
+ const scheduleSchema = new mongoose.Schema({
260
+ scheduleType: { type: String, enum: ['daily', 'weekly', 'monthly'], required: true },
261
+ lastBackup: Date,
262
+ nextBackup: Date,
263
+ isActive: { type: Boolean, default: true },
264
+ createdBy: String
265
+ });
266
+
267
+ const Schedule = mongoose.model('Schedule', scheduleSchema);
268
+
269
+ // Create default daily backup schedule
270
+ await Schedule.create({
271
+ scheduleType: 'daily',
272
+ lastBackup: null,
273
+ nextBackup: new Date(Date.now() + 24 * 60 * 60 * 1000), // Tomorrow
274
+ isActive: true,
275
+ createdBy: 'system'
276
+ });
277
+
278
+ console.log('✅ Automated backup schedule created (daily)');
279
+
280
+ } catch (error) {
281
+ console.error('❌ Error setting up backup scheduling:', error);
282
+ }
283
+ },
284
+
285
+ // Verify backup integrity
286
+ async verifyBackupIntegrity(backupName) {
287
+ try {
288
+ console.log(`🔍 Verifying backup integrity: ${backupName}`);
289
+
290
+ const backupPath = path.join(__dirname, 'backups', `${backupName}.json`);
291
+ const backupData = JSON.parse(await fs.readFile(backupPath, 'utf8'));
292
+
293
+ let verifiedCount = 0;
294
+ let failedCount = 0;
295
+
296
+ // Verify each collection
297
+ for (const [collection, data] of Object.entries(backupData.data)) {
298
+ try {
299
+ const currentCount = await mongoose.connection.db.collection(collection).countDocuments();
300
+ const backupCount = data.length;
301
+
302
+ if (currentCount === backupCount) {
303
+ verifiedCount++;
304
+ console.log(` ✅ ${collection}: ${backupCount} records verified`);
305
+ } else {
306
+ failedCount++;
307
+ console.log(` ❌ ${collection}: ${backupCount} in backup, ${currentCount} in database`);
308
+ }
309
+ } catch (error) {
310
+ failedCount++;
311
+ console.log(` ❌ ${collection}: verification failed`);
312
+ }
313
+ }
314
+
315
+ console.log(`🔍 Integrity verification complete:`);
316
+ console.log(` - Verified: ${verifiedCount} collections`);
317
+ console.log(` - Failed: ${failedCount} collections`);
318
+
319
+ return { verifiedCount, failedCount };
320
+
321
+ } catch (error) {
322
+ console.error('❌ Error verifying backup integrity:', error);
323
+ }
324
+ }
325
+ };
326
+
327
+ // Main function
328
+ const main = async () => {
329
+ try {
330
+ console.log('🚀 Starting backup and version control system...');
331
+
332
+ // Create comprehensive backup
333
+ const backupName = await backupVersionControl.createBackup();
334
+
335
+ // Create version control system
336
+ await backupVersionControl.createVersionControl();
337
+
338
+ // Set up automated backups
339
+ await backupVersionControl.scheduleBackups();
340
+
341
+ // List available backups
342
+ await backupVersionControl.listBackups();
343
+
344
+ console.log('\n🎉 Backup and version control system ready!');
345
+ console.log('\n📋 Available functions:');
346
+ console.log(' - createBackup(): Create new backup');
347
+ console.log(' - restoreFromBackup(name): Restore from backup');
348
+ console.log(' - listBackups(): List available backups');
349
+ console.log(' - trackChange(): Track content changes');
350
+ console.log(' - getContentHistory(): Get version history');
351
+ console.log(' - verifyBackupIntegrity(): Verify backup integrity');
352
+
353
+ } catch (error) {
354
+ console.error('❌ Error in backup and version control system:', error);
355
+ } finally {
356
+ await mongoose.disconnect();
357
+ console.log('🔌 Disconnected from MongoDB');
358
+ }
359
+ };
360
+
361
+ // Run the system
362
+ connectDB().then(() => {
363
+ main();
364
+ });
backups/backup-2025-08-04T06-24-14-836Z.json ADDED
@@ -0,0 +1,617 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "timestamp": "2025-08-04T06-24-14-836Z",
3
+ "collections": {
4
+ "users": [
5
+ {
6
+ "_id": "688f54a81fdede6af7eb6b14",
7
+ "email": "admin@example.com",
8
+ "__v": 0,
9
+ "createdAt": "2025-08-03T12:23:03.851Z",
10
+ "lastActive": "2025-08-03T12:23:03.851Z",
11
+ "password": "$2a$10$jI6X7HdNN/IkHDvo4nIK.OSwxweid1zLhjXLeeaaHGUZjBGcdra0G",
12
+ "role": "admin",
13
+ "targetCultures": [],
14
+ "username": "admin"
15
+ },
16
+ {
17
+ "_id": "688f54a81fdede6af7eb6b15",
18
+ "email": "student@example.com",
19
+ "__v": 0,
20
+ "createdAt": "2025-08-03T12:23:04.253Z",
21
+ "lastActive": "2025-08-03T12:23:04.253Z",
22
+ "password": "$2a$10$PJhcfgmQqiwdevAhvUg0ZuWZou68C3R00oWO3yNfEN.uQEaT3GT7G",
23
+ "role": "student",
24
+ "targetCultures": [],
25
+ "username": "student"
26
+ }
27
+ ],
28
+ "sourceTexts": [
29
+ {
30
+ "_id": "688eb20e6660d8ead04d5c59",
31
+ "title": "Week 3 Practice: Emotional Translation",
32
+ "content": "Translate this emotional expression: \"I am so happy\" for different cultural contexts where emotional expression varies.",
33
+ "sourceLanguage": "English",
34
+ "sourceType": "manual",
35
+ "category": "weekly-practice",
36
+ "weekNumber": 3,
37
+ "culturalElements": [],
38
+ "difficulty": "intermediate",
39
+ "tags": [],
40
+ "targetCultures": [],
41
+ "isActive": true,
42
+ "usageCount": 0,
43
+ "averageRating": 0,
44
+ "ratingCount": 0,
45
+ "isCurrent": false,
46
+ "createdAt": "2025-08-03T00:49:18.875Z",
47
+ "updatedAt": "2025-08-04T05:15:10.580Z",
48
+ "__v": 0
49
+ },
50
+ {
51
+ "_id": "688eb20e6660d8ead04d5c5b",
52
+ "title": "Week 4 Practice: Humor Translation",
53
+ "content": "Adapt this joke: \"Why did the chicken cross the road? To get to the other side!\" for a culture that doesn't have this idiom.",
54
+ "sourceLanguage": "English",
55
+ "sourceType": "manual",
56
+ "category": "weekly-practice",
57
+ "weekNumber": 4,
58
+ "culturalElements": [],
59
+ "difficulty": "intermediate",
60
+ "tags": [],
61
+ "targetCultures": [],
62
+ "isActive": true,
63
+ "usageCount": 0,
64
+ "averageRating": 0,
65
+ "ratingCount": 0,
66
+ "isCurrent": false,
67
+ "createdAt": "2025-08-03T00:49:18.876Z",
68
+ "updatedAt": "2025-08-04T05:15:10.580Z",
69
+ "__v": 0
70
+ },
71
+ {
72
+ "_id": "688eb20e6660d8ead04d5c5d",
73
+ "title": "Week 5 Practice: Formal vs Informal",
74
+ "content": "Translate this business communication: \"We would appreciate your prompt response\" for different formality levels.",
75
+ "sourceLanguage": "English",
76
+ "sourceType": "manual",
77
+ "category": "weekly-practice",
78
+ "weekNumber": 5,
79
+ "culturalElements": [],
80
+ "difficulty": "intermediate",
81
+ "tags": [],
82
+ "targetCultures": [],
83
+ "isActive": true,
84
+ "usageCount": 0,
85
+ "averageRating": 0,
86
+ "ratingCount": 0,
87
+ "isCurrent": false,
88
+ "createdAt": "2025-08-03T00:49:18.877Z",
89
+ "updatedAt": "2025-08-04T05:15:10.580Z",
90
+ "__v": 0
91
+ },
92
+ {
93
+ "_id": "688eb20e6660d8ead04d5c5f",
94
+ "title": "Week 6 Practice: Cultural Values",
95
+ "content": "Adapt this value statement: \"Success comes from hard work\" for cultures with different views on success and work.",
96
+ "sourceLanguage": "English",
97
+ "sourceType": "manual",
98
+ "category": "weekly-practice",
99
+ "weekNumber": 6,
100
+ "culturalElements": [],
101
+ "difficulty": "intermediate",
102
+ "tags": [],
103
+ "targetCultures": [],
104
+ "isActive": true,
105
+ "usageCount": 0,
106
+ "averageRating": 0,
107
+ "ratingCount": 0,
108
+ "isCurrent": false,
109
+ "createdAt": "2025-08-03T00:49:18.878Z",
110
+ "updatedAt": "2025-08-04T05:15:10.580Z",
111
+ "__v": 0
112
+ },
113
+ {
114
+ "_id": "689041ded51e5886c0205aaa",
115
+ "title": "Chinese Pun 1",
116
+ "content": "为什么睡前一定要吃夜宵?因为这样才不会做饿梦。",
117
+ "sourceLanguage": "Chinese",
118
+ "sourceType": "manual",
119
+ "category": "weekly-practice",
120
+ "weekNumber": 1,
121
+ "difficulty": "intermediate",
122
+ "tags": [],
123
+ "targetCultures": [],
124
+ "isActive": true,
125
+ "usageCount": 0,
126
+ "averageRating": 0,
127
+ "ratingCount": 0,
128
+ "isCurrent": false,
129
+ "culturalElements": [],
130
+ "__v": 0,
131
+ "createdAt": "2025-08-04T05:15:10.506Z",
132
+ "updatedAt": "2025-08-04T05:15:10.506Z"
133
+ },
134
+ {
135
+ "_id": "689041ded51e5886c0205aab",
136
+ "title": "Chinese Pun 2",
137
+ "content": "女娲用什么补天?强扭的瓜。",
138
+ "sourceLanguage": "Chinese",
139
+ "sourceType": "manual",
140
+ "category": "weekly-practice",
141
+ "weekNumber": 1,
142
+ "difficulty": "intermediate",
143
+ "tags": [],
144
+ "targetCultures": [],
145
+ "isActive": true,
146
+ "usageCount": 0,
147
+ "averageRating": 0,
148
+ "ratingCount": 0,
149
+ "isCurrent": false,
150
+ "culturalElements": [],
151
+ "__v": 0,
152
+ "createdAt": "2025-08-04T05:15:10.506Z",
153
+ "updatedAt": "2025-08-04T05:15:10.506Z"
154
+ },
155
+ {
156
+ "_id": "689041ded51e5886c0205aac",
157
+ "title": "Chinese Pun 3",
158
+ "content": "什么动物最容易摔倒?狐狸,因为它狡猾(脚滑)。",
159
+ "sourceLanguage": "Chinese",
160
+ "sourceType": "manual",
161
+ "category": "weekly-practice",
162
+ "weekNumber": 1,
163
+ "difficulty": "intermediate",
164
+ "tags": [],
165
+ "targetCultures": [],
166
+ "isActive": true,
167
+ "usageCount": 0,
168
+ "averageRating": 0,
169
+ "ratingCount": 0,
170
+ "isCurrent": false,
171
+ "culturalElements": [],
172
+ "__v": 0,
173
+ "createdAt": "2025-08-04T05:15:10.506Z",
174
+ "updatedAt": "2025-08-04T05:15:10.506Z"
175
+ },
176
+ {
177
+ "_id": "689041ded51e5886c0205aad",
178
+ "title": "English Joke 1",
179
+ "content": "Why don't scientists trust atoms? Because they make up everything!",
180
+ "sourceLanguage": "English",
181
+ "sourceType": "manual",
182
+ "category": "weekly-practice",
183
+ "weekNumber": 1,
184
+ "difficulty": "intermediate",
185
+ "tags": [],
186
+ "targetCultures": [],
187
+ "isActive": true,
188
+ "usageCount": 0,
189
+ "averageRating": 0,
190
+ "ratingCount": 0,
191
+ "isCurrent": false,
192
+ "culturalElements": [],
193
+ "__v": 0,
194
+ "createdAt": "2025-08-04T05:15:10.506Z",
195
+ "updatedAt": "2025-08-04T05:15:10.506Z"
196
+ },
197
+ {
198
+ "_id": "689041ded51e5886c0205aae",
199
+ "title": "English Joke 2",
200
+ "content": "What do you call a fake noodle? An impasta!",
201
+ "sourceLanguage": "English",
202
+ "sourceType": "manual",
203
+ "category": "weekly-practice",
204
+ "weekNumber": 1,
205
+ "difficulty": "intermediate",
206
+ "tags": [],
207
+ "targetCultures": [],
208
+ "isActive": true,
209
+ "usageCount": 0,
210
+ "averageRating": 0,
211
+ "ratingCount": 0,
212
+ "isCurrent": false,
213
+ "culturalElements": [],
214
+ "__v": 0,
215
+ "createdAt": "2025-08-04T05:15:10.506Z",
216
+ "updatedAt": "2025-08-04T05:15:10.506Z"
217
+ },
218
+ {
219
+ "_id": "689041ded51e5886c0205aaf",
220
+ "title": "English Joke 3",
221
+ "content": "Why did the scarecrow win an award? Because he was outstanding in his field!",
222
+ "sourceLanguage": "English",
223
+ "sourceType": "manual",
224
+ "category": "weekly-practice",
225
+ "weekNumber": 1,
226
+ "difficulty": "intermediate",
227
+ "tags": [],
228
+ "targetCultures": [],
229
+ "isActive": true,
230
+ "usageCount": 0,
231
+ "averageRating": 0,
232
+ "ratingCount": 0,
233
+ "isCurrent": false,
234
+ "culturalElements": [],
235
+ "__v": 0,
236
+ "createdAt": "2025-08-04T05:15:10.506Z",
237
+ "updatedAt": "2025-08-04T05:15:10.506Z"
238
+ },
239
+ {
240
+ "_id": "68904513483967a3d97c65e5",
241
+ "title": "Tutorial Task 1 - Gulangyu Stone",
242
+ "content": "本屿西南临海处,有一奇礁,传为\"鼓浪石\"。\"鼓浪\"一名不仅名出实据且诗意磅礴,因此逐渐取代早前随意粗陋的\"圆洲仔\",成为本岛雅名。日光岩上现存年代最早的石刻,便是曾任泉州府同知的丁一中,于明代万历元年手书的\"鼓浪洞天\"四字。",
243
+ "sourceLanguage": "Chinese",
244
+ "sourceType": "manual",
245
+ "category": "tutorial",
246
+ "weekNumber": 1,
247
+ "translationBrief": "The municipal government of Xiamen plans to launch an international campaign to attract foreign investors and tourists. As part of the campaign, they're commissioning an English translation of a Chinese booklet about the city's history of foreign trade and cultural exchange, in addition to its tourist attractions. You've been provided with the following excerpt for a test translation. They are aware that certain elements in the source text might not work well when translated into English for promotional purposes. So you're given the license to make changes where appropriate, but a list of such changes should be provided for the reference of the commissioner.",
248
+ "difficulty": "intermediate",
249
+ "tags": [],
250
+ "targetCultures": [],
251
+ "isActive": true,
252
+ "usageCount": 0,
253
+ "averageRating": 0,
254
+ "ratingCount": 0,
255
+ "isCurrent": false,
256
+ "culturalElements": [],
257
+ "__v": 0,
258
+ "createdAt": "2025-08-04T05:28:51.743Z",
259
+ "updatedAt": "2025-08-04T05:28:51.743Z"
260
+ },
261
+ {
262
+ "_id": "68904513483967a3d97c65e6",
263
+ "title": "Tutorial Task 2 - Gulangyu Geology",
264
+ "content": "鼓浪屿的地质由1亿多年前的燕山晚期中粒花岗岩构成。而鼓浪屿的植被似乎比磬石还要坚挺,千奇百怪的树木花草,如天外来客,见隙扎根, 逢��生长。岩有多高,它们就长在多高;石有多峭,它们就长得多俏,近看是翠润点点,杂树生花;远观是青岚片片,染上云间。大自然的鬼斧神工,独钟神秀,把鼓浪屿雕塑成人工造景望尘莫及的纯天然景观。如此地形地貌,再衬之以碧海蓝天,成就了小小鼓浪屿,宛如伊人,在水中央,波光潋滟,旖旎无限的绝世美感。",
265
+ "sourceLanguage": "Chinese",
266
+ "sourceType": "manual",
267
+ "category": "tutorial",
268
+ "weekNumber": 1,
269
+ "translationBrief": "The municipal government of Xiamen plans to launch an international campaign to attract foreign investors and tourists. As part of the campaign, they're commissioning an English translation of a Chinese booklet about the city's history of foreign trade and cultural exchange, in addition to its tourist attractions. You've been provided with the following excerpt for a test translation. They are aware that certain elements in the source text might not work well when translated into English for promotional purposes. So you're given the license to make changes where appropriate, but a list of such changes should be provided for the reference of the commissioner.",
270
+ "difficulty": "intermediate",
271
+ "tags": [],
272
+ "targetCultures": [],
273
+ "isActive": true,
274
+ "usageCount": 0,
275
+ "averageRating": 0,
276
+ "ratingCount": 0,
277
+ "isCurrent": false,
278
+ "culturalElements": [],
279
+ "__v": 0,
280
+ "createdAt": "2025-08-04T05:28:51.743Z",
281
+ "updatedAt": "2025-08-04T05:28:51.743Z"
282
+ },
283
+ {
284
+ "_id": "68904513483967a3d97c65e7",
285
+ "title": "Tutorial Task 3 - Opium War History",
286
+ "content": "鸦片战争期间,英国舰队侵入厦门。随后,厦门被划为\"五口通商\"口岸之一,西方列强得以名正言顺地进驻厦门。传教士、政客、商人等各行各业人等,纷至沓来,把鼓浪屿变成大清国的化外之地,安居天堂。他们在岛上设立现代管理机构,如工部局、会审公堂、理船厅公所等;开办公司、企业、洋行、如大北电报公司、汇丰银行、德记洋行、和记洋行等;创办现代医疗机构推行西医疗法,如救世、博爱,还有为妇女专设的威廉明娜等医院;建造宗教场所传播福音,如福音堂、天主堂、三一堂等。。。",
287
+ "sourceLanguage": "Chinese",
288
+ "sourceType": "manual",
289
+ "category": "tutorial",
290
+ "weekNumber": 1,
291
+ "translationBrief": "The municipal government of Xiamen plans to launch an international campaign to attract foreign investors and tourists. As part of the campaign, they're commissioning an English translation of a Chinese booklet about the city's history of foreign trade and cultural exchange, in addition to its tourist attractions. You've been provided with the following excerpt for a test translation. They are aware that certain elements in the source text might not work well when translated into English for promotional purposes. So you're given the license to make changes where appropriate, but a list of such changes should be provided for the reference of the commissioner.",
292
+ "difficulty": "intermediate",
293
+ "tags": [],
294
+ "targetCultures": [],
295
+ "isActive": true,
296
+ "usageCount": 0,
297
+ "averageRating": 0,
298
+ "ratingCount": 0,
299
+ "isCurrent": false,
300
+ "culturalElements": [],
301
+ "__v": 0,
302
+ "createdAt": "2025-08-04T05:28:51.744Z",
303
+ "updatedAt": "2025-08-04T05:28:51.744Z"
304
+ },
305
+ {
306
+ "_id": "68904513483967a3d97c65ea",
307
+ "title": "Tutorial Task 1 - Dog Puffer Jacket",
308
+ "content": "Dog Puffer Jacket - Premium quality winter wear for your beloved pet.",
309
+ "sourceLanguage": "English",
310
+ "sourceType": "manual",
311
+ "category": "tutorial",
312
+ "weekNumber": 2,
313
+ "translationBrief": "You have been asked to translate the following for marketing the product in a country where your LOTE is spoken.",
314
+ "imageUrl": "https://example.com/dog-puffer-1.jpg",
315
+ "imageAlt": "Dog Puffer Jacket 1",
316
+ "difficulty": "intermediate",
317
+ "tags": [],
318
+ "targetCultures": [],
319
+ "isActive": true,
320
+ "usageCount": 0,
321
+ "averageRating": 0,
322
+ "ratingCount": 0,
323
+ "isCurrent": false,
324
+ "culturalElements": [],
325
+ "__v": 0,
326
+ "createdAt": "2025-08-04T05:28:51.846Z",
327
+ "updatedAt": "2025-08-04T05:28:51.846Z"
328
+ },
329
+ {
330
+ "_id": "68904513483967a3d97c65eb",
331
+ "title": "Tutorial Task 2 - Dog Puffer Jacket",
332
+ "content": "Keep your dog warm and stylish with our comfortable puffer jacket.",
333
+ "sourceLanguage": "English",
334
+ "sourceType": "manual",
335
+ "category": "tutorial",
336
+ "weekNumber": 2,
337
+ "translationBrief": "You have been asked to translate the following for marketing the product in a country where your LOTE is spoken.",
338
+ "imageUrl": "https://example.com/dog-puffer-2.jpg",
339
+ "imageAlt": "Dog Puffer Jacket 2",
340
+ "difficulty": "intermediate",
341
+ "tags": [],
342
+ "targetCultures": [],
343
+ "isActive": true,
344
+ "usageCount": 0,
345
+ "averageRating": 0,
346
+ "ratingCount": 0,
347
+ "isCurrent": false,
348
+ "culturalElements": [],
349
+ "__v": 0,
350
+ "createdAt": "2025-08-04T05:28:51.846Z",
351
+ "updatedAt": "2025-08-04T05:28:51.846Z"
352
+ },
353
+ {
354
+ "_id": "68904513483967a3d97c65ec",
355
+ "title": "Tutorial Task 3 - Dog Puffer Jacket",
356
+ "content": "Water-resistant material ensures your dog stays dry in any weather.",
357
+ "sourceLanguage": "English",
358
+ "sourceType": "manual",
359
+ "category": "tutorial",
360
+ "weekNumber": 2,
361
+ "translationBrief": "You have been asked to translate the following for marketing the product in a country where your LOTE is spoken.",
362
+ "imageUrl": "https://example.com/dog-puffer-3.jpg",
363
+ "imageAlt": "Dog Puffer Jacket 3",
364
+ "difficulty": "intermediate",
365
+ "tags": [],
366
+ "targetCultures": [],
367
+ "isActive": true,
368
+ "usageCount": 0,
369
+ "averageRating": 0,
370
+ "ratingCount": 0,
371
+ "isCurrent": false,
372
+ "culturalElements": [],
373
+ "__v": 0,
374
+ "createdAt": "2025-08-04T05:28:51.846Z",
375
+ "updatedAt": "2025-08-04T05:28:51.846Z"
376
+ },
377
+ {
378
+ "_id": "68904513483967a3d97c65ed",
379
+ "title": "Tutorial Task 4 - Dog Puffer Jacket",
380
+ "content": "Available in multiple sizes to fit dogs of all breeds and ages.",
381
+ "sourceLanguage": "English",
382
+ "sourceType": "manual",
383
+ "category": "tutorial",
384
+ "weekNumber": 2,
385
+ "translationBrief": "You have been asked to translate the following for marketing the product in a country where your LOTE is spoken.",
386
+ "imageUrl": "https://example.com/dog-puffer-4.jpg",
387
+ "imageAlt": "Dog Puffer Jacket 4",
388
+ "difficulty": "intermediate",
389
+ "tags": [],
390
+ "targetCultures": [],
391
+ "isActive": true,
392
+ "usageCount": 0,
393
+ "averageRating": 0,
394
+ "ratingCount": 0,
395
+ "isCurrent": false,
396
+ "culturalElements": [],
397
+ "__v": 0,
398
+ "createdAt": "2025-08-04T05:28:51.846Z",
399
+ "updatedAt": "2025-08-04T05:28:51.846Z"
400
+ },
401
+ {
402
+ "_id": "68904513483967a3d97c65ee",
403
+ "title": "Tutorial Task 5 - Dog Puffer Jacket",
404
+ "content": "Easy to put on and take off with secure Velcro fastenings.",
405
+ "sourceLanguage": "English",
406
+ "sourceType": "manual",
407
+ "category": "tutorial",
408
+ "weekNumber": 2,
409
+ "translationBrief": "You have been asked to translate the following for marketing the product in a country where your LOTE is spoken.",
410
+ "imageUrl": "https://example.com/dog-puffer-5.jpg",
411
+ "imageAlt": "Dog Puffer Jacket 5",
412
+ "difficulty": "intermediate",
413
+ "tags": [],
414
+ "targetCultures": [],
415
+ "isActive": true,
416
+ "usageCount": 0,
417
+ "averageRating": 0,
418
+ "ratingCount": 0,
419
+ "isCurrent": false,
420
+ "culturalElements": [],
421
+ "__v": 0,
422
+ "createdAt": "2025-08-04T05:28:51.846Z",
423
+ "updatedAt": "2025-08-04T05:28:51.846Z"
424
+ },
425
+ {
426
+ "_id": "68904513483967a3d97c65ef",
427
+ "title": "Tutorial Task 6 - Dog Puffer Jacket",
428
+ "content": "Lightweight design allows for maximum comfort and mobility.",
429
+ "sourceLanguage": "English",
430
+ "sourceType": "manual",
431
+ "category": "tutorial",
432
+ "weekNumber": 2,
433
+ "translationBrief": "You have been asked to translate the following for marketing the product in a country where your LOTE is spoken.",
434
+ "imageUrl": "https://example.com/dog-puffer-6.jpg",
435
+ "imageAlt": "Dog Puffer Jacket 6",
436
+ "difficulty": "intermediate",
437
+ "tags": [],
438
+ "targetCultures": [],
439
+ "isActive": true,
440
+ "usageCount": 0,
441
+ "averageRating": 0,
442
+ "ratingCount": 0,
443
+ "isCurrent": false,
444
+ "culturalElements": [],
445
+ "__v": 0,
446
+ "createdAt": "2025-08-04T05:28:51.846Z",
447
+ "updatedAt": "2025-08-04T05:28:51.846Z"
448
+ },
449
+ {
450
+ "_id": "68904513483967a3d97c65f0",
451
+ "title": "Tutorial Task 7 - Dog Puffer Jacket",
452
+ "content": "Reflective strips for visibility during evening walks.",
453
+ "sourceLanguage": "English",
454
+ "sourceType": "manual",
455
+ "category": "tutorial",
456
+ "weekNumber": 2,
457
+ "translationBrief": "You have been asked to translate the following for marketing the product in a country where your LOTE is spoken.",
458
+ "imageUrl": "https://example.com/dog-puffer-7.jpg",
459
+ "imageAlt": "Dog Puffer Jacket 7",
460
+ "difficulty": "intermediate",
461
+ "tags": [],
462
+ "targetCultures": [],
463
+ "isActive": true,
464
+ "usageCount": 0,
465
+ "averageRating": 0,
466
+ "ratingCount": 0,
467
+ "isCurrent": false,
468
+ "culturalElements": [],
469
+ "__v": 0,
470
+ "createdAt": "2025-08-04T05:28:51.847Z",
471
+ "updatedAt": "2025-08-04T05:28:51.847Z"
472
+ },
473
+ {
474
+ "_id": "68904513483967a3d97c65f1",
475
+ "title": "Tutorial Task 8 - Dog Puffer Jacket",
476
+ "content": "Machine washable for easy care and maintenance.",
477
+ "sourceLanguage": "English",
478
+ "sourceType": "manual",
479
+ "category": "tutorial",
480
+ "weekNumber": 2,
481
+ "translationBrief": "You have been asked to translate the following for marketing the product in a country where your LOTE is spoken.",
482
+ "imageUrl": "https://example.com/dog-puffer-8.jpg",
483
+ "imageAlt": "Dog Puffer Jacket 8",
484
+ "difficulty": "intermediate",
485
+ "tags": [],
486
+ "targetCultures": [],
487
+ "isActive": true,
488
+ "usageCount": 0,
489
+ "averageRating": 0,
490
+ "ratingCount": 0,
491
+ "isCurrent": false,
492
+ "culturalElements": [],
493
+ "__v": 0,
494
+ "createdAt": "2025-08-04T05:28:51.847Z",
495
+ "updatedAt": "2025-08-04T05:28:51.847Z"
496
+ },
497
+ {
498
+ "_id": "68904513483967a3d97c65f2",
499
+ "title": "Tutorial Task 9 - Dog Puffer Jacket",
500
+ "content": "Durable construction ensures long-lasting performance.",
501
+ "sourceLanguage": "English",
502
+ "sourceType": "manual",
503
+ "category": "tutorial",
504
+ "weekNumber": 2,
505
+ "translationBrief": "You have been asked to translate the following for marketing the product in a country where your LOTE is spoken.",
506
+ "imageUrl": "https://example.com/dog-puffer-9.jpg",
507
+ "imageAlt": "Dog Puffer Jacket 9",
508
+ "difficulty": "intermediate",
509
+ "tags": [],
510
+ "targetCultures": [],
511
+ "isActive": true,
512
+ "usageCount": 0,
513
+ "averageRating": 0,
514
+ "ratingCount": 0,
515
+ "isCurrent": false,
516
+ "culturalElements": [],
517
+ "__v": 0,
518
+ "createdAt": "2025-08-04T05:28:51.847Z",
519
+ "updatedAt": "2025-08-04T05:28:51.847Z"
520
+ },
521
+ {
522
+ "_id": "68904513483967a3d97c65f3",
523
+ "title": "Tutorial Task 10 - Dog Puffer Jacket",
524
+ "content": "Perfect for cold weather protection during outdoor activities.",
525
+ "sourceLanguage": "English",
526
+ "sourceType": "manual",
527
+ "category": "tutorial",
528
+ "weekNumber": 2,
529
+ "translationBrief": "You have been asked to translate the following for marketing the product in a country where your LOTE is spoken.",
530
+ "imageUrl": "https://example.com/dog-puffer-10.jpg",
531
+ "imageAlt": "Dog Puffer Jacket 10",
532
+ "difficulty": "intermediate",
533
+ "tags": [],
534
+ "targetCultures": [],
535
+ "isActive": true,
536
+ "usageCount": 0,
537
+ "averageRating": 0,
538
+ "ratingCount": 0,
539
+ "isCurrent": false,
540
+ "culturalElements": [],
541
+ "__v": 0,
542
+ "createdAt": "2025-08-04T05:28:51.847Z",
543
+ "updatedAt": "2025-08-04T05:28:51.847Z"
544
+ },
545
+ {
546
+ "_id": "68904513483967a3d97c65f4",
547
+ "title": "Tutorial Task 11 - Dog Puffer Jacket",
548
+ "content": "Breathable fabric prevents overheating during active play.",
549
+ "sourceLanguage": "English",
550
+ "sourceType": "manual",
551
+ "category": "tutorial",
552
+ "weekNumber": 2,
553
+ "translationBrief": "You have been asked to translate the following for marketing the product in a country where your LOTE is spoken.",
554
+ "imageUrl": "https://example.com/dog-puffer-11.jpg",
555
+ "imageAlt": "Dog Puffer Jacket 11",
556
+ "difficulty": "intermediate",
557
+ "tags": [],
558
+ "targetCultures": [],
559
+ "isActive": true,
560
+ "usageCount": 0,
561
+ "averageRating": 0,
562
+ "ratingCount": 0,
563
+ "isCurrent": false,
564
+ "culturalElements": [],
565
+ "__v": 0,
566
+ "createdAt": "2025-08-04T05:28:51.847Z",
567
+ "updatedAt": "2025-08-04T05:28:51.847Z"
568
+ },
569
+ {
570
+ "_id": "68904513483967a3d97c65f5",
571
+ "title": "Tutorial Task 12 - Dog Puffer Jacket",
572
+ "content": "Stylish design that complements your dog's personality.",
573
+ "sourceLanguage": "English",
574
+ "sourceType": "manual",
575
+ "category": "tutorial",
576
+ "weekNumber": 2,
577
+ "translationBrief": "You have been asked to translate the following for marketing the product in a country where your LOTE is spoken.",
578
+ "imageUrl": "https://example.com/dog-puffer-12.jpg",
579
+ "imageAlt": "Dog Puffer Jacket 12",
580
+ "difficulty": "intermediate",
581
+ "tags": [],
582
+ "targetCultures": [],
583
+ "isActive": true,
584
+ "usageCount": 0,
585
+ "averageRating": 0,
586
+ "ratingCount": 0,
587
+ "isCurrent": false,
588
+ "culturalElements": [],
589
+ "__v": 0,
590
+ "createdAt": "2025-08-04T05:28:51.847Z",
591
+ "updatedAt": "2025-08-04T05:28:51.847Z"
592
+ },
593
+ {
594
+ "_id": "6890456c7488d1d08d79de66",
595
+ "title": "Week 2 Weekly Practice - Nike Video",
596
+ "content": "Nike video with subtitles for translation practice. Video features Nike advertising content with multiple segments for subtitle translation.",
597
+ "sourceLanguage": "English",
598
+ "sourceType": "manual",
599
+ "category": "weekly-practice",
600
+ "weekNumber": 2,
601
+ "difficulty": "intermediate",
602
+ "tags": [],
603
+ "targetCultures": [],
604
+ "isActive": true,
605
+ "usageCount": 0,
606
+ "averageRating": 0,
607
+ "ratingCount": 0,
608
+ "isCurrent": false,
609
+ "culturalElements": [],
610
+ "createdAt": "2025-08-04T05:30:20.760Z",
611
+ "updatedAt": "2025-08-04T05:30:20.760Z",
612
+ "__v": 0
613
+ }
614
+ ],
615
+ "submissions": []
616
+ }
617
+ }
backups/complete-backup-2025-08-10T07-59-20-407Z/code/backend/index.js ADDED
@@ -0,0 +1,252 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ const express = require('express');
2
+ const cors = require('cors');
3
+ const mongoose = require('mongoose');
4
+ const dotenv = require('dotenv');
5
+ const rateLimit = require('express-rate-limit');
6
+
7
+ // Import routes
8
+ const { router: authRoutes } = require('./routes/auth');
9
+ const sourceTextRoutes = require('./routes/sourceTexts');
10
+ const submissionRoutes = require('./routes/submissions');
11
+ const searchRoutes = require('./routes/search');
12
+ const subtitleRoutes = require('./routes/subtitles');
13
+ const subtitleSubmissionRoutes = require('./routes/subtitleSubmissions');
14
+
15
+ dotenv.config();
16
+
17
+ // Global error handlers to prevent crashes
18
+ process.on('uncaughtException', (error) => {
19
+ console.error('Uncaught Exception:', error);
20
+ // Don't exit immediately, try to log and continue
21
+ console.error('Stack trace:', error.stack);
22
+ });
23
+
24
+ process.on('unhandledRejection', (reason, promise) => {
25
+ console.error('Unhandled Rejection at:', promise, 'reason:', reason);
26
+ // Don't exit immediately, try to log and continue
27
+ console.error('Stack trace:', reason?.stack);
28
+ });
29
+
30
+ // Memory leak prevention
31
+ process.on('warning', (warning) => {
32
+ console.warn('Node.js warning:', warning.name, warning.message);
33
+ });
34
+
35
+ const app = express();
36
+ const PORT = process.env.PORT || 5000;
37
+
38
+ // Trust proxy for rate limiting
39
+ app.set('trust proxy', 1);
40
+
41
+ // Rate limiting - Increased limits to prevent 429 errors
42
+ const limiter = rateLimit({
43
+ windowMs: 15 * 60 * 1000, // 15 minutes
44
+ max: 1000, // Increased from 100 to 1000 requests per windowMs
45
+ message: { error: 'Too many requests, please try again later.' },
46
+ standardHeaders: true,
47
+ legacyHeaders: false,
48
+ skip: (req) => {
49
+ // Skip rate limiting for health checks
50
+ return req.path === '/health' || req.path === '/api/health';
51
+ }
52
+ });
53
+
54
+ // Middleware
55
+ app.use(cors());
56
+ app.use(express.json({ limit: '10mb' }));
57
+ app.use(limiter);
58
+
59
+ // Database connection with better error handling
60
+ mongoose.connect(process.env.MONGODB_URI || 'mongodb://localhost:27017/transcreation-sandbox', {
61
+ maxPoolSize: 10,
62
+ serverSelectionTimeoutMS: 5000,
63
+ socketTimeoutMS: 45000,
64
+ })
65
+ .then(() => {
66
+ console.log('Connected to MongoDB');
67
+ })
68
+ .catch(err => {
69
+ console.error('MongoDB connection error:', err);
70
+ // Don't exit immediately, try to reconnect
71
+ setTimeout(() => {
72
+ console.log('Attempting to reconnect to MongoDB...');
73
+ mongoose.connect(process.env.MONGODB_URI || 'mongodb://localhost:27017/transcreation-sandbox');
74
+ }, 5000);
75
+ });
76
+
77
+ // Handle MongoDB connection errors
78
+ mongoose.connection.on('error', (err) => {
79
+ console.error('MongoDB connection error:', err);
80
+ });
81
+
82
+ mongoose.connection.on('disconnected', () => {
83
+ console.log('MongoDB disconnected');
84
+ });
85
+
86
+ // Routes
87
+ app.use('/api/auth', authRoutes);
88
+ app.use('/api/source-texts', sourceTextRoutes);
89
+ app.use('/api/submissions', submissionRoutes);
90
+ app.use('/api/search', searchRoutes);
91
+ app.use('/api/subtitles', subtitleRoutes);
92
+ app.use('/api/subtitle-submissions', subtitleSubmissionRoutes);
93
+
94
+ // Health check endpoint
95
+ app.get('/api/health', (req, res) => {
96
+ res.json({ status: 'OK', message: 'Transcreation Sandbox API is running - Auth middleware fixed for time code editing' });
97
+ });
98
+
99
+ // Simple health check for Hugging Face Spaces
100
+ app.get('/health', (req, res) => {
101
+ res.status(200).send('OK');
102
+ });
103
+
104
+ // Error handling middleware
105
+ app.use((err, req, res, next) => {
106
+ console.error(err.stack);
107
+ res.status(500).json({ error: 'Something went wrong!' });
108
+ });
109
+
110
+ app.listen(PORT, () => {
111
+ console.log(`Server running on port ${PORT}`);
112
+
113
+ // Initialize week 1 tutorial tasks and weekly practice by default
114
+ const initializeWeek1 = async () => {
115
+ try {
116
+ const SourceText = require('./models/SourceText');
117
+
118
+ // Check if week 1 tutorial tasks exist
119
+ const existingTutorialTasks = await SourceText.find({
120
+ category: 'tutorial',
121
+ weekNumber: 1
122
+ });
123
+
124
+ if (existingTutorialTasks.length === 0) {
125
+ console.log('Initializing week 1 tutorial tasks...');
126
+ const tutorialTasks = [
127
+ {
128
+ title: 'Tutorial Task 1 - Introduction',
129
+ content: '欢迎来到我们的翻译课程。今天我们将学习如何翻译产品介绍。',
130
+ category: 'tutorial',
131
+ weekNumber: 1,
132
+ sourceLanguage: 'Chinese',
133
+ sourceCulture: 'Chinese',
134
+ translationBrief: 'You have been asked to translate the following for marketing the product in a country where your LOTE is spoken.'
135
+ },
136
+ {
137
+ title: 'Tutorial Task 2 - Development',
138
+ content: '这个产品具有独特的设计理念,融合了传统与现代元素。',
139
+ category: 'tutorial',
140
+ weekNumber: 1,
141
+ sourceLanguage: 'Chinese',
142
+ sourceCulture: 'Chinese',
143
+ translationBrief: 'You have been asked to translate the following for marketing the product in a country where your LOTE is spoken.'
144
+ },
145
+ {
146
+ title: 'Tutorial Task 3 - Conclusion',
147
+ content: '我们相信这个产品能够满足您的所有需求,为您提供最佳体验。',
148
+ category: 'tutorial',
149
+ weekNumber: 1,
150
+ sourceLanguage: 'Chinese',
151
+ sourceCulture: 'Chinese',
152
+ translationBrief: 'You have been asked to translate the following for marketing the product in a country where your LOTE is spoken.'
153
+ }
154
+ ];
155
+ await SourceText.insertMany(tutorialTasks);
156
+ console.log('Week 1 tutorial tasks initialized successfully');
157
+ }
158
+
159
+ // Check if week 1 weekly practice exists
160
+ const existingWeeklyPractice = await SourceText.find({
161
+ category: 'weekly-practice',
162
+ weekNumber: 1
163
+ });
164
+
165
+ if (existingWeeklyPractice.length === 0) {
166
+ console.log('Initializing week 1 weekly practice...');
167
+ const weeklyPractice = [
168
+ {
169
+ title: 'Chinese Pun 1',
170
+ content: '为什么睡前一定要吃夜宵?因为这样才不会做饿梦。',
171
+ category: 'weekly-practice',
172
+ weekNumber: 1,
173
+ sourceLanguage: 'Chinese',
174
+ sourceCulture: 'Chinese'
175
+ },
176
+ {
177
+ title: 'Chinese Pun 2',
178
+ content: '女娲用什么补天?强扭的瓜。',
179
+ category: 'weekly-practice',
180
+ weekNumber: 1,
181
+ sourceLanguage: 'Chinese',
182
+ sourceCulture: 'Chinese'
183
+ },
184
+ {
185
+ title: 'Chinese Pun 3',
186
+ content: '你知道如何区分真假大象吗?把他们仍进水中,真相会浮出水面的。',
187
+ category: 'weekly-practice',
188
+ weekNumber: 1,
189
+ sourceLanguage: 'Chinese',
190
+ sourceCulture: 'Chinese'
191
+ },
192
+ {
193
+ title: 'English Pun 1',
194
+ content: 'What if Soy milk is just regular milk introducing itself in Spanish.',
195
+ category: 'weekly-practice',
196
+ weekNumber: 1,
197
+ sourceLanguage: 'English',
198
+ sourceCulture: 'Western'
199
+ },
200
+ {
201
+ title: 'English Pun 2',
202
+ content: 'I can\'t believe I got fired from the calendar factory. All I did was take a day off.',
203
+ category: 'weekly-practice',
204
+ weekNumber: 1,
205
+ sourceLanguage: 'English',
206
+ sourceCulture: 'Western'
207
+ },
208
+ {
209
+ title: 'English Pun 3',
210
+ content: 'When life gives you melons, you might be dyslexic.',
211
+ category: 'weekly-practice',
212
+ weekNumber: 1,
213
+ sourceLanguage: 'English',
214
+ sourceCulture: 'Western'
215
+ }
216
+ ];
217
+ await SourceText.insertMany(weeklyPractice);
218
+ console.log('Week 1 weekly practice initialized successfully');
219
+ }
220
+ } catch (error) {
221
+ console.error('Error initializing week 1 data:', error);
222
+ }
223
+ };
224
+
225
+ // Auto-initialization disabled to prevent overwriting definitive data
226
+ // initializeWeek1();
227
+ });
228
+
229
+ // Graceful shutdown
230
+ process.on('SIGTERM', async () => {
231
+ console.log('SIGTERM received, shutting down gracefully');
232
+ try {
233
+ await mongoose.connection.close();
234
+ console.log('MongoDB connection closed');
235
+ process.exit(0);
236
+ } catch (error) {
237
+ console.error('Error closing MongoDB connection:', error);
238
+ process.exit(1);
239
+ }
240
+ });
241
+
242
+ process.on('SIGINT', async () => {
243
+ console.log('SIGINT received, shutting down gracefully');
244
+ try {
245
+ await mongoose.connection.close();
246
+ console.log('MongoDB connection closed');
247
+ process.exit(0);
248
+ } catch (error) {
249
+ console.error('Error closing MongoDB connection:', error);
250
+ process.exit(1);
251
+ }
252
+ });
backups/complete-backup-2025-08-10T07-59-20-407Z/code/backend/models/SourceText.js ADDED
@@ -0,0 +1,75 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ const mongoose = require('mongoose');
2
+
3
+ const culturalElementSchema = new mongoose.Schema({
4
+ element: { type: String, required: true },
5
+ description: { type: String, required: true },
6
+ significance: { type: String, required: true }
7
+ });
8
+
9
+ const sourceTextSchema = new mongoose.Schema({
10
+ title: { type: String, required: true },
11
+ content: { type: String, required: true },
12
+ sourceLanguage: { type: String, required: true },
13
+
14
+ sourceType: {
15
+ type: String,
16
+ enum: ['api', 'manual', 'practice', 'tutorial', 'weekly-practice'],
17
+ default: 'manual'
18
+ },
19
+ category: {
20
+ type: String,
21
+ enum: ['practice', 'tutorial', 'weekly-practice'],
22
+ required: true
23
+ },
24
+ weekNumber: {
25
+ type: Number,
26
+ required: function() { return this.category !== 'practice'; }
27
+ },
28
+ translationBrief: { type: String },
29
+ imageUrl: { type: String },
30
+ imageAlt: { type: String },
31
+ // Image-only configuration
32
+ imageSize: { type: Number, default: 200 },
33
+ imageAlignment: { type: String, enum: ['left', 'center', 'right'], default: 'center' },
34
+ culturalElements: [culturalElementSchema],
35
+ difficulty: {
36
+ type: String,
37
+ enum: ['beginner', 'intermediate', 'advanced'],
38
+ default: 'intermediate'
39
+ },
40
+ tags: [String],
41
+ targetCultures: [String],
42
+ isActive: { type: Boolean, default: true },
43
+ usageCount: { type: Number, default: 0 },
44
+ averageRating: { type: Number, default: 0 },
45
+ ratingCount: { type: Number, default: 0 },
46
+
47
+ // Video subtitling specific fields
48
+ interfaceType: { type: String, enum: ['standard', 'video-subtitling'] },
49
+ videoSource: { type: String },
50
+ totalSegments: { type: Number },
51
+ segmentId: { type: Number },
52
+ startTime: { type: String },
53
+ endTime: { type: String },
54
+ duration: { type: String },
55
+ isCurrent: { type: Boolean, default: false },
56
+ parentTask: { type: String },
57
+
58
+ // Configuration fields
59
+ configType: { type: String },
60
+ description: { type: String },
61
+
62
+ // Protection fields
63
+ isProtected: { type: Boolean, default: false },
64
+ protectedReason: { type: String },
65
+ lastModified: { type: Date, default: Date.now },
66
+ modificationHistory: [{
67
+ action: { type: String, required: true },
68
+ timestamp: { type: Date, default: Date.now },
69
+ reason: { type: String }
70
+ }]
71
+ }, {
72
+ timestamps: true
73
+ });
74
+
75
+ module.exports = mongoose.model('SourceText', sourceTextSchema);
backups/complete-backup-2025-08-10T07-59-20-407Z/code/backend/models/Subtitle.js ADDED
@@ -0,0 +1,168 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ const mongoose = require('mongoose');
2
+
3
+ const subtitleSchema = new mongoose.Schema({
4
+ segmentId: {
5
+ type: Number,
6
+ required: true,
7
+ unique: true
8
+ },
9
+ startTime: {
10
+ type: String,
11
+ required: true,
12
+ validate: {
13
+ validator: function(v) {
14
+ // Validate time format: HH:MM:SS,mmm
15
+ return /^\d{2}:\d{2}:\d{2},\d{3}$/.test(v);
16
+ },
17
+ message: 'Start time must be in format HH:MM:SS,mmm'
18
+ }
19
+ },
20
+ endTime: {
21
+ type: String,
22
+ required: true,
23
+ validate: {
24
+ validator: function(v) {
25
+ // Validate time format: HH:MM:SS,mmm
26
+ return /^\d{2}:\d{2}:\d{2},\d{3}$/.test(v);
27
+ },
28
+ message: 'End time must be in format HH:MM:SS,mmm'
29
+ }
30
+ },
31
+ duration: {
32
+ type: String,
33
+ required: true
34
+ },
35
+ englishText: {
36
+ type: String,
37
+ required: true,
38
+ trim: true
39
+ },
40
+ chineseTranslation: {
41
+ type: String,
42
+ default: '',
43
+ trim: true
44
+ },
45
+ isProtected: {
46
+ type: Boolean,
47
+ default: false
48
+ },
49
+ protectedReason: {
50
+ type: String
51
+ },
52
+ lastModified: {
53
+ type: Date,
54
+ default: Date.now
55
+ },
56
+ modificationHistory: [{
57
+ action: {
58
+ type: String,
59
+ required: true
60
+ },
61
+ timestamp: {
62
+ type: Date,
63
+ default: Date.now
64
+ },
65
+ reason: {
66
+ type: String
67
+ }
68
+ }]
69
+ }, {
70
+ timestamps: true
71
+ });
72
+
73
+ // Index for efficient queries
74
+ subtitleSchema.index({ segmentId: 1 });
75
+ subtitleSchema.index({ startTime: 1 });
76
+ subtitleSchema.index({ isProtected: 1 });
77
+
78
+ // Virtual for calculating duration in seconds
79
+ subtitleSchema.virtual('durationInSeconds').get(function() {
80
+ const start = this.parseTimeToSeconds(this.startTime);
81
+ const end = this.parseTimeToSeconds(this.endTime);
82
+ return end - start;
83
+ });
84
+
85
+ // Method to parse time string to seconds
86
+ subtitleSchema.methods.parseTimeToSeconds = function(timeString) {
87
+ const parts = timeString.split(':');
88
+ const seconds = parseInt(parts[2].split(',')[0]);
89
+ const minutes = parseInt(parts[1]);
90
+ const hours = parseInt(parts[0]);
91
+ return hours * 3600 + minutes * 60 + seconds;
92
+ };
93
+
94
+ // Static method to get all subtitles ordered by segment ID
95
+ subtitleSchema.statics.getAllOrdered = function() {
96
+ return this.find().sort({ segmentId: 1 });
97
+ };
98
+
99
+ // Static method to get protected subtitles
100
+ subtitleSchema.statics.getProtected = function() {
101
+ return this.find({ isProtected: true });
102
+ };
103
+
104
+ // Static method to update subtitle safely
105
+ subtitleSchema.statics.safeUpdate = function(segmentId, updateData) {
106
+ return this.findOneAndUpdate(
107
+ { segmentId: segmentId },
108
+ {
109
+ ...updateData,
110
+ lastModified: new Date(),
111
+ $push: {
112
+ modificationHistory: {
113
+ action: 'update',
114
+ timestamp: new Date(),
115
+ reason: updateData.reason || 'Manual update'
116
+ }
117
+ }
118
+ },
119
+ { new: true, runValidators: true }
120
+ );
121
+ };
122
+
123
+ // Static method to protect subtitle
124
+ subtitleSchema.statics.protectSubtitle = function(segmentId, reason) {
125
+ return this.findOneAndUpdate(
126
+ { segmentId: segmentId },
127
+ {
128
+ isProtected: true,
129
+ protectedReason: reason,
130
+ lastModified: new Date(),
131
+ $push: {
132
+ modificationHistory: {
133
+ action: 'protect',
134
+ timestamp: new Date(),
135
+ reason: reason
136
+ }
137
+ }
138
+ },
139
+ { new: true }
140
+ );
141
+ };
142
+
143
+ // Static method to unlock subtitle
144
+ subtitleSchema.statics.unlockSubtitle = function(segmentId, unlockKey) {
145
+ // In a real implementation, you'd verify the unlock key
146
+ if (unlockKey !== 'ADMIN_UNLOCK_KEY_2024') {
147
+ throw new Error('Invalid unlock key');
148
+ }
149
+
150
+ return this.findOneAndUpdate(
151
+ { segmentId: segmentId },
152
+ {
153
+ isProtected: false,
154
+ protectedReason: null,
155
+ lastModified: new Date(),
156
+ $push: {
157
+ modificationHistory: {
158
+ action: 'unlock',
159
+ timestamp: new Date(),
160
+ reason: 'Admin unlock'
161
+ }
162
+ }
163
+ },
164
+ { new: true }
165
+ );
166
+ };
167
+
168
+ module.exports = mongoose.model('Subtitle', subtitleSchema);
backups/complete-backup-2025-08-10T07-59-20-407Z/code/backend/models/SubtitleSubmission.js ADDED
@@ -0,0 +1,102 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ const mongoose = require('mongoose');
2
+
3
+ const subtitleSubmissionSchema = new mongoose.Schema({
4
+ userId: {
5
+ type: mongoose.Schema.Types.ObjectId,
6
+ ref: 'User',
7
+ required: true
8
+ },
9
+ username: {
10
+ type: String,
11
+ required: true,
12
+ trim: true
13
+ },
14
+ segmentId: {
15
+ type: Number,
16
+ required: true,
17
+ min: 1,
18
+ max: 100
19
+ },
20
+ chineseTranslation: {
21
+ type: String,
22
+ required: true,
23
+ trim: true,
24
+ maxlength: 500
25
+ },
26
+ submissionDate: {
27
+ type: Date,
28
+ default: Date.now
29
+ },
30
+ isAnonymous: {
31
+ type: Boolean,
32
+ default: false
33
+ },
34
+ weekNumber: {
35
+ type: Number,
36
+ required: true,
37
+ default: 2 // Week 2 for Nike video
38
+ },
39
+ // For future features
40
+ status: {
41
+ type: String,
42
+ enum: ['submitted', 'reviewed', 'approved'],
43
+ default: 'submitted'
44
+ },
45
+ notes: {
46
+ type: String,
47
+ trim: true,
48
+ maxlength: 1000
49
+ }
50
+ }, {
51
+ timestamps: true
52
+ });
53
+
54
+ // Indexes for efficient queries
55
+ subtitleSubmissionSchema.index({ segmentId: 1, weekNumber: 1 });
56
+ subtitleSubmissionSchema.index({ userId: 1, weekNumber: 1 });
57
+ subtitleSubmissionSchema.index({ submissionDate: -1 });
58
+
59
+ // Virtual for display name (anonymous or username)
60
+ subtitleSubmissionSchema.virtual('displayName').get(function() {
61
+ return this.isAnonymous ? 'Anonymous' : this.username;
62
+ });
63
+
64
+ // Static method to get submissions for a specific segment
65
+ subtitleSubmissionSchema.statics.getSubmissionsForSegment = function(segmentId, weekNumber = 2) {
66
+ return this.find({ segmentId, weekNumber })
67
+ .sort({ submissionDate: -1 })
68
+ .select('username chineseTranslation submissionDate isAnonymous status notes');
69
+ };
70
+
71
+ // Static method to get user's submissions for a week
72
+ subtitleSubmissionSchema.statics.getUserSubmissions = function(userId, weekNumber = 2) {
73
+ return this.find({ userId, weekNumber })
74
+ .sort({ segmentId: 1, submissionDate: -1 });
75
+ };
76
+
77
+ // Static method to get submission count for a segment
78
+ subtitleSubmissionSchema.statics.getSubmissionCount = function(segmentId, weekNumber = 2) {
79
+ return this.countDocuments({ segmentId, weekNumber });
80
+ };
81
+
82
+ // Method to check if user has already submitted for this segment
83
+ subtitleSubmissionSchema.statics.hasUserSubmitted = function(userId, segmentId, weekNumber = 2) {
84
+ return this.exists({ userId, segmentId, weekNumber });
85
+ };
86
+
87
+ // Method to update existing submission
88
+ subtitleSubmissionSchema.statics.updateSubmission = function(userId, segmentId, chineseTranslation, weekNumber = 2) {
89
+ return this.findOneAndUpdate(
90
+ { userId, segmentId, weekNumber },
91
+ {
92
+ chineseTranslation,
93
+ submissionDate: new Date(),
94
+ isAnonymous: false // Reset to false on update
95
+ },
96
+ { new: true, upsert: true }
97
+ );
98
+ };
99
+
100
+ const SubtitleSubmission = mongoose.model('SubtitleSubmission', subtitleSubmissionSchema);
101
+
102
+ module.exports = SubtitleSubmission;
backups/complete-backup-2025-08-10T07-59-20-407Z/code/backend/routes/auth.js ADDED
@@ -0,0 +1,354 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+ const express = require('express');
3
+ const router = express.Router();
4
+
5
+ // Pre-defined users
6
+ const PREDEFINED_USERS = {
7
+ 'mche0278@student.monash.edu': { name: 'Michelle Chen', email: 'mche0278@student.monash.edu', role: 'student' },
8
+ 'zfan0011@student.monash.edu': { name: 'Fang Zhou', email: 'zfan0011@student.monash.edu', role: 'student' },
9
+ 'yfuu0071@student.monash.edu': { name: 'Fu Yitong', email: 'yfuu0071@student.monash.edu', role: 'student' },
10
+ 'hhan0022@student.monash.edu': { name: 'Han Heyang', email: 'hhan0022@student.monash.edu', role: 'student' },
11
+ 'ylei0024@student.monash.edu': { name: 'Lei Yang', email: 'ylei0024@student.monash.edu', role: 'student' },
12
+ 'wlii0217@student.monash.edu': { name: 'Li Wenhui', email: 'wlii0217@student.monash.edu', role: 'student' },
13
+ 'zlia0091@student.monash.edu': { name: 'Liao Zhixin', email: 'zlia0091@student.monash.edu', role: 'student' },
14
+ 'hmaa0054@student.monash.edu': { name: 'Ma Huachen', email: 'hmaa0054@student.monash.edu', role: 'student' },
15
+ 'csun0059@student.monash.edu': { name: 'Sun Chongkai', email: 'csun0059@student.monash.edu', role: 'student' },
16
+ 'pwon0030@student.monash.edu': { name: 'Wong Prisca Si-Heng', email: 'pwon0030@student.monash.edu', role: 'student' },
17
+ 'zxia0017@student.monash.edu': { name: 'Xia Zechen', email: 'zxia0017@student.monash.edu', role: 'student' },
18
+ 'lyou0021@student.monash.edu': { name: 'You Lianxiang', email: 'lyou0021@student.monash.edu', role: 'student' },
19
+ 'wzhe0034@student.monash.edu': { name: 'Zheng Weijie', email: 'wzhe0034@student.monash.edu', role: 'student' },
20
+ 'szhe0055@student.monash.edu': { name: 'Zheng Siyuan', email: 'szhe0055@student.monash.edu', role: 'student' },
21
+ 'xzhe0055@student.monash.edu': { name: 'Zheng Xiang', email: 'xzhe0055@student.monash.edu', role: 'student' },
22
+ 'hongchang.yu@monash.edu': { name: 'Tristan', email: 'hongchang.yu@monash.edu', role: 'admin' }
23
+ };
24
+
25
+ // Middleware to verify token (simplified)
26
+ const authenticateToken = (req, res, next) => {
27
+ const authHeader = req.headers['authorization'];
28
+ const token = authHeader && authHeader.split(' ')[1];
29
+
30
+ if (!token) {
31
+ return res.status(401).json({ error: 'Access token required' });
32
+ }
33
+
34
+ // For our simplified system, just check if token exists and has the right format
35
+ if (token.startsWith('user_') || token.startsWith('visitor_')) {
36
+ req.user = { token };
37
+ next();
38
+ } else {
39
+ return res.status(403).json({ error: 'Invalid token' });
40
+ }
41
+ };
42
+
43
+ // Middleware to check if user is admin
44
+ const requireAdmin = (req, res, next) => {
45
+ const userRole = req.headers['user-role'] || req.body.role;
46
+ if (userRole !== 'admin') {
47
+ return res.status(403).json({ error: 'Admin access required' });
48
+ }
49
+ next();
50
+ };
51
+
52
+ // Login endpoint (simplified)
53
+ router.post('/login', async (req, res) => {
54
+ try {
55
+ const { email } = req.body;
56
+ const user = PREDEFINED_USERS[email];
57
+ if (user) {
58
+ const token = `user_${Date.now()}`;
59
+ res.json({ success: true, token, user: { name: user.name, email: user.email, role: user.role } });
60
+ } else {
61
+ const visitorUser = { name: 'Visitor', email, role: 'visitor' };
62
+ const token = `visitor_${Date.now()}`;
63
+ res.json({ success: true, token, user: visitorUser });
64
+ }
65
+ } catch (error) {
66
+ console.error('Login error:', error);
67
+ res.status(500).json({ error: 'Login failed' });
68
+ }
69
+ });
70
+
71
+ // Get user profile
72
+ router.get('/profile', authenticateToken, async (req, res) => {
73
+ try {
74
+ res.json({ success: true, user: { name: 'User', email: 'user@example.com', role: 'student' } });
75
+ } catch (error) {
76
+ console.error('Profile error:', error);
77
+ res.status(500).json({ error: 'Failed to get profile' });
78
+ }
79
+ });
80
+
81
+ // Admin endpoints
82
+ router.get('/admin/users', authenticateToken, async (req, res) => {
83
+ try {
84
+ const users = Object.values(PREDEFINED_USERS);
85
+ res.json({ success: true, users });
86
+ } catch (error) {
87
+ console.error('Get users error:', error);
88
+ res.status(500).json({ error: 'Failed to get users' });
89
+ }
90
+ });
91
+
92
+ router.get('/admin/stats', authenticateToken, async (req, res) => {
93
+ try {
94
+ const SourceText = require('../models/SourceText');
95
+ const Submission = require('../models/Submission');
96
+ const stats = {
97
+ totalUsers: Object.keys(PREDEFINED_USERS).length,
98
+ practiceExamples: await SourceText.countDocuments({ sourceType: 'practice' }),
99
+ totalSubmissions: await Submission.countDocuments(),
100
+ activeSessions: 1
101
+ };
102
+ res.json({ success: true, stats });
103
+ } catch (error) {
104
+ console.error('Get stats error:', error);
105
+ res.status(500).json({ error: 'Failed to get statistics' });
106
+ }
107
+ });
108
+
109
+ // Practice examples
110
+ router.get('/admin/practice-examples', authenticateToken, async (req, res) => {
111
+ try {
112
+ const SourceText = require('../models/SourceText');
113
+ const examples = await SourceText.find({ sourceType: 'practice' }).sort({ createdAt: -1 });
114
+ res.json({ success: true, examples });
115
+ } catch (error) {
116
+ console.error('Get practice examples error:', error);
117
+ res.status(500).json({ error: 'Failed to get practice examples' });
118
+ }
119
+ });
120
+
121
+ router.post('/admin/practice-examples', authenticateToken, async (req, res) => {
122
+ try {
123
+ const SourceText = require('../models/SourceText');
124
+ const { title, content, sourceLanguage, culturalElements, difficulty } = req.body;
125
+ const newExample = new SourceText({ title, content, sourceLanguage, sourceType: 'practice', culturalElements: culturalElements || [], difficulty: difficulty || 'intermediate' });
126
+ await newExample.save();
127
+ res.status(201).json({ success: true, message: 'Practice example added successfully', example: newExample });
128
+ } catch (error) {
129
+ console.error('Add practice example error:', error);
130
+ res.status(500).json({ error: 'Failed to add practice example' });
131
+ }
132
+ });
133
+
134
+ router.put('/admin/practice-examples/:id', authenticateToken, async (req, res) => {
135
+ try {
136
+ const SourceText = require('../models/SourceText');
137
+ const { title, content, sourceLanguage, culturalElements, difficulty } = req.body;
138
+ const updatedExample = await SourceText.findByIdAndUpdate(req.params.id, { title, content, sourceLanguage, culturalElements: culturalElements || [], difficulty: difficulty || 'intermediate' }, { new: true, runValidators: true });
139
+ if (!updatedExample) return res.status(404).json({ error: 'Practice example not found' });
140
+ res.json({ success: true, message: 'Practice example updated successfully', example: updatedExample });
141
+ } catch (error) {
142
+ console.error('Update practice example error:', error);
143
+ res.status(500).json({ error: 'Failed to update practice example' });
144
+ }
145
+ });
146
+
147
+ router.delete('/admin/practice-examples/:id', authenticateToken, async (req, res) => {
148
+ try {
149
+ const SourceText = require('../models/SourceText');
150
+ const deletedExample = await SourceText.findByIdAndDelete(req.params.id);
151
+ if (!deletedExample) return res.status(404).json({ error: 'Practice example not found' });
152
+ res.json({ success: true, message: 'Practice example deleted successfully' });
153
+ } catch (error) {
154
+ console.error('Delete practice example error:', error);
155
+ res.status(500).json({ error: 'Failed to delete practice example' });
156
+ }
157
+ });
158
+
159
+ // ===== TUTORIAL TASKS MANAGEMENT =====
160
+ router.get('/admin/tutorial-tasks', authenticateToken, async (req, res) => {
161
+ try {
162
+ const SourceText = require('../models/SourceText');
163
+ const tutorialTasks = await SourceText.find({ category: 'tutorial' }).sort({ weekNumber: 1, createdAt: -1 });
164
+ res.json({ success: true, tutorialTasks });
165
+ } catch (error) {
166
+ console.error('Get tutorial tasks error:', error);
167
+ res.status(500).json({ error: 'Failed to get tutorial tasks' });
168
+ }
169
+ });
170
+
171
+ router.post('/admin/tutorial-tasks', authenticateToken, requireAdmin, async (req, res) => {
172
+ try {
173
+ const SourceText = require('../models/SourceText');
174
+ const { content, weekNumber, category, imageUrl, imageAlt, imageSize, imageAlignment, translationBrief, title } = req.body;
175
+
176
+ if (!weekNumber) return res.status(400).json({ error: 'Week number is required' });
177
+ if (parseInt(weekNumber) >= 3) {
178
+ if ((!content || content.trim() === '') && !imageUrl) return res.status(400).json({ error: 'Either content or imageUrl is required' });
179
+ } else {
180
+ if (!content || content.trim() === '') return res.status(400).json({ error: 'Content is required' });
181
+ }
182
+
183
+ const newTask = new SourceText({
184
+ content: content || (imageUrl ? 'Image-based task' : ''),
185
+ weekNumber: parseInt(weekNumber),
186
+ category: category || 'tutorial',
187
+ title: title || `Tutorial Task Week ${weekNumber}`,
188
+ sourceLanguage: 'English',
189
+ sourceType: 'tutorial',
190
+ imageUrl,
191
+ imageAlt,
192
+ ...(imageUrl && (!content || content.trim() === '') && { imageSize: imageSize || 200 }),
193
+ ...(imageUrl && (!content || content.trim() === '') && { imageAlignment: imageAlignment || 'center' }),
194
+ translationBrief
195
+ });
196
+
197
+ const savedTask = await newTask.save();
198
+ res.status(201).json({ success: true, message: 'Tutorial task created successfully', task: savedTask });
199
+ } catch (error) {
200
+ console.error('Create tutorial task error:', error);
201
+ res.status(500).json({ error: 'Failed to create tutorial task' });
202
+ }
203
+ });
204
+
205
+ router.put('/admin/tutorial-tasks/:id', authenticateToken, requireAdmin, async (req, res) => {
206
+ try {
207
+ const SourceText = require('../models/SourceText');
208
+ const { content, translationBrief, weekNumber, imageUrl, imageAlt, imageSize, imageAlignment, title } = req.body;
209
+ const updateData = { content, translationBrief, weekNumber: parseInt(weekNumber), imageUrl, imageAlt, title };
210
+ if (imageUrl && (!content || content.trim() === '')) {
211
+ updateData.imageSize = imageSize;
212
+ updateData.imageAlignment = imageAlignment;
213
+ }
214
+ const updatedTask = await SourceText.findByIdAndUpdate(req.params.id, updateData, { new: true, runValidators: true });
215
+ if (!updatedTask) return res.status(404).json({ error: 'Tutorial task not found' });
216
+ res.json({ success: true, message: 'Tutorial task updated successfully', task: updatedTask });
217
+ } catch (error) {
218
+ console.error('Update tutorial task error:', error);
219
+ res.status(500).json({ error: 'Failed to update tutorial task' });
220
+ }
221
+ });
222
+
223
+ router.delete('/admin/tutorial-tasks/:id', authenticateToken, requireAdmin, async (req, res) => {
224
+ try {
225
+ const SourceText = require('../models/SourceText');
226
+ const deletedTask = await SourceText.findByIdAndDelete(req.params.id);
227
+ if (!deletedTask) return res.status(404).json({ error: 'Tutorial task not found' });
228
+ res.json({ success: true, message: 'Tutorial task deleted successfully' });
229
+ } catch (error) {
230
+ console.error('Delete tutorial task error:', error);
231
+ res.status(500).json({ error: 'Failed to delete tutorial task' });
232
+ }
233
+ });
234
+
235
+ // ===== WEEKLY PRACTICE MANAGEMENT =====
236
+ router.get('/admin/weekly-practice', authenticateToken, async (req, res) => {
237
+ try {
238
+ const SourceText = require('../models/SourceText');
239
+ const weeklyPractice = await SourceText.find({ category: 'weekly-practice' }).sort({ weekNumber: 1, createdAt: -1 });
240
+ res.json({ success: true, weeklyPractice });
241
+ } catch (error) {
242
+ console.error('Get weekly practice error:', error);
243
+ res.status(500).json({ error: 'Failed to get weekly practice' });
244
+ }
245
+ });
246
+
247
+ router.post('/admin/weekly-practice', authenticateToken, requireAdmin, async (req, res) => {
248
+ try {
249
+ const SourceText = require('../models/SourceText');
250
+ const { content, weekNumber, category, imageUrl, imageAlt, imageSize, imageAlignment, translationBrief, title } = req.body;
251
+
252
+ if (!weekNumber) return res.status(400).json({ error: 'Week number is required' });
253
+ if (parseInt(weekNumber) >= 3) {
254
+ if ((!content || content.trim() === '') && !imageUrl) return res.status(400).json({ error: 'Either content or imageUrl is required' });
255
+ } else {
256
+ if (!content || content.trim() === '') return res.status(400).json({ error: 'Content is required' });
257
+ }
258
+
259
+ const newPractice = new SourceText({
260
+ content: content || (imageUrl ? 'Image-based practice' : ''),
261
+ weekNumber: parseInt(weekNumber),
262
+ category: category || 'weekly-practice',
263
+ title: title || `Weekly Practice Week ${weekNumber}`,
264
+ sourceLanguage: 'English',
265
+ sourceType: 'weekly-practice',
266
+ imageUrl,
267
+ imageAlt,
268
+ ...(imageUrl && (!content || content.trim() === '') && { imageSize: imageSize || 200 }),
269
+ ...(imageUrl && (!content || content.trim() === '') && { imageAlignment: imageAlignment || 'center' }),
270
+ translationBrief
271
+ });
272
+
273
+ const savedPractice = await newPractice.save();
274
+ res.status(201).json({ success: true, message: 'Weekly practice created successfully', practice: savedPractice });
275
+ } catch (error) {
276
+ console.error('Create weekly practice error:', error);
277
+ res.status(500).json({ error: 'Failed to create weekly practice' });
278
+ }
279
+ });
280
+
281
+ router.put('/admin/weekly-practice/:id', authenticateToken, requireAdmin, async (req, res) => {
282
+ try {
283
+ const SourceText = require('../models/SourceText');
284
+ const { title, content, sourceLanguage, sourceCulture, weekNumber, difficulty, culturalElements, imageUrl, imageAlt, imageSize, imageAlignment } = req.body;
285
+ const updateData = { title, content, sourceLanguage, weekNumber: parseInt(weekNumber), difficulty: difficulty || 'intermediate', culturalElements: culturalElements || [], imageUrl, imageAlt };
286
+ if (imageUrl && (!content || content.trim() === '')) {
287
+ updateData.imageSize = imageSize;
288
+ updateData.imageAlignment = imageAlignment;
289
+ }
290
+ const updatedPractice = await SourceText.findByIdAndUpdate(req.params.id, updateData, { new: true, runValidators: true });
291
+ if (!updatedPractice) return res.status(404).json({ error: 'Weekly practice not found' });
292
+ res.json({ success: true, message: 'Weekly practice updated successfully', weeklyPractice: updatedPractice });
293
+ } catch (error) {
294
+ console.error('Update weekly practice error:', error);
295
+ res.status(500).json({ error: 'Failed to update weekly practice' });
296
+ }
297
+ });
298
+
299
+ router.delete('/admin/weekly-practice/:id', authenticateToken, requireAdmin, async (req, res) => {
300
+ try {
301
+ const SourceText = require('../models/SourceText');
302
+ const deletedPractice = await SourceText.findByIdAndDelete(req.params.id);
303
+ if (!deletedPractice) return res.status(404).json({ error: 'Weekly practice not found' });
304
+ res.json({ success: true, message: 'Weekly practice deleted successfully' });
305
+ } catch (error) {
306
+ console.error('Delete weekly practice error:', error);
307
+ res.status(500).json({ error: 'Failed to delete weekly practice' });
308
+ }
309
+ });
310
+
311
+ // Translation brief helpers
312
+ router.post('/admin/translation-brief', authenticateToken, requireAdmin, async (req, res) => {
313
+ try {
314
+ const SourceText = require('../models/SourceText');
315
+ const { weekNumber, translationBrief, type } = req.body;
316
+ if (!weekNumber || !translationBrief || !type) return res.status(400).json({ error: 'Week number, translation brief, and type are required' });
317
+ const result = await SourceText.updateMany({ category: type === 'tutorial' ? 'tutorial' : 'weekly-practice', weekNumber: parseInt(weekNumber) }, { translationBrief });
318
+ if (result.modifiedCount === 0) return res.status(404).json({ error: 'No tasks found for the specified week and type' });
319
+ res.json({ success: true, message: `Translation brief added successfully to ${result.modifiedCount} tasks`, modifiedCount: result.modifiedCount });
320
+ } catch (error) {
321
+ console.error('Add translation brief error:', error);
322
+ res.status(500).json({ error: 'Failed to add translation brief' });
323
+ }
324
+ });
325
+
326
+ router.put('/admin/tutorial-brief/:weekNumber', authenticateToken, requireAdmin, async (req, res) => {
327
+ try {
328
+ const SourceText = require('../models/SourceText');
329
+ const { translationBrief } = req.body;
330
+ const weekNumber = parseInt(req.params.weekNumber);
331
+ if (translationBrief === undefined || translationBrief === null) return res.status(400).json({ error: 'Translation brief is required' });
332
+ const result = await SourceText.updateMany({ category: 'tutorial', weekNumber }, { translationBrief });
333
+ res.json({ success: true, message: `Translation brief updated successfully for ${result.modifiedCount} tutorial tasks`, modifiedCount: result.modifiedCount });
334
+ } catch (error) {
335
+ console.error('Update tutorial brief error:', error);
336
+ res.status(500).json({ error: 'Failed to update translation brief' });
337
+ }
338
+ });
339
+
340
+ router.put('/admin/weekly-brief/:weekNumber', authenticateToken, requireAdmin, async (req, res) => {
341
+ try {
342
+ const SourceText = require('../models/SourceText');
343
+ const { translationBrief } = req.body;
344
+ const weekNumber = parseInt(req.params.weekNumber);
345
+ if (translationBrief === undefined || translationBrief === null) return res.status(400).json({ error: 'Translation brief is required' });
346
+ const result = await SourceText.updateMany({ category: 'weekly-practice', weekNumber }, { translationBrief });
347
+ res.json({ success: true, message: `Translation brief updated successfully for ${result.modifiedCount} weekly practice tasks`, modifiedCount: result.modifiedCount });
348
+ } catch (error) {
349
+ console.error('Update weekly practice brief error:', error);
350
+ res.status(500).json({ error: 'Failed to update translation brief' });
351
+ }
352
+ });
353
+
354
+ module.exports = { router, authenticateToken };
backups/complete-backup-2025-08-10T07-59-20-407Z/code/backend/routes/subtitleSubmissions.js ADDED
@@ -0,0 +1,287 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ const express = require('express');
2
+ const router = express.Router();
3
+ const mongoose = require('mongoose');
4
+ const crypto = require('crypto');
5
+ const SubtitleSubmission = require('../models/SubtitleSubmission');
6
+ const auth = require('../middleware/auth');
7
+
8
+ // GET submissions for a specific segment (public route)
9
+ router.get('/segment/:segmentId', async (req, res) => {
10
+ try {
11
+ const segmentId = parseInt(req.params.segmentId);
12
+ const weekNumber = parseInt(req.query.week) || 2;
13
+
14
+ if (!segmentId || segmentId < 1 || segmentId > 100) {
15
+ return res.status(400).json({
16
+ success: false,
17
+ message: 'Invalid segment ID'
18
+ });
19
+ }
20
+
21
+ const submissions = await SubtitleSubmission.getSubmissionsForSegment(segmentId, weekNumber);
22
+
23
+ // Format submissions for frontend
24
+ const formattedSubmissions = submissions.map(submission => ({
25
+ _id: submission._id,
26
+ username: submission.username,
27
+ chineseTranslation: submission.chineseTranslation,
28
+ submissionDate: submission.submissionDate,
29
+ isAnonymous: submission.isAnonymous,
30
+ status: submission.status,
31
+ notes: submission.notes
32
+ }));
33
+
34
+ res.json({
35
+ success: true,
36
+ data: formattedSubmissions,
37
+ count: formattedSubmissions.length
38
+ });
39
+ } catch (error) {
40
+ console.error('Error fetching subtitle submissions:', error);
41
+ res.status(500).json({
42
+ success: false,
43
+ message: 'Error fetching subtitle submissions',
44
+ error: error.message
45
+ });
46
+ }
47
+ });
48
+
49
+ // POST new subtitle submission (authenticated users)
50
+ router.post('/submit', auth, async (req, res) => {
51
+ try {
52
+ const { segmentId, chineseTranslation, isAnonymous = false, weekNumber = 2, username: requestUsername } = req.body;
53
+
54
+ if (!segmentId || !chineseTranslation) {
55
+ return res.status(400).json({
56
+ success: false,
57
+ message: 'Segment ID and Chinese translation are required'
58
+ });
59
+ }
60
+
61
+ if (segmentId < 1 || segmentId > 100) {
62
+ return res.status(400).json({
63
+ success: false,
64
+ message: 'Invalid segment ID'
65
+ });
66
+ }
67
+
68
+ // Get user info from auth middleware
69
+ const user = req.user?.userInfo || {};
70
+ // Use username from request body first, then fall back to user info
71
+ const displayUsername = requestUsername || user.username || 'Unknown User';
72
+ const userId = user._id || 'unknown';
73
+
74
+ // Generate a valid ObjectId if userId is not a valid ObjectId
75
+ let validUserId;
76
+ try {
77
+ validUserId = mongoose.Types.ObjectId(userId);
78
+ } catch (error) {
79
+ // If userId is not a valid ObjectId, create a consistent one based on username
80
+ const usernameHash = crypto.createHash('md5').update(displayUsername).digest('hex');
81
+ validUserId = new mongoose.Types.ObjectId(usernameHash.substring(0, 24));
82
+ }
83
+
84
+ // Check if user already submitted for this segment
85
+ const existingSubmission = await SubtitleSubmission.findOne({
86
+ userId: validUserId,
87
+ segmentId,
88
+ weekNumber
89
+ });
90
+
91
+ let submission;
92
+ if (existingSubmission) {
93
+ // Update existing submission
94
+ existingSubmission.chineseTranslation = chineseTranslation;
95
+ existingSubmission.isAnonymous = isAnonymous;
96
+ existingSubmission.submissionDate = new Date();
97
+ submission = await existingSubmission.save();
98
+ } else {
99
+ // Create new submission
100
+ submission = new SubtitleSubmission({
101
+ userId: validUserId,
102
+ username: displayUsername,
103
+ segmentId,
104
+ chineseTranslation,
105
+ isAnonymous,
106
+ weekNumber
107
+ });
108
+ await submission.save();
109
+ }
110
+
111
+ res.status(201).json({
112
+ success: true,
113
+ message: existingSubmission ? 'Translation updated' : 'Translation submitted',
114
+ data: {
115
+ _id: submission._id,
116
+ username: submission.isAnonymous ? 'Anonymous' : submission.username,
117
+ chineseTranslation: submission.chineseTranslation,
118
+ submissionDate: submission.submissionDate,
119
+ isAnonymous: submission.isAnonymous
120
+ }
121
+ });
122
+ } catch (error) {
123
+ console.error('Error submitting subtitle translation:', error);
124
+ res.status(500).json({
125
+ success: false,
126
+ message: 'Error submitting subtitle translation',
127
+ error: error.message
128
+ });
129
+ }
130
+ });
131
+
132
+ // GET user's submissions for a week (authenticated users)
133
+ router.get('/user/:weekNumber', auth, async (req, res) => {
134
+ try {
135
+ const weekNumber = parseInt(req.params.weekNumber) || 2;
136
+ const user = req.user?.userInfo || {};
137
+ const userId = user._id || 'unknown';
138
+
139
+ const submissions = await SubtitleSubmission.getUserSubmissions(userId, weekNumber);
140
+
141
+ const formattedSubmissions = submissions.map(submission => ({
142
+ _id: submission._id,
143
+ segmentId: submission.segmentId,
144
+ chineseTranslation: submission.chineseTranslation,
145
+ submissionDate: submission.submissionDate,
146
+ isAnonymous: submission.isAnonymous,
147
+ status: submission.status
148
+ }));
149
+
150
+ res.json({
151
+ success: true,
152
+ data: formattedSubmissions,
153
+ count: formattedSubmissions.length
154
+ });
155
+ } catch (error) {
156
+ console.error('Error fetching user submissions:', error);
157
+ res.status(500).json({
158
+ success: false,
159
+ message: 'Error fetching user submissions',
160
+ error: error.message
161
+ });
162
+ }
163
+ });
164
+
165
+ // GET submission count for a segment (public route)
166
+ router.get('/count/:segmentId', async (req, res) => {
167
+ try {
168
+ const segmentId = parseInt(req.params.segmentId);
169
+ const weekNumber = parseInt(req.query.week) || 2;
170
+
171
+ if (!segmentId || segmentId < 1 || segmentId > 100) {
172
+ return res.status(400).json({
173
+ success: false,
174
+ message: 'Invalid segment ID'
175
+ });
176
+ }
177
+
178
+ const count = await SubtitleSubmission.getSubmissionCount(segmentId, weekNumber);
179
+
180
+ res.json({
181
+ success: true,
182
+ count
183
+ });
184
+ } catch (error) {
185
+ console.error('Error getting submission count:', error);
186
+ res.status(500).json({
187
+ success: false,
188
+ message: 'Error getting submission count',
189
+ error: error.message
190
+ });
191
+ }
192
+ });
193
+
194
+ // DELETE user's submission (authenticated users)
195
+ router.delete('/:submissionId', auth, async (req, res) => {
196
+ try {
197
+ const submissionId = req.params.submissionId;
198
+ const userInfo = req.user?.userInfo || {};
199
+ const userId = userInfo._id || 'unknown';
200
+ const isAdmin = userInfo.role === 'admin';
201
+
202
+ const submission = await SubtitleSubmission.findById(submissionId);
203
+
204
+ if (!submission) {
205
+ return res.status(404).json({
206
+ success: false,
207
+ message: 'Submission not found'
208
+ });
209
+ }
210
+
211
+ // Check if user owns this submission or is admin
212
+ if (!isAdmin && submission.userId.toString() !== userId) {
213
+ return res.status(403).json({
214
+ success: false,
215
+ message: 'You can only delete your own submissions'
216
+ });
217
+ }
218
+
219
+ await SubtitleSubmission.findByIdAndDelete(submissionId);
220
+
221
+ res.json({
222
+ success: true,
223
+ message: 'Submission deleted successfully'
224
+ });
225
+ } catch (error) {
226
+ console.error('Error deleting submission:', error);
227
+ res.status(500).json({
228
+ success: false,
229
+ message: 'Error deleting submission',
230
+ error: error.message
231
+ });
232
+ }
233
+ });
234
+
235
+ // PUT update user's submission (authenticated users)
236
+ router.put('/:submissionId', auth, async (req, res) => {
237
+ try {
238
+ const submissionId = req.params.submissionId;
239
+ const { chineseTranslation, isAnonymous } = req.body;
240
+ const user = JSON.parse(req.headers['user-info'] || '{}');
241
+ const userId = user._id || 'unknown';
242
+
243
+ const submission = await SubtitleSubmission.findById(submissionId);
244
+
245
+ if (!submission) {
246
+ return res.status(404).json({
247
+ success: false,
248
+ message: 'Submission not found'
249
+ });
250
+ }
251
+
252
+ // Check if user owns this submission
253
+ if (submission.userId.toString() !== userId) {
254
+ return res.status(403).json({
255
+ success: false,
256
+ message: 'You can only update your own submissions'
257
+ });
258
+ }
259
+
260
+ submission.chineseTranslation = chineseTranslation;
261
+ submission.isAnonymous = isAnonymous || false;
262
+ submission.submissionDate = new Date();
263
+
264
+ const updatedSubmission = await submission.save();
265
+
266
+ res.json({
267
+ success: true,
268
+ message: 'Submission updated successfully',
269
+ data: {
270
+ _id: updatedSubmission._id,
271
+ username: updatedSubmission.isAnonymous ? 'Anonymous' : updatedSubmission.username,
272
+ chineseTranslation: updatedSubmission.chineseTranslation,
273
+ submissionDate: updatedSubmission.submissionDate,
274
+ isAnonymous: updatedSubmission.isAnonymous
275
+ }
276
+ });
277
+ } catch (error) {
278
+ console.error('Error updating submission:', error);
279
+ res.status(500).json({
280
+ success: false,
281
+ message: 'Error updating submission',
282
+ error: error.message
283
+ });
284
+ }
285
+ });
286
+
287
+ module.exports = router;
backups/complete-backup-2025-08-10T07-59-20-407Z/code/backend/routes/subtitles.js ADDED
@@ -0,0 +1,343 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ const express = require('express');
2
+ const router = express.Router();
3
+ const Subtitle = require('../models/Subtitle');
4
+ const auth = require('../middleware/auth');
5
+
6
+ // GET all subtitles (ordered by segment ID) - Public route
7
+ router.get('/all', async (req, res) => {
8
+ try {
9
+ const subtitles = await Subtitle.getAllOrdered();
10
+ res.json({
11
+ success: true,
12
+ count: subtitles.length,
13
+ data: subtitles
14
+ });
15
+ } catch (error) {
16
+ console.error('Error fetching subtitles:', error);
17
+ res.status(500).json({
18
+ success: false,
19
+ message: 'Error fetching subtitles',
20
+ error: error.message
21
+ });
22
+ }
23
+ });
24
+
25
+ // GET subtitle by segment ID - Public route
26
+ router.get('/segment/:segmentId', async (req, res) => {
27
+ try {
28
+ const segmentId = parseInt(req.params.segmentId);
29
+ const subtitle = await Subtitle.findOne({ segmentId });
30
+
31
+ if (!subtitle) {
32
+ return res.status(404).json({
33
+ success: false,
34
+ message: 'Subtitle not found'
35
+ });
36
+ }
37
+
38
+ res.json({
39
+ success: true,
40
+ data: subtitle
41
+ });
42
+ } catch (error) {
43
+ console.error('Error fetching subtitle:', error);
44
+ res.status(500).json({
45
+ success: false,
46
+ message: 'Error fetching subtitle',
47
+ error: error.message
48
+ });
49
+ }
50
+ });
51
+
52
+ // POST new subtitle (admin only)
53
+ router.post('/create', auth, async (req, res) => {
54
+ try {
55
+ // Check if user is admin
56
+ if (req.user.role !== 'admin') {
57
+ return res.status(403).json({
58
+ success: false,
59
+ message: 'Admin access required'
60
+ });
61
+ }
62
+
63
+ const subtitle = new Subtitle(req.body);
64
+ await subtitle.save();
65
+
66
+ res.status(201).json({
67
+ success: true,
68
+ message: 'Subtitle created successfully',
69
+ data: subtitle
70
+ });
71
+ } catch (error) {
72
+ console.error('Error creating subtitle:', error);
73
+ res.status(500).json({
74
+ success: false,
75
+ message: 'Error creating subtitle',
76
+ error: error.message
77
+ });
78
+ }
79
+ });
80
+
81
+ // PUT update subtitle (admin only)
82
+ router.put('/update/:segmentId', auth, async (req, res) => {
83
+ try {
84
+ // Check if user is admin
85
+ if (req.user.role !== 'admin') {
86
+ return res.status(403).json({
87
+ success: false,
88
+ message: 'Admin access required'
89
+ });
90
+ }
91
+
92
+ const segmentId = parseInt(req.params.segmentId);
93
+ const subtitle = await Subtitle.findOne({ segmentId });
94
+
95
+ if (!subtitle) {
96
+ return res.status(404).json({
97
+ success: false,
98
+ message: 'Subtitle not found'
99
+ });
100
+ }
101
+
102
+ // Check if subtitle is protected
103
+ if (subtitle.isProtected) {
104
+ return res.status(403).json({
105
+ success: false,
106
+ message: 'Cannot update protected subtitle',
107
+ reason: subtitle.protectedReason
108
+ });
109
+ }
110
+
111
+ const updatedSubtitle = await Subtitle.safeUpdate(segmentId, {
112
+ ...req.body,
113
+ reason: req.body.reason || 'Manual update'
114
+ });
115
+
116
+ res.json({
117
+ success: true,
118
+ message: 'Subtitle updated successfully',
119
+ data: updatedSubtitle
120
+ });
121
+ } catch (error) {
122
+ console.error('Error updating subtitle:', error);
123
+ res.status(500).json({
124
+ success: false,
125
+ message: 'Error updating subtitle',
126
+ error: error.message
127
+ });
128
+ }
129
+ });
130
+
131
+ // PUT update Chinese translation (any authenticated user)
132
+ router.put('/translate/:segmentId', auth, async (req, res) => {
133
+ try {
134
+ const segmentId = parseInt(req.params.segmentId);
135
+ const { chineseTranslation } = req.body;
136
+
137
+ if (!chineseTranslation) {
138
+ return res.status(400).json({
139
+ success: false,
140
+ message: 'Chinese translation is required'
141
+ });
142
+ }
143
+
144
+ const subtitle = await Subtitle.findOne({ segmentId });
145
+
146
+ if (!subtitle) {
147
+ return res.status(404).json({
148
+ success: false,
149
+ message: 'Subtitle not found'
150
+ });
151
+ }
152
+
153
+ // Check if subtitle is protected
154
+ if (subtitle.isProtected) {
155
+ return res.status(403).json({
156
+ success: false,
157
+ message: 'Cannot update protected subtitle',
158
+ reason: subtitle.protectedReason
159
+ });
160
+ }
161
+
162
+ const updatedSubtitle = await Subtitle.safeUpdate(segmentId, {
163
+ chineseTranslation,
164
+ reason: 'Translation update'
165
+ });
166
+
167
+ res.json({
168
+ success: true,
169
+ message: 'Translation updated successfully',
170
+ data: updatedSubtitle
171
+ });
172
+ } catch (error) {
173
+ console.error('Error updating translation:', error);
174
+ res.status(500).json({
175
+ success: false,
176
+ message: 'Error updating translation',
177
+ error: error.message
178
+ });
179
+ }
180
+ });
181
+
182
+ // POST protect subtitle (admin only)
183
+ router.post('/protect/:segmentId', auth, async (req, res) => {
184
+ try {
185
+ // Check if user is admin
186
+ if (req.user.role !== 'admin') {
187
+ return res.status(403).json({
188
+ success: false,
189
+ message: 'Admin access required'
190
+ });
191
+ }
192
+
193
+ const segmentId = parseInt(req.params.segmentId);
194
+ const { reason } = req.body;
195
+
196
+ const subtitle = await Subtitle.findOne({ segmentId });
197
+
198
+ if (!subtitle) {
199
+ return res.status(404).json({
200
+ success: false,
201
+ message: 'Subtitle not found'
202
+ });
203
+ }
204
+
205
+ const protectedSubtitle = await Subtitle.protectSubtitle(segmentId, reason);
206
+
207
+ res.json({
208
+ success: true,
209
+ message: 'Subtitle protected successfully',
210
+ data: protectedSubtitle
211
+ });
212
+ } catch (error) {
213
+ console.error('Error protecting subtitle:', error);
214
+ res.status(500).json({
215
+ success: false,
216
+ message: 'Error protecting subtitle',
217
+ error: error.message
218
+ });
219
+ }
220
+ });
221
+
222
+ // POST unlock subtitle (admin only)
223
+ router.post('/unlock/:segmentId', auth, async (req, res) => {
224
+ try {
225
+ // Check if user is admin
226
+ if (req.user.role !== 'admin') {
227
+ return res.status(403).json({
228
+ success: false,
229
+ message: 'Admin access required'
230
+ });
231
+ }
232
+
233
+ const segmentId = parseInt(req.params.segmentId);
234
+ const { unlockKey } = req.body;
235
+
236
+ if (!unlockKey) {
237
+ return res.status(400).json({
238
+ success: false,
239
+ message: 'Unlock key is required'
240
+ });
241
+ }
242
+
243
+ const subtitle = await Subtitle.findOne({ segmentId });
244
+
245
+ if (!subtitle) {
246
+ return res.status(404).json({
247
+ success: false,
248
+ message: 'Subtitle not found'
249
+ });
250
+ }
251
+
252
+ const unlockedSubtitle = await Subtitle.unlockSubtitle(segmentId, unlockKey);
253
+
254
+ res.json({
255
+ success: true,
256
+ message: 'Subtitle unlocked successfully',
257
+ data: unlockedSubtitle
258
+ });
259
+ } catch (error) {
260
+ console.error('Error unlocking subtitle:', error);
261
+ res.status(500).json({
262
+ success: false,
263
+ message: 'Error unlocking subtitle',
264
+ error: error.message
265
+ });
266
+ }
267
+ });
268
+
269
+ // GET protected subtitles
270
+ router.get('/protected', auth, async (req, res) => {
271
+ try {
272
+ // Check if user is admin
273
+ if (req.user.role !== 'admin') {
274
+ return res.status(403).json({
275
+ success: false,
276
+ message: 'Admin access required'
277
+ });
278
+ }
279
+
280
+ const protectedSubtitles = await Subtitle.getProtected();
281
+
282
+ res.json({
283
+ success: true,
284
+ count: protectedSubtitles.length,
285
+ data: protectedSubtitles
286
+ });
287
+ } catch (error) {
288
+ console.error('Error fetching protected subtitles:', error);
289
+ res.status(500).json({
290
+ success: false,
291
+ message: 'Error fetching protected subtitles',
292
+ error: error.message
293
+ });
294
+ }
295
+ });
296
+
297
+ // DELETE subtitle (admin only)
298
+ router.delete('/delete/:segmentId', auth, async (req, res) => {
299
+ try {
300
+ // Check if user is admin
301
+ if (req.user.role !== 'admin') {
302
+ return res.status(403).json({
303
+ success: false,
304
+ message: 'Admin access required'
305
+ });
306
+ }
307
+
308
+ const segmentId = parseInt(req.params.segmentId);
309
+ const subtitle = await Subtitle.findOne({ segmentId });
310
+
311
+ if (!subtitle) {
312
+ return res.status(404).json({
313
+ success: false,
314
+ message: 'Subtitle not found'
315
+ });
316
+ }
317
+
318
+ // Check if subtitle is protected
319
+ if (subtitle.isProtected) {
320
+ return res.status(403).json({
321
+ success: false,
322
+ message: 'Cannot delete protected subtitle',
323
+ reason: subtitle.protectedReason
324
+ });
325
+ }
326
+
327
+ await Subtitle.deleteOne({ segmentId });
328
+
329
+ res.json({
330
+ success: true,
331
+ message: 'Subtitle deleted successfully'
332
+ });
333
+ } catch (error) {
334
+ console.error('Error deleting subtitle:', error);
335
+ res.status(500).json({
336
+ success: false,
337
+ message: 'Error deleting subtitle',
338
+ error: error.message
339
+ });
340
+ }
341
+ });
342
+
343
+ module.exports = router;
backups/complete-backup-2025-08-10T07-59-20-407Z/code/backend/seed-atlas-subtitles.js ADDED
@@ -0,0 +1,87 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ const mongoose = require('mongoose');
2
+ const Subtitle = require('./models/Subtitle');
3
+
4
+ // Atlas MongoDB connection string
5
+ const MONGODB_URI = 'mongodb+srv://nothingyu:wSg3lbO1PkHiRMq9@sandbox.ecysggv.mongodb.net/test?retryWrites=true&w=majority&appName=sandbox';
6
+
7
+ // Nike video subtitle data
8
+ const subtitleData = [
9
+ { segmentId: 1, startTime: '00:00:00,640', endTime: '00:00:02,400', duration: '1s 760ms', englishText: 'Am I a bad person?' },
10
+ { segmentId: 2, startTime: '00:00:06,320', endTime: '00:00:07,860', duration: '1s 540ms', englishText: 'Tell me. Am I?' },
11
+ { segmentId: 3, startTime: '00:00:08,480', endTime: '00:00:09,740', duration: '1s 260ms', englishText: 'I\'m single minded.' },
12
+ { segmentId: 4, startTime: '00:00:10,570', endTime: '00:00:11,780', duration: '1s 210ms', englishText: 'I\'m deceptive.' },
13
+ { segmentId: 5, startTime: '00:00:12,050', endTime: '00:00:13,490', duration: '1s 440ms', englishText: 'I\'m obsessive.' },
14
+ { segmentId: 6, startTime: '00:00:13,780', endTime: '00:00:14,910', duration: '1s 130ms', englishText: 'I\'m selfish.' },
15
+ { segmentId: 7, startTime: '00:00:15,120', endTime: '00:00:17,200', duration: '2s 80ms', englishText: 'Does that make me a bad person?' },
16
+ { segmentId: 8, startTime: '00:00:18,010', endTime: '00:00:19,660', duration: '1s 650ms', englishText: 'Am I a bad person?' },
17
+ { segmentId: 9, startTime: '00:00:20,870', endTime: '00:00:21,870', duration: '1s 0ms', englishText: 'Am I?' },
18
+ { segmentId: 10, startTime: '00:00:23,120', endTime: '00:00:24,390', duration: '1s 270ms', englishText: 'I have no empathy.' },
19
+ { segmentId: 11, startTime: '00:00:25,540', endTime: '00:00:27,170', duration: '1s 630ms', englishText: 'I don\'t respect you.' },
20
+ { segmentId: 12, startTime: '00:00:28,550', endTime: '00:00:29,880', duration: '1s 330ms', englishText: 'I\'m never satisfied.' },
21
+ { segmentId: 13, startTime: '00:00:30,440', endTime: '00:00:33,180', duration: '2s 740ms', englishText: 'I have an obsession with power.' },
22
+ { segmentId: 14, startTime: '00:00:37,850', endTime: '00:00:38,950', duration: '1s 100ms', englishText: 'I\'m irrational.' },
23
+ { segmentId: 15, startTime: '00:00:39,930', endTime: '00:00:41,520', duration: '1s 590ms', englishText: 'I have zero remorse.' },
24
+ { segmentId: 16, startTime: '00:00:41,770', endTime: '00:00:43,900', duration: '2s 130ms', englishText: 'I have no sense of compassion.' },
25
+ { segmentId: 17, startTime: '00:00:44,480', endTime: '00:00:46,650', duration: '2s 170ms', englishText: 'I\'m delusional. I\'m maniacal.' },
26
+ { segmentId: 18, startTime: '00:00:46,960', endTime: '00:00:48,980', duration: '2s 20ms', englishText: 'You think I\'m a bad person?' },
27
+ { segmentId: 19, startTime: '00:00:49,320', endTime: '00:00:52,700', duration: '3s 380ms', englishText: 'Tell me. Tell me. Tell me.\nTell me. Am I?' },
28
+ { segmentId: 20, startTime: '00:00:52,990', endTime: '00:00:55,136', duration: '2s 146ms', englishText: 'I think I\'m better than everyone else.' },
29
+ { segmentId: 21, startTime: '00:00:55,170', endTime: '00:00:57,820', duration: '2s 650ms', englishText: 'I want to take what\'s yours and never give it back.' },
30
+ { segmentId: 22, startTime: '00:00:57,840', endTime: '00:01:00,640', duration: '2s 800ms', englishText: 'What\'s mine is mine and what\'s yours is mine.' },
31
+ { segmentId: 23, startTime: '00:01:06,920', endTime: '00:01:08,290', duration: '1s 370ms', englishText: 'Am I a bad person?' },
32
+ { segmentId: 24, startTime: '00:01:08,840', endTime: '00:01:10,420', duration: '1s 580ms', englishText: 'Tell me. Am I?' },
33
+ { segmentId: 25, startTime: '00:01:21,500', endTime: '00:01:23,650', duration: '2s 150ms', englishText: 'Does that make me a bad person?' },
34
+ { segmentId: 26, startTime: '00:01:25,060', endTime: '00:01:26,900', duration: '1s 840ms', englishText: 'Tell me. Does it?' }
35
+ ];
36
+
37
+ async function seedAtlasSubtitles() {
38
+ try {
39
+ console.log('🔗 Connecting to Atlas MongoDB...');
40
+ await mongoose.connect(MONGODB_URI);
41
+ console.log('✅ Connected to Atlas MongoDB');
42
+ console.log('📊 Database:', mongoose.connection.db.databaseName);
43
+
44
+ // Check if subtitles collection exists
45
+ const collections = await mongoose.connection.db.listCollections().toArray();
46
+ const subtitleExists = collections.some(col => col.name === 'subtitles');
47
+ console.log('📋 Existing collections:', collections.map(col => col.name));
48
+ console.log('✅ Subtitles collection exists:', subtitleExists);
49
+
50
+ if (subtitleExists) {
51
+ // Clear existing subtitles
52
+ console.log('🗑️ Clearing existing subtitle data...');
53
+ await Subtitle.deleteMany({});
54
+ console.log('✅ Cleared existing subtitle data');
55
+ }
56
+
57
+ // Insert new subtitle data
58
+ console.log('📝 Inserting subtitle data...');
59
+ const result = await Subtitle.insertMany(subtitleData);
60
+ console.log(`✅ Successfully inserted ${result.length} subtitle segments`);
61
+
62
+ // Verify the data
63
+ console.log('🔍 Verifying data...');
64
+ const count = await Subtitle.countDocuments();
65
+ console.log(`📊 Total subtitle segments in Atlas: ${count}`);
66
+
67
+ // Show sample data
68
+ const sample = await Subtitle.findOne().sort({ segmentId: 1 });
69
+ console.log('📋 Sample subtitle data:');
70
+ console.log(JSON.stringify(sample, null, 2));
71
+
72
+ console.log('🎉 Atlas subtitle seeding completed successfully!');
73
+
74
+ } catch (error) {
75
+ console.error('❌ Error seeding Atlas subtitles:', error);
76
+ } finally {
77
+ await mongoose.disconnect();
78
+ console.log('🔌 Disconnected from Atlas MongoDB');
79
+ }
80
+ }
81
+
82
+ // Run the seeding function
83
+ if (require.main === module) {
84
+ seedAtlasSubtitles();
85
+ }
86
+
87
+ module.exports = { seedAtlasSubtitles, subtitleData };
backups/complete-backup-2025-08-10T07-59-20-407Z/code/backend/seed-subtitle-submissions.js ADDED
@@ -0,0 +1,178 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ const mongoose = require('mongoose');
2
+ const SubtitleSubmission = require('./models/SubtitleSubmission');
3
+
4
+ const MONGODB_URI = 'mongodb+srv://nothingyu:wSg3lbO1PkHiRMq9@sandbox.ecysggv.mongodb.net/test?retryWrites=true&w=majority&appName=sandbox';
5
+
6
+ // Generate valid ObjectIds for users
7
+ const generateUserId = () => new mongoose.Types.ObjectId();
8
+
9
+ const sampleSubmissions = [
10
+ // Segment 1 submissions
11
+ {
12
+ userId: generateUserId(),
13
+ username: 'Student A',
14
+ segmentId: 1,
15
+ chineseTranslation: '我是不是一个坏人?',
16
+ submissionDate: new Date(Date.now() - 2 * 60 * 60 * 1000), // 2 hours ago
17
+ isAnonymous: false,
18
+ weekNumber: 2
19
+ },
20
+ {
21
+ userId: generateUserId(),
22
+ username: 'Student B',
23
+ segmentId: 1,
24
+ chineseTranslation: '我是否是一个坏人?',
25
+ submissionDate: new Date(Date.now() - 1 * 60 * 60 * 1000), // 1 hour ago
26
+ isAnonymous: false,
27
+ weekNumber: 2
28
+ },
29
+ {
30
+ userId: generateUserId(),
31
+ username: 'Student C',
32
+ segmentId: 1,
33
+ chineseTranslation: '我是否是个坏人?',
34
+ submissionDate: new Date(Date.now() - 30 * 60 * 1000), // 30 minutes ago
35
+ isAnonymous: false,
36
+ weekNumber: 2
37
+ },
38
+
39
+ // Segment 7 submissions (longer text)
40
+ {
41
+ userId: generateUserId(),
42
+ username: 'Student D',
43
+ segmentId: 7,
44
+ chineseTranslation: '这让我成为一个坏人吗?',
45
+ submissionDate: new Date(Date.now() - 3 * 60 * 60 * 1000), // 3 hours ago
46
+ isAnonymous: false,
47
+ weekNumber: 2
48
+ },
49
+ {
50
+ userId: generateUserId(),
51
+ username: 'Student E',
52
+ segmentId: 7,
53
+ chineseTranslation: '这是否使我成为一个坏人?',
54
+ submissionDate: new Date(Date.now() - 45 * 60 * 1000), // 45 minutes ago
55
+ isAnonymous: false,
56
+ weekNumber: 2
57
+ },
58
+
59
+ // Segment 13 submissions (very long text)
60
+ {
61
+ userId: generateUserId(),
62
+ username: 'Student F',
63
+ segmentId: 13,
64
+ chineseTranslation: '我对权力有一种执念。',
65
+ submissionDate: new Date(Date.now() - 4 * 60 * 60 * 1000), // 4 hours ago
66
+ isAnonymous: false,
67
+ weekNumber: 2
68
+ },
69
+ {
70
+ userId: generateUserId(),
71
+ username: 'Student G',
72
+ segmentId: 13,
73
+ chineseTranslation: '我对权力有着执念。',
74
+ submissionDate: new Date(Date.now() - 20 * 60 * 1000), // 20 minutes ago
75
+ isAnonymous: false,
76
+ weekNumber: 2
77
+ },
78
+
79
+ // Segment 19 submissions (multiple lines)
80
+ {
81
+ userId: generateUserId(),
82
+ username: 'Student H',
83
+ segmentId: 19,
84
+ chineseTranslation: '告诉我。告诉我。告诉我。\n告诉我。我是吗?',
85
+ submissionDate: new Date(Date.now() - 1.5 * 60 * 60 * 1000), // 1.5 hours ago
86
+ isAnonymous: false,
87
+ weekNumber: 2
88
+ },
89
+ {
90
+ userId: generateUserId(),
91
+ username: 'Student I',
92
+ segmentId: 19,
93
+ chineseTranslation: '告诉我。告诉我。告诉我。告诉我。我是吗?',
94
+ submissionDate: new Date(Date.now() - 15 * 60 * 1000), // 15 minutes ago
95
+ isAnonymous: false,
96
+ weekNumber: 2
97
+ },
98
+
99
+ // Segment 26 submissions (final segment)
100
+ {
101
+ userId: generateUserId(),
102
+ username: 'Student J',
103
+ segmentId: 26,
104
+ chineseTranslation: '告诉我。是吗?',
105
+ submissionDate: new Date(Date.now() - 5 * 60 * 60 * 1000), // 5 hours ago
106
+ isAnonymous: false,
107
+ weekNumber: 2
108
+ },
109
+ {
110
+ userId: generateUserId(),
111
+ username: 'Student K',
112
+ segmentId: 26,
113
+ chineseTranslation: '告诉我。是这样吗?',
114
+ submissionDate: new Date(Date.now() - 10 * 60 * 1000), // 10 minutes ago
115
+ isAnonymous: false,
116
+ weekNumber: 2
117
+ },
118
+
119
+ // Additional submissions with real usernames
120
+ {
121
+ userId: generateUserId(),
122
+ username: 'Student L',
123
+ segmentId: 1,
124
+ chineseTranslation: '我是否是一个坏人呢?',
125
+ submissionDate: new Date(Date.now() - 25 * 60 * 1000), // 25 minutes ago
126
+ isAnonymous: false,
127
+ weekNumber: 2
128
+ },
129
+ {
130
+ userId: generateUserId(),
131
+ username: 'Student M',
132
+ segmentId: 7,
133
+ chineseTranslation: '这让我成为坏人吗?',
134
+ submissionDate: new Date(Date.now() - 35 * 60 * 1000), // 35 minutes ago
135
+ isAnonymous: false,
136
+ weekNumber: 2
137
+ }
138
+ ];
139
+
140
+ const seedSubmissions = async () => {
141
+ try {
142
+ console.log('🔧 Connecting to MongoDB...');
143
+ await mongoose.connect(MONGODB_URI);
144
+ console.log('✅ Connected to MongoDB');
145
+
146
+ // Clear existing submissions
147
+ console.log('🧹 Clearing existing subtitle submissions...');
148
+ await SubtitleSubmission.deleteMany({});
149
+ console.log('✅ Cleared existing submissions');
150
+
151
+ // Insert sample submissions
152
+ console.log('📝 Inserting sample subtitle submissions...');
153
+ const result = await SubtitleSubmission.insertMany(sampleSubmissions);
154
+ console.log(`✅ Inserted ${result.length} sample submissions`);
155
+
156
+ // Display summary
157
+ console.log('\n📊 Submission Summary:');
158
+ const segmentCounts = await SubtitleSubmission.aggregate([
159
+ { $group: { _id: '$segmentId', count: { $sum: 1 } } },
160
+ { $sort: { _id: 1 } }
161
+ ]);
162
+
163
+ segmentCounts.forEach(({ _id, count }) => {
164
+ console.log(` Segment ${_id}: ${count} submission${count !== 1 ? 's' : ''}`);
165
+ });
166
+
167
+ console.log('\n🎉 Sample subtitle submissions seeded successfully!');
168
+
169
+ } catch (error) {
170
+ console.error('❌ Error seeding subtitle submissions:', error);
171
+ } finally {
172
+ await mongoose.disconnect();
173
+ console.log('🔌 Disconnected from MongoDB');
174
+ }
175
+ };
176
+
177
+ // Run the seeding
178
+ seedSubmissions();
backups/complete-backup-2025-08-10T07-59-20-407Z/code/frontend/Layout.tsx ADDED
@@ -0,0 +1,191 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React, { useState, useEffect, useRef } from 'react';
2
+ import { Link, useLocation } from 'react-router-dom';
3
+ import {
4
+ HomeIcon,
5
+ AcademicCapIcon,
6
+ BookOpenIcon,
7
+ HandThumbUpIcon,
8
+ UserIcon,
9
+ ArrowRightOnRectangleIcon
10
+ } from '@heroicons/react/24/outline';
11
+
12
+ interface User {
13
+ name: string;
14
+ email: string;
15
+ role: string;
16
+ }
17
+
18
+ const Layout: React.FC<{ children: React.ReactNode }> = ({ children }) => {
19
+ const location = useLocation();
20
+ const [isTransitioning, setIsTransitioning] = useState(false);
21
+ const previousPathRef = useRef(location.pathname);
22
+ const userData = localStorage.getItem('user');
23
+ const user: User | null = userData ? JSON.parse(userData) : null;
24
+
25
+ // Handle page transitions
26
+ useEffect(() => {
27
+ // Only trigger transition if path actually changed
28
+ if (location.pathname !== previousPathRef.current) {
29
+ setIsTransitioning(true);
30
+ previousPathRef.current = location.pathname;
31
+
32
+ // Reset week selection to Week 1 when navigating between pages
33
+ const previousPath = previousPathRef.current;
34
+ if (location.pathname === '/tutorial-tasks' &&
35
+ previousPath &&
36
+ !previousPath.includes('/tutorial-tasks')) {
37
+ // Use URL parameter to force Week 1
38
+ window.history.replaceState(null, '', '/tutorial-tasks?week=1');
39
+ localStorage.setItem('selectedTutorialWeek', '1');
40
+ } else if (location.pathname === '/weekly-practice' &&
41
+ previousPath &&
42
+ !previousPath.includes('/weekly-practice')) {
43
+ // Use URL parameter to force Week 1
44
+ window.history.replaceState(null, '', '/weekly-practice?week=1');
45
+ localStorage.setItem('selectedWeeklyPracticeWeek', '1');
46
+ }
47
+
48
+ // Determine transition duration based on destination page
49
+ let transitionDuration = 800; // Default duration
50
+
51
+ // Longer duration for heavy pages
52
+ if (location.pathname === '/tutorial-tasks' || location.pathname === '/weekly-practice') {
53
+ transitionDuration = 1200; // Longer for content-heavy pages
54
+ }
55
+
56
+ // Special case: Weekly Practice → Tutorial Tasks (add 500ms delay)
57
+ if (location.pathname === '/tutorial-tasks' &&
58
+ previousPath &&
59
+ previousPath.includes('/weekly-practice')) {
60
+ // Check if navigating to Week 2 (use localStorage since URL might not be updated yet)
61
+ const tutorialWeek = localStorage.getItem('selectedTutorialWeek');
62
+ if (tutorialWeek === '2') {
63
+ transitionDuration = 2500; // Extended duration for Week 2 (2000 + 500)
64
+ } else {
65
+ transitionDuration = 1700; // Standard duration for Week 1 (1200 + 500)
66
+ }
67
+ }
68
+
69
+ // End transition after content is loaded (wait for DOM updates)
70
+ const timer = setTimeout(() => {
71
+ setIsTransitioning(false);
72
+ }, transitionDuration);
73
+
74
+ return () => clearTimeout(timer);
75
+ }
76
+ }, [location.pathname]);
77
+
78
+ const handleLogout = () => {
79
+ localStorage.removeItem('token');
80
+ localStorage.removeItem('user');
81
+ window.location.href = '/';
82
+ };
83
+
84
+ const navigation = [
85
+ { name: 'Home', href: '/dashboard', icon: HomeIcon },
86
+ { name: 'Tutorial Tasks', href: '/tutorial-tasks', icon: AcademicCapIcon },
87
+ { name: 'Weekly Practice', href: '/weekly-practice', icon: BookOpenIcon },
88
+ { name: 'Votes', href: '/votes', icon: HandThumbUpIcon },
89
+ ];
90
+
91
+ // Add Manage link for admin users
92
+ if (user?.role === 'admin') {
93
+ navigation.push({ name: 'Manage', href: '/manage', icon: UserIcon });
94
+ }
95
+
96
+ return (
97
+ <div className="min-h-screen bg-gray-50">
98
+ {/* Navigation */}
99
+ <nav className="bg-white shadow-sm border-b border-gray-200">
100
+ <div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
101
+ <div className="flex justify-between h-16">
102
+ <div className="flex">
103
+ <div className="flex-shrink-0 flex items-center">
104
+ <Link to="/dashboard" className="text-xl font-bold text-indigo-600">
105
+ Transcreation
106
+ </Link>
107
+ </div>
108
+ <div className="hidden sm:ml-6 sm:flex sm:space-x-8">
109
+ {navigation.map((item) => {
110
+ const isActive = location.pathname === item.href;
111
+ return (
112
+ <Link
113
+ key={item.name}
114
+ to={item.href}
115
+ className={`inline-flex items-center px-1 pt-1 border-b-2 text-sm font-medium transition-all duration-200 ease-in-out ${
116
+ isActive
117
+ ? 'border-indigo-500 text-gray-900'
118
+ : 'border-transparent text-gray-500 hover:border-gray-300 hover:text-gray-700'
119
+ }`}
120
+ >
121
+ <item.icon className="h-4 w-4 mr-1" />
122
+ {item.name}
123
+ </Link>
124
+ );
125
+ })}
126
+ </div>
127
+ </div>
128
+ <div className="flex items-center">
129
+ {user && (
130
+ <div className="flex items-center space-x-4">
131
+ <span className="text-sm text-gray-700">
132
+ Welcome, {user.name}
133
+ </span>
134
+ <button
135
+ onClick={handleLogout}
136
+ className="text-gray-500 hover:text-gray-700 flex items-center"
137
+ >
138
+ <ArrowRightOnRectangleIcon className="h-4 w-4 mr-1" />
139
+ Logout
140
+ </button>
141
+ </div>
142
+ )}
143
+ </div>
144
+ </div>
145
+ </div>
146
+ </nav>
147
+
148
+ {/* Mobile Navigation */}
149
+ <div className="sm:hidden">
150
+ <div className="pt-2 pb-3 space-y-1">
151
+ {navigation.map((item) => {
152
+ const isActive = location.pathname === item.href;
153
+ return (
154
+ <Link
155
+ key={item.name}
156
+ to={item.href}
157
+ className={`block pl-3 pr-4 py-2 border-l-4 text-base font-medium transition-all duration-200 ease-in-out ${
158
+ isActive
159
+ ? 'bg-indigo-50 border-indigo-500 text-indigo-700'
160
+ : 'border-transparent text-gray-600 hover:bg-gray-50 hover:border-gray-300 hover:text-gray-800'
161
+ }`}
162
+ >
163
+ <div className="flex items-center">
164
+ <item.icon className="h-4 w-4 mr-2" />
165
+ {item.name}
166
+ </div>
167
+ </Link>
168
+ );
169
+ })}
170
+ </div>
171
+ </div>
172
+
173
+ {/* Main Content */}
174
+ <main>
175
+ {!isTransitioning && children}
176
+ </main>
177
+
178
+ {/* Transition Loading Indicator */}
179
+ {isTransitioning && (
180
+ <div className="fixed top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2 z-50">
181
+ <div className="bg-white rounded-lg shadow-lg p-4 flex items-center space-x-3">
182
+ <div className="animate-spin rounded-full h-6 w-6 border-b-2 border-indigo-600"></div>
183
+ <span className="text-gray-700 font-medium">Loading...</span>
184
+ </div>
185
+ </div>
186
+ )}
187
+ </div>
188
+ );
189
+ };
190
+
191
+ export default Layout;
backups/complete-backup-2025-08-10T07-59-20-407Z/code/frontend/TutorialTasks.tsx ADDED
@@ -0,0 +1,1724 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React, { useState, useEffect, useCallback } from 'react';
2
+ import { useNavigate } from 'react-router-dom';
3
+ import { api } from '../services/api';
4
+ import {
5
+ AcademicCapIcon,
6
+ DocumentTextIcon,
7
+ CheckCircleIcon,
8
+ ClockIcon,
9
+ ArrowRightIcon,
10
+ PencilIcon,
11
+ XMarkIcon,
12
+ CheckIcon,
13
+ PlusIcon,
14
+ TrashIcon
15
+ } from '@heroicons/react/24/outline';
16
+
17
+ interface TutorialTask {
18
+ _id: string;
19
+ content: string;
20
+ weekNumber: number;
21
+ translationBrief?: string;
22
+ imageUrl?: string;
23
+ imageAlt?: string;
24
+ imageSize?: number;
25
+ imageAlignment?: 'left' | 'center' | 'right';
26
+ }
27
+
28
+ interface TutorialWeek {
29
+ weekNumber: number;
30
+ translationBrief?: string;
31
+ tasks: TutorialTask[];
32
+ }
33
+
34
+ interface UserSubmission {
35
+ _id: string;
36
+ transcreation: string;
37
+ status: string;
38
+ score: number;
39
+ groupNumber?: number;
40
+ isOwner?: boolean;
41
+ userId?: {
42
+ _id: string;
43
+ username: string;
44
+ };
45
+ voteCounts: {
46
+ '1': number;
47
+ '2': number;
48
+ '3': number;
49
+ };
50
+ }
51
+
52
+ const TutorialTasks: React.FC = () => {
53
+ const [selectedWeek, setSelectedWeek] = useState<number>(() => {
54
+ const savedWeek = localStorage.getItem('selectedTutorialWeek');
55
+ return savedWeek ? parseInt(savedWeek) : 1;
56
+ });
57
+ const [isWeekTransitioning, setIsWeekTransitioning] = useState(false);
58
+ const [tutorialTasks, setTutorialTasks] = useState<TutorialTask[]>([]);
59
+ const [tutorialWeek, setTutorialWeek] = useState<TutorialWeek | null>(null);
60
+ const [userSubmissions, setUserSubmissions] = useState<{[key: string]: UserSubmission[]}>({});
61
+ const [loading, setLoading] = useState(true);
62
+ const [submitting, setSubmitting] = useState<{[key: string]: boolean}>({});
63
+ const [translationText, setTranslationText] = useState<{[key: string]: string}>({});
64
+ const [selectedGroups, setSelectedGroups] = useState<{[key: string]: number}>({});
65
+ const [expandedSections, setExpandedSections] = useState<{[key: string]: boolean}>({});
66
+
67
+ const [editingTask, setEditingTask] = useState<string | null>(null);
68
+ const [editingBrief, setEditingBrief] = useState<{[key: number]: boolean}>({});
69
+ const [addingTask, setAddingTask] = useState<boolean>(false);
70
+ const [addingImage, setAddingImage] = useState<boolean>(false);
71
+ const [editForm, setEditForm] = useState<{
72
+ content: string;
73
+ translationBrief: string;
74
+ imageUrl: string;
75
+ imageAlt: string;
76
+ }>({
77
+ content: '',
78
+ translationBrief: '',
79
+ imageUrl: '',
80
+ imageAlt: ''
81
+ });
82
+ const [imageForm, setImageForm] = useState<{
83
+ imageUrl: string;
84
+ imageAlt: string;
85
+ imageSize: number;
86
+ imageAlignment: 'left' | 'center' | 'right';
87
+ }>({
88
+ imageUrl: '',
89
+ imageAlt: '',
90
+ imageSize: 200,
91
+ imageAlignment: 'center'
92
+ });
93
+ const [selectedFile, setSelectedFile] = useState<File | null>(null);
94
+ const [uploading, setUploading] = useState(false);
95
+ const [saving, setSaving] = useState(false);
96
+ const navigate = useNavigate();
97
+
98
+ const weeks = [1, 2, 3, 4, 5, 6];
99
+
100
+ const handleWeekChange = async (week: number) => {
101
+ setIsWeekTransitioning(true);
102
+
103
+ // Clear existing data first
104
+ setTutorialTasks([]);
105
+ setTutorialWeek(null);
106
+ setUserSubmissions({});
107
+
108
+ // Update state and localStorage
109
+ setSelectedWeek(week);
110
+ localStorage.setItem('selectedTutorialWeek', week.toString());
111
+
112
+ // Force a small delay to ensure state is updated
113
+ await new Promise(resolve => setTimeout(resolve, 50));
114
+
115
+ // Wait for actual content to load before ending animation
116
+ try {
117
+ // Fetch new week's data with the updated selectedWeek
118
+ const response = await api.get(`/api/search/tutorial-tasks/${week}`);
119
+
120
+ if (response.data) {
121
+ const tasks = response.data;
122
+ console.log('Fetched tasks for week', week, ':', tasks);
123
+
124
+ // Ensure tasks are sorted by title
125
+ const sortedTasks = tasks.sort((a: any, b: any) => {
126
+ const aNum = parseInt(a.title?.match(/ST (\d+)/)?.[1] || '0');
127
+ const bNum = parseInt(b.title?.match(/ST (\d+)/)?.[1] || '0');
128
+ return aNum - bNum;
129
+ });
130
+
131
+ setTutorialTasks(sortedTasks);
132
+
133
+ // Use translation brief from tasks or localStorage
134
+ let translationBrief = null;
135
+ if (tasks.length > 0) {
136
+ translationBrief = tasks[0].translationBrief;
137
+ } else {
138
+ const briefKey = `translationBrief_week_${week}`;
139
+ translationBrief = localStorage.getItem(briefKey);
140
+ }
141
+
142
+ const tutorialWeekData: TutorialWeek = {
143
+ weekNumber: week,
144
+ translationBrief: translationBrief,
145
+ tasks: tasks
146
+ };
147
+ setTutorialWeek(tutorialWeekData);
148
+
149
+ // Fetch user submissions for the new tasks
150
+ await fetchUserSubmissions(tasks);
151
+ }
152
+
153
+ // Wait longer for DOM to update with new content (especially for Week 2)
154
+ const delay = week === 2 ? 400 : 200;
155
+ await new Promise(resolve => setTimeout(resolve, delay));
156
+ } catch (error) {
157
+ console.error('Error loading week data:', error);
158
+ } finally {
159
+ // End transition after content is loaded and rendered
160
+ setIsWeekTransitioning(false);
161
+ }
162
+ };
163
+
164
+ const handleFileUpload = async (file: File): Promise<string> => {
165
+ try {
166
+ setUploading(true);
167
+
168
+ // Convert file to data URL for display
169
+ return new Promise((resolve, reject) => {
170
+ const reader = new FileReader();
171
+ reader.onload = () => {
172
+ const dataUrl = reader.result as string;
173
+ console.log('File uploaded:', file.name, 'Size:', file.size);
174
+ console.log('Generated data URL:', dataUrl.substring(0, 50) + '...');
175
+ resolve(dataUrl);
176
+ };
177
+ reader.onerror = () => {
178
+ console.error('Error reading file:', reader.error);
179
+ reject(reader.error);
180
+ };
181
+ reader.readAsDataURL(file);
182
+ });
183
+ } catch (error) {
184
+ console.error('Error uploading file:', error);
185
+ throw error;
186
+ } finally {
187
+ setUploading(false);
188
+ }
189
+ };
190
+
191
+ const handleFileChange = (event: React.ChangeEvent<HTMLInputElement>) => {
192
+ const file = event.target.files?.[0];
193
+ if (file) {
194
+ setSelectedFile(file);
195
+ }
196
+ };
197
+
198
+ const toggleExpanded = (taskId: string) => {
199
+ setExpandedSections(prev => ({
200
+ ...prev,
201
+ [taskId]: !prev[taskId]
202
+ }));
203
+ };
204
+
205
+ const fetchUserSubmissions = useCallback(async (tasks: TutorialTask[]) => {
206
+ try {
207
+ const token = localStorage.getItem('token');
208
+ const user = localStorage.getItem('user');
209
+
210
+ if (!token || !user) {
211
+ return;
212
+ }
213
+
214
+ const response = await api.get('/api/submissions/my-submissions');
215
+
216
+ if (response.data && response.data.submissions) {
217
+ const data = response.data;
218
+
219
+ const groupedSubmissions: {[key: string]: UserSubmission[]} = {};
220
+
221
+ // Initialize all tasks with empty arrays
222
+ tasks.forEach(task => {
223
+ groupedSubmissions[task._id] = [];
224
+ });
225
+
226
+ // Then populate with actual submissions
227
+ if (data.submissions && Array.isArray(data.submissions)) {
228
+ data.submissions.forEach((submission: any) => {
229
+ // Extract the actual ID from sourceTextId (could be string or object)
230
+ const sourceTextId = submission.sourceTextId?._id || submission.sourceTextId;
231
+
232
+ if (sourceTextId && groupedSubmissions[sourceTextId]) {
233
+ groupedSubmissions[sourceTextId].push(submission);
234
+ }
235
+ });
236
+ }
237
+
238
+ setUserSubmissions(groupedSubmissions);
239
+ }
240
+ } catch (error) {
241
+ console.error('Error fetching user submissions:', error);
242
+ }
243
+ }, []);
244
+
245
+ const fetchTutorialTasks = useCallback(async (showLoading = true) => {
246
+ try {
247
+ if (showLoading) {
248
+ setLoading(true);
249
+ }
250
+ const token = localStorage.getItem('token');
251
+ const user = localStorage.getItem('user');
252
+
253
+ if (!token || !user) {
254
+ navigate('/login');
255
+ return;
256
+ }
257
+
258
+ const response = await api.get(`/api/search/tutorial-tasks/${selectedWeek}`);
259
+
260
+ if (response.data) {
261
+ const tasks = response.data;
262
+ console.log('Fetched tasks:', tasks);
263
+ console.log('Tasks with images:', tasks.filter((task: TutorialTask) => task.imageUrl));
264
+
265
+ // Debug: Log each task's fields
266
+ tasks.forEach((task: any, index: number) => {
267
+ console.log(`Task ${index} fields:`, {
268
+ _id: task._id,
269
+ content: task.content,
270
+ imageUrl: task.imageUrl,
271
+ imageAlt: task.imageAlt,
272
+ translationBrief: task.translationBrief,
273
+ weekNumber: task.weekNumber,
274
+ category: task.category
275
+ });
276
+ console.log(`Task ${index} imageUrl:`, task.imageUrl);
277
+ console.log(`Task ${index} translationBrief:`, task.translationBrief);
278
+ });
279
+
280
+ // Ensure tasks are sorted by title to maintain correct order (Tutorial ST 1, Tutorial ST 2, etc.)
281
+ const sortedTasks = tasks.sort((a: any, b: any) => {
282
+ const aNum = parseInt(a.title?.match(/ST (\d+)/)?.[1] || '0');
283
+ const bNum = parseInt(b.title?.match(/ST (\d+)/)?.[1] || '0');
284
+ return aNum - bNum;
285
+ });
286
+ setTutorialTasks(sortedTasks);
287
+
288
+ // Use translation brief from tasks or localStorage
289
+ let translationBrief = null;
290
+ if (tasks.length > 0) {
291
+ translationBrief = tasks[0].translationBrief;
292
+ console.log('Translation brief from task:', translationBrief);
293
+ } else {
294
+ // Check localStorage for brief if no tasks exist
295
+ const briefKey = `translationBrief_week_${selectedWeek}`;
296
+ translationBrief = localStorage.getItem(briefKey);
297
+ console.log('Translation brief from localStorage:', translationBrief);
298
+ console.log('localStorage key:', briefKey);
299
+ }
300
+
301
+ console.log('Final translation brief:', translationBrief);
302
+ const tutorialWeekData: TutorialWeek = {
303
+ weekNumber: selectedWeek,
304
+ translationBrief: translationBrief,
305
+ tasks: tasks
306
+ };
307
+ setTutorialWeek(tutorialWeekData);
308
+
309
+ await fetchUserSubmissions(tasks);
310
+ } else {
311
+ console.error('Failed to fetch tutorial tasks');
312
+ }
313
+ } catch (error) {
314
+ console.error('Error fetching tutorial tasks:', error);
315
+ } finally {
316
+ if (showLoading) {
317
+ setLoading(false);
318
+ }
319
+ }
320
+ }, [selectedWeek, fetchUserSubmissions, navigate]);
321
+
322
+ useEffect(() => {
323
+ const user = localStorage.getItem('user');
324
+ if (!user) {
325
+ navigate('/login');
326
+ return;
327
+ }
328
+ fetchTutorialTasks();
329
+ }, [fetchTutorialTasks, navigate]);
330
+
331
+ // Listen for week reset events from page navigation
332
+ useEffect(() => {
333
+ const handleWeekReset = (event: CustomEvent) => {
334
+ if (event.detail.page === 'tutorial-tasks') {
335
+ console.log('Week reset event received for tutorial tasks');
336
+ setSelectedWeek(event.detail.week);
337
+ localStorage.setItem('selectedTutorialWeek', event.detail.week.toString());
338
+ }
339
+ };
340
+
341
+ window.addEventListener('weekReset', handleWeekReset as EventListener);
342
+ return () => {
343
+ window.removeEventListener('weekReset', handleWeekReset as EventListener);
344
+ };
345
+ }, []);
346
+
347
+ // Refresh submissions when user changes (after login/logout)
348
+ useEffect(() => {
349
+ const user = localStorage.getItem('user');
350
+ if (user && tutorialTasks.length > 0) {
351
+ fetchUserSubmissions(tutorialTasks);
352
+ }
353
+ }, [tutorialTasks, fetchUserSubmissions]);
354
+
355
+ const handleSubmitTranslation = async (taskId: string) => {
356
+ if (!translationText[taskId]?.trim()) {
357
+ return;
358
+ }
359
+
360
+ if (!selectedGroups[taskId]) {
361
+ return;
362
+ }
363
+
364
+ try {
365
+ setSubmitting({ ...submitting, [taskId]: true });
366
+ const user = JSON.parse(localStorage.getItem('user') || '{}');
367
+ const response = await api.post('/api/submissions', {
368
+ sourceTextId: taskId,
369
+ transcreation: translationText[taskId],
370
+ groupNumber: selectedGroups[taskId],
371
+ culturalAdaptations: [],
372
+ username: user.name || 'Unknown'
373
+ });
374
+
375
+ if (response.status >= 200 && response.status < 300) {
376
+ const result = response.data;
377
+ console.log('Submission created successfully:', result);
378
+
379
+ setTranslationText({ ...translationText, [taskId]: '' });
380
+ setSelectedGroups({ ...selectedGroups, [taskId]: 0 });
381
+ await fetchUserSubmissions(tutorialTasks);
382
+ } else {
383
+ console.error('Failed to submit translation:', response.data);
384
+ }
385
+ } catch (error) {
386
+ console.error('Error submitting translation:', error);
387
+
388
+ } finally {
389
+ setSubmitting({ ...submitting, [taskId]: false });
390
+ }
391
+ };
392
+
393
+ const [editingSubmission, setEditingSubmission] = useState<{id: string, text: string} | null>(null);
394
+ const [editSubmissionText, setEditSubmissionText] = useState('');
395
+
396
+ const handleEditSubmission = async (submissionId: string, currentText: string) => {
397
+ setEditingSubmission({ id: submissionId, text: currentText });
398
+ setEditSubmissionText(currentText);
399
+ };
400
+
401
+ const saveEditedSubmission = async () => {
402
+ if (!editingSubmission || !editSubmissionText.trim()) return;
403
+
404
+ try {
405
+ const response = await api.put(`/api/submissions/${editingSubmission.id}`, {
406
+ transcreation: editSubmissionText
407
+ });
408
+
409
+ if (response.status >= 200 && response.status < 300) {
410
+
411
+ setEditingSubmission(null);
412
+ setEditSubmissionText('');
413
+ await fetchUserSubmissions(tutorialTasks);
414
+ } else {
415
+ console.error('Failed to update translation:', response.data);
416
+ }
417
+ } catch (error) {
418
+ console.error('Error updating translation:', error);
419
+
420
+ }
421
+ };
422
+
423
+ const cancelEditSubmission = () => {
424
+ setEditingSubmission(null);
425
+ setEditSubmissionText('');
426
+ };
427
+
428
+ const handleDeleteSubmission = async (submissionId: string) => {
429
+
430
+
431
+ try {
432
+ const response = await api.delete(`/api/submissions/${submissionId}`);
433
+
434
+ if (response.status === 200) {
435
+
436
+ await fetchUserSubmissions(tutorialTasks);
437
+ } else {
438
+
439
+ }
440
+ } catch (error) {
441
+ console.error('Error deleting submission:', error);
442
+
443
+ }
444
+ };
445
+
446
+ const getStatusIcon = (status: string) => {
447
+ switch (status) {
448
+ case 'approved':
449
+ return <CheckCircleIcon className="h-5 w-5 text-green-500" />;
450
+ case 'pending':
451
+ return <ClockIcon className="h-5 w-5 text-yellow-500" />;
452
+ default:
453
+ return <ClockIcon className="h-5 w-5 text-gray-500" />;
454
+ }
455
+ };
456
+
457
+ const startEditing = (task: TutorialTask) => {
458
+ setEditingTask(task._id);
459
+ setEditForm({
460
+ content: task.content,
461
+ translationBrief: task.translationBrief || '',
462
+ imageUrl: task.imageUrl || '',
463
+ imageAlt: task.imageAlt || ''
464
+ });
465
+ };
466
+
467
+ const startEditingBrief = () => {
468
+ setEditingBrief(prev => ({ ...prev, [selectedWeek]: true }));
469
+ setEditForm({
470
+ content: '',
471
+ translationBrief: tutorialWeek?.translationBrief || '',
472
+ imageUrl: '',
473
+ imageAlt: ''
474
+ });
475
+ };
476
+
477
+ const startAddingBrief = () => {
478
+ setEditingBrief(prev => ({ ...prev, [selectedWeek]: true }));
479
+ setEditForm({
480
+ content: '',
481
+ translationBrief: '',
482
+ imageUrl: '',
483
+ imageAlt: ''
484
+ });
485
+ };
486
+
487
+ const removeBrief = async () => {
488
+
489
+
490
+ try {
491
+ setSaving(true);
492
+ const token = localStorage.getItem('token');
493
+ const user = JSON.parse(localStorage.getItem('user') || '{}');
494
+
495
+ // Check if user is admin
496
+ if (user.role !== 'admin') {
497
+
498
+ return;
499
+ }
500
+
501
+ const response = await api.put(`/api/auth/admin/tutorial-brief/${selectedWeek}`, {
502
+ translationBrief: '',
503
+ weekNumber: selectedWeek
504
+ });
505
+
506
+ if (response.status >= 200 && response.status < 300) {
507
+ const briefKey = `translationBrief_week_${selectedWeek}`;
508
+ localStorage.removeItem(briefKey);
509
+ setEditForm((prev) => ({ ...prev, translationBrief: '' }));
510
+ await fetchTutorialTasks();
511
+
512
+ } else {
513
+ console.error('Failed to remove translation brief:', response.data);
514
+
515
+ }
516
+ } catch (error) {
517
+ console.error('Failed to remove translation brief:', error);
518
+
519
+ } finally {
520
+ setSaving(false);
521
+ }
522
+ };
523
+
524
+ const cancelEditing = () => {
525
+ setEditingTask(null);
526
+ setEditingBrief(prev => ({ ...prev, [selectedWeek]: false }));
527
+ setEditForm({
528
+ content: '',
529
+ translationBrief: '',
530
+ imageUrl: '',
531
+ imageAlt: ''
532
+ });
533
+ setSelectedFile(null);
534
+ };
535
+
536
+ const saveTask = async () => {
537
+ if (!editingTask) return;
538
+
539
+ try {
540
+ setSaving(true);
541
+ const token = localStorage.getItem('token');
542
+ const user = JSON.parse(localStorage.getItem('user') || '{}');
543
+
544
+ // Check if user is admin
545
+ if (user.role !== 'admin') {
546
+
547
+ return;
548
+ }
549
+
550
+ const updateData = {
551
+ ...editForm,
552
+ weekNumber: selectedWeek
553
+ };
554
+ console.log('Saving task with data:', updateData);
555
+
556
+ const response = await api.put(`/api/auth/admin/tutorial-tasks/${editingTask}`, updateData);
557
+
558
+ if (response.status >= 200 && response.status < 300) {
559
+ await fetchTutorialTasks(false);
560
+ setEditingTask(null);
561
+
562
+ } else {
563
+ console.error('Failed to update tutorial task:', response.data);
564
+
565
+ }
566
+ } catch (error) {
567
+ console.error('Failed to update tutorial task:', error);
568
+
569
+ } finally {
570
+ setSaving(false);
571
+ }
572
+ };
573
+
574
+ const saveBrief = async () => {
575
+ try {
576
+ setSaving(true);
577
+ const user = JSON.parse(localStorage.getItem('user') || '{}');
578
+
579
+ // Check if user is admin
580
+ if (user.role !== 'admin') {
581
+ return;
582
+ }
583
+
584
+ console.log('Saving brief for week:', selectedWeek);
585
+ console.log('Brief content:', editForm.translationBrief);
586
+
587
+ // Save brief by creating or updating the first task of the week
588
+ if (tutorialTasks.length > 0) {
589
+ const firstTask = tutorialTasks[0];
590
+ console.log('Updating first task with brief:', firstTask._id);
591
+
592
+ const response = await api.put(`/api/auth/admin/tutorial-tasks/${firstTask._id}`, {
593
+ ...firstTask,
594
+ translationBrief: editForm.translationBrief,
595
+ weekNumber: selectedWeek
596
+ });
597
+
598
+ if (response.status >= 200 && response.status < 300) {
599
+ console.log('Brief saved successfully');
600
+ // Optimistic UI update
601
+ const briefKey = `translationBrief_week_${selectedWeek}`;
602
+ localStorage.setItem(briefKey, editForm.translationBrief);
603
+ setTutorialWeek(prev => prev ? { ...prev, translationBrief: editForm.translationBrief } : prev);
604
+ setEditingBrief(prev => ({ ...prev, [selectedWeek]: false }));
605
+ // Background refresh
606
+ fetchTutorialTasks(false);
607
+ } else {
608
+ console.error('Failed to save brief:', response.data);
609
+ }
610
+ } else {
611
+ // If no tasks exist, save the brief to localStorage
612
+ console.log('No tasks available to save brief to - saving to localStorage');
613
+ const briefKey = `translationBrief_week_${selectedWeek}`;
614
+ localStorage.setItem(briefKey, editForm.translationBrief);
615
+ setEditingBrief(prev => ({ ...prev, [selectedWeek]: false }));
616
+ }
617
+ } catch (error) {
618
+ console.error('Failed to update translation brief:', error);
619
+ } finally {
620
+ setSaving(false);
621
+ }
622
+ };
623
+
624
+ const startAddingTask = () => {
625
+ setAddingTask(true);
626
+ setEditForm({
627
+ content: '',
628
+ translationBrief: '',
629
+ imageUrl: '',
630
+ imageAlt: ''
631
+ });
632
+ };
633
+
634
+ const cancelAddingTask = () => {
635
+ setAddingTask(false);
636
+ setEditForm({
637
+ content: '',
638
+ translationBrief: '',
639
+ imageUrl: '',
640
+ imageAlt: ''
641
+ });
642
+ setSelectedFile(null);
643
+ };
644
+
645
+ const startAddingImage = () => {
646
+ setAddingImage(true);
647
+ setImageForm({
648
+ imageUrl: '',
649
+ imageAlt: '',
650
+ imageSize: 200,
651
+ imageAlignment: 'center'
652
+ });
653
+ };
654
+
655
+ const cancelAddingImage = () => {
656
+ setAddingImage(false);
657
+ setImageForm({
658
+ imageUrl: '',
659
+ imageAlt: '',
660
+ imageSize: 200,
661
+ imageAlignment: 'center'
662
+ });
663
+ };
664
+
665
+
666
+
667
+ const saveNewTask = async () => {
668
+ try {
669
+ setSaving(true);
670
+ const user = JSON.parse(localStorage.getItem('user') || '{}');
671
+
672
+ // Check if user is admin
673
+ if (user.role !== 'admin') {
674
+ return;
675
+ }
676
+
677
+ // Allow either content or imageUrl, but not both empty
678
+ if (!editForm.content.trim() && !editForm.imageUrl.trim()) {
679
+ return;
680
+ }
681
+
682
+ console.log('Saving new task for week:', selectedWeek);
683
+ console.log('Task content:', editForm.content);
684
+ console.log('Image URL:', editForm.imageUrl);
685
+ console.log('Image Alt:', editForm.imageAlt);
686
+
687
+ const taskData = {
688
+ title: `Week ${selectedWeek} Tutorial Task`,
689
+ content: editForm.content,
690
+ sourceLanguage: 'English',
691
+ weekNumber: selectedWeek,
692
+ category: 'tutorial',
693
+ imageUrl: editForm.imageUrl || null,
694
+ imageAlt: editForm.imageAlt || null,
695
+ // Add imageSize and imageAlignment for image-only content
696
+ ...(editForm.imageUrl && !editForm.content.trim() && { imageSize: 200 }),
697
+ ...(editForm.imageUrl && !editForm.content.trim() && { imageAlignment: 'center' })
698
+ };
699
+
700
+ console.log('Task data being sent:', JSON.stringify(taskData, null, 2));
701
+
702
+ console.log('Sending task data:', taskData);
703
+
704
+ const response = await api.post('/api/auth/admin/tutorial-tasks', taskData);
705
+
706
+ console.log('Task save response:', response.data);
707
+
708
+ if (response.status >= 200 && response.status < 300) {
709
+ console.log('Task saved successfully');
710
+ console.log('Saved task response:', response.data);
711
+ console.log('Saved task response keys:', Object.keys(response.data || {}));
712
+ console.log('Saved task response.task:', response.data?.task);
713
+ console.log('Saved task response.task.imageUrl:', response.data?.task?.imageUrl);
714
+ console.log('Saved task response.task.translationBrief:', response.data?.task?.translationBrief);
715
+ await fetchTutorialTasks(false);
716
+ setAddingTask(false);
717
+
718
+ } else {
719
+ console.error('Failed to add tutorial task:', response.data);
720
+ }
721
+ } catch (error) {
722
+ console.error('Failed to add tutorial task:', error);
723
+ } finally {
724
+ setSaving(false);
725
+ }
726
+ };
727
+
728
+ const saveNewImage = async () => {
729
+ try {
730
+ setSaving(true);
731
+ const user = JSON.parse(localStorage.getItem('user') || '{}');
732
+
733
+ // Check if user is admin
734
+ if (user.role !== 'admin') {
735
+ return;
736
+ }
737
+
738
+ if (!imageForm.imageUrl.trim()) {
739
+ return;
740
+ }
741
+
742
+ const payload = {
743
+ title: `Week ${selectedWeek} Image Task`,
744
+ content: '', // Empty content for image-only task
745
+ sourceLanguage: 'English',
746
+ weekNumber: selectedWeek,
747
+ category: 'tutorial',
748
+ imageUrl: imageForm.imageUrl.trim(),
749
+ imageAlt: imageForm.imageAlt.trim() || null,
750
+ imageSize: imageForm.imageSize,
751
+ imageAlignment: imageForm.imageAlignment
752
+ };
753
+
754
+ console.log('Saving new image task with payload:', payload);
755
+
756
+ const response = await api.post('/api/auth/admin/tutorial-tasks', payload);
757
+
758
+ if (response.data) {
759
+ console.log('Image task saved successfully:', response.data);
760
+ await fetchTutorialTasks(false);
761
+ setAddingImage(false);
762
+ } else {
763
+ console.error('Failed to save image task');
764
+ }
765
+ } catch (error) {
766
+ console.error('Failed to add image task:', error);
767
+ } finally {
768
+ setSaving(false);
769
+ }
770
+ };
771
+
772
+ const deleteTask = async (taskId: string) => {
773
+
774
+
775
+ try {
776
+ const token = localStorage.getItem('token');
777
+ const user = JSON.parse(localStorage.getItem('user') || '{}');
778
+
779
+ // Check if user is admin
780
+ if (user.role !== 'admin') {
781
+
782
+ return;
783
+ }
784
+
785
+ const response = await api.delete(`/api/auth/admin/tutorial-tasks/${taskId}`);
786
+
787
+ if (response.status >= 200 && response.status < 300) {
788
+ await fetchTutorialTasks(false);
789
+
790
+ } else {
791
+ console.error('Failed to delete tutorial task:', response.data);
792
+
793
+ }
794
+ } catch (error) {
795
+ console.error('Failed to delete tutorial task:', error);
796
+
797
+ }
798
+ };
799
+
800
+ // Remove intrusive loading screen - just show content with loading state
801
+
802
+ return (
803
+ <div className="min-h-screen bg-gray-50 py-8">
804
+ <div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
805
+ {/* Header */}
806
+ <div className="mb-8">
807
+ <div className="flex items-center mb-4">
808
+ <AcademicCapIcon className="h-8 w-8 text-indigo-900 mr-3" />
809
+ <h1 className="text-3xl font-bold text-gray-900">Tutorial Tasks</h1>
810
+ </div>
811
+ <p className="text-gray-600">
812
+ Complete weekly tutorial tasks with your group to practice collaborative translation skills.
813
+ </p>
814
+ </div>
815
+
816
+ {/* Week Selector */}
817
+ <div className="mb-6">
818
+ <div className="flex space-x-2 overflow-x-auto pb-2">
819
+ {weeks.map((week) => (
820
+ <button
821
+ key={week}
822
+ onClick={() => handleWeekChange(week)}
823
+ className={`px-4 py-2 rounded-lg font-medium whitespace-nowrap transition-all duration-200 ease-in-out ${
824
+ selectedWeek === week
825
+ ? 'bg-indigo-600 text-white'
826
+ : 'bg-white text-gray-700 hover:bg-gray-50 border border-gray-300'
827
+ }`}
828
+ >
829
+ Week {week}
830
+ </button>
831
+ ))}
832
+ </div>
833
+ </div>
834
+
835
+ {/* Week Transition Loading Spinner */}
836
+ {isWeekTransitioning && (
837
+ <div className="fixed top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2 z-50">
838
+ <div className="bg-white rounded-lg shadow-lg p-4 flex items-center space-x-3">
839
+ <div className="animate-spin rounded-full h-6 w-6 border-b-2 border-indigo-600"></div>
840
+ <span className="text-gray-700 font-medium">Loading...</span>
841
+ </div>
842
+ </div>
843
+ )}
844
+
845
+ {!isWeekTransitioning && (
846
+ <>
847
+ {/* Translation Brief - Shown once at the top */}
848
+ {tutorialWeek && tutorialWeek.translationBrief ? (
849
+ <div className="bg-white rounded-lg p-6 mb-8 border border-gray-200 shadow-sm">
850
+ <div className="flex items-center justify-between mb-4">
851
+ <div className="flex items-center space-x-3">
852
+ <div className="bg-indigo-600 rounded-lg p-2">
853
+ <DocumentTextIcon className="h-5 w-5 text-white" />
854
+ </div>
855
+ <h3 className="text-indigo-900 font-semibold text-xl">Translation Brief</h3>
856
+ </div>
857
+ {JSON.parse(localStorage.getItem('user') || '{}').role === 'admin' && (
858
+ <div className="flex items-center space-x-2">
859
+ {editingBrief[selectedWeek] ? (
860
+ <>
861
+ <button
862
+ onClick={saveBrief}
863
+ disabled={saving}
864
+ className="bg-indigo-500 hover:bg-indigo-600 text-white px-3 py-1 rounded-md transition-colors duration-200 text-sm"
865
+ >
866
+ {saving ? (
867
+ <div className="animate-spin rounded-full h-4 w-4 border-b-2 border-white"></div>
868
+ ) : (
869
+ <CheckIcon className="h-4 w-4" />
870
+ )}
871
+ </button>
872
+ <button
873
+ onClick={cancelEditing}
874
+ className="bg-gray-500 hover:bg-gray-600 text-white px-3 py-1 rounded-md transition-colors duration-200 text-sm"
875
+ >
876
+ <XMarkIcon className="h-4 w-4" />
877
+ </button>
878
+ </>
879
+ ) : (
880
+ <>
881
+ <button
882
+ onClick={startEditingBrief}
883
+ className="bg-gray-100 hover:bg-gray-200 text-gray-700 px-3 py-1 rounded-md transition-colors duration-200 text-sm"
884
+ >
885
+ <PencilIcon className="h-4 w-4" />
886
+ </button>
887
+ <button
888
+ onClick={() => removeBrief()}
889
+ className="bg-red-100 hover:bg-red-200 text-red-700 px-3 py-1 rounded-md transition-colors duration-200 text-sm"
890
+ >
891
+ <TrashIcon className="h-4 w-4" />
892
+ </button>
893
+ </>
894
+ )}
895
+ </div>
896
+ )}
897
+ </div>
898
+ {editingBrief[selectedWeek] ? (
899
+ <textarea
900
+ value={editForm.translationBrief}
901
+ onChange={(e) => setEditForm({ ...editForm, translationBrief: e.target.value })}
902
+ className="w-full p-4 border border-gray-300 rounded-lg text-gray-900 leading-relaxed text-base bg-white focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500"
903
+ rows={6}
904
+ placeholder="Enter translation brief..."
905
+ />
906
+ ) : (
907
+ <p className="text-gray-900 leading-relaxed text-lg font-smiley">{tutorialWeek.translationBrief}</p>
908
+ )}
909
+ <div className="mt-4 p-3 bg-indigo-50 rounded-lg">
910
+ <p className="text-indigo-900 text-sm">
911
+ <strong>Note:</strong> Tutorial tasks are completed as group submissions. Each group should collaborate to create a single translation per task.
912
+ </p>
913
+ </div>
914
+ </div>
915
+ ) : (
916
+ // Show add brief button when no brief exists
917
+ JSON.parse(localStorage.getItem('user') || '{}').role === 'admin' && (
918
+ <div className="bg-white rounded-lg p-6 mb-8 border border-gray-200 border-dashed shadow-sm">
919
+ <div className="flex items-center justify-between mb-4">
920
+ <div className="flex items-center space-x-3">
921
+ <div className="bg-indigo-100 rounded-lg p-2">
922
+ <DocumentTextIcon className="h-5 w-5 text-indigo-900" />
923
+ </div>
924
+ <h3 className="text-indigo-900 font-semibold text-xl">Translation Brief</h3>
925
+ </div>
926
+ <button
927
+ onClick={startAddingBrief}
928
+ className="bg-indigo-600 hover:bg-indigo-700 text-white px-6 py-3 rounded-lg font-medium flex items-center space-x-2 shadow-sm"
929
+ >
930
+ <PlusIcon className="h-5 w-5" />
931
+ <span className="font-medium">Add Brief</span>
932
+ </button>
933
+ </div>
934
+ {editingBrief[selectedWeek] && (
935
+ <div className="space-y-4">
936
+ <textarea
937
+ value={editForm.translationBrief}
938
+ onChange={(e) => setEditForm({ ...editForm, translationBrief: e.target.value })}
939
+ className="w-full p-4 border border-gray-300 rounded-lg text-gray-900 leading-relaxed text-base bg-white focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500"
940
+ rows={6}
941
+ placeholder="Enter translation brief..."
942
+ />
943
+ <div className="flex justify-end space-x-2">
944
+ <button
945
+ onClick={saveBrief}
946
+ disabled={saving}
947
+ className="bg-indigo-600 hover:bg-indigo-700 text-white px-6 py-3 rounded-lg font-medium flex items-center space-x-2 shadow-sm"
948
+ >
949
+ {saving ? (
950
+ <div className="animate-spin rounded-full h-4 w-4 border-b-2 border-white"></div>
951
+ ) : (
952
+ <>
953
+ <CheckIcon className="h-5 w-5" />
954
+ <span className="font-medium">Save Brief</span>
955
+ </>
956
+ )}
957
+ </button>
958
+ <button
959
+ onClick={cancelEditing}
960
+ className="bg-gray-500 hover:bg-gray-600 text-white px-6 py-3 rounded-lg font-medium flex items-center space-x-2 shadow-sm"
961
+ >
962
+ <XMarkIcon className="h-5 w-5" />
963
+ <span className="font-medium">Cancel</span>
964
+ </button>
965
+ </div>
966
+ </div>
967
+ )}
968
+ </div>
969
+ )
970
+ )}
971
+
972
+ {/* Tutorial Tasks */}
973
+ <div className="space-y-6">
974
+ {/* Add New Tutorial Task Section */}
975
+ {JSON.parse(localStorage.getItem('user') || '{}').role === 'admin' && (
976
+ <div className="mb-8">
977
+ {addingTask ? (
978
+ <div className="bg-white rounded-lg p-6 border border-gray-200 shadow-sm">
979
+ <div className="flex items-center space-x-3 mb-4">
980
+ <div className="bg-gray-100 rounded-lg p-2">
981
+ <PlusIcon className="h-4 w-4 text-gray-600" />
982
+ </div>
983
+ <h4 className="text-gray-900 font-semibold text-lg">New Tutorial Task</h4>
984
+ </div>
985
+ <div className="space-y-4">
986
+ <div>
987
+ <label className="block text-sm font-medium text-gray-700 mb-2">
988
+ Task Content *
989
+ </label>
990
+ <textarea
991
+ value={editForm.content}
992
+ onChange={(e) => setEditForm({ ...editForm, content: e.target.value })}
993
+ className="w-full px-4 py-3 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500 bg-white"
994
+ rows={4}
995
+ placeholder="Enter tutorial task content..."
996
+ />
997
+ </div>
998
+ <div className="space-y-4">
999
+ <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
1000
+ <div>
1001
+ <label className="block text-sm font-medium text-gray-700 mb-2">
1002
+ Image URL (Optional)
1003
+ </label>
1004
+ <input
1005
+ type="url"
1006
+ value={editForm.imageUrl}
1007
+ onChange={(e) => setEditForm({ ...editForm, imageUrl: e.target.value })}
1008
+ 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"
1009
+ placeholder="https://example.com/image.jpg"
1010
+ />
1011
+ </div>
1012
+ <div>
1013
+ <label className="block text-sm font-medium text-gray-700 mb-2">
1014
+ Image Alt Text (Optional)
1015
+ </label>
1016
+ <input
1017
+ type="text"
1018
+ value={editForm.imageAlt}
1019
+ onChange={(e) => setEditForm({ ...editForm, imageAlt: e.target.value })}
1020
+ 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"
1021
+ placeholder="Description of the image"
1022
+ />
1023
+ </div>
1024
+ </div>
1025
+
1026
+ {/* File Upload Section - Only for Week 2+ */}
1027
+ {selectedWeek >= 2 && (
1028
+ <div className="border-t pt-4">
1029
+ <label className="block text-sm font-medium text-gray-700 mb-2">
1030
+ Upload Local Image (Optional)
1031
+ </label>
1032
+ <div className="space-y-2">
1033
+ <input
1034
+ type="file"
1035
+ accept="image/*"
1036
+ onChange={handleFileChange}
1037
+ 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"
1038
+ />
1039
+ {selectedFile && (
1040
+ <div className="flex items-center space-x-2">
1041
+ <span className="text-sm text-gray-600">{selectedFile.name}</span>
1042
+ <button
1043
+ type="button"
1044
+ onClick={async () => {
1045
+ try {
1046
+ const imageUrl = await handleFileUpload(selectedFile);
1047
+ setEditForm({ ...editForm, imageUrl });
1048
+ setSelectedFile(null);
1049
+ } catch (error) {
1050
+ console.error('Upload error:', error);
1051
+ }
1052
+ }}
1053
+ disabled={uploading}
1054
+ className="bg-green-500 hover:bg-green-600 disabled:bg-gray-400 text-white px-3 py-1 rounded text-sm"
1055
+ >
1056
+ {uploading ? 'Uploading...' : 'Upload'}
1057
+ </button>
1058
+ </div>
1059
+ )}
1060
+ </div>
1061
+ </div>
1062
+ )}
1063
+ </div>
1064
+ </div>
1065
+ <div className="flex justify-end space-x-2 mt-4">
1066
+ <button
1067
+ onClick={saveNewTask}
1068
+ disabled={saving}
1069
+ className="bg-indigo-500 hover:bg-indigo-600 text-white px-4 py-2 rounded-lg transition-colors duration-200 flex items-center space-x-2 text-sm"
1070
+ >
1071
+ {saving ? (
1072
+ <div className="animate-spin rounded-full h-4 w-4 border-b-2 border-white"></div>
1073
+ ) : (
1074
+ <>
1075
+ <CheckIcon className="h-4 w-4" />
1076
+ <span>Save Task</span>
1077
+ </>
1078
+ )}
1079
+ </button>
1080
+ <button
1081
+ onClick={cancelAddingTask}
1082
+ className="bg-gray-500 hover:bg-gray-600 text-white px-4 py-2 rounded-lg transition-colors duration-200 flex items-center space-x-2 text-sm"
1083
+ >
1084
+ <XMarkIcon className="h-4 w-4" />
1085
+ <span>Cancel</span>
1086
+ </button>
1087
+ </div>
1088
+ </div>
1089
+ ) : addingImage ? (
1090
+ <div className="bg-white rounded-lg p-6 border border-gray-200 shadow-sm">
1091
+ <div className="flex items-center space-x-3 mb-4">
1092
+ <div className="bg-blue-100 rounded-lg p-2">
1093
+ <PlusIcon className="h-4 w-4 text-blue-600" />
1094
+ </div>
1095
+ <h4 className="text-blue-900 font-semibold text-lg">New Image Task</h4>
1096
+ </div>
1097
+ <div className="space-y-4">
1098
+ <div>
1099
+ <label className="block text-sm font-medium text-gray-700 mb-2">
1100
+ Image URL *
1101
+ </label>
1102
+ <input
1103
+ type="url"
1104
+ value={imageForm.imageUrl}
1105
+ onChange={(e) => setImageForm({ ...imageForm, imageUrl: e.target.value })}
1106
+ className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
1107
+ placeholder="https://example.com/image.jpg"
1108
+ />
1109
+ </div>
1110
+ <div>
1111
+ <label className="block text-sm font-medium text-gray-700 mb-2">
1112
+ Image Alt Text (Optional)
1113
+ </label>
1114
+ <input
1115
+ type="text"
1116
+ value={imageForm.imageAlt}
1117
+ onChange={(e) => setImageForm({ ...imageForm, imageAlt: e.target.value })}
1118
+ className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
1119
+ placeholder="Description of the image"
1120
+ />
1121
+ </div>
1122
+
1123
+ {/* File Upload Section */}
1124
+ <div className="border-t pt-4">
1125
+ <label className="block text-sm font-medium text-gray-700 mb-2">
1126
+ Upload Local Image (Optional)
1127
+ </label>
1128
+ <div className="space-y-2">
1129
+ <input
1130
+ type="file"
1131
+ accept="image/*"
1132
+ onChange={handleFileChange}
1133
+ className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
1134
+ />
1135
+ {selectedFile && (
1136
+ <div className="flex items-center space-x-2">
1137
+ <span className="text-sm text-gray-600">{selectedFile.name}</span>
1138
+ <button
1139
+ type="button"
1140
+ onClick={async () => {
1141
+ try {
1142
+ const imageUrl = await handleFileUpload(selectedFile);
1143
+ setImageForm({ ...imageForm, imageUrl });
1144
+ setSelectedFile(null);
1145
+ } catch (error) {
1146
+ console.error('Upload error:', error);
1147
+ }
1148
+ }}
1149
+ disabled={uploading}
1150
+ className="bg-green-500 hover:bg-green-600 disabled:bg-gray-400 text-white px-3 py-1 rounded text-sm"
1151
+ >
1152
+ {uploading ? 'Uploading...' : 'Upload'}
1153
+ </button>
1154
+ </div>
1155
+ )}
1156
+ </div>
1157
+ </div>
1158
+
1159
+ <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
1160
+ <div>
1161
+ <label className="block text-sm font-medium text-gray-700 mb-2">
1162
+ Image Size
1163
+ </label>
1164
+ <select
1165
+ value={imageForm.imageSize}
1166
+ onChange={(e) => setImageForm({ ...imageForm, imageSize: parseInt(e.target.value) })}
1167
+ className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
1168
+ >
1169
+ <option value={150}>150px</option>
1170
+ <option value={200}>200px</option>
1171
+ <option value={300}>300px</option>
1172
+ </select>
1173
+ </div>
1174
+ <div>
1175
+ <label className="block text-sm font-medium text-gray-700 mb-2">
1176
+ Image Alignment
1177
+ </label>
1178
+ <select
1179
+ value={imageForm.imageAlignment}
1180
+ onChange={(e) => setImageForm({ ...imageForm, imageAlignment: e.target.value as 'left' | 'center' | 'right' })}
1181
+ className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
1182
+ >
1183
+ <option value="left">Left</option>
1184
+ <option value="center">Center</option>
1185
+ <option value="right">Right</option>
1186
+ </select>
1187
+ </div>
1188
+ </div>
1189
+ </div>
1190
+ <div className="flex justify-end space-x-2 mt-4">
1191
+ <button
1192
+ onClick={saveNewImage}
1193
+ disabled={saving}
1194
+ className="bg-blue-500 hover:bg-blue-600 text-white px-4 py-2 rounded-lg transition-colors duration-200 flex items-center space-x-2 text-sm"
1195
+ >
1196
+ {saving ? (
1197
+ <div className="animate-spin rounded-full h-4 w-4 border-b-2 border-white"></div>
1198
+ ) : (
1199
+ <>
1200
+ <CheckIcon className="h-4 w-4" />
1201
+ <span>Save Image</span>
1202
+ </>
1203
+ )}
1204
+ </button>
1205
+ <button
1206
+ onClick={cancelAddingImage}
1207
+ className="bg-gray-500 hover:bg-gray-600 text-white px-4 py-2 rounded-lg transition-colors duration-200 flex items-center space-x-2 text-sm"
1208
+ >
1209
+ <XMarkIcon className="h-4 w-4" />
1210
+ <span>Cancel</span>
1211
+ </button>
1212
+ </div>
1213
+ </div>
1214
+ ) : (
1215
+ <div className="bg-white rounded-lg p-6 border border-gray-200 border-dashed shadow-sm">
1216
+ <div className="flex items-center justify-between">
1217
+ <div className="flex items-center space-x-3">
1218
+ <div className="bg-gray-100 rounded-lg p-2">
1219
+ <PlusIcon className="h-5 w-5 text-gray-600" />
1220
+ </div>
1221
+ <div>
1222
+ <h3 className="text-lg font-semibold text-indigo-900">Add New Tutorial Task</h3>
1223
+ <p className="text-gray-600 text-sm">Create a new tutorial task for Week {selectedWeek}</p>
1224
+ </div>
1225
+ </div>
1226
+ <div className="flex space-x-3">
1227
+ <div className="flex space-x-3">
1228
+ <button
1229
+ onClick={startAddingTask}
1230
+ className="bg-indigo-600 hover:bg-indigo-700 text-white px-6 py-3 rounded-lg font-medium flex items-center space-x-2 shadow-sm"
1231
+ >
1232
+ <PlusIcon className="h-5 w-5" />
1233
+ <span className="font-medium">Add Task</span>
1234
+ </button>
1235
+ {selectedWeek >= 3 && (
1236
+ <button
1237
+ onClick={startAddingImage}
1238
+ className="bg-blue-600 hover:bg-blue-700 text-white px-6 py-3 rounded-lg font-medium flex items-center space-x-2 shadow-sm"
1239
+ >
1240
+ <PlusIcon className="h-5 w-5" />
1241
+ <span className="font-medium">Add Image</span>
1242
+ </button>
1243
+ )}
1244
+ </div>
1245
+
1246
+ </div>
1247
+ </div>
1248
+ </div>
1249
+ )}
1250
+ </div>
1251
+ )}
1252
+
1253
+ {tutorialTasks.length === 0 && !addingTask ? (
1254
+ <div className="text-center py-12">
1255
+ <DocumentTextIcon className="h-12 w-12 text-gray-400 mx-auto mb-4" />
1256
+ <h3 className="text-lg font-medium text-gray-900 mb-2">
1257
+ No tutorial tasks available
1258
+ </h3>
1259
+ <p className="text-gray-600">
1260
+ Tutorial tasks for Week {selectedWeek} haven't been set up yet.
1261
+ </p>
1262
+ </div>
1263
+ ) : (
1264
+ tutorialTasks.map((task) => (
1265
+ <div key={task._id} className="bg-white rounded-xl shadow-lg border border-gray-100 p-8 hover:shadow-xl transition-shadow duration-300">
1266
+ <div className="mb-6">
1267
+ <div className="flex items-center justify-between mb-4">
1268
+ <div className="flex items-center space-x-3">
1269
+ <div className="bg-indigo-100 rounded-full p-2">
1270
+ <DocumentTextIcon className="h-5 w-5 text-indigo-900" />
1271
+ </div>
1272
+ <div>
1273
+ <h3 className="text-lg font-semibold text-gray-900">Source Text #{tutorialTasks.indexOf(task) + 1}</h3>
1274
+ </div>
1275
+ </div>
1276
+ {JSON.parse(localStorage.getItem('user') || '{}').role === 'admin' && (
1277
+ <div className="flex items-center space-x-2">
1278
+ {editingTask === task._id ? (
1279
+ <>
1280
+ <button
1281
+ onClick={saveTask}
1282
+ disabled={saving}
1283
+ className="bg-green-100 hover:bg-green-200 text-green-700 px-3 py-1 rounded-lg transition-colors duration-200"
1284
+ >
1285
+ {saving ? (
1286
+ <div className="animate-spin rounded-full h-4 w-4 border-b-2 border-green-600"></div>
1287
+ ) : (
1288
+ <CheckIcon className="h-4 w-4" />
1289
+ )}
1290
+ </button>
1291
+ <button
1292
+ onClick={cancelEditing}
1293
+ className="bg-red-100 hover:bg-red-200 text-red-700 px-3 py-1 rounded-lg transition-colors duration-200"
1294
+ >
1295
+ <XMarkIcon className="h-4 w-4" />
1296
+ </button>
1297
+ </>
1298
+ ) : (
1299
+ <>
1300
+ <button
1301
+ onClick={() => startEditing(task)}
1302
+ className="bg-gray-100 hover:bg-gray-200 text-gray-700 px-3 py-1 rounded-md transition-colors duration-200 text-sm"
1303
+ >
1304
+ <PencilIcon className="h-4 w-4" />
1305
+ </button>
1306
+ <button
1307
+ onClick={() => deleteTask(task._id)}
1308
+ className="bg-red-100 hover:bg-red-200 text-red-700 px-3 py-1 rounded-lg transition-colors duration-200"
1309
+ >
1310
+ <TrashIcon className="h-4 w-4" />
1311
+ </button>
1312
+ </>
1313
+ )}
1314
+ </div>
1315
+ )}
1316
+ </div>
1317
+
1318
+ {/* Content - Clean styling with image support */}
1319
+ <div className="bg-gradient-to-r from-indigo-50 to-blue-50 rounded-xl p-6 mb-6 border border-indigo-200">
1320
+ {editingTask === task._id ? (
1321
+ <div className="space-y-4">
1322
+ <textarea
1323
+ value={editForm.content}
1324
+ onChange={(e) => setEditForm({...editForm, content: e.target.value})}
1325
+ className="w-full px-4 py-3 border border-indigo-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500 bg-white"
1326
+ rows={5}
1327
+ placeholder="Enter source text..."
1328
+ />
1329
+ <div className="space-y-4">
1330
+ <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
1331
+ <div>
1332
+ <label className="block text-sm font-medium text-gray-700 mb-2">Image URL</label>
1333
+ <input
1334
+ type="url"
1335
+ value={editForm.imageUrl}
1336
+ onChange={(e) => setEditForm({...editForm, imageUrl: e.target.value})}
1337
+ 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"
1338
+ placeholder="https://example.com/image.jpg"
1339
+ />
1340
+ </div>
1341
+ <div>
1342
+ <label className="block text-sm font-medium text-gray-700 mb-2">Image Alt Text</label>
1343
+ <input
1344
+ type="text"
1345
+ value={editForm.imageAlt}
1346
+ onChange={(e) => setEditForm({...editForm, imageAlt: e.target.value})}
1347
+ 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"
1348
+ placeholder="Description of the image"
1349
+ />
1350
+ </div>
1351
+ </div>
1352
+
1353
+ {/* File Upload Section - Only for Week 2+ */}
1354
+ {selectedWeek >= 2 && (
1355
+ <div className="border-t pt-4">
1356
+ <label className="block text-sm font-medium text-gray-700 mb-2">
1357
+ Upload Local Image (Optional)
1358
+ </label>
1359
+ <div className="space-y-2">
1360
+ <input
1361
+ type="file"
1362
+ accept="image/*"
1363
+ onChange={handleFileChange}
1364
+ 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"
1365
+ />
1366
+ {selectedFile && (
1367
+ <div className="flex items-center space-x-2">
1368
+ <span className="text-sm text-gray-600">{selectedFile.name}</span>
1369
+ <button
1370
+ type="button"
1371
+ onClick={async () => {
1372
+ try {
1373
+ const imageUrl = await handleFileUpload(selectedFile);
1374
+ console.log('Uploaded image URL:', imageUrl);
1375
+ setEditForm({ ...editForm, imageUrl });
1376
+ console.log('Updated editForm:', { ...editForm, imageUrl });
1377
+
1378
+ // Save the task with the new image URL
1379
+ if (editingTask) {
1380
+ console.log('Saving task with image URL:', imageUrl);
1381
+ const response = await api.put(`/api/auth/admin/tutorial-tasks/${editingTask}`, {
1382
+ ...editForm,
1383
+ imageUrl,
1384
+ weekNumber: selectedWeek
1385
+ });
1386
+ console.log('Task save response:', response.data);
1387
+
1388
+ if (response.status >= 200 && response.status < 300) {
1389
+ await fetchTutorialTasks(false); // Refresh tasks
1390
+ }
1391
+ }
1392
+
1393
+ setSelectedFile(null);
1394
+ } catch (error) {
1395
+ console.error('Upload error:', error);
1396
+ }
1397
+ }}
1398
+ disabled={uploading}
1399
+ className="bg-green-500 hover:bg-green-600 disabled:bg-gray-400 text-white px-3 py-1 rounded text-sm"
1400
+ >
1401
+ {uploading ? 'Uploading...' : 'Upload'}
1402
+ </button>
1403
+ </div>
1404
+ )}
1405
+ </div>
1406
+ </div>
1407
+ )}
1408
+ </div>
1409
+ </div>
1410
+ ) : (
1411
+ <div className="space-y-4">
1412
+ {task.imageUrl ? (
1413
+ // Check if this is an image-only task (created via "Add Image" function)
1414
+ task.content === 'Image-based task' ? (
1415
+ // Image-only layout with dynamic sizing and alignment
1416
+ <div className={`flex flex-col md:flex-row gap-6 items-start ${
1417
+ task.imageAlignment === 'left' ? 'md:flex-row' :
1418
+ task.imageAlignment === 'right' ? 'md:flex-row-reverse' :
1419
+ 'md:flex-col'
1420
+ }`}>
1421
+ {/* Image section */}
1422
+ <div className={`${
1423
+ task.imageAlignment === 'left' ? 'w-full md:w-1/2' :
1424
+ task.imageAlignment === 'right' ? 'w-full md:w-1/2' :
1425
+ 'w-full'
1426
+ } flex ${
1427
+ task.imageAlignment === 'left' ? 'justify-start' :
1428
+ task.imageAlignment === 'right' ? 'justify-end' :
1429
+ 'justify-center'
1430
+ }`}>
1431
+ <div className="inline-block rounded-lg shadow-md overflow-hidden">
1432
+ <img
1433
+ src={task.imageUrl}
1434
+ alt={task.imageAlt || 'Uploaded image'}
1435
+ className="w-full h-auto"
1436
+ style={{
1437
+ height: `${task.imageSize || 200}px`,
1438
+ width: 'auto',
1439
+ objectFit: 'contain'
1440
+ }}
1441
+ onError={(e) => {
1442
+ console.error('Image failed to load:', e);
1443
+ e.currentTarget.style.display = 'none';
1444
+ }}
1445
+ onLoad={() => {
1446
+ console.log('🔧 Debug - Task image loaded:', {
1447
+ imageUrl: task.imageUrl,
1448
+ imageSize: task.imageSize,
1449
+ content: task.content,
1450
+ weekNumber: task.weekNumber
1451
+ });
1452
+ }}
1453
+ />
1454
+ {task.imageAlt && (
1455
+ <div className="text-xs text-gray-500 mt-2 text-center">Alt: {task.imageAlt}</div>
1456
+ )}
1457
+ </div>
1458
+ </div>
1459
+ </div>
1460
+ ) : (
1461
+ // Regular task layout (original side-by-side for ALL weeks)
1462
+ <div className="flex flex-col md:flex-row gap-6 items-start">
1463
+ {/* Image on the left - 50% width */}
1464
+ <div className="w-full md:w-1/2 flex justify-center">
1465
+ {task.imageUrl.startsWith('data:') ? (
1466
+ // Show actual image if it's a data URL
1467
+ <div className="inline-block rounded-lg shadow-md overflow-hidden">
1468
+ <img
1469
+ src={task.imageUrl}
1470
+ alt={task.imageAlt || 'Uploaded image'}
1471
+ className="w-full h-auto"
1472
+ style={{ height: '200px', width: 'auto', objectFit: 'contain' }} // Fixed height for consistency
1473
+ onError={(e) => {
1474
+ console.error('Image failed to load:', e);
1475
+ e.currentTarget.style.display = 'none';
1476
+ }}
1477
+ />
1478
+ </div>
1479
+ ) : (
1480
+ // Show placeholder if it's not a data URL
1481
+ <div className="inline-block rounded-lg shadow-md bg-green-500 text-white p-6 text-center">
1482
+ <div className="text-3xl mb-2">📷</div>
1483
+ <div className="font-semibold">Image Uploaded</div>
1484
+ <div className="text-sm opacity-75">{task.imageUrl}</div>
1485
+ </div>
1486
+ )}
1487
+ </div>
1488
+ {/* Text on the right - 50% width */}
1489
+ <div className="w-full md:w-1/2">
1490
+ <div className="text-indigo-900 leading-relaxed text-lg font-source-text whitespace-pre-wrap">{task.content}</div>
1491
+ </div>
1492
+ </div>
1493
+ )
1494
+ ) : (
1495
+ // Text only when no image
1496
+ <div>
1497
+ <div className="text-indigo-900 leading-relaxed text-lg font-source-text whitespace-pre-wrap">{task.content}</div>
1498
+ </div>
1499
+ )}
1500
+ {(() => { console.log('Task imageUrl check:', task._id, task.imageUrl, !!task.imageUrl); return null; })()}
1501
+ </div>
1502
+ )}
1503
+ </div>
1504
+
1505
+
1506
+ </div>
1507
+
1508
+ {/* All Submissions for this Task */}
1509
+ {userSubmissions[task._id] && userSubmissions[task._id].length > 0 && (
1510
+ <div className="bg-gradient-to-r from-white to-indigo-50 rounded-xl p-6 mb-6 border border-stone-200">
1511
+ <div className="flex items-center justify-between mb-4">
1512
+ <div className="flex items-center space-x-2">
1513
+ <div className="bg-indigo-100 rounded-full p-1">
1514
+ <CheckCircleIcon className="h-4 w-4 text-indigo-900" />
1515
+ </div>
1516
+ <h4 className="text-stone-900 font-semibold text-lg">All Submissions ({userSubmissions[task._id].length})</h4>
1517
+ </div>
1518
+ <button
1519
+ onClick={() => toggleExpanded(task._id)}
1520
+ className="flex items-center space-x-1 text-indigo-900 hover:text-indigo-900 text-sm font-medium"
1521
+ >
1522
+ <span>{expandedSections[task._id] ? 'Collapse' : 'Expand'}</span>
1523
+ <svg
1524
+ className={`w-4 h-4 transition-transform duration-200 ${expandedSections[task._id] ? 'rotate-180' : ''}`}
1525
+ fill="none"
1526
+ stroke="currentColor"
1527
+ viewBox="0 0 24 24"
1528
+ >
1529
+ <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
1530
+ </svg>
1531
+ </button>
1532
+ </div>
1533
+ <div className={`grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-3 transition-all duration-300 ${
1534
+ expandedSections[task._id]
1535
+ ? 'max-h-none overflow-visible'
1536
+ : 'max-h-0 overflow-hidden'
1537
+ }`}>
1538
+ {userSubmissions[task._id].map((submission, index) => (
1539
+ <div key={submission._id} className="bg-white rounded-lg p-3 border border-stone-200 flex flex-col justify-between h-full">
1540
+ <div className="flex items-center justify-between mb-2">
1541
+ <div className="flex items-center space-x-2">
1542
+ {submission.isOwner && (
1543
+ <span className="inline-block bg-purple-100 text-purple-800 text-xs px-1.5 py-0.5 rounded-full">
1544
+ Your Submission
1545
+ </span>
1546
+ )}
1547
+ </div>
1548
+ {getStatusIcon(submission.status)}
1549
+ </div>
1550
+ <p className="text-stone-800 leading-relaxed text-base mb-2 font-smiley">{submission.transcreation}</p>
1551
+ <div className="flex items-center space-x-4 text-xs text-stone-700 mt-auto">
1552
+ <div className="flex items-center space-x-1">
1553
+ <span className="font-medium">Group:</span>
1554
+ <span className="bg-indigo-100 px-1.5 py-0.5 rounded-full text-indigo-900 text-xs">
1555
+ {submission.groupNumber}
1556
+ </span>
1557
+ </div>
1558
+ <div className="flex items-center space-x-1">
1559
+ <span className="font-medium">Votes:</span>
1560
+ <span className="bg-indigo-100 px-1.5 py-0.5 rounded-full text-indigo-900 text-xs">
1561
+ {(submission.voteCounts?.['1'] || 0) + (submission.voteCounts?.['2'] || 0) + (submission.voteCounts?.['3'] || 0)}
1562
+ </span>
1563
+ </div>
1564
+ </div>
1565
+ <div className="flex items-center space-x-2 mt-2">
1566
+ {submission.isOwner && (
1567
+ <button
1568
+ onClick={() => handleEditSubmission(submission._id, submission.transcreation)}
1569
+ className="text-indigo-900 hover:text-indigo-900 text-sm font-medium"
1570
+ >
1571
+ Edit
1572
+ </button>
1573
+ )}
1574
+ {JSON.parse(localStorage.getItem('user') || '{}').role === 'admin' && (
1575
+ <button
1576
+ onClick={() => handleDeleteSubmission(submission._id)}
1577
+ className="text-red-600 hover:text-red-800 text-sm font-medium"
1578
+ >
1579
+ Delete
1580
+ </button>
1581
+ )}
1582
+ </div>
1583
+ </div>
1584
+ ))}
1585
+ </div>
1586
+ </div>
1587
+ )}
1588
+
1589
+ {/* Translation Input (always show if user is logged in, but hide for image-only content) */}
1590
+ {localStorage.getItem('token') && task.content !== 'Image-based task' && (
1591
+ <div className="bg-white rounded-lg p-6 border border-gray-200 shadow-sm">
1592
+ <div className="flex items-center space-x-3 mb-4">
1593
+ <div className="bg-gray-100 rounded-lg p-2">
1594
+ <DocumentTextIcon className="h-4 w-4 text-gray-600" />
1595
+ </div>
1596
+ <h4 className="text-gray-900 font-semibold text-lg">Group Translation</h4>
1597
+ </div>
1598
+
1599
+ {/* Group Selection */}
1600
+ <div className="mb-4">
1601
+ <label className="block text-sm font-medium text-gray-700 mb-2">
1602
+ Select Your Group *
1603
+ </label>
1604
+ <select
1605
+ value={selectedGroups[task._id] || ''}
1606
+ onChange={(e) => setSelectedGroups({ ...selectedGroups, [task._id]: parseInt(e.target.value) })}
1607
+ className="w-48 px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500 bg-white text-sm"
1608
+ required
1609
+ >
1610
+ <option value="">Choose your group...</option>
1611
+ {[1, 2, 3, 4, 5, 6, 7, 8].map((group) => (
1612
+ <option key={group} value={group}>
1613
+ Group {group}
1614
+ </option>
1615
+ ))}
1616
+ </select>
1617
+ </div>
1618
+
1619
+ <div className="mb-4">
1620
+ <label className="block text-sm font-medium text-gray-700 mb-2">
1621
+ Your Group's Translation *
1622
+ </label>
1623
+ <textarea
1624
+ value={translationText[task._id] || ''}
1625
+ onChange={(e) => setTranslationText({ ...translationText, [task._id]: e.target.value })}
1626
+ className="w-full px-4 py-3 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500 bg-white"
1627
+ rows={4}
1628
+ placeholder="Enter your group's translation here..."
1629
+ />
1630
+ </div>
1631
+
1632
+ <button
1633
+ onClick={() => handleSubmitTranslation(task._id)}
1634
+ disabled={submitting[task._id]}
1635
+ className="bg-indigo-500 hover:bg-indigo-600 disabled:bg-gray-400 text-white px-6 py-3 rounded-lg font-medium flex items-center justify-center transition-all duration-200"
1636
+ >
1637
+ {submitting[task._id] ? (
1638
+ <>
1639
+ <div className="animate-spin rounded-full h-4 w-4 border-b-2 border-white mr-2"></div>
1640
+ Submitting...
1641
+ </>
1642
+ ) : (
1643
+ <>
1644
+ Submit Group Translation
1645
+ <ArrowRightIcon className="h-4 w-4 ml-2" />
1646
+ </>
1647
+ )}
1648
+ </button>
1649
+ </div>
1650
+ )}
1651
+
1652
+ {/* Show login message for visitors */}
1653
+ {!localStorage.getItem('token') && (
1654
+ <div className="bg-gradient-to-r from-gray-50 to-indigo-50 rounded-xl p-6 border border-gray-200">
1655
+ <div className="flex items-center space-x-2 mb-4">
1656
+ <div className="bg-gray-100 rounded-full p-1">
1657
+ <DocumentTextIcon className="h-4 w-4 text-gray-600" />
1658
+ </div>
1659
+ <h4 className="text-gray-900 font-semibold text-lg">Login Required</h4>
1660
+ </div>
1661
+ <p className="text-gray-700 mb-4">
1662
+ Please log in to submit translations for this tutorial task.
1663
+ </p>
1664
+ <button
1665
+ onClick={() => window.location.href = '/login'}
1666
+ className="bg-indigo-500 hover:bg-indigo-600 text-white px-6 py-3 rounded-lg font-medium flex items-center justify-center transition-all duration-200"
1667
+ >
1668
+ Go to Login
1669
+ <ArrowRightIcon className="h-4 w-4 ml-2" />
1670
+ </button>
1671
+ </div>
1672
+ )}
1673
+ </div>
1674
+ ))
1675
+ )}
1676
+ </div>
1677
+ </>
1678
+ )}
1679
+ </div>
1680
+
1681
+ {/* Edit Submission Modal */}
1682
+ {editingSubmission && (
1683
+ <div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
1684
+ <div className="bg-white rounded-lg p-6 w-full max-w-2xl mx-4">
1685
+ <div className="flex items-center justify-between mb-4">
1686
+ <h3 className="text-lg font-semibold text-gray-900">Edit Translation</h3>
1687
+ <button
1688
+ onClick={cancelEditSubmission}
1689
+ className="text-gray-400 hover:text-gray-600"
1690
+ >
1691
+ <XMarkIcon className="h-6 w-6" />
1692
+ </button>
1693
+ </div>
1694
+ <div className="mb-4">
1695
+ <textarea
1696
+ value={editSubmissionText}
1697
+ onChange={(e) => setEditSubmissionText(e.target.value)}
1698
+ className="w-full px-4 py-3 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-purple-500 focus:border-purple-500"
1699
+ rows={6}
1700
+ placeholder="Enter your translation..."
1701
+ />
1702
+ </div>
1703
+ <div className="flex justify-end space-x-3">
1704
+ <button
1705
+ onClick={cancelEditSubmission}
1706
+ className="px-4 py-2 text-gray-600 hover:text-gray-800 border border-gray-300 rounded-lg"
1707
+ >
1708
+ Cancel
1709
+ </button>
1710
+ <button
1711
+ onClick={saveEditedSubmission}
1712
+ className="px-4 py-2 bg-purple-600 text-white rounded-lg hover:bg-purple-700"
1713
+ >
1714
+ Save Changes
1715
+ </button>
1716
+ </div>
1717
+ </div>
1718
+ </div>
1719
+ )}
1720
+ </div>
1721
+ );
1722
+ };
1723
+
1724
+ export default TutorialTasks;
backups/complete-backup-2025-08-10T07-59-20-407Z/code/frontend/WeeklyPractice.tsx ADDED
The diff for this file is too large to render. See raw diff
 
backups/complete-backup-2025-08-10T07-59-20-407Z/code/frontend/api.ts ADDED
@@ -0,0 +1,91 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import axios from 'axios';
2
+
3
+ // Create axios instance with base configuration
4
+ // FORCE REBUILD: Fixed double /api issue by removing /api from baseURL
5
+ const api = axios.create({
6
+ baseURL: 'https://linguabot-transcreation-backend.hf.space',
7
+ headers: {
8
+ 'Content-Type': 'application/json',
9
+ },
10
+ timeout: 10000, // 10 second timeout
11
+ });
12
+
13
+ // Debug: Log the API URL being used
14
+ console.log('🔧 API CONFIGURATION DEBUG - FIXED DOUBLE /API ISSUE:');
15
+ console.log('API Base URL: https://linguabot-transcreation-backend.hf.space');
16
+ console.log('Environment variables:', {
17
+ REACT_APP_API_URL: process.env.REACT_APP_API_URL,
18
+ NODE_ENV: process.env.NODE_ENV
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(
26
+ (config) => {
27
+ const token = localStorage.getItem('token');
28
+ if (token) {
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
+ }
46
+ }
47
+
48
+ // Debug: Log the actual request URL
49
+ console.log('🚀 Making API request to:', (config.baseURL || '') + (config.url || ''));
50
+ console.log('🔑 Auth token:', token ? 'Present' : 'Missing');
51
+
52
+ return config;
53
+ },
54
+ (error) => {
55
+ return Promise.reject(error);
56
+ }
57
+ );
58
+
59
+ // Response interceptor to handle errors
60
+ api.interceptors.response.use(
61
+ (response) => {
62
+ console.log('✅ API response received:', response.config.url);
63
+ return response;
64
+ },
65
+ (error) => {
66
+ console.error('❌ API request failed:', error.config?.url, error.message);
67
+
68
+ // Don't auto-redirect for admin operations - let the component handle it
69
+ if (error.response?.status === 401 && !error.config?.url?.includes('/subtitles/update/')) {
70
+ // Token expired or invalid - only redirect for non-admin operations
71
+ localStorage.removeItem('token');
72
+ localStorage.removeItem('user');
73
+ window.location.href = '/login';
74
+ } else if (error.response?.status === 429) {
75
+ // Rate limit exceeded - retry after delay
76
+ console.warn('Rate limit exceeded, retrying after delay...');
77
+ return new Promise(resolve => {
78
+ setTimeout(() => {
79
+ resolve(api.request(error.config));
80
+ }, 2000); // Wait 2 seconds before retry
81
+ });
82
+ } else if (error.response?.status === 500) {
83
+ console.error('Server error:', error.response.data);
84
+ } else if (error.code === 'ECONNABORTED') {
85
+ console.error('Request timeout');
86
+ }
87
+ return Promise.reject(error);
88
+ }
89
+ );
90
+
91
+ export { api };
backups/complete-backup-2025-08-10T07-59-20-407Z/manifest.json ADDED
@@ -0,0 +1,49 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "backupInfo": {
3
+ "timestamp": "2025-08-10T07:59:22.048Z",
4
+ "backupType": "Complete Backup",
5
+ "description": "Full backup including database collections and key code files",
6
+ "version": "1.0"
7
+ },
8
+ "database": {
9
+ "collections": [
10
+ "users",
11
+ "sourcetexts",
12
+ "submissions",
13
+ "subtitles",
14
+ "subtitlesubmissions"
15
+ ],
16
+ "totalDocuments": 107
17
+ },
18
+ "codeFiles": {
19
+ "frontend": [
20
+ "../frontend/client/src/pages/WeeklyPractice.tsx",
21
+ "../frontend/client/src/pages/TutorialTasks.tsx",
22
+ "../frontend/client/src/components/Layout.tsx",
23
+ "../frontend/client/src/services/api.ts"
24
+ ],
25
+ "backend": [
26
+ "index.js",
27
+ "routes/auth.js",
28
+ "routes/subtitles.js",
29
+ "routes/subtitleSubmissions.js",
30
+ "models/SourceText.js",
31
+ "models/Subtitle.js",
32
+ "models/SubtitleSubmission.js",
33
+ "seed-atlas-subtitles.js",
34
+ "seed-subtitle-submissions.js"
35
+ ]
36
+ },
37
+ "features": [
38
+ "Database collections backup",
39
+ "Key frontend components backup",
40
+ "Backend routes and models backup",
41
+ "Seed data scripts backup",
42
+ "Manifest with backup details"
43
+ ],
44
+ "restoreInstructions": {
45
+ "database": "Use the JSON files to restore collections to MongoDB",
46
+ "code": "Copy the code files back to their original locations",
47
+ "verification": "Check manifest.json for backup contents and details"
48
+ }
49
+ }
backups/complete-backup-2025-08-10T07-59-20-407Z/sourcetexts.json ADDED
The diff for this file is too large to render. See raw diff
 
backups/complete-backup-2025-08-10T07-59-20-407Z/submissions.json ADDED
@@ -0,0 +1,1361 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ [
2
+ {
3
+ "_id": "68936c2dfffd7a32515e55b7",
4
+ "sourceTextId": "6890c5676f4532c7d2f0f88c",
5
+ "userId": "3c6298fe9c723538a5fec5a8",
6
+ "username": "Visitor",
7
+ "groupNumber": 8,
8
+ "targetCulture": "Western",
9
+ "targetLanguage": "English",
10
+ "transcreation": "皮皮果宠物用品店| 狗狗防风防水羽绒马甲,内置牵引扣,加厚北极绒内里",
11
+ "explanation": "Translation submission",
12
+ "culturalAdaptations": [],
13
+ "isAnonymous": true,
14
+ "status": "submitted",
15
+ "difficulty": "intermediate",
16
+ "votes": [
17
+ {
18
+ "userId": "3132017cf466170455c5518c",
19
+ "rank": 1,
20
+ "createdAt": "2025-08-06T23:52:52.602Z",
21
+ "_id": "6893ead4fffd7a32515e6000"
22
+ },
23
+ {
24
+ "userId": "36222902424b6b5e2a2d5177",
25
+ "rank": 1,
26
+ "createdAt": "2025-08-06T23:53:55.985Z",
27
+ "_id": "6893eb13fffd7a32515e610b"
28
+ },
29
+ {
30
+ "userId": "b859743558631947ffa7921b",
31
+ "rank": 3,
32
+ "createdAt": "2025-08-07T03:17:22.949Z",
33
+ "_id": "68941ac2fffd7a32515e65a3"
34
+ }
35
+ ],
36
+ "feedback": [],
37
+ "createdAt": "2025-08-06T14:52:29.612Z",
38
+ "updatedAt": "2025-08-07T03:17:22.949Z",
39
+ "__v": 5
40
+ },
41
+ {
42
+ "_id": "68936cd2fffd7a32515e55be",
43
+ "sourceTextId": "6890c5676f4532c7d2f0f88e",
44
+ "userId": "3c6298fe9c723538a5fec5a8",
45
+ "username": "Visitor",
46
+ "groupNumber": 8,
47
+ "targetCulture": "Western",
48
+ "targetLanguage": "English",
49
+ "transcreation": "产品详情\n\n一件顶两件!外套+胸背带完美结合。\n内置胸背带设计 | 胸背带与外套一体化,配备坚固D型环,牵引更安全。\n出门超省心 | 冬天带毛孩子出门,再也不用里三层外三层!拉链一拉、扣环一扣,即刻出发。",
50
+ "explanation": "Translation submission",
51
+ "culturalAdaptations": [],
52
+ "isAnonymous": true,
53
+ "status": "submitted",
54
+ "difficulty": "intermediate",
55
+ "votes": [
56
+ {
57
+ "userId": "b859743558631947ffa7921b",
58
+ "rank": 3,
59
+ "createdAt": "2025-08-07T04:59:02.447Z",
60
+ "_id": "68943296fffd7a32515e6bd0"
61
+ }
62
+ ],
63
+ "feedback": [],
64
+ "createdAt": "2025-08-06T14:55:14.322Z",
65
+ "updatedAt": "2025-08-07T04:59:02.455Z",
66
+ "__v": 1
67
+ },
68
+ {
69
+ "_id": "68936cfbfffd7a32515e55c5",
70
+ "sourceTextId": "6890c6b1e6b9f36dd5b51ee7",
71
+ "userId": "3c6298fe9c723538a5fec5a8",
72
+ "username": "Visitor",
73
+ "groupNumber": 8,
74
+ "targetCulture": "Western",
75
+ "targetLanguage": "English",
76
+ "transcreation": "抓绒内里+厚实棉层,狗狗一穿就不想脱。",
77
+ "explanation": "Translation submission",
78
+ "culturalAdaptations": [],
79
+ "isAnonymous": true,
80
+ "status": "submitted",
81
+ "difficulty": "intermediate",
82
+ "votes": [],
83
+ "feedback": [],
84
+ "createdAt": "2025-08-06T14:55:55.810Z",
85
+ "updatedAt": "2025-08-06T14:55:55.811Z",
86
+ "__v": 0
87
+ },
88
+ {
89
+ "_id": "68936d2afffd7a32515e55cc",
90
+ "sourceTextId": "6890c6b1e6b9f36dd5b51eea",
91
+ "userId": "3c6298fe9c723538a5fec5a8",
92
+ "username": "Visitor",
93
+ "groupNumber": 8,
94
+ "targetCulture": "Western",
95
+ "targetLanguage": "English",
96
+ "transcreation": "防风防水面料,不惧风雨。",
97
+ "explanation": "Translation submission",
98
+ "culturalAdaptations": [],
99
+ "isAnonymous": true,
100
+ "status": "submitted",
101
+ "difficulty": "intermediate",
102
+ "votes": [],
103
+ "feedback": [],
104
+ "createdAt": "2025-08-06T14:56:42.681Z",
105
+ "updatedAt": "2025-08-06T14:56:42.682Z",
106
+ "__v": 0
107
+ },
108
+ {
109
+ "_id": "68936d44fffd7a32515e55d3",
110
+ "sourceTextId": "6890c6b1e6b9f36dd5b51eed",
111
+ "userId": "3c6298fe9c723538a5fec5a8",
112
+ "username": "Visitor",
113
+ "groupNumber": 8,
114
+ "targetCulture": "Western",
115
+ "targetLanguage": "English",
116
+ "transcreation": "拉链挡风片设计,防止风雨从拉链缝隙钻入。",
117
+ "explanation": "Translation submission",
118
+ "culturalAdaptations": [],
119
+ "isAnonymous": true,
120
+ "status": "submitted",
121
+ "difficulty": "intermediate",
122
+ "votes": [
123
+ {
124
+ "userId": "b859743558631947ffa7921b",
125
+ "rank": 1,
126
+ "createdAt": "2025-08-07T05:03:05.127Z",
127
+ "_id": "68943389fffd7a32515e6daa"
128
+ }
129
+ ],
130
+ "feedback": [],
131
+ "createdAt": "2025-08-06T14:57:08.907Z",
132
+ "updatedAt": "2025-08-07T05:03:05.128Z",
133
+ "__v": 1
134
+ },
135
+ {
136
+ "_id": "68936e04fffd7a32515e55da",
137
+ "sourceTextId": "6890c6b1e6b9f36dd5b51ef0",
138
+ "userId": "3c6298fe9c723538a5fec5a8",
139
+ "username": "Visitor",
140
+ "groupNumber": 8,
141
+ "targetCulture": "Western",
142
+ "targetLanguage": "English",
143
+ "transcreation": "一体式牵引\n\n内置环绕式加固胸背带,受力均匀,狗狗用力拉扯也不用担心脱落,遛狗更安心。",
144
+ "explanation": "Translation submission",
145
+ "culturalAdaptations": [],
146
+ "isAnonymous": true,
147
+ "status": "submitted",
148
+ "difficulty": "intermediate",
149
+ "votes": [],
150
+ "feedback": [],
151
+ "createdAt": "2025-08-06T15:00:20.746Z",
152
+ "updatedAt": "2025-08-06T15:00:20.747Z",
153
+ "__v": 0
154
+ },
155
+ {
156
+ "_id": "68936e12fffd7a32515e55e1",
157
+ "sourceTextId": "6890c6b1e6b9f36dd5b51ef3",
158
+ "userId": "3c6298fe9c723538a5fec5a8",
159
+ "username": "Visitor",
160
+ "groupNumber": 8,
161
+ "targetCulture": "Western",
162
+ "targetLanguage": "English",
163
+ "transcreation": "两色可选,满足您和爱犬的时尚需求!",
164
+ "explanation": "Translation submission",
165
+ "culturalAdaptations": [],
166
+ "isAnonymous": true,
167
+ "status": "submitted",
168
+ "difficulty": "intermediate",
169
+ "votes": [
170
+ {
171
+ "userId": "b859743558631947ffa7921b",
172
+ "rank": 3,
173
+ "createdAt": "2025-08-07T03:38:14.480Z",
174
+ "_id": "68941fa6fffd7a32515e671a"
175
+ }
176
+ ],
177
+ "feedback": [],
178
+ "createdAt": "2025-08-06T15:00:34.362Z",
179
+ "updatedAt": "2025-08-07T03:38:14.521Z",
180
+ "__v": 1
181
+ },
182
+ {
183
+ "_id": "68936e33fffd7a32515e55e8",
184
+ "sourceTextId": "6890c6b1e6b9f36dd5b51ef6",
185
+ "userId": "3c6298fe9c723538a5fec5a8",
186
+ "username": "Visitor",
187
+ "groupNumber": 8,
188
+ "targetCulture": "Western",
189
+ "targetLanguage": "English",
190
+ "transcreation": "温暖过冬,就选它!",
191
+ "explanation": "Translation submission",
192
+ "culturalAdaptations": [],
193
+ "isAnonymous": true,
194
+ "status": "submitted",
195
+ "difficulty": "intermediate",
196
+ "votes": [
197
+ {
198
+ "userId": "b859743558631947ffa7921b",
199
+ "rank": 1,
200
+ "createdAt": "2025-08-07T03:20:11.280Z",
201
+ "_id": "68941b6bfffd7a32515e661c"
202
+ }
203
+ ],
204
+ "feedback": [],
205
+ "createdAt": "2025-08-06T15:01:07.931Z",
206
+ "updatedAt": "2025-08-07T03:20:11.281Z",
207
+ "__v": 1
208
+ },
209
+ {
210
+ "_id": "68936e83fffd7a32515e55ef",
211
+ "sourceTextId": "6890c6b1e6b9f36dd5b51ef9",
212
+ "userId": "3c6298fe9c723538a5fec5a8",
213
+ "username": "Visitor",
214
+ "groupNumber": 8,
215
+ "targetCulture": "Western",
216
+ "targetLanguage": "English",
217
+ "transcreation": "无论是寒风凛冽的冬日漫步。。。",
218
+ "explanation": "Translation submission",
219
+ "culturalAdaptations": [],
220
+ "isAnonymous": true,
221
+ "status": "submitted",
222
+ "difficulty": "intermediate",
223
+ "votes": [
224
+ {
225
+ "userId": "b859743558631947ffa7921b",
226
+ "rank": 3,
227
+ "createdAt": "2025-08-07T03:59:02.319Z",
228
+ "_id": "68942486fffd7a32515e67e0"
229
+ }
230
+ ],
231
+ "feedback": [],
232
+ "createdAt": "2025-08-06T15:02:27.052Z",
233
+ "updatedAt": "2025-08-07T03:59:02.320Z",
234
+ "__v": 1
235
+ },
236
+ {
237
+ "_id": "68936ee3fffd7a32515e55f6",
238
+ "sourceTextId": "6890c6b1e6b9f36dd5b51efc",
239
+ "userId": "3c6298fe9c723538a5fec5a8",
240
+ "username": "Visitor",
241
+ "groupNumber": 8,
242
+ "targetCulture": "Western",
243
+ "targetLanguage": "English",
244
+ "transcreation": "还是悠闲自在的家中时光。。。",
245
+ "explanation": "Translation submission",
246
+ "culturalAdaptations": [],
247
+ "isAnonymous": true,
248
+ "status": "submitted",
249
+ "difficulty": "intermediate",
250
+ "votes": [
251
+ {
252
+ "userId": "36222902424b6b5e2a2d5177",
253
+ "rank": 2,
254
+ "createdAt": "2025-08-06T23:55:48.009Z",
255
+ "_id": "6893eb84fffd7a32515e61dd"
256
+ },
257
+ {
258
+ "userId": "b859743558631947ffa7921b",
259
+ "rank": 3,
260
+ "createdAt": "2025-08-07T03:30:22.487Z",
261
+ "_id": "68941dcefffd7a32515e66c5"
262
+ },
263
+ {
264
+ "userId": "dbd9b0a6d957cd7ba6a39b9a",
265
+ "rank": 3,
266
+ "createdAt": "2025-08-07T09:52:51.307Z",
267
+ "_id": "68947773fffd7a32515ea29c"
268
+ }
269
+ ],
270
+ "feedback": [],
271
+ "createdAt": "2025-08-06T15:04:03.037Z",
272
+ "updatedAt": "2025-08-07T09:52:51.308Z",
273
+ "__v": 3
274
+ },
275
+ {
276
+ "_id": "68936f0ffffd7a32515e55fd",
277
+ "sourceTextId": "6890c6b2e6b9f36dd5b51eff",
278
+ "userId": "3c6298fe9c723538a5fec5a8",
279
+ "username": "Visitor",
280
+ "groupNumber": 8,
281
+ "targetCulture": "Western",
282
+ "targetLanguage": "English",
283
+ "transcreation": "都能给予狗狗最贴心的呵护!",
284
+ "explanation": "Translation submission",
285
+ "culturalAdaptations": [],
286
+ "isAnonymous": true,
287
+ "status": "submitted",
288
+ "difficulty": "intermediate",
289
+ "votes": [
290
+ {
291
+ "userId": "36222902424b6b5e2a2d5177",
292
+ "rank": 2,
293
+ "createdAt": "2025-08-06T23:56:36.656Z",
294
+ "_id": "6893ebb4fffd7a32515e628b"
295
+ },
296
+ {
297
+ "userId": "b859743558631947ffa7921b",
298
+ "rank": 3,
299
+ "createdAt": "2025-08-07T03:49:25.488Z",
300
+ "_id": "68942245fffd7a32515e6778"
301
+ },
302
+ {
303
+ "userId": "dbd9b0a6d957cd7ba6a39b9a",
304
+ "rank": 3,
305
+ "createdAt": "2025-08-07T09:53:06.725Z",
306
+ "_id": "68947782fffd7a32515ea356"
307
+ }
308
+ ],
309
+ "feedback": [],
310
+ "createdAt": "2025-08-06T15:04:47.746Z",
311
+ "updatedAt": "2025-08-07T09:53:06.726Z",
312
+ "__v": 3
313
+ },
314
+ {
315
+ "_id": "68936fb8fffd7a32515e5604",
316
+ "sourceTextId": "6890c6b2e6b9f36dd5b51f02",
317
+ "userId": "3c6298fe9c723538a5fec5a8",
318
+ "username": "Visitor",
319
+ "groupNumber": 8,
320
+ "targetCulture": "Western",
321
+ "targetLanguage": "English",
322
+ "transcreation": "温馨提示:耐用舒适面料,防水性能佳,适合小雨天气,不建议暴雨中长时间使用。\n\n大雨天虽然衣服表面会被打湿,但水分不会渗透,毛孩子身体依然干爽舒适!\n\n快为爱犬添置一件冬日新装吧!",
323
+ "explanation": "Translation submission",
324
+ "culturalAdaptations": [],
325
+ "isAnonymous": true,
326
+ "status": "submitted",
327
+ "difficulty": "intermediate",
328
+ "votes": [
329
+ {
330
+ "userId": "b859743558631947ffa7921b",
331
+ "rank": 1,
332
+ "createdAt": "2025-08-07T04:03:28.838Z",
333
+ "_id": "68942590fffd7a32515e689e"
334
+ },
335
+ {
336
+ "userId": "dbd9b0a6d957cd7ba6a39b9a",
337
+ "rank": 2,
338
+ "createdAt": "2025-08-07T09:53:39.279Z",
339
+ "_id": "689477a3fffd7a32515ea3d5"
340
+ }
341
+ ],
342
+ "feedback": [],
343
+ "createdAt": "2025-08-06T15:07:36.624Z",
344
+ "updatedAt": "2025-08-07T09:53:39.280Z",
345
+ "__v": 2
346
+ },
347
+ {
348
+ "_id": "6893df40fffd7a32515e58c6",
349
+ "sourceTextId": "6890c5676f4532c7d2f0f88c",
350
+ "userId": "36222902424b6b5e2a2d5177",
351
+ "username": "You Lianxiang",
352
+ "groupNumber": 1,
353
+ "targetCulture": "Western",
354
+ "targetLanguage": "English",
355
+ "transcreation": "PIPCO宠物用品——狗狗羽绒服带内置牵引带|防水防风冬季背心带牵引绳接口|给狗狗的暖和极地绒填充防风外套",
356
+ "explanation": "Translation submission",
357
+ "culturalAdaptations": [],
358
+ "isAnonymous": true,
359
+ "status": "submitted",
360
+ "difficulty": "intermediate",
361
+ "votes": [
362
+ {
363
+ "userId": "dbd9b0a6d957cd7ba6a39b9a",
364
+ "rank": 2,
365
+ "createdAt": "2025-08-06T23:52:34.774Z",
366
+ "_id": "6893eac2fffd7a32515e5fe9"
367
+ },
368
+ {
369
+ "userId": "3132017cf466170455c5518c",
370
+ "rank": 3,
371
+ "createdAt": "2025-08-06T23:53:25.577Z",
372
+ "_id": "6893eaf5fffd7a32515e6065"
373
+ },
374
+ {
375
+ "userId": "36222902424b6b5e2a2d5177",
376
+ "rank": 2,
377
+ "createdAt": "2025-08-06T23:54:06.941Z",
378
+ "_id": "6893eb1efffd7a32515e6136"
379
+ },
380
+ {
381
+ "userId": "b859743558631947ffa7921b",
382
+ "rank": 1,
383
+ "createdAt": "2025-08-07T03:17:39.701Z",
384
+ "_id": "68941ad3fffd7a32515e65d4"
385
+ }
386
+ ],
387
+ "feedback": [],
388
+ "createdAt": "2025-08-06T23:03:28.376Z",
389
+ "updatedAt": "2025-08-07T03:17:39.707Z",
390
+ "__v": 4
391
+ },
392
+ {
393
+ "_id": "6893e129fffd7a32515e598a",
394
+ "sourceTextId": "6890c5676f4532c7d2f0f88c",
395
+ "userId": "4a2e687e4f763331dda9bdd4",
396
+ "username": "Han Heyang",
397
+ "groupNumber": 3,
398
+ "targetCulture": "Western",
399
+ "targetLanguage": "English",
400
+ "transcreation": "PIPCO PETS - 狗狗内置背带羽绒夹克|防水防风冬季背心-带牵引绳扣|极地暖绒填充狗狗冲锋衣\n\n",
401
+ "explanation": "Translation submission",
402
+ "culturalAdaptations": [],
403
+ "isAnonymous": true,
404
+ "status": "submitted",
405
+ "difficulty": "intermediate",
406
+ "votes": [
407
+ {
408
+ "userId": "dbd9b0a6d957cd7ba6a39b9a",
409
+ "rank": 1,
410
+ "createdAt": "2025-08-06T23:52:30.645Z",
411
+ "_id": "6893eabefffd7a32515e5fe2"
412
+ },
413
+ {
414
+ "userId": "3132017cf466170455c5518c",
415
+ "rank": 2,
416
+ "createdAt": "2025-08-06T23:53:11.449Z",
417
+ "_id": "6893eae7fffd7a32515e6043"
418
+ },
419
+ {
420
+ "userId": "36222902424b6b5e2a2d5177",
421
+ "rank": 3,
422
+ "createdAt": "2025-08-06T23:54:18.308Z",
423
+ "_id": "6893eb2afffd7a32515e6159"
424
+ },
425
+ {
426
+ "userId": "b859743558631947ffa7921b",
427
+ "rank": 2,
428
+ "createdAt": "2025-08-07T03:17:35.407Z",
429
+ "_id": "68941acffffd7a32515e65bb"
430
+ }
431
+ ],
432
+ "feedback": [],
433
+ "createdAt": "2025-08-06T23:11:37.368Z",
434
+ "updatedAt": "2025-08-07T03:17:35.409Z",
435
+ "__v": 6
436
+ },
437
+ {
438
+ "_id": "6893e2ecfffd7a32515e5a19",
439
+ "sourceTextId": "6890c5676f4532c7d2f0f88e",
440
+ "userId": "36222902424b6b5e2a2d5177",
441
+ "username": "You Lianxiang",
442
+ "groupNumber": 1,
443
+ "targetCulture": "Western",
444
+ "targetLanguage": "English",
445
+ "transcreation": "产品介绍\n完美二合一!保暖外套 + 内置牵引带,一步到位。\n内置牵引带:牵引带与夹克融为一体,坚固的 D 形环可用于固定牵引带。\n日常便捷:寒冬带毛孩子出门?无需再给它层层叠穿或费力将牵引带穿过外套孔洞,穿上我们的外套,一拉(拉链)、一扣(牵引绳)、马上出发!",
446
+ "explanation": "Translation submission",
447
+ "culturalAdaptations": [],
448
+ "isAnonymous": true,
449
+ "status": "submitted",
450
+ "difficulty": "intermediate",
451
+ "votes": [
452
+ {
453
+ "userId": "b859743558631947ffa7921b",
454
+ "rank": 2,
455
+ "createdAt": "2025-08-07T04:59:02.940Z",
456
+ "_id": "68943296fffd7a32515e6bd3"
457
+ }
458
+ ],
459
+ "feedback": [],
460
+ "createdAt": "2025-08-06T23:19:08.840Z",
461
+ "updatedAt": "2025-08-07T04:59:02.940Z",
462
+ "__v": 1
463
+ },
464
+ {
465
+ "_id": "6893e495fffd7a32515e5ab9",
466
+ "sourceTextId": "6890c6b1e6b9f36dd5b51ee7",
467
+ "userId": "36222902424b6b5e2a2d5177",
468
+ "username": "You Lianxiang",
469
+ "groupNumber": 1,
470
+ "targetCulture": "Western",
471
+ "targetLanguage": "English",
472
+ "transcreation": "超暖呵护:舒适绗缝设计 + 柔软摇粒绒内衬,让狗狗时刻温暖又自在",
473
+ "explanation": "Translation submission",
474
+ "culturalAdaptations": [],
475
+ "isAnonymous": true,
476
+ "status": "submitted",
477
+ "difficulty": "intermediate",
478
+ "votes": [
479
+ {
480
+ "userId": "b859743558631947ffa7921b",
481
+ "rank": 1,
482
+ "createdAt": "2025-08-07T05:01:28.660Z",
483
+ "_id": "68943328fffd7a32515e6c83"
484
+ }
485
+ ],
486
+ "feedback": [],
487
+ "createdAt": "2025-08-06T23:26:13.393Z",
488
+ "updatedAt": "2025-08-07T05:01:28.661Z",
489
+ "__v": 1
490
+ },
491
+ {
492
+ "_id": "6893e4b1fffd7a32515e5ac0",
493
+ "sourceTextId": "6890c5676f4532c7d2f0f88e",
494
+ "userId": "4a2e687e4f763331dda9bdd4",
495
+ "username": "Han Heyang",
496
+ "groupNumber": 3,
497
+ "targetCulture": "Western",
498
+ "targetLanguage": "English",
499
+ "transcreation": "产品描述\n外套与牵引一体设计,完美二合一!\n内置式牵引装置\n牵引绳固定环直接集成在外套中,配有结实的 D 型金属环,方便连接牵引绳,出门更省心。\n日常出行更方便\n天气寒冷,还在把毛孩子包成粽子或费劲地穿牵引绳?\n这款外套帮你轻松搞定!只需拉拉链,扣牵引,马上出发!\n",
500
+ "explanation": "Translation submission",
501
+ "culturalAdaptations": [],
502
+ "isAnonymous": true,
503
+ "status": "submitted",
504
+ "difficulty": "intermediate",
505
+ "votes": [
506
+ {
507
+ "userId": "b859743558631947ffa7921b",
508
+ "rank": 1,
509
+ "createdAt": "2025-08-07T04:59:04.673Z",
510
+ "_id": "68943298fffd7a32515e6c26"
511
+ }
512
+ ],
513
+ "feedback": [],
514
+ "createdAt": "2025-08-06T23:26:41.181Z",
515
+ "updatedAt": "2025-08-07T04:59:04.674Z",
516
+ "__v": 1
517
+ },
518
+ {
519
+ "_id": "6893e4e2fffd7a32515e5ad8",
520
+ "sourceTextId": "6890c6b1e6b9f36dd5b51eea",
521
+ "userId": "36222902424b6b5e2a2d5177",
522
+ "username": "You Lianxiang",
523
+ "groupNumber": 1,
524
+ "targetCulture": "Western",
525
+ "targetLanguage": "English",
526
+ "transcreation": "防风防水外层*,为您的爱犬提供抵御寒风细雨的保护",
527
+ "explanation": "Translation submission",
528
+ "culturalAdaptations": [],
529
+ "isAnonymous": true,
530
+ "status": "submitted",
531
+ "difficulty": "intermediate",
532
+ "votes": [
533
+ {
534
+ "userId": "b859743558631947ffa7921b",
535
+ "rank": 3,
536
+ "createdAt": "2025-08-07T05:02:14.640Z",
537
+ "_id": "68943356fffd7a32515e6cde"
538
+ }
539
+ ],
540
+ "feedback": [],
541
+ "createdAt": "2025-08-06T23:27:30.197Z",
542
+ "updatedAt": "2025-08-07T05:02:14.641Z",
543
+ "__v": 1
544
+ },
545
+ {
546
+ "_id": "6893e533fffd7a32515e5b01",
547
+ "sourceTextId": "6890c6b1e6b9f36dd5b51ee7",
548
+ "userId": "4a2e687e4f763331dda9bdd4",
549
+ "username": "Han Heyang",
550
+ "groupNumber": 3,
551
+ "targetCulture": "Western",
552
+ "targetLanguage": "English",
553
+ "transcreation": "超柔软绗缝设计搭配抓绒内衬,为狗狗带来满满温暖感!",
554
+ "explanation": "Translation submission",
555
+ "culturalAdaptations": [],
556
+ "isAnonymous": true,
557
+ "status": "submitted",
558
+ "difficulty": "intermediate",
559
+ "votes": [
560
+ {
561
+ "userId": "b859743558631947ffa7921b",
562
+ "rank": 3,
563
+ "createdAt": "2025-08-07T05:01:27.327Z",
564
+ "_id": "68943327fffd7a32515e6c52"
565
+ }
566
+ ],
567
+ "feedback": [],
568
+ "createdAt": "2025-08-06T23:28:51.569Z",
569
+ "updatedAt": "2025-08-07T05:01:27.328Z",
570
+ "__v": 1
571
+ },
572
+ {
573
+ "_id": "6893e561fffd7a32515e5b88",
574
+ "sourceTextId": "6890c6b1e6b9f36dd5b51eed",
575
+ "userId": "36222902424b6b5e2a2d5177",
576
+ "username": "You Lianxiang",
577
+ "groupNumber": 1,
578
+ "targetCulture": "Western",
579
+ "targetLanguage": "English",
580
+ "transcreation": "防夹挡片保护您的爱犬免受风雨侵扰",
581
+ "explanation": "Translation submission",
582
+ "culturalAdaptations": [],
583
+ "isAnonymous": true,
584
+ "status": "submitted",
585
+ "difficulty": "intermediate",
586
+ "votes": [
587
+ {
588
+ "userId": "b859743558631947ffa7921b",
589
+ "rank": 3,
590
+ "createdAt": "2025-08-07T05:03:03.380Z",
591
+ "_id": "68943387fffd7a32515e6d73"
592
+ }
593
+ ],
594
+ "feedback": [],
595
+ "createdAt": "2025-08-06T23:29:37.759Z",
596
+ "updatedAt": "2025-08-07T05:03:03.381Z",
597
+ "__v": 1
598
+ },
599
+ {
600
+ "_id": "6893e569fffd7a32515e5bd9",
601
+ "sourceTextId": "6890c6b1e6b9f36dd5b51eea",
602
+ "userId": "4a2e687e4f763331dda9bdd4",
603
+ "username": "Han Heyang",
604
+ "groupNumber": 3,
605
+ "targetCulture": "Western",
606
+ "targetLanguage": "English",
607
+ "transcreation": "防风防水外层,有效抵挡寒风与细雨,让爱犬冬日出行无忧。",
608
+ "explanation": "Translation submission",
609
+ "culturalAdaptations": [],
610
+ "isAnonymous": true,
611
+ "status": "submitted",
612
+ "difficulty": "intermediate",
613
+ "votes": [
614
+ {
615
+ "userId": "b859743558631947ffa7921b",
616
+ "rank": 1,
617
+ "createdAt": "2025-08-07T05:02:16.338Z",
618
+ "_id": "68943358fffd7a32515e6d12"
619
+ }
620
+ ],
621
+ "feedback": [],
622
+ "createdAt": "2025-08-06T23:29:45.280Z",
623
+ "updatedAt": "2025-08-07T05:02:16.339Z",
624
+ "__v": 1
625
+ },
626
+ {
627
+ "_id": "6893e5aafffd7a32515e5bf3",
628
+ "sourceTextId": "6890c5676f4532c7d2f0f88c",
629
+ "userId": "779501db783372d27c41e31e",
630
+ "username": "Li Wenhui",
631
+ "groupNumber": 2,
632
+ "targetCulture": "Western",
633
+ "targetLanguage": "English",
634
+ "transcreation": "PIPCO-PETS宠物用品- 内置胸背带的小狗羽绒背心|防水,防风 附牵引绳扣冬季背心|保暖摇粒绒加厚狗用挡风外套\n\n",
635
+ "explanation": "Translation submission",
636
+ "culturalAdaptations": [],
637
+ "isAnonymous": true,
638
+ "status": "submitted",
639
+ "difficulty": "intermediate",
640
+ "votes": [
641
+ {
642
+ "userId": "dbd9b0a6d957cd7ba6a39b9a",
643
+ "rank": 3,
644
+ "createdAt": "2025-08-06T23:52:38.321Z",
645
+ "_id": "6893eac6fffd7a32515e5ff1"
646
+ }
647
+ ],
648
+ "feedback": [],
649
+ "createdAt": "2025-08-06T23:30:50.978Z",
650
+ "updatedAt": "2025-08-06T23:52:38.322Z",
651
+ "__v": 1
652
+ },
653
+ {
654
+ "_id": "6893e66afffd7a32515e5c20",
655
+ "sourceTextId": "6890c6b1e6b9f36dd5b51eed",
656
+ "userId": "4a2e687e4f763331dda9bdd4",
657
+ "username": "Han Heyang",
658
+ "groupNumber": 3,
659
+ "targetCulture": "Western",
660
+ "targetLanguage": "English",
661
+ "transcreation": "贴心挡风片设计,防止风雨通过拉链渗入。",
662
+ "explanation": "Translation submission",
663
+ "culturalAdaptations": [],
664
+ "isAnonymous": true,
665
+ "status": "submitted",
666
+ "difficulty": "intermediate",
667
+ "votes": [
668
+ {
669
+ "userId": "b859743558631947ffa7921b",
670
+ "rank": 2,
671
+ "createdAt": "2025-08-07T05:03:04.407Z",
672
+ "_id": "68943388fffd7a32515e6d77"
673
+ }
674
+ ],
675
+ "feedback": [],
676
+ "createdAt": "2025-08-06T23:34:02.512Z",
677
+ "updatedAt": "2025-08-07T05:03:04.408Z",
678
+ "__v": 1
679
+ },
680
+ {
681
+ "_id": "6893e66efffd7a32515e5c27",
682
+ "sourceTextId": "6890c5676f4532c7d2f0f88e",
683
+ "userId": "779501db783372d27c41e31e",
684
+ "username": "Li Wenhui",
685
+ "groupNumber": 2,
686
+ "targetCulture": "Western",
687
+ "targetLanguage": "English",
688
+ "transcreation": "产品描述——这是一件完美契合外套与胸背带的二合一狗狗夹克|内置胸背带与马甲融为一体,配有坚固的 D 型环用于系遛狗绳|轻装上阵。\n想和在寒冷的冬天和毛孩子出门吗?只要穿上我们的马甲,一拉,一扣,即刻出发!省下给狗狗“一层又一层或在袖管间穿插安装胸背带的烦恼 。\n\n",
689
+ "explanation": "Translation submission",
690
+ "culturalAdaptations": [],
691
+ "isAnonymous": true,
692
+ "status": "submitted",
693
+ "difficulty": "intermediate",
694
+ "votes": [],
695
+ "feedback": [],
696
+ "createdAt": "2025-08-06T23:34:06.910Z",
697
+ "updatedAt": "2025-08-06T23:53:14.454Z",
698
+ "__v": 0
699
+ },
700
+ {
701
+ "_id": "6893e69cfffd7a32515e5c53",
702
+ "sourceTextId": "6890c6b1e6b9f36dd5b51ef0",
703
+ "userId": "36222902424b6b5e2a2d5177",
704
+ "username": "You Lianxiang",
705
+ "groupNumber": 1,
706
+ "targetCulture": "Western",
707
+ "targetLanguage": "English",
708
+ "transcreation": "安全牵引绳设计\n有别于普通牵引带易被爱宠扯断的设计,我们坚固的牵引系统贯穿外套内部,环绕狗狗全身形成保护圈,全方位守护毛孩子安全,让您安心无忧。",
709
+ "explanation": "Translation submission",
710
+ "culturalAdaptations": [],
711
+ "isAnonymous": true,
712
+ "status": "submitted",
713
+ "difficulty": "intermediate",
714
+ "votes": [
715
+ {
716
+ "userId": "b859743558631947ffa7921b",
717
+ "rank": 3,
718
+ "createdAt": "2025-08-07T05:32:28.363Z",
719
+ "_id": "68943a6cfffd7a32515e6f1f"
720
+ }
721
+ ],
722
+ "feedback": [],
723
+ "createdAt": "2025-08-06T23:34:52.402Z",
724
+ "updatedAt": "2025-08-07T05:32:28.364Z",
725
+ "__v": 1
726
+ },
727
+ {
728
+ "_id": "6893e69ffffd7a32515e5c5a",
729
+ "sourceTextId": "6890c6b1e6b9f36dd5b51ee7",
730
+ "userId": "779501db783372d27c41e31e",
731
+ "username": "Li Wenhui",
732
+ "groupNumber": 2,
733
+ "targetCulture": "Western",
734
+ "targetLanguage": "English",
735
+ "transcreation": "剪裁超舒适,软绒衬里,温暖又贴身。",
736
+ "explanation": "Translation submission",
737
+ "culturalAdaptations": [],
738
+ "isAnonymous": true,
739
+ "status": "submitted",
740
+ "difficulty": "intermediate",
741
+ "votes": [
742
+ {
743
+ "userId": "b859743558631947ffa7921b",
744
+ "rank": 2,
745
+ "createdAt": "2025-08-07T05:01:28.041Z",
746
+ "_id": "68943328fffd7a32515e6c55"
747
+ }
748
+ ],
749
+ "feedback": [],
750
+ "createdAt": "2025-08-06T23:34:55.465Z",
751
+ "updatedAt": "2025-08-07T05:01:28.042Z",
752
+ "__v": 1
753
+ },
754
+ {
755
+ "_id": "6893e6d3fffd7a32515e5c94",
756
+ "sourceTextId": "6890c6b1e6b9f36dd5b51eea",
757
+ "userId": "779501db783372d27c41e31e",
758
+ "username": "Li Wenhui",
759
+ "groupNumber": 2,
760
+ "targetCulture": "Western",
761
+ "targetLanguage": "English",
762
+ "transcreation": "防风防水*的材质可保护您的爱犬免受风寒和小雨的侵蚀。",
763
+ "explanation": "Translation submission",
764
+ "culturalAdaptations": [],
765
+ "isAnonymous": true,
766
+ "status": "submitted",
767
+ "difficulty": "intermediate",
768
+ "votes": [
769
+ {
770
+ "userId": "b859743558631947ffa7921b",
771
+ "rank": 2,
772
+ "createdAt": "2025-08-07T05:02:15.351Z",
773
+ "_id": "68943357fffd7a32515e6ce1"
774
+ }
775
+ ],
776
+ "feedback": [],
777
+ "createdAt": "2025-08-06T23:35:47.298Z",
778
+ "updatedAt": "2025-08-07T05:02:15.352Z",
779
+ "__v": 1
780
+ },
781
+ {
782
+ "_id": "6893e6dffffd7a32515e5c9b",
783
+ "sourceTextId": "6890c6b1e6b9f36dd5b51ef3",
784
+ "userId": "36222902424b6b5e2a2d5177",
785
+ "username": "You Lianxiang",
786
+ "groupNumber": 1,
787
+ "targetCulture": "Western",
788
+ "targetLanguage": "English",
789
+ "transcreation": "两种时尚颜色可选",
790
+ "explanation": "Translation submission",
791
+ "culturalAdaptations": [],
792
+ "isAnonymous": true,
793
+ "status": "submitted",
794
+ "difficulty": "intermediate",
795
+ "votes": [],
796
+ "feedback": [],
797
+ "createdAt": "2025-08-06T23:35:59.944Z",
798
+ "updatedAt": "2025-08-06T23:35:59.944Z",
799
+ "__v": 0
800
+ },
801
+ {
802
+ "_id": "6893e708fffd7a32515e5cb3",
803
+ "sourceTextId": "6890c6b1e6b9f36dd5b51ef6",
804
+ "userId": "36222902424b6b5e2a2d5177",
805
+ "username": "You Lianxiang",
806
+ "groupNumber": 1,
807
+ "targetCulture": "Western",
808
+ "targetLanguage": "English",
809
+ "transcreation": "重要的事情说三遍!\n冬天也可保持舒适!",
810
+ "explanation": "Translation submission",
811
+ "culturalAdaptations": [],
812
+ "isAnonymous": true,
813
+ "status": "submitted",
814
+ "difficulty": "intermediate",
815
+ "votes": [],
816
+ "feedback": [],
817
+ "createdAt": "2025-08-06T23:36:40.211Z",
818
+ "updatedAt": "2025-08-06T23:36:40.211Z",
819
+ "__v": 0
820
+ },
821
+ {
822
+ "_id": "6893e755fffd7a32515e5cdc",
823
+ "sourceTextId": "6890c6b1e6b9f36dd5b51eed",
824
+ "userId": "779501db783372d27c41e31e",
825
+ "username": "Li Wenhui",
826
+ "groupNumber": 2,
827
+ "targetCulture": "Western",
828
+ "targetLanguage": "English",
829
+ "transcreation": "拉链具有挡片设计为狗狗挡风防水。",
830
+ "explanation": "Translation submission",
831
+ "culturalAdaptations": [],
832
+ "isAnonymous": true,
833
+ "status": "submitted",
834
+ "difficulty": "intermediate",
835
+ "votes": [],
836
+ "feedback": [],
837
+ "createdAt": "2025-08-06T23:37:57.682Z",
838
+ "updatedAt": "2025-08-06T23:37:57.682Z",
839
+ "__v": 0
840
+ },
841
+ {
842
+ "_id": "6893e78cfffd7a32515e5ce7",
843
+ "sourceTextId": "6890c6b1e6b9f36dd5b51ef0",
844
+ "userId": "779501db783372d27c41e31e",
845
+ "username": "Li Wenhui",
846
+ "groupNumber": 2,
847
+ "targetCulture": "Western",
848
+ "targetLanguage": "English",
849
+ "transcreation": "\"牢固的内置胸背带设计\n与一些狗狗一扯就会断掉的胸背带不同,我们坚实牢固的胸背带穿过马甲包裹着狗狗的身体,让狗狗的安全得到保障。“",
850
+ "explanation": "Translation submission",
851
+ "culturalAdaptations": [],
852
+ "isAnonymous": true,
853
+ "status": "submitted",
854
+ "difficulty": "intermediate",
855
+ "votes": [
856
+ {
857
+ "userId": "b859743558631947ffa7921b",
858
+ "rank": 2,
859
+ "createdAt": "2025-08-07T05:32:28.871Z",
860
+ "_id": "68943a6cfffd7a32515e6f24"
861
+ }
862
+ ],
863
+ "feedback": [],
864
+ "createdAt": "2025-08-06T23:38:52.700Z",
865
+ "updatedAt": "2025-08-07T05:32:28.872Z",
866
+ "__v": 1
867
+ },
868
+ {
869
+ "_id": "6893e793fffd7a32515e5cee",
870
+ "sourceTextId": "6890c6b1e6b9f36dd5b51ef9",
871
+ "userId": "36222902424b6b5e2a2d5177",
872
+ "username": "You Lianxiang",
873
+ "groupNumber": 1,
874
+ "targetCulture": "Western",
875
+ "targetLanguage": "English",
876
+ "transcreation": "寒冬漫步...",
877
+ "explanation": "Translation submission",
878
+ "culturalAdaptations": [],
879
+ "isAnonymous": true,
880
+ "status": "submitted",
881
+ "difficulty": "intermediate",
882
+ "votes": [],
883
+ "feedback": [],
884
+ "createdAt": "2025-08-06T23:38:59.432Z",
885
+ "updatedAt": "2025-08-06T23:38:59.432Z",
886
+ "__v": 0
887
+ },
888
+ {
889
+ "_id": "6893e7c1fffd7a32515e5d26",
890
+ "sourceTextId": "6890c6b1e6b9f36dd5b51ef3",
891
+ "userId": "779501db783372d27c41e31e",
892
+ "username": "Li Wenhui",
893
+ "groupNumber": 2,
894
+ "targetCulture": "Western",
895
+ "targetLanguage": "English",
896
+ "transcreation": "两款时尚颜色供您选择。",
897
+ "explanation": "Translation submission",
898
+ "culturalAdaptations": [],
899
+ "isAnonymous": true,
900
+ "status": "submitted",
901
+ "difficulty": "intermediate",
902
+ "votes": [
903
+ {
904
+ "userId": "b859743558631947ffa7921b",
905
+ "rank": 1,
906
+ "createdAt": "2025-08-07T03:38:23.004Z",
907
+ "_id": "68941faffffd7a32515e6757"
908
+ }
909
+ ],
910
+ "feedback": [],
911
+ "createdAt": "2025-08-06T23:39:45.761Z",
912
+ "updatedAt": "2025-08-07T03:38:23.008Z",
913
+ "__v": 1
914
+ },
915
+ {
916
+ "_id": "6893e7f4fffd7a32515e5d4f",
917
+ "sourceTextId": "6890c6b1e6b9f36dd5b51efc",
918
+ "userId": "36222902424b6b5e2a2d5177",
919
+ "username": "You Lianxiang",
920
+ "groupNumber": 1,
921
+ "targetCulture": "Western",
922
+ "targetLanguage": "English",
923
+ "transcreation": "或在寒冷的天气放松一下...",
924
+ "explanation": "Translation submission",
925
+ "culturalAdaptations": [],
926
+ "isAnonymous": true,
927
+ "status": "submitted",
928
+ "difficulty": "intermediate",
929
+ "votes": [
930
+ {
931
+ "userId": "36222902424b6b5e2a2d5177",
932
+ "rank": 1,
933
+ "createdAt": "2025-08-06T23:55:41.051Z",
934
+ "_id": "6893eb7dfffd7a32515e61ce"
935
+ },
936
+ {
937
+ "userId": "b859743558631947ffa7921b",
938
+ "rank": 1,
939
+ "createdAt": "2025-08-07T03:30:25.865Z",
940
+ "_id": "68941dd1fffd7a32515e66fd"
941
+ },
942
+ {
943
+ "userId": "dbd9b0a6d957cd7ba6a39b9a",
944
+ "rank": 2,
945
+ "createdAt": "2025-08-07T09:52:49.236Z",
946
+ "_id": "68947771fffd7a32515ea260"
947
+ }
948
+ ],
949
+ "feedback": [],
950
+ "createdAt": "2025-08-06T23:40:36.768Z",
951
+ "updatedAt": "2025-08-07T09:52:49.237Z",
952
+ "__v": 3
953
+ },
954
+ {
955
+ "_id": "6893e80ffffd7a32515e5d67",
956
+ "sourceTextId": "6890c6b2e6b9f36dd5b51eff",
957
+ "userId": "36222902424b6b5e2a2d5177",
958
+ "username": "You Lianxiang",
959
+ "groupNumber": 1,
960
+ "targetCulture": "Western",
961
+ "targetLanguage": "English",
962
+ "transcreation": "有我们“罩着”您!",
963
+ "explanation": "Translation submission",
964
+ "culturalAdaptations": [],
965
+ "isAnonymous": true,
966
+ "status": "submitted",
967
+ "difficulty": "intermediate",
968
+ "votes": [
969
+ {
970
+ "userId": "36222902424b6b5e2a2d5177",
971
+ "rank": 1,
972
+ "createdAt": "2025-08-06T23:56:28.867Z",
973
+ "_id": "6893ebacfffd7a32515e6279"
974
+ },
975
+ {
976
+ "userId": "b859743558631947ffa7921b",
977
+ "rank": 2,
978
+ "createdAt": "2025-08-07T03:49:27.432Z",
979
+ "_id": "68942247fffd7a32515e679a"
980
+ },
981
+ {
982
+ "userId": "dbd9b0a6d957cd7ba6a39b9a",
983
+ "rank": 1,
984
+ "createdAt": "2025-08-07T09:53:02.157Z",
985
+ "_id": "6894777efffd7a32515ea2d9"
986
+ }
987
+ ],
988
+ "feedback": [],
989
+ "createdAt": "2025-08-06T23:41:03.344Z",
990
+ "updatedAt": "2025-08-07T09:53:02.158Z",
991
+ "__v": 3
992
+ },
993
+ {
994
+ "_id": "6893e86efffd7a32515e5da1",
995
+ "sourceTextId": "6890c6b1e6b9f36dd5b51ef0",
996
+ "userId": "4a2e687e4f763331dda9bdd4",
997
+ "username": "Han Heyang",
998
+ "groupNumber": 3,
999
+ "targetCulture": "Western",
1000
+ "targetLanguage": "English",
1001
+ "transcreation": "安全一体式背带设计\n不同于普通款式,我们的背带牢牢地固定在外套上,不容易被拉扯撕裂,环绕狗狗全身,放心牵引不担心!\n",
1002
+ "explanation": "Translation submission",
1003
+ "culturalAdaptations": [],
1004
+ "isAnonymous": true,
1005
+ "status": "submitted",
1006
+ "difficulty": "intermediate",
1007
+ "votes": [
1008
+ {
1009
+ "userId": "b859743558631947ffa7921b",
1010
+ "rank": 1,
1011
+ "createdAt": "2025-08-07T05:32:29.637Z",
1012
+ "_id": "68943a6dfffd7a32515e6f8d"
1013
+ }
1014
+ ],
1015
+ "feedback": [],
1016
+ "createdAt": "2025-08-06T23:42:38.618Z",
1017
+ "updatedAt": "2025-08-07T05:32:29.637Z",
1018
+ "__v": 1
1019
+ },
1020
+ {
1021
+ "_id": "6893e887fffd7a32515e5db9",
1022
+ "sourceTextId": "6890c6b2e6b9f36dd5b51eff",
1023
+ "userId": "779501db783372d27c41e31e",
1024
+ "username": "Li Wenhui",
1025
+ "groupNumber": 2,
1026
+ "targetCulture": "Western",
1027
+ "targetLanguage": "English",
1028
+ "transcreation": "都能满足狗狗的保暖需求!",
1029
+ "explanation": "Translation submission",
1030
+ "culturalAdaptations": [],
1031
+ "isAnonymous": true,
1032
+ "status": "submitted",
1033
+ "difficulty": "intermediate",
1034
+ "votes": [
1035
+ {
1036
+ "userId": "36222902424b6b5e2a2d5177",
1037
+ "rank": 3,
1038
+ "createdAt": "2025-08-06T23:56:40.437Z",
1039
+ "_id": "6893ebb8fffd7a32515e629e"
1040
+ },
1041
+ {
1042
+ "userId": "b859743558631947ffa7921b",
1043
+ "rank": 1,
1044
+ "createdAt": "2025-08-07T03:49:28.455Z",
1045
+ "_id": "68942248fffd7a32515e67bd"
1046
+ },
1047
+ {
1048
+ "userId": "dbd9b0a6d957cd7ba6a39b9a",
1049
+ "rank": 2,
1050
+ "createdAt": "2025-08-07T09:53:05.019Z",
1051
+ "_id": "68947781fffd7a32515ea317"
1052
+ }
1053
+ ],
1054
+ "feedback": [],
1055
+ "createdAt": "2025-08-06T23:43:03.564Z",
1056
+ "updatedAt": "2025-08-07T09:53:05.020Z",
1057
+ "__v": 3
1058
+ },
1059
+ {
1060
+ "_id": "6893e8e3fffd7a32515e5de3",
1061
+ "sourceTextId": "6890c6b1e6b9f36dd5b51ef3",
1062
+ "userId": "4a2e687e4f763331dda9bdd4",
1063
+ "username": "Han Heyang",
1064
+ "groupNumber": 3,
1065
+ "targetCulture": "Western",
1066
+ "targetLanguage": "English",
1067
+ "transcreation": "两种时尚配色可选,百搭又好看。",
1068
+ "explanation": "Translation submission",
1069
+ "culturalAdaptations": [],
1070
+ "isAnonymous": true,
1071
+ "status": "submitted",
1072
+ "difficulty": "intermediate",
1073
+ "votes": [
1074
+ {
1075
+ "userId": "b859743558631947ffa7921b",
1076
+ "rank": 2,
1077
+ "createdAt": "2025-08-07T03:38:20.297Z",
1078
+ "_id": "68941facfffd7a32515e6738"
1079
+ }
1080
+ ],
1081
+ "feedback": [],
1082
+ "createdAt": "2025-08-06T23:44:35.229Z",
1083
+ "updatedAt": "2025-08-07T03:38:20.298Z",
1084
+ "__v": 1
1085
+ },
1086
+ {
1087
+ "_id": "6893e90afffd7a32515e5e48",
1088
+ "sourceTextId": "6890c6b1e6b9f36dd5b51ef6",
1089
+ "userId": "779501db783372d27c41e31e",
1090
+ "username": "Li Wenhui",
1091
+ "groupNumber": 2,
1092
+ "targetCulture": "Western",
1093
+ "targetLanguage": "English",
1094
+ "transcreation": "温暖整个冬天!",
1095
+ "explanation": "Translation submission",
1096
+ "culturalAdaptations": [],
1097
+ "isAnonymous": true,
1098
+ "status": "submitted",
1099
+ "difficulty": "intermediate",
1100
+ "votes": [
1101
+ {
1102
+ "userId": "b859743558631947ffa7921b",
1103
+ "rank": 2,
1104
+ "createdAt": "2025-08-07T03:20:08.284Z",
1105
+ "_id": "68941b68fffd7a32515e6603"
1106
+ }
1107
+ ],
1108
+ "feedback": [],
1109
+ "createdAt": "2025-08-06T23:45:14.323Z",
1110
+ "updatedAt": "2025-08-07T03:20:08.285Z",
1111
+ "__v": 1
1112
+ },
1113
+ {
1114
+ "_id": "6893e92afffd7a32515e5e4f",
1115
+ "sourceTextId": "6890c6b1e6b9f36dd5b51ef9",
1116
+ "userId": "779501db783372d27c41e31e",
1117
+ "username": "Li Wenhui",
1118
+ "groupNumber": 2,
1119
+ "targetCulture": "Western",
1120
+ "targetLanguage": "English",
1121
+ "transcreation": "无论是寒冬散步...",
1122
+ "explanation": "Translation submission",
1123
+ "culturalAdaptations": [],
1124
+ "isAnonymous": true,
1125
+ "status": "submitted",
1126
+ "difficulty": "intermediate",
1127
+ "votes": [
1128
+ {
1129
+ "userId": "b859743558631947ffa7921b",
1130
+ "rank": 2,
1131
+ "createdAt": "2025-08-07T03:59:03.430Z",
1132
+ "_id": "68942487fffd7a32515e67e4"
1133
+ }
1134
+ ],
1135
+ "feedback": [],
1136
+ "createdAt": "2025-08-06T23:45:46.279Z",
1137
+ "updatedAt": "2025-08-07T03:59:03.430Z",
1138
+ "__v": 1
1139
+ },
1140
+ {
1141
+ "_id": "6893e930fffd7a32515e5e56",
1142
+ "sourceTextId": "6890c6b1e6b9f36dd5b51ef6",
1143
+ "userId": "4a2e687e4f763331dda9bdd4",
1144
+ "username": "Han Heyang",
1145
+ "groupNumber": 3,
1146
+ "targetCulture": "Western",
1147
+ "targetLanguage": "English",
1148
+ "transcreation": "冬日时光尽享舒适。",
1149
+ "explanation": "Translation submission",
1150
+ "culturalAdaptations": [],
1151
+ "isAnonymous": true,
1152
+ "status": "submitted",
1153
+ "difficulty": "intermediate",
1154
+ "votes": [
1155
+ {
1156
+ "userId": "b859743558631947ffa7921b",
1157
+ "rank": 3,
1158
+ "createdAt": "2025-08-07T03:20:02.653Z",
1159
+ "_id": "68941b62fffd7a32515e65eb"
1160
+ }
1161
+ ],
1162
+ "feedback": [],
1163
+ "createdAt": "2025-08-06T23:45:52.118Z",
1164
+ "updatedAt": "2025-08-07T03:20:02.660Z",
1165
+ "__v": 1
1166
+ },
1167
+ {
1168
+ "_id": "6893e941fffd7a32515e5e6e",
1169
+ "sourceTextId": "6890c6b2e6b9f36dd5b51f02",
1170
+ "userId": "36222902424b6b5e2a2d5177",
1171
+ "username": "You Lianxiang",
1172
+ "groupNumber": 1,
1173
+ "targetCulture": "Western",
1174
+ "targetLanguage": "English",
1175
+ "transcreation": "温馨提示:\n*我们的耐用舒适面料具有防水功能,可有效抵御细雨,但不适合在暴雨天气使用。\n尽管在小雨天,长时间的防雨可会使衣服表面被浸湿,但请您放心!接触狗狗毛发的一侧不会被雨水渗透!您家的毛孩子会干爽舒适!\n寒冬必备,给爱宠的保暖神器,即刻拥有!",
1176
+ "explanation": "Translation submission",
1177
+ "culturalAdaptations": [],
1178
+ "isAnonymous": true,
1179
+ "status": "submitted",
1180
+ "difficulty": "intermediate",
1181
+ "votes": [
1182
+ {
1183
+ "userId": "b859743558631947ffa7921b",
1184
+ "rank": 3,
1185
+ "createdAt": "2025-08-07T04:03:17.197Z",
1186
+ "_id": "68942585fffd7a32515e684f"
1187
+ }
1188
+ ],
1189
+ "feedback": [],
1190
+ "createdAt": "2025-08-06T23:46:09.224Z",
1191
+ "updatedAt": "2025-08-07T04:03:17.197Z",
1192
+ "__v": 1
1193
+ },
1194
+ {
1195
+ "_id": "6893e967fffd7a32515e5e93",
1196
+ "sourceTextId": "6890c6b1e6b9f36dd5b51efc",
1197
+ "userId": "779501db783372d27c41e31e",
1198
+ "username": "Li Wenhui",
1199
+ "groupNumber": 2,
1200
+ "targetCulture": "Western",
1201
+ "targetLanguage": "English",
1202
+ "transcreation": "还是天冷时宅家放松...",
1203
+ "explanation": "Translation submission",
1204
+ "culturalAdaptations": [],
1205
+ "isAnonymous": true,
1206
+ "status": "submitted",
1207
+ "difficulty": "intermediate",
1208
+ "votes": [
1209
+ {
1210
+ "userId": "b859743558631947ffa7921b",
1211
+ "rank": 2,
1212
+ "createdAt": "2025-08-07T03:30:24.735Z",
1213
+ "_id": "68941dd0fffd7a32515e66e0"
1214
+ }
1215
+ ],
1216
+ "feedback": [],
1217
+ "createdAt": "2025-08-06T23:46:47.239Z",
1218
+ "updatedAt": "2025-08-07T03:30:24.736Z",
1219
+ "__v": 1
1220
+ },
1221
+ {
1222
+ "_id": "6893ea47fffd7a32515e5f58",
1223
+ "sourceTextId": "6890c6b1e6b9f36dd5b51ef9",
1224
+ "userId": "4a2e687e4f763331dda9bdd4",
1225
+ "username": "Han Heyang",
1226
+ "groupNumber": 3,
1227
+ "targetCulture": "Western",
1228
+ "targetLanguage": "English",
1229
+ "transcreation": "无论是寒风中散步,",
1230
+ "explanation": "Translation submission",
1231
+ "culturalAdaptations": [],
1232
+ "isAnonymous": true,
1233
+ "status": "submitted",
1234
+ "difficulty": "intermediate",
1235
+ "votes": [
1236
+ {
1237
+ "userId": "b859743558631947ffa7921b",
1238
+ "rank": 1,
1239
+ "createdAt": "2025-08-07T03:59:04.564Z",
1240
+ "_id": "68942488fffd7a32515e6829"
1241
+ }
1242
+ ],
1243
+ "feedback": [],
1244
+ "createdAt": "2025-08-06T23:50:31.332Z",
1245
+ "updatedAt": "2025-08-07T03:59:04.565Z",
1246
+ "__v": 1
1247
+ },
1248
+ {
1249
+ "_id": "6893ea4bfffd7a32515e5f63",
1250
+ "sourceTextId": "6890c6b1e6b9f36dd5b51efc",
1251
+ "userId": "4a2e687e4f763331dda9bdd4",
1252
+ "username": "Han Heyang",
1253
+ "groupNumber": 3,
1254
+ "targetCulture": "Western",
1255
+ "targetLanguage": "English",
1256
+ "transcreation": "或是即便寒气侵袭,狗狗也能悠闲自得,\n",
1257
+ "explanation": "Translation submission",
1258
+ "culturalAdaptations": [],
1259
+ "isAnonymous": true,
1260
+ "status": "submitted",
1261
+ "difficulty": "intermediate",
1262
+ "votes": [
1263
+ {
1264
+ "userId": "36222902424b6b5e2a2d5177",
1265
+ "rank": 3,
1266
+ "createdAt": "2025-08-06T23:55:51.900Z",
1267
+ "_id": "6893eb87fffd7a32515e61ed"
1268
+ },
1269
+ {
1270
+ "userId": "dbd9b0a6d957cd7ba6a39b9a",
1271
+ "rank": 1,
1272
+ "createdAt": "2025-08-07T09:52:39.479Z",
1273
+ "_id": "68947767fffd7a32515ea225"
1274
+ }
1275
+ ],
1276
+ "feedback": [],
1277
+ "createdAt": "2025-08-06T23:50:35.738Z",
1278
+ "updatedAt": "2025-08-07T09:52:39.485Z",
1279
+ "__v": 2
1280
+ },
1281
+ {
1282
+ "_id": "6893ea55fffd7a32515e5f6a",
1283
+ "sourceTextId": "6890c6b2e6b9f36dd5b51f02",
1284
+ "userId": "779501db783372d27c41e31e",
1285
+ "username": "Li Wenhui",
1286
+ "groupNumber": 2,
1287
+ "targetCulture": "Western",
1288
+ "targetLanguage": "English",
1289
+ "transcreation": "温馨提示:*这款耐用舒适的面料具有防水功能,能在小雨中保护狗狗,但并不适合在暴雨中使用。\n下小雨时,面料表面可能随着时间略显潮湿,但请放心,水分不会渗透至内层——您的狗狗依然能保持干爽!\n>>> 为狗狗保暖从今日做起,快来选购吧!",
1290
+ "explanation": "Translation submission",
1291
+ "culturalAdaptations": [],
1292
+ "isAnonymous": true,
1293
+ "status": "submitted",
1294
+ "difficulty": "intermediate",
1295
+ "votes": [
1296
+ {
1297
+ "userId": "b859743558631947ffa7921b",
1298
+ "rank": 2,
1299
+ "createdAt": "2025-08-07T04:03:19.333Z",
1300
+ "_id": "68942587fffd7a32515e6876"
1301
+ },
1302
+ {
1303
+ "userId": "dbd9b0a6d957cd7ba6a39b9a",
1304
+ "rank": 1,
1305
+ "createdAt": "2025-08-07T09:53:37.578Z",
1306
+ "_id": "689477a1fffd7a32515ea395"
1307
+ }
1308
+ ],
1309
+ "feedback": [],
1310
+ "createdAt": "2025-08-06T23:50:45.440Z",
1311
+ "updatedAt": "2025-08-07T09:53:37.579Z",
1312
+ "__v": 2
1313
+ },
1314
+ {
1315
+ "_id": "6893ea7bfffd7a32515e5f91",
1316
+ "sourceTextId": "6890c6b2e6b9f36dd5b51eff",
1317
+ "userId": "4a2e687e4f763331dda9bdd4",
1318
+ "username": "Han Heyang",
1319
+ "groupNumber": 3,
1320
+ "targetCulture": "Western",
1321
+ "targetLanguage": "English",
1322
+ "transcreation": "这一件就足够!",
1323
+ "explanation": "Translation submission",
1324
+ "culturalAdaptations": [],
1325
+ "isAnonymous": true,
1326
+ "status": "submitted",
1327
+ "difficulty": "intermediate",
1328
+ "votes": [],
1329
+ "feedback": [],
1330
+ "createdAt": "2025-08-06T23:51:23.161Z",
1331
+ "updatedAt": "2025-08-06T23:51:23.162Z",
1332
+ "__v": 0
1333
+ },
1334
+ {
1335
+ "_id": "6893ebecfffd7a32515e6330",
1336
+ "sourceTextId": "6890c6b2e6b9f36dd5b51f02",
1337
+ "userId": "4a2e687e4f763331dda9bdd4",
1338
+ "username": "Han Heyang",
1339
+ "groupNumber": 3,
1340
+ "targetCulture": "Western",
1341
+ "targetLanguage": "English",
1342
+ "transcreation": "* 外套采用舒适耐用的防水面料,可有效抵御毛毛细雨,但不适用于暴雨天气。在轻微降雨下,表面可能略有潮湿,但不会渗透到内层,狗狗依然保持干爽舒适。\n>>>快给你的狗狗保暖吧!\n",
1343
+ "explanation": "Translation submission",
1344
+ "culturalAdaptations": [],
1345
+ "isAnonymous": true,
1346
+ "status": "submitted",
1347
+ "difficulty": "intermediate",
1348
+ "votes": [
1349
+ {
1350
+ "userId": "dbd9b0a6d957cd7ba6a39b9a",
1351
+ "rank": 3,
1352
+ "createdAt": "2025-08-07T09:53:40.970Z",
1353
+ "_id": "689477a4fffd7a32515ea415"
1354
+ }
1355
+ ],
1356
+ "feedback": [],
1357
+ "createdAt": "2025-08-06T23:57:32.763Z",
1358
+ "updatedAt": "2025-08-07T09:53:40.971Z",
1359
+ "__v": 1
1360
+ }
1361
+ ]
backups/complete-backup-2025-08-10T07-59-20-407Z/subtitles.json ADDED
@@ -0,0 +1,608 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ [
2
+ {
3
+ "_id": "689172dded762dabc93d9c51",
4
+ "segmentId": 1,
5
+ "startTime": "00:00:00,640",
6
+ "endTime": "00:00:02,150",
7
+ "duration": "1s 509.9999999999998ms",
8
+ "englishText": "Am I a bad person?",
9
+ "chineseTranslation": "我是坏人吗?",
10
+ "isProtected": false,
11
+ "lastModified": "2025-08-05T23:54:19.250Z",
12
+ "modificationHistory": [
13
+ {
14
+ "action": "update",
15
+ "timestamp": "2025-08-05T03:00:08.465Z",
16
+ "reason": "Time code update",
17
+ "_id": "689173b8fffd7a32515e365c"
18
+ },
19
+ {
20
+ "action": "update",
21
+ "timestamp": "2025-08-05T03:00:40.532Z",
22
+ "reason": "Time code update",
23
+ "_id": "689173d8fffd7a32515e3665"
24
+ },
25
+ {
26
+ "action": "update",
27
+ "timestamp": "2025-08-05T03:00:48.610Z",
28
+ "reason": "Time code update",
29
+ "_id": "689173e0fffd7a32515e366c"
30
+ },
31
+ {
32
+ "action": "update",
33
+ "timestamp": "2025-08-05T03:01:06.212Z",
34
+ "reason": "Time code update",
35
+ "_id": "689173f2fffd7a32515e3675"
36
+ },
37
+ {
38
+ "action": "update",
39
+ "timestamp": "2025-08-05T03:01:15.209Z",
40
+ "reason": "Time code update",
41
+ "_id": "689173fbfffd7a32515e3680"
42
+ },
43
+ {
44
+ "action": "update",
45
+ "timestamp": "2025-08-05T03:01:30.182Z",
46
+ "reason": "Time code update",
47
+ "_id": "6891740afffd7a32515e368d"
48
+ },
49
+ {
50
+ "action": "update",
51
+ "timestamp": "2025-08-05T03:01:41.243Z",
52
+ "reason": "Time code update",
53
+ "_id": "68917415fffd7a32515e369c"
54
+ },
55
+ {
56
+ "action": "update",
57
+ "timestamp": "2025-08-05T05:46:35.281Z",
58
+ "reason": "Translation update",
59
+ "_id": "68919abbfffd7a32515e44f3"
60
+ },
61
+ {
62
+ "action": "update",
63
+ "timestamp": "2025-08-05T23:54:19.250Z",
64
+ "reason": "Translation update",
65
+ "_id": "689299abfffd7a32515e52ba"
66
+ }
67
+ ],
68
+ "__v": 0,
69
+ "createdAt": "2025-08-05T02:56:29.213Z",
70
+ "updatedAt": "2025-08-05T23:54:19.250Z"
71
+ },
72
+ {
73
+ "_id": "689172dded762dabc93d9c52",
74
+ "segmentId": 2,
75
+ "startTime": "00:00:06,320",
76
+ "endTime": "00:00:07,700",
77
+ "duration": "1s 380ms",
78
+ "englishText": "Tell me. Am I?",
79
+ "chineseTranslation": "",
80
+ "isProtected": false,
81
+ "lastModified": "2025-08-05T14:23:18.534Z",
82
+ "modificationHistory": [
83
+ {
84
+ "action": "update",
85
+ "timestamp": "2025-08-05T03:00:25.265Z",
86
+ "reason": "Time code update",
87
+ "_id": "689173c9fffd7a32515e3660"
88
+ },
89
+ {
90
+ "action": "update",
91
+ "timestamp": "2025-08-05T14:23:18.534Z",
92
+ "reason": "Time code update",
93
+ "_id": "689213d6fffd7a32515e51c1"
94
+ }
95
+ ],
96
+ "__v": 0,
97
+ "createdAt": "2025-08-05T02:56:29.213Z",
98
+ "updatedAt": "2025-08-05T14:23:18.534Z"
99
+ },
100
+ {
101
+ "_id": "689172dded762dabc93d9c53",
102
+ "segmentId": 3,
103
+ "startTime": "00:00:08,350",
104
+ "endTime": "00:00:09,740",
105
+ "duration": "1s 390.00000000000045ms",
106
+ "englishText": "I'm single minded.",
107
+ "chineseTranslation": "",
108
+ "isProtected": false,
109
+ "lastModified": "2025-08-05T14:23:32.282Z",
110
+ "modificationHistory": [
111
+ {
112
+ "action": "update",
113
+ "timestamp": "2025-08-05T14:23:32.282Z",
114
+ "reason": "Time code update",
115
+ "_id": "689213e4fffd7a32515e51c6"
116
+ }
117
+ ],
118
+ "__v": 0,
119
+ "createdAt": "2025-08-05T02:56:29.213Z",
120
+ "updatedAt": "2025-08-05T14:23:32.282Z"
121
+ },
122
+ {
123
+ "_id": "689172dded762dabc93d9c54",
124
+ "segmentId": 4,
125
+ "startTime": "00:00:10,300",
126
+ "endTime": "00:00:11,680",
127
+ "duration": "1s 379.9999999999991ms",
128
+ "englishText": "I'm deceptive.",
129
+ "chineseTranslation": "",
130
+ "isProtected": false,
131
+ "lastModified": "2025-08-05T03:30:30.066Z",
132
+ "modificationHistory": [
133
+ {
134
+ "action": "update",
135
+ "timestamp": "2025-08-05T03:30:13.381Z",
136
+ "reason": "Time code update",
137
+ "_id": "68917ac5fffd7a32515e373d"
138
+ },
139
+ {
140
+ "action": "update",
141
+ "timestamp": "2025-08-05T03:30:30.066Z",
142
+ "reason": "Time code update",
143
+ "_id": "68917ad6fffd7a32515e3742"
144
+ }
145
+ ],
146
+ "__v": 0,
147
+ "createdAt": "2025-08-05T02:56:29.213Z",
148
+ "updatedAt": "2025-08-05T03:30:30.066Z"
149
+ },
150
+ {
151
+ "_id": "689172dded762dabc93d9c55",
152
+ "segmentId": 5,
153
+ "startTime": "00:00:12,050",
154
+ "endTime": "00:00:13,200",
155
+ "duration": "1s 149.99999999999864ms",
156
+ "englishText": "I'm obsessive.",
157
+ "chineseTranslation": "",
158
+ "isProtected": false,
159
+ "lastModified": "2025-08-05T03:30:59.970Z",
160
+ "modificationHistory": [
161
+ {
162
+ "action": "update",
163
+ "timestamp": "2025-08-05T03:30:48.091Z",
164
+ "reason": "Time code update",
165
+ "_id": "68917ae8fffd7a32515e3747"
166
+ },
167
+ {
168
+ "action": "update",
169
+ "timestamp": "2025-08-05T03:30:59.970Z",
170
+ "reason": "Time code update",
171
+ "_id": "68917af3fffd7a32515e374c"
172
+ }
173
+ ],
174
+ "__v": 0,
175
+ "createdAt": "2025-08-05T02:56:29.213Z",
176
+ "updatedAt": "2025-08-05T03:30:59.970Z"
177
+ },
178
+ {
179
+ "_id": "689172dded762dabc93d9c56",
180
+ "segmentId": 6,
181
+ "startTime": "00:00:13,780",
182
+ "endTime": "00:00:14,710",
183
+ "duration": "0s 930.0000000000015ms",
184
+ "englishText": "I'm selfish.",
185
+ "chineseTranslation": "",
186
+ "isProtected": false,
187
+ "lastModified": "2025-08-06T00:42:56.551Z",
188
+ "modificationHistory": [
189
+ {
190
+ "action": "update",
191
+ "timestamp": "2025-08-06T00:42:56.551Z",
192
+ "reason": "Time code update",
193
+ "_id": "6892a510fffd7a32515e53d6"
194
+ }
195
+ ],
196
+ "__v": 0,
197
+ "createdAt": "2025-08-05T02:56:29.213Z",
198
+ "updatedAt": "2025-08-06T00:42:56.552Z"
199
+ },
200
+ {
201
+ "_id": "689172dded762dabc93d9c57",
202
+ "segmentId": 7,
203
+ "startTime": "00:00:15,120",
204
+ "endTime": "00:00:17,200",
205
+ "duration": "2s 80ms",
206
+ "englishText": "Does that make me a bad person?",
207
+ "chineseTranslation": "",
208
+ "isProtected": false,
209
+ "lastModified": "2025-08-06T00:42:46.574Z",
210
+ "modificationHistory": [
211
+ {
212
+ "action": "update",
213
+ "timestamp": "2025-08-06T00:42:19.426Z",
214
+ "reason": "Time code update",
215
+ "_id": "6892a4ebfffd7a32515e53bf"
216
+ },
217
+ {
218
+ "action": "update",
219
+ "timestamp": "2025-08-06T00:42:37.867Z",
220
+ "reason": "Time code update",
221
+ "_id": "6892a4fdfffd7a32515e53c6"
222
+ },
223
+ {
224
+ "action": "update",
225
+ "timestamp": "2025-08-06T00:42:46.574Z",
226
+ "reason": "Time code update",
227
+ "_id": "6892a506fffd7a32515e53ce"
228
+ }
229
+ ],
230
+ "__v": 0,
231
+ "createdAt": "2025-08-05T02:56:29.213Z",
232
+ "updatedAt": "2025-08-06T00:42:46.574Z"
233
+ },
234
+ {
235
+ "_id": "689172dded762dabc93d9c58",
236
+ "segmentId": 8,
237
+ "startTime": "00:00:18,010",
238
+ "endTime": "00:00:19,660",
239
+ "duration": "1s 650ms",
240
+ "englishText": "Am I a bad person?",
241
+ "chineseTranslation": "",
242
+ "isProtected": false,
243
+ "lastModified": "2025-08-05T02:56:29.206Z",
244
+ "modificationHistory": [],
245
+ "__v": 0,
246
+ "createdAt": "2025-08-05T02:56:29.213Z",
247
+ "updatedAt": "2025-08-05T02:56:29.213Z"
248
+ },
249
+ {
250
+ "_id": "689172dded762dabc93d9c59",
251
+ "segmentId": 9,
252
+ "startTime": "00:00:20,870",
253
+ "endTime": "00:00:21,200",
254
+ "duration": "0s 329.9999999999983ms",
255
+ "englishText": "Am I?",
256
+ "chineseTranslation": "",
257
+ "isProtected": false,
258
+ "lastModified": "2025-08-05T03:02:44.633Z",
259
+ "modificationHistory": [
260
+ {
261
+ "action": "update",
262
+ "timestamp": "2025-08-05T03:02:31.358Z",
263
+ "reason": "Time code update",
264
+ "_id": "68917447fffd7a32515e36a6"
265
+ },
266
+ {
267
+ "action": "update",
268
+ "timestamp": "2025-08-05T03:02:37.977Z",
269
+ "reason": "Time code update",
270
+ "_id": "6891744dfffd7a32515e36ab"
271
+ },
272
+ {
273
+ "action": "update",
274
+ "timestamp": "2025-08-05T03:02:44.633Z",
275
+ "reason": "Time code update",
276
+ "_id": "68917454fffd7a32515e36b2"
277
+ }
278
+ ],
279
+ "__v": 0,
280
+ "createdAt": "2025-08-05T02:56:29.213Z",
281
+ "updatedAt": "2025-08-05T03:02:44.633Z"
282
+ },
283
+ {
284
+ "_id": "689172dded762dabc93d9c5a",
285
+ "segmentId": 10,
286
+ "startTime": "00:00:23,120",
287
+ "endTime": "00:00:24,390",
288
+ "duration": "1s 270ms",
289
+ "englishText": "I have no empathy.",
290
+ "chineseTranslation": "",
291
+ "isProtected": false,
292
+ "lastModified": "2025-08-05T02:56:29.206Z",
293
+ "modificationHistory": [],
294
+ "__v": 0,
295
+ "createdAt": "2025-08-05T02:56:29.214Z",
296
+ "updatedAt": "2025-08-05T02:56:29.214Z"
297
+ },
298
+ {
299
+ "_id": "689172dded762dabc93d9c5b",
300
+ "segmentId": 11,
301
+ "startTime": "00:00:25,540",
302
+ "endTime": "00:00:27,170",
303
+ "duration": "1s 630ms",
304
+ "englishText": "I don't respect you.",
305
+ "chineseTranslation": "",
306
+ "isProtected": false,
307
+ "lastModified": "2025-08-05T02:56:29.207Z",
308
+ "modificationHistory": [],
309
+ "__v": 0,
310
+ "createdAt": "2025-08-05T02:56:29.214Z",
311
+ "updatedAt": "2025-08-05T02:56:29.214Z"
312
+ },
313
+ {
314
+ "_id": "689172dded762dabc93d9c5c",
315
+ "segmentId": 12,
316
+ "startTime": "00:00:28,550",
317
+ "endTime": "00:00:29,880",
318
+ "duration": "1s 330ms",
319
+ "englishText": "I'm never satisfied.",
320
+ "chineseTranslation": "",
321
+ "isProtected": false,
322
+ "lastModified": "2025-08-05T02:56:29.207Z",
323
+ "modificationHistory": [],
324
+ "__v": 0,
325
+ "createdAt": "2025-08-05T02:56:29.214Z",
326
+ "updatedAt": "2025-08-05T02:56:29.214Z"
327
+ },
328
+ {
329
+ "_id": "689172dded762dabc93d9c5d",
330
+ "segmentId": 13,
331
+ "startTime": "00:00:30,440",
332
+ "endTime": "00:00:33,180",
333
+ "duration": "2s 740ms",
334
+ "englishText": "I have an obsession with power.",
335
+ "chineseTranslation": "",
336
+ "isProtected": false,
337
+ "lastModified": "2025-08-05T02:56:29.207Z",
338
+ "modificationHistory": [],
339
+ "__v": 0,
340
+ "createdAt": "2025-08-05T02:56:29.214Z",
341
+ "updatedAt": "2025-08-05T02:56:29.214Z"
342
+ },
343
+ {
344
+ "_id": "689172dded762dabc93d9c5e",
345
+ "segmentId": 14,
346
+ "startTime": "00:00:37,850",
347
+ "endTime": "00:00:38,950",
348
+ "duration": "1s 100ms",
349
+ "englishText": "I'm irrational.",
350
+ "chineseTranslation": "",
351
+ "isProtected": false,
352
+ "lastModified": "2025-08-05T02:56:29.207Z",
353
+ "modificationHistory": [],
354
+ "__v": 0,
355
+ "createdAt": "2025-08-05T02:56:29.214Z",
356
+ "updatedAt": "2025-08-05T02:56:29.214Z"
357
+ },
358
+ {
359
+ "_id": "689172dded762dabc93d9c5f",
360
+ "segmentId": 15,
361
+ "startTime": "00:00:39,930",
362
+ "endTime": "00:00:41,520",
363
+ "duration": "1s 590ms",
364
+ "englishText": "I have zero remorse.",
365
+ "chineseTranslation": "",
366
+ "isProtected": false,
367
+ "lastModified": "2025-08-05T02:56:29.207Z",
368
+ "modificationHistory": [],
369
+ "__v": 0,
370
+ "createdAt": "2025-08-05T02:56:29.214Z",
371
+ "updatedAt": "2025-08-05T02:56:29.214Z"
372
+ },
373
+ {
374
+ "_id": "689172dded762dabc93d9c60",
375
+ "segmentId": 16,
376
+ "startTime": "00:00:41,770",
377
+ "endTime": "00:00:43,900",
378
+ "duration": "2s 130ms",
379
+ "englishText": "I have no sense of compassion.",
380
+ "chineseTranslation": "",
381
+ "isProtected": false,
382
+ "lastModified": "2025-08-05T02:56:29.207Z",
383
+ "modificationHistory": [],
384
+ "__v": 0,
385
+ "createdAt": "2025-08-05T02:56:29.214Z",
386
+ "updatedAt": "2025-08-05T02:56:29.214Z"
387
+ },
388
+ {
389
+ "_id": "689172dded762dabc93d9c61",
390
+ "segmentId": 17,
391
+ "startTime": "00:00:44,480",
392
+ "endTime": "00:00:46,650",
393
+ "duration": "2s 170ms",
394
+ "englishText": "I'm delusional. I'm maniacal.",
395
+ "chineseTranslation": "",
396
+ "isProtected": false,
397
+ "lastModified": "2025-08-05T02:56:29.207Z",
398
+ "modificationHistory": [],
399
+ "__v": 0,
400
+ "createdAt": "2025-08-05T02:56:29.214Z",
401
+ "updatedAt": "2025-08-05T02:56:29.214Z"
402
+ },
403
+ {
404
+ "_id": "689172dded762dabc93d9c62",
405
+ "segmentId": 18,
406
+ "startTime": "00:00:46,960",
407
+ "endTime": "00:00:48,980",
408
+ "duration": "2s 20ms",
409
+ "englishText": "You think I'm a bad person?",
410
+ "chineseTranslation": "",
411
+ "isProtected": false,
412
+ "lastModified": "2025-08-05T02:56:29.207Z",
413
+ "modificationHistory": [],
414
+ "__v": 0,
415
+ "createdAt": "2025-08-05T02:56:29.214Z",
416
+ "updatedAt": "2025-08-05T02:56:29.214Z"
417
+ },
418
+ {
419
+ "_id": "689172dded762dabc93d9c63",
420
+ "segmentId": 19,
421
+ "startTime": "00:00:49,320",
422
+ "endTime": "00:00:52,500",
423
+ "duration": "3s 179.99999999999955ms",
424
+ "englishText": "Tell me. Tell me. Tell me.\nTell me. Am I?",
425
+ "chineseTranslation": "",
426
+ "isProtected": false,
427
+ "lastModified": "2025-08-05T03:31:16.927Z",
428
+ "modificationHistory": [
429
+ {
430
+ "action": "update",
431
+ "timestamp": "2025-08-05T03:31:16.927Z",
432
+ "reason": "Time code update",
433
+ "_id": "68917b04fffd7a32515e3751"
434
+ }
435
+ ],
436
+ "__v": 0,
437
+ "createdAt": "2025-08-05T02:56:29.214Z",
438
+ "updatedAt": "2025-08-05T03:31:16.927Z"
439
+ },
440
+ {
441
+ "_id": "689172dded762dabc93d9c64",
442
+ "segmentId": 20,
443
+ "startTime": "00:00:52,990",
444
+ "endTime": "00:00:54,700",
445
+ "duration": "1s 710.0000000000009ms",
446
+ "englishText": "I think I'm better than everyone else.",
447
+ "chineseTranslation": "",
448
+ "isProtected": false,
449
+ "lastModified": "2025-08-05T03:32:49.204Z",
450
+ "modificationHistory": [
451
+ {
452
+ "action": "update",
453
+ "timestamp": "2025-08-05T03:31:48.300Z",
454
+ "reason": "Time code update",
455
+ "_id": "68917b24fffd7a32515e3755"
456
+ },
457
+ {
458
+ "action": "update",
459
+ "timestamp": "2025-08-05T03:32:00.689Z",
460
+ "reason": "Time code update",
461
+ "_id": "68917b30fffd7a32515e375a"
462
+ },
463
+ {
464
+ "action": "update",
465
+ "timestamp": "2025-08-05T03:32:33.635Z",
466
+ "reason": "Time code update",
467
+ "_id": "68917b51fffd7a32515e376d"
468
+ },
469
+ {
470
+ "action": "update",
471
+ "timestamp": "2025-08-05T03:32:49.204Z",
472
+ "reason": "Time code update",
473
+ "_id": "68917b61fffd7a32515e3776"
474
+ }
475
+ ],
476
+ "__v": 0,
477
+ "createdAt": "2025-08-05T02:56:29.214Z",
478
+ "updatedAt": "2025-08-05T03:32:49.204Z"
479
+ },
480
+ {
481
+ "_id": "689172dded762dabc93d9c65",
482
+ "segmentId": 21,
483
+ "startTime": "00:00:55,300",
484
+ "endTime": "00:00:57,500",
485
+ "duration": "2s 200.00000000000273ms",
486
+ "englishText": "I want to take what's yours and never give it back.",
487
+ "chineseTranslation": "",
488
+ "isProtected": false,
489
+ "lastModified": "2025-08-05T03:32:17.995Z",
490
+ "modificationHistory": [
491
+ {
492
+ "action": "update",
493
+ "timestamp": "2025-08-05T03:21:58.147Z",
494
+ "reason": "Time code update",
495
+ "_id": "689178d6fffd7a32515e370d"
496
+ },
497
+ {
498
+ "action": "update",
499
+ "timestamp": "2025-08-05T03:22:11.295Z",
500
+ "reason": "Time code update",
501
+ "_id": "689178e3fffd7a32515e3712"
502
+ },
503
+ {
504
+ "action": "update",
505
+ "timestamp": "2025-08-05T03:22:19.995Z",
506
+ "reason": "Time code update",
507
+ "_id": "689178ebfffd7a32515e3719"
508
+ },
509
+ {
510
+ "action": "update",
511
+ "timestamp": "2025-08-05T03:22:29.207Z",
512
+ "reason": "Time code update",
513
+ "_id": "689178f5fffd7a32515e3722"
514
+ },
515
+ {
516
+ "action": "update",
517
+ "timestamp": "2025-08-05T03:32:17.995Z",
518
+ "reason": "Time code update",
519
+ "_id": "68917b41fffd7a32515e3763"
520
+ }
521
+ ],
522
+ "__v": 0,
523
+ "createdAt": "2025-08-05T02:56:29.214Z",
524
+ "updatedAt": "2025-08-05T03:32:17.995Z"
525
+ },
526
+ {
527
+ "_id": "689172dded762dabc93d9c66",
528
+ "segmentId": 22,
529
+ "startTime": "00:00:57,850",
530
+ "endTime": "00:01:00,640",
531
+ "duration": "2s 789.9999999999991ms",
532
+ "englishText": "What's mine is mine and what's yours is mine.",
533
+ "chineseTranslation": "",
534
+ "isProtected": false,
535
+ "lastModified": "2025-08-05T03:21:43.226Z",
536
+ "modificationHistory": [
537
+ {
538
+ "action": "update",
539
+ "timestamp": "2025-08-05T03:21:43.226Z",
540
+ "reason": "Time code update",
541
+ "_id": "689178c7fffd7a32515e3709"
542
+ }
543
+ ],
544
+ "__v": 0,
545
+ "createdAt": "2025-08-05T02:56:29.214Z",
546
+ "updatedAt": "2025-08-05T03:21:43.226Z"
547
+ },
548
+ {
549
+ "_id": "689172dded762dabc93d9c67",
550
+ "segmentId": 23,
551
+ "startTime": "00:01:06,920",
552
+ "endTime": "00:01:08,290",
553
+ "duration": "1s 370ms",
554
+ "englishText": "Am I a bad person?",
555
+ "chineseTranslation": "",
556
+ "isProtected": false,
557
+ "lastModified": "2025-08-05T02:56:29.208Z",
558
+ "modificationHistory": [],
559
+ "__v": 0,
560
+ "createdAt": "2025-08-05T02:56:29.214Z",
561
+ "updatedAt": "2025-08-05T02:56:29.214Z"
562
+ },
563
+ {
564
+ "_id": "689172dded762dabc93d9c68",
565
+ "segmentId": 24,
566
+ "startTime": "00:01:08,840",
567
+ "endTime": "00:01:10,420",
568
+ "duration": "1s 580ms",
569
+ "englishText": "Tell me. Am I?",
570
+ "chineseTranslation": "",
571
+ "isProtected": false,
572
+ "lastModified": "2025-08-05T02:56:29.208Z",
573
+ "modificationHistory": [],
574
+ "__v": 0,
575
+ "createdAt": "2025-08-05T02:56:29.214Z",
576
+ "updatedAt": "2025-08-05T02:56:29.214Z"
577
+ },
578
+ {
579
+ "_id": "689172dded762dabc93d9c69",
580
+ "segmentId": 25,
581
+ "startTime": "00:01:21,500",
582
+ "endTime": "00:01:23,650",
583
+ "duration": "2s 150ms",
584
+ "englishText": "Does that make me a bad person?",
585
+ "chineseTranslation": "",
586
+ "isProtected": false,
587
+ "lastModified": "2025-08-05T02:56:29.208Z",
588
+ "modificationHistory": [],
589
+ "__v": 0,
590
+ "createdAt": "2025-08-05T02:56:29.214Z",
591
+ "updatedAt": "2025-08-05T02:56:29.214Z"
592
+ },
593
+ {
594
+ "_id": "689172dded762dabc93d9c6a",
595
+ "segmentId": 26,
596
+ "startTime": "00:01:25,060",
597
+ "endTime": "00:01:26,900",
598
+ "duration": "1s 840ms",
599
+ "englishText": "Tell me. Does it?",
600
+ "chineseTranslation": "",
601
+ "isProtected": false,
602
+ "lastModified": "2025-08-05T02:56:29.208Z",
603
+ "modificationHistory": [],
604
+ "__v": 0,
605
+ "createdAt": "2025-08-05T02:56:29.214Z",
606
+ "updatedAt": "2025-08-05T02:56:29.214Z"
607
+ }
608
+ ]
backups/complete-backup-2025-08-10T07-59-20-407Z/subtitlesubmissions.json ADDED
@@ -0,0 +1 @@
 
 
1
+ []
backups/complete-backup-2025-08-10T07-59-20-407Z/users.json ADDED
@@ -0,0 +1,24 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ [
2
+ {
3
+ "_id": "688f54a81fdede6af7eb6b14",
4
+ "email": "hongchang.yu@monash.edu",
5
+ "__v": 0,
6
+ "createdAt": "2025-08-03T12:23:03.851Z",
7
+ "lastActive": "2025-08-03T12:23:03.851Z",
8
+ "password": "$2a$10$jI6X7HdNN/IkHDvo4nIK.OSwxweid1zLhjXLeeaaHGUZjBGcdra0G",
9
+ "role": "admin",
10
+ "targetCultures": [],
11
+ "username": "Tristan"
12
+ },
13
+ {
14
+ "_id": "688f54a81fdede6af7eb6b15",
15
+ "email": "student@example.com",
16
+ "__v": 0,
17
+ "createdAt": "2025-08-03T12:23:04.253Z",
18
+ "lastActive": "2025-08-03T12:23:04.253Z",
19
+ "password": "$2a$10$PJhcfgmQqiwdevAhvUg0ZuWZou68C3R00oWO3yNfEN.uQEaT3GT7G",
20
+ "role": "student",
21
+ "targetCultures": [],
22
+ "username": "student"
23
+ }
24
+ ]
backups/releases/release-2025-08-10T10-33-12-791Z/backend-a2c8b99.tar.gz ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:cd7316df6f9d14c57bbc956d6ef7101e513ed2fb947986ac501c5225c6a229a3
3
+ size 133
backups/releases/release-2025-08-10T10-33-12-791Z/db/sourcetexts.json ADDED
@@ -0,0 +1,449 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ [
2
+ {
3
+ "_id": "688eb20e6660d8ead04d5c3f",
4
+ "title": "Tutorial Task 1",
5
+ "content": "The early bird catches the worm.",
6
+ "sourceLanguage": "English",
7
+ "sourceType": "manual",
8
+ "category": "tutorial",
9
+ "weekNumber": 1,
10
+ "translationBrief": "Translate this proverb into Chinese, maintaining its cultural meaning.",
11
+ "difficulty": "beginner",
12
+ "tags": [],
13
+ "targetCultures": [],
14
+ "isActive": true,
15
+ "usageCount": 0,
16
+ "averageRating": 0,
17
+ "ratingCount": 0,
18
+ "culturalElements": [],
19
+ "createdAt": "2025-08-03T00:49:18.853Z",
20
+ "updatedAt": "2025-08-03T00:49:18.853Z",
21
+ "__v": 0
22
+ },
23
+ {
24
+ "_id": "688eb20e6660d8ead04d5c41",
25
+ "title": "Tutorial Task 2",
26
+ "content": "Actions speak louder than words.",
27
+ "sourceLanguage": "English",
28
+ "sourceType": "manual",
29
+ "category": "tutorial",
30
+ "weekNumber": 1,
31
+ "translationBrief": "Translate this saying into Chinese, preserving its idiomatic nature.",
32
+ "difficulty": "beginner",
33
+ "tags": [],
34
+ "targetCultures": [],
35
+ "isActive": true,
36
+ "usageCount": 0,
37
+ "averageRating": 0,
38
+ "ratingCount": 0,
39
+ "culturalElements": [],
40
+ "createdAt": "2025-08-03T00:49:18.856Z",
41
+ "updatedAt": "2025-08-03T00:49:18.856Z",
42
+ "__v": 0
43
+ },
44
+ {
45
+ "_id": "688eb20e6660d8ead04d5c43",
46
+ "title": "Tutorial Task 3",
47
+ "content": "A picture is worth a thousand words.",
48
+ "sourceLanguage": "English",
49
+ "sourceType": "manual",
50
+ "category": "tutorial",
51
+ "weekNumber": 1,
52
+ "translationBrief": "Translate this expression into Chinese, keeping its metaphorical meaning.",
53
+ "difficulty": "beginner",
54
+ "tags": [],
55
+ "targetCultures": [],
56
+ "isActive": true,
57
+ "usageCount": 0,
58
+ "averageRating": 0,
59
+ "ratingCount": 0,
60
+ "culturalElements": [],
61
+ "createdAt": "2025-08-03T00:49:18.858Z",
62
+ "updatedAt": "2025-08-03T00:49:18.858Z",
63
+ "__v": 0
64
+ },
65
+ {
66
+ "_id": "688eb20e6660d8ead04d5c45",
67
+ "title": "Tutorial Task 1 - Week 2",
68
+ "content": "A picture is worth a thousand words.",
69
+ "sourceLanguage": "English",
70
+ "sourceType": "manual",
71
+ "category": "tutorial",
72
+ "weekNumber": 2,
73
+ "translationBrief": "Translate this saying into Chinese, considering the visual context of the image.",
74
+ "imageUrl": "https://images.unsplash.com/photo-1506905925346-21bda4d32df4?w=800&h=600&fit=crop",
75
+ "imageAlt": "A beautiful landscape photograph showing mountains and lake",
76
+ "difficulty": "intermediate",
77
+ "tags": [],
78
+ "targetCultures": [],
79
+ "isActive": true,
80
+ "usageCount": 0,
81
+ "averageRating": 0,
82
+ "ratingCount": 0,
83
+ "culturalElements": [],
84
+ "createdAt": "2025-08-03T00:49:18.861Z",
85
+ "updatedAt": "2025-08-03T00:49:18.861Z",
86
+ "__v": 0
87
+ },
88
+ {
89
+ "_id": "688eb20e6660d8ead04d5c47",
90
+ "title": "Tutorial Task 2 - Week 2",
91
+ "content": "The early bird catches the worm.",
92
+ "sourceLanguage": "English",
93
+ "sourceType": "manual",
94
+ "category": "tutorial",
95
+ "weekNumber": 2,
96
+ "translationBrief": "Translate this proverb into Chinese, considering the visual elements in the image.",
97
+ "imageUrl": "https://images.unsplash.com/photo-1444464666168-49d633b86797?w=800&h=600&fit=crop",
98
+ "imageAlt": "A bird perched on a branch during sunrise",
99
+ "difficulty": "intermediate",
100
+ "tags": [],
101
+ "targetCultures": [],
102
+ "isActive": true,
103
+ "usageCount": 0,
104
+ "averageRating": 0,
105
+ "ratingCount": 0,
106
+ "culturalElements": [],
107
+ "createdAt": "2025-08-03T00:49:18.863Z",
108
+ "updatedAt": "2025-08-03T00:49:18.863Z",
109
+ "__v": 0
110
+ },
111
+ {
112
+ "_id": "688eb20e6660d8ead04d5c49",
113
+ "title": "Tutorial Task 3 - Week 2",
114
+ "content": "Actions speak louder than words.",
115
+ "sourceLanguage": "English",
116
+ "sourceType": "manual",
117
+ "category": "tutorial",
118
+ "weekNumber": 2,
119
+ "translationBrief": "Translate this saying into Chinese, considering the visual context provided.",
120
+ "imageUrl": "https://images.unsplash.com/photo-1557804506-669a67965ba0?w=800&h=600&fit=crop",
121
+ "imageAlt": "People working together in a collaborative environment",
122
+ "difficulty": "intermediate",
123
+ "tags": [],
124
+ "targetCultures": [],
125
+ "isActive": true,
126
+ "usageCount": 0,
127
+ "averageRating": 0,
128
+ "ratingCount": 0,
129
+ "culturalElements": [],
130
+ "createdAt": "2025-08-03T00:49:18.864Z",
131
+ "updatedAt": "2025-08-03T00:49:18.864Z",
132
+ "__v": 0
133
+ },
134
+ {
135
+ "_id": "688eb20e6660d8ead04d5c4b",
136
+ "title": "Week 1 Practice 1",
137
+ "content": "为什么睡前一定要吃夜宵?因为这样才不会做饿梦。",
138
+ "sourceLanguage": "Chinese",
139
+ "sourceType": "manual",
140
+ "category": "weekly-practice",
141
+ "weekNumber": 1,
142
+ "translationBrief": "Translate this humorous Chinese text into English, maintaining its wordplay and cultural humor.",
143
+ "difficulty": "intermediate",
144
+ "tags": [],
145
+ "targetCultures": [],
146
+ "isActive": true,
147
+ "usageCount": 0,
148
+ "averageRating": 0,
149
+ "ratingCount": 0,
150
+ "culturalElements": [],
151
+ "createdAt": "2025-08-03T00:49:18.866Z",
152
+ "updatedAt": "2025-08-03T00:49:18.866Z",
153
+ "__v": 0
154
+ },
155
+ {
156
+ "_id": "688eb20e6660d8ead04d5c4d",
157
+ "title": "Week 1 Practice 2",
158
+ "content": "女娲用什么补天?强扭的瓜。",
159
+ "sourceLanguage": "Chinese",
160
+ "sourceType": "manual",
161
+ "category": "weekly-practice",
162
+ "weekNumber": 1,
163
+ "translationBrief": "Translate this Chinese riddle into English, preserving its clever wordplay.",
164
+ "difficulty": "intermediate",
165
+ "tags": [],
166
+ "targetCultures": [],
167
+ "isActive": true,
168
+ "usageCount": 0,
169
+ "averageRating": 0,
170
+ "ratingCount": 0,
171
+ "culturalElements": [],
172
+ "createdAt": "2025-08-03T00:49:18.868Z",
173
+ "updatedAt": "2025-08-03T00:49:18.868Z",
174
+ "__v": 0
175
+ },
176
+ {
177
+ "_id": "688eb20e6660d8ead04d5c4f",
178
+ "title": "Week 1 Practice 3",
179
+ "content": "你知道如何区分真假大象吗?把他们仍进水中,真相会浮出水面的。",
180
+ "sourceLanguage": "Chinese",
181
+ "sourceType": "manual",
182
+ "category": "weekly-practice",
183
+ "weekNumber": 1,
184
+ "translationBrief": "Translate this Chinese joke into English, maintaining its pun and humor.",
185
+ "difficulty": "intermediate",
186
+ "tags": [],
187
+ "targetCultures": [],
188
+ "isActive": true,
189
+ "usageCount": 0,
190
+ "averageRating": 0,
191
+ "ratingCount": 0,
192
+ "culturalElements": [],
193
+ "createdAt": "2025-08-03T00:49:18.869Z",
194
+ "updatedAt": "2025-08-03T00:49:18.869Z",
195
+ "__v": 0
196
+ },
197
+ {
198
+ "_id": "688eb20e6660d8ead04d5c51",
199
+ "title": "Week 1 Practice 4",
200
+ "content": "What if Soy milk is just regular milk introducing itself in Spanish.",
201
+ "sourceLanguage": "English",
202
+ "sourceType": "manual",
203
+ "category": "weekly-practice",
204
+ "weekNumber": 1,
205
+ "translationBrief": "Translate this English joke into Chinese, preserving its linguistic humor.",
206
+ "difficulty": "intermediate",
207
+ "tags": [],
208
+ "targetCultures": [],
209
+ "isActive": true,
210
+ "usageCount": 0,
211
+ "averageRating": 0,
212
+ "ratingCount": 0,
213
+ "culturalElements": [],
214
+ "createdAt": "2025-08-03T00:49:18.870Z",
215
+ "updatedAt": "2025-08-03T00:49:18.870Z",
216
+ "__v": 0
217
+ },
218
+ {
219
+ "_id": "688eb20e6660d8ead04d5c53",
220
+ "title": "Week 1 Practice 5",
221
+ "content": "I can't believe I got fired from the calendar factory. All I did was take a day off.",
222
+ "sourceLanguage": "English",
223
+ "sourceType": "manual",
224
+ "category": "weekly-practice",
225
+ "weekNumber": 1,
226
+ "translationBrief": "Translate this English joke into Chinese, maintaining its wordplay humor.",
227
+ "difficulty": "intermediate",
228
+ "tags": [],
229
+ "targetCultures": [],
230
+ "isActive": true,
231
+ "usageCount": 0,
232
+ "averageRating": 0,
233
+ "ratingCount": 0,
234
+ "culturalElements": [],
235
+ "createdAt": "2025-08-03T00:49:18.871Z",
236
+ "updatedAt": "2025-08-03T00:49:18.871Z",
237
+ "__v": 0
238
+ },
239
+ {
240
+ "_id": "688eb20e6660d8ead04d5c55",
241
+ "title": "Week 1 Practice 6",
242
+ "content": "When life gives you melons, you might be dyslexic.",
243
+ "sourceLanguage": "English",
244
+ "sourceType": "manual",
245
+ "category": "weekly-practice",
246
+ "weekNumber": 1,
247
+ "translationBrief": "Translate this English wordplay into Chinese, preserving its linguistic cleverness.",
248
+ "difficulty": "intermediate",
249
+ "tags": [],
250
+ "targetCultures": [],
251
+ "isActive": true,
252
+ "usageCount": 0,
253
+ "averageRating": 0,
254
+ "ratingCount": 0,
255
+ "culturalElements": [],
256
+ "createdAt": "2025-08-03T00:49:18.872Z",
257
+ "updatedAt": "2025-08-03T00:49:18.872Z",
258
+ "__v": 0
259
+ },
260
+ {
261
+ "_id": "688eb20e6660d8ead04d5c57",
262
+ "title": "Week 2 Practice: Marketing Adaptation",
263
+ "content": "Adapt this product description: \"Revolutionary technology that changes everything\" for a skeptical audience.",
264
+ "sourceLanguage": "English",
265
+ "sourceType": "manual",
266
+ "category": "weekly-practice",
267
+ "weekNumber": 2,
268
+ "translationBrief": "Make this claim more credible and less hyperbolic for a skeptical, evidence-based audience.",
269
+ "difficulty": "intermediate",
270
+ "tags": [],
271
+ "targetCultures": [],
272
+ "isActive": true,
273
+ "usageCount": 0,
274
+ "averageRating": 0,
275
+ "ratingCount": 0,
276
+ "culturalElements": [],
277
+ "createdAt": "2025-08-03T00:49:18.873Z",
278
+ "updatedAt": "2025-08-03T13:16:38.390Z",
279
+ "__v": 0
280
+ },
281
+ {
282
+ "_id": "688eb20e6660d8ead04d5c59",
283
+ "title": "Week 3 Practice: Emotional Translation",
284
+ "content": "Translate this emotional expression: \"I am so happy\" for different cultural contexts where emotional expression varies.",
285
+ "sourceLanguage": "English",
286
+ "sourceType": "manual",
287
+ "category": "weekly-practice",
288
+ "weekNumber": 3,
289
+ "translationBrief": "Adapt this expression for cultures that value emotional restraint and those that encourage emotional expression.",
290
+ "difficulty": "intermediate",
291
+ "tags": [],
292
+ "targetCultures": [],
293
+ "isActive": true,
294
+ "usageCount": 0,
295
+ "averageRating": 0,
296
+ "ratingCount": 0,
297
+ "culturalElements": [],
298
+ "createdAt": "2025-08-03T00:49:18.875Z",
299
+ "updatedAt": "2025-08-03T13:16:38.392Z",
300
+ "__v": 0
301
+ },
302
+ {
303
+ "_id": "688eb20e6660d8ead04d5c5b",
304
+ "title": "Week 4 Practice: Humor Translation",
305
+ "content": "Adapt this joke: \"Why did the chicken cross the road? To get to the other side!\" for a culture that doesn't have this idiom.",
306
+ "sourceLanguage": "English",
307
+ "sourceType": "manual",
308
+ "category": "weekly-practice",
309
+ "weekNumber": 4,
310
+ "translationBrief": "Create a culturally appropriate version that maintains the humor while being accessible to the target culture.",
311
+ "difficulty": "intermediate",
312
+ "tags": [],
313
+ "targetCultures": [],
314
+ "isActive": true,
315
+ "usageCount": 0,
316
+ "averageRating": 0,
317
+ "ratingCount": 0,
318
+ "culturalElements": [],
319
+ "createdAt": "2025-08-03T00:49:18.876Z",
320
+ "updatedAt": "2025-08-03T13:16:38.393Z",
321
+ "__v": 0
322
+ },
323
+ {
324
+ "_id": "688eb20e6660d8ead04d5c5d",
325
+ "title": "Week 5 Practice: Formal vs Informal",
326
+ "content": "Translate this business communication: \"We would appreciate your prompt response\" for different formality levels.",
327
+ "sourceLanguage": "English",
328
+ "sourceType": "manual",
329
+ "category": "weekly-practice",
330
+ "weekNumber": 5,
331
+ "translationBrief": "Create versions for very formal, moderately formal, and casual business contexts.",
332
+ "difficulty": "intermediate",
333
+ "tags": [],
334
+ "targetCultures": [],
335
+ "isActive": true,
336
+ "usageCount": 0,
337
+ "averageRating": 0,
338
+ "ratingCount": 0,
339
+ "culturalElements": [],
340
+ "createdAt": "2025-08-03T00:49:18.877Z",
341
+ "updatedAt": "2025-08-03T13:16:38.395Z",
342
+ "__v": 0
343
+ },
344
+ {
345
+ "_id": "688eb20e6660d8ead04d5c5f",
346
+ "title": "Week 6 Practice: Cultural Values",
347
+ "content": "Adapt this value statement: \"Success comes from hard work\" for cultures with different views on success and work.",
348
+ "sourceLanguage": "English",
349
+ "sourceType": "manual",
350
+ "category": "weekly-practice",
351
+ "weekNumber": 6,
352
+ "translationBrief": "Consider cultures that value collaboration over individual achievement, and those that emphasize luck or fate.",
353
+ "difficulty": "intermediate",
354
+ "tags": [],
355
+ "targetCultures": [],
356
+ "isActive": true,
357
+ "usageCount": 0,
358
+ "averageRating": 0,
359
+ "ratingCount": 0,
360
+ "culturalElements": [],
361
+ "createdAt": "2025-08-03T00:49:18.878Z",
362
+ "updatedAt": "2025-08-03T13:16:38.396Z",
363
+ "__v": 0
364
+ },
365
+ {
366
+ "_id": "688f6136478f8e71297262a5",
367
+ "category": "tutorial",
368
+ "title": "Cultural Adaptation Exercise",
369
+ "weekNumber": 1,
370
+ "__v": 0,
371
+ "averageRating": 0,
372
+ "content": "Translate the following marketing slogan for a Western audience: \"Our product brings harmony to your life.\" Consider cultural differences in how harmony is perceived.",
373
+ "createdAt": "2025-08-03T13:16:38.377Z",
374
+ "culturalElements": [],
375
+ "difficulty": "intermediate",
376
+ "isActive": true,
377
+ "ratingCount": 0,
378
+ "sourceLanguage": "English",
379
+ "sourceType": "manual",
380
+ "tags": [],
381
+ "targetCultures": [],
382
+ "translationBrief": "Adapt this slogan for a Western audience, focusing on individualism and personal achievement rather than collective harmony.",
383
+ "updatedAt": "2025-08-03T13:16:38.377Z",
384
+ "usageCount": 0
385
+ },
386
+ {
387
+ "_id": "688f6136478f8e71297262a6",
388
+ "weekNumber": 1,
389
+ "title": "Localization Challenge",
390
+ "category": "tutorial",
391
+ "__v": 0,
392
+ "averageRating": 0,
393
+ "content": "Adapt this restaurant menu item: \"Spicy Dragon Noodles\" for a conservative American audience.",
394
+ "createdAt": "2025-08-03T13:16:38.384Z",
395
+ "culturalElements": [],
396
+ "difficulty": "intermediate",
397
+ "isActive": true,
398
+ "ratingCount": 0,
399
+ "sourceLanguage": "English",
400
+ "sourceType": "manual",
401
+ "tags": [],
402
+ "targetCultures": [],
403
+ "translationBrief": "Make this dish more appealing to conservative American diners who might be unfamiliar with Asian cuisine.",
404
+ "updatedAt": "2025-08-03T13:16:38.384Z",
405
+ "usageCount": 0
406
+ },
407
+ {
408
+ "_id": "688f6136478f8e71297262a7",
409
+ "weekNumber": 1,
410
+ "title": "Brand Voice Translation",
411
+ "category": "tutorial",
412
+ "__v": 0,
413
+ "averageRating": 0,
414
+ "content": "Translate this luxury brand tagline: \"Excellence in every detail\" for a younger, more casual audience.",
415
+ "createdAt": "2025-08-03T13:16:38.386Z",
416
+ "culturalElements": [],
417
+ "difficulty": "intermediate",
418
+ "isActive": true,
419
+ "ratingCount": 0,
420
+ "sourceLanguage": "English",
421
+ "sourceType": "manual",
422
+ "tags": [],
423
+ "targetCultures": [],
424
+ "translationBrief": "Maintain the premium feel while making it more accessible and relatable to younger consumers.",
425
+ "updatedAt": "2025-08-03T13:16:38.386Z",
426
+ "usageCount": 0
427
+ },
428
+ {
429
+ "_id": "688f6136478f8e71297262a8",
430
+ "category": "weekly-practice",
431
+ "title": "Week 1 Practice: Cultural Nuances",
432
+ "weekNumber": 1,
433
+ "__v": 0,
434
+ "averageRating": 0,
435
+ "content": "Translate this greeting: \"How are you?\" for different cultural contexts. Consider formality levels and cultural expectations.",
436
+ "createdAt": "2025-08-03T13:16:38.388Z",
437
+ "culturalElements": [],
438
+ "difficulty": "intermediate",
439
+ "isActive": true,
440
+ "ratingCount": 0,
441
+ "sourceLanguage": "English",
442
+ "sourceType": "manual",
443
+ "tags": [],
444
+ "targetCultures": [],
445
+ "translationBrief": "Adapt this greeting for formal business settings, casual social situations, and family contexts.",
446
+ "updatedAt": "2025-08-03T13:16:38.388Z",
447
+ "usageCount": 0
448
+ }
449
+ ]
backups/releases/release-2025-08-10T10-33-12-791Z/db/submissions.json ADDED
@@ -0,0 +1,56 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ [
2
+ {
3
+ "_id": "688821c4bd12424220ef62bf",
4
+ "sourceTextId": "68876bc88b4948b8d901fb71",
5
+ "userId": "61fed5b42908334dcf885324",
6
+ "targetCulture": "Western",
7
+ "targetLanguage": "English",
8
+ "transcreation": "Why raid the fridge right before lights-out? Because if you skip it, sweet dreams turn into bite-mares.",
9
+ "explanation": "Translation submission",
10
+ "culturalAdaptations": [],
11
+ "isAnonymous": false,
12
+ "status": "submitted",
13
+ "difficulty": "intermediate",
14
+ "votes": [],
15
+ "feedback": [],
16
+ "createdAt": "2025-07-29T01:20:04.973Z",
17
+ "updatedAt": "2025-07-29T01:20:04.977Z",
18
+ "__v": 0
19
+ },
20
+ {
21
+ "_id": "688821d7bd12424220ef62c6",
22
+ "sourceTextId": "68876bc88b4948b8d901fb72",
23
+ "userId": "61fed5b42908334dcf885324",
24
+ "targetCulture": "Western",
25
+ "targetLanguage": "English",
26
+ "transcreation": "What did the goddess Nuwa use to plug the hole in the sky? A square peg—because even a deity sometimes “fixes” a round hole the hard way.",
27
+ "explanation": "Translation submission",
28
+ "culturalAdaptations": [],
29
+ "isAnonymous": false,
30
+ "status": "submitted",
31
+ "difficulty": "intermediate",
32
+ "votes": [],
33
+ "feedback": [],
34
+ "createdAt": "2025-07-29T01:20:23.396Z",
35
+ "updatedAt": "2025-07-29T01:20:23.397Z",
36
+ "__v": 0
37
+ },
38
+ {
39
+ "_id": "688821debd12424220ef62cd",
40
+ "sourceTextId": "68876bc88b4948b8d901fb73",
41
+ "userId": "61fed5b42908334dcf885324",
42
+ "targetCulture": "Western",
43
+ "targetLanguage": "English",
44
+ "transcreation": "How can you tell a real elephant from a fake one? Toss them both in the pool—give it a moment and the ele-fact will float to the top.",
45
+ "explanation": "Translation submission",
46
+ "culturalAdaptations": [],
47
+ "isAnonymous": false,
48
+ "status": "submitted",
49
+ "difficulty": "intermediate",
50
+ "votes": [],
51
+ "feedback": [],
52
+ "createdAt": "2025-07-29T01:20:30.502Z",
53
+ "updatedAt": "2025-07-29T01:20:30.502Z",
54
+ "__v": 0
55
+ }
56
+ ]
backups/releases/release-2025-08-10T10-33-12-791Z/db/subtitles.json ADDED
@@ -0,0 +1,392 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ [
2
+ {
3
+ "_id": "6890e04422f0d04331e9caaf",
4
+ "segmentId": 1,
5
+ "startTime": "00:00:00,640",
6
+ "endTime": "00:00:02,400",
7
+ "duration": "1s 760ms",
8
+ "englishText": "Am I a bad person?",
9
+ "chineseTranslation": "",
10
+ "isProtected": false,
11
+ "lastModified": "2025-08-04T16:31:00.464Z",
12
+ "modificationHistory": [],
13
+ "__v": 0,
14
+ "createdAt": "2025-08-04T16:31:00.471Z",
15
+ "updatedAt": "2025-08-04T16:31:00.471Z"
16
+ },
17
+ {
18
+ "_id": "6890e04422f0d04331e9cab0",
19
+ "segmentId": 2,
20
+ "startTime": "00:00:06,320",
21
+ "endTime": "00:00:07,860",
22
+ "duration": "1s 540ms",
23
+ "englishText": "Tell me. Am I?",
24
+ "chineseTranslation": "",
25
+ "isProtected": false,
26
+ "lastModified": "2025-08-04T16:31:00.465Z",
27
+ "modificationHistory": [],
28
+ "__v": 0,
29
+ "createdAt": "2025-08-04T16:31:00.472Z",
30
+ "updatedAt": "2025-08-04T16:31:00.472Z"
31
+ },
32
+ {
33
+ "_id": "6890e04422f0d04331e9cab1",
34
+ "segmentId": 3,
35
+ "startTime": "00:00:08,480",
36
+ "endTime": "00:00:09,740",
37
+ "duration": "1s 260ms",
38
+ "englishText": "I'm single minded.",
39
+ "chineseTranslation": "",
40
+ "isProtected": false,
41
+ "lastModified": "2025-08-04T16:31:00.465Z",
42
+ "modificationHistory": [],
43
+ "__v": 0,
44
+ "createdAt": "2025-08-04T16:31:00.472Z",
45
+ "updatedAt": "2025-08-04T16:31:00.472Z"
46
+ },
47
+ {
48
+ "_id": "6890e04422f0d04331e9cab2",
49
+ "segmentId": 4,
50
+ "startTime": "00:00:10,570",
51
+ "endTime": "00:00:11,780",
52
+ "duration": "1s 210ms",
53
+ "englishText": "I'm deceptive.",
54
+ "chineseTranslation": "",
55
+ "isProtected": false,
56
+ "lastModified": "2025-08-04T16:31:00.465Z",
57
+ "modificationHistory": [],
58
+ "__v": 0,
59
+ "createdAt": "2025-08-04T16:31:00.472Z",
60
+ "updatedAt": "2025-08-04T16:31:00.472Z"
61
+ },
62
+ {
63
+ "_id": "6890e04422f0d04331e9cab3",
64
+ "segmentId": 5,
65
+ "startTime": "00:00:12,050",
66
+ "endTime": "00:00:13,490",
67
+ "duration": "1s 440ms",
68
+ "englishText": "I'm obsessive.",
69
+ "chineseTranslation": "",
70
+ "isProtected": false,
71
+ "lastModified": "2025-08-04T16:31:00.465Z",
72
+ "modificationHistory": [],
73
+ "__v": 0,
74
+ "createdAt": "2025-08-04T16:31:00.472Z",
75
+ "updatedAt": "2025-08-04T16:31:00.472Z"
76
+ },
77
+ {
78
+ "_id": "6890e04422f0d04331e9cab4",
79
+ "segmentId": 6,
80
+ "startTime": "00:00:13,780",
81
+ "endTime": "00:00:14,910",
82
+ "duration": "1s 130ms",
83
+ "englishText": "I'm selfish.",
84
+ "chineseTranslation": "",
85
+ "isProtected": false,
86
+ "lastModified": "2025-08-04T16:31:00.466Z",
87
+ "modificationHistory": [],
88
+ "__v": 0,
89
+ "createdAt": "2025-08-04T16:31:00.472Z",
90
+ "updatedAt": "2025-08-04T16:31:00.472Z"
91
+ },
92
+ {
93
+ "_id": "6890e04422f0d04331e9cab5",
94
+ "segmentId": 7,
95
+ "startTime": "00:00:15,120",
96
+ "endTime": "00:00:17,200",
97
+ "duration": "2s 80ms",
98
+ "englishText": "Does that make me a bad person?",
99
+ "chineseTranslation": "",
100
+ "isProtected": false,
101
+ "lastModified": "2025-08-04T16:31:00.466Z",
102
+ "modificationHistory": [],
103
+ "__v": 0,
104
+ "createdAt": "2025-08-04T16:31:00.472Z",
105
+ "updatedAt": "2025-08-04T16:31:00.472Z"
106
+ },
107
+ {
108
+ "_id": "6890e04422f0d04331e9cab6",
109
+ "segmentId": 8,
110
+ "startTime": "00:00:18,010",
111
+ "endTime": "00:00:19,660",
112
+ "duration": "1s 650ms",
113
+ "englishText": "Am I a bad person?",
114
+ "chineseTranslation": "",
115
+ "isProtected": false,
116
+ "lastModified": "2025-08-04T16:31:00.466Z",
117
+ "modificationHistory": [],
118
+ "__v": 0,
119
+ "createdAt": "2025-08-04T16:31:00.472Z",
120
+ "updatedAt": "2025-08-04T16:31:00.472Z"
121
+ },
122
+ {
123
+ "_id": "6890e04422f0d04331e9cab7",
124
+ "segmentId": 9,
125
+ "startTime": "00:00:20,870",
126
+ "endTime": "00:00:21,870",
127
+ "duration": "1s 0ms",
128
+ "englishText": "Am I?",
129
+ "chineseTranslation": "",
130
+ "isProtected": false,
131
+ "lastModified": "2025-08-04T16:31:00.466Z",
132
+ "modificationHistory": [],
133
+ "__v": 0,
134
+ "createdAt": "2025-08-04T16:31:00.472Z",
135
+ "updatedAt": "2025-08-04T16:31:00.472Z"
136
+ },
137
+ {
138
+ "_id": "6890e04422f0d04331e9cab8",
139
+ "segmentId": 10,
140
+ "startTime": "00:00:23,120",
141
+ "endTime": "00:00:24,390",
142
+ "duration": "1s 270ms",
143
+ "englishText": "I have no empathy.",
144
+ "chineseTranslation": "",
145
+ "isProtected": false,
146
+ "lastModified": "2025-08-04T16:31:00.466Z",
147
+ "modificationHistory": [],
148
+ "__v": 0,
149
+ "createdAt": "2025-08-04T16:31:00.472Z",
150
+ "updatedAt": "2025-08-04T16:31:00.472Z"
151
+ },
152
+ {
153
+ "_id": "6890e04422f0d04331e9cab9",
154
+ "segmentId": 11,
155
+ "startTime": "00:00:25,540",
156
+ "endTime": "00:00:27,170",
157
+ "duration": "1s 630ms",
158
+ "englishText": "I don't respect you.",
159
+ "chineseTranslation": "",
160
+ "isProtected": false,
161
+ "lastModified": "2025-08-04T16:31:00.466Z",
162
+ "modificationHistory": [],
163
+ "__v": 0,
164
+ "createdAt": "2025-08-04T16:31:00.472Z",
165
+ "updatedAt": "2025-08-04T16:31:00.472Z"
166
+ },
167
+ {
168
+ "_id": "6890e04422f0d04331e9caba",
169
+ "segmentId": 12,
170
+ "startTime": "00:00:28,550",
171
+ "endTime": "00:00:29,880",
172
+ "duration": "1s 330ms",
173
+ "englishText": "I'm never satisfied.",
174
+ "chineseTranslation": "",
175
+ "isProtected": false,
176
+ "lastModified": "2025-08-04T16:31:00.467Z",
177
+ "modificationHistory": [],
178
+ "__v": 0,
179
+ "createdAt": "2025-08-04T16:31:00.472Z",
180
+ "updatedAt": "2025-08-04T16:31:00.472Z"
181
+ },
182
+ {
183
+ "_id": "6890e04422f0d04331e9cabb",
184
+ "segmentId": 13,
185
+ "startTime": "00:00:30,440",
186
+ "endTime": "00:00:33,180",
187
+ "duration": "2s 740ms",
188
+ "englishText": "I have an obsession with power.",
189
+ "chineseTranslation": "",
190
+ "isProtected": false,
191
+ "lastModified": "2025-08-04T16:31:00.467Z",
192
+ "modificationHistory": [],
193
+ "__v": 0,
194
+ "createdAt": "2025-08-04T16:31:00.472Z",
195
+ "updatedAt": "2025-08-04T16:31:00.472Z"
196
+ },
197
+ {
198
+ "_id": "6890e04422f0d04331e9cabc",
199
+ "segmentId": 14,
200
+ "startTime": "00:00:37,850",
201
+ "endTime": "00:00:38,950",
202
+ "duration": "1s 100ms",
203
+ "englishText": "I'm irrational.",
204
+ "chineseTranslation": "",
205
+ "isProtected": false,
206
+ "lastModified": "2025-08-04T16:31:00.467Z",
207
+ "modificationHistory": [],
208
+ "__v": 0,
209
+ "createdAt": "2025-08-04T16:31:00.472Z",
210
+ "updatedAt": "2025-08-04T16:31:00.472Z"
211
+ },
212
+ {
213
+ "_id": "6890e04422f0d04331e9cabd",
214
+ "segmentId": 15,
215
+ "startTime": "00:00:39,930",
216
+ "endTime": "00:00:41,520",
217
+ "duration": "1s 590ms",
218
+ "englishText": "I have zero remorse.",
219
+ "chineseTranslation": "",
220
+ "isProtected": false,
221
+ "lastModified": "2025-08-04T16:31:00.467Z",
222
+ "modificationHistory": [],
223
+ "__v": 0,
224
+ "createdAt": "2025-08-04T16:31:00.472Z",
225
+ "updatedAt": "2025-08-04T16:31:00.472Z"
226
+ },
227
+ {
228
+ "_id": "6890e04422f0d04331e9cabe",
229
+ "segmentId": 16,
230
+ "startTime": "00:00:41,770",
231
+ "endTime": "00:00:43,900",
232
+ "duration": "2s 130ms",
233
+ "englishText": "I have no sense of compassion.",
234
+ "chineseTranslation": "",
235
+ "isProtected": false,
236
+ "lastModified": "2025-08-04T16:31:00.467Z",
237
+ "modificationHistory": [],
238
+ "__v": 0,
239
+ "createdAt": "2025-08-04T16:31:00.472Z",
240
+ "updatedAt": "2025-08-04T16:31:00.472Z"
241
+ },
242
+ {
243
+ "_id": "6890e04422f0d04331e9cabf",
244
+ "segmentId": 17,
245
+ "startTime": "00:00:44,480",
246
+ "endTime": "00:00:46,650",
247
+ "duration": "2s 170ms",
248
+ "englishText": "I'm delusional. I'm maniacal.",
249
+ "chineseTranslation": "",
250
+ "isProtected": false,
251
+ "lastModified": "2025-08-04T16:31:00.467Z",
252
+ "modificationHistory": [],
253
+ "__v": 0,
254
+ "createdAt": "2025-08-04T16:31:00.472Z",
255
+ "updatedAt": "2025-08-04T16:31:00.472Z"
256
+ },
257
+ {
258
+ "_id": "6890e04422f0d04331e9cac0",
259
+ "segmentId": 18,
260
+ "startTime": "00:00:46,960",
261
+ "endTime": "00:00:48,980",
262
+ "duration": "2s 20ms",
263
+ "englishText": "You think I'm a bad person?",
264
+ "chineseTranslation": "",
265
+ "isProtected": false,
266
+ "lastModified": "2025-08-04T16:31:00.467Z",
267
+ "modificationHistory": [],
268
+ "__v": 0,
269
+ "createdAt": "2025-08-04T16:31:00.472Z",
270
+ "updatedAt": "2025-08-04T16:31:00.472Z"
271
+ },
272
+ {
273
+ "_id": "6890e04422f0d04331e9cac1",
274
+ "segmentId": 19,
275
+ "startTime": "00:00:49,320",
276
+ "endTime": "00:00:52,700",
277
+ "duration": "3s 380ms",
278
+ "englishText": "Tell me. Tell me. Tell me.\nTell me. Am I?",
279
+ "chineseTranslation": "",
280
+ "isProtected": false,
281
+ "lastModified": "2025-08-04T16:31:00.467Z",
282
+ "modificationHistory": [],
283
+ "__v": 0,
284
+ "createdAt": "2025-08-04T16:31:00.472Z",
285
+ "updatedAt": "2025-08-04T16:31:00.472Z"
286
+ },
287
+ {
288
+ "_id": "6890e04422f0d04331e9cac2",
289
+ "segmentId": 20,
290
+ "startTime": "00:00:52,990",
291
+ "endTime": "00:00:55,136",
292
+ "duration": "2s 146ms",
293
+ "englishText": "I think I'm better than everyone else.",
294
+ "chineseTranslation": "",
295
+ "isProtected": false,
296
+ "lastModified": "2025-08-04T16:31:00.467Z",
297
+ "modificationHistory": [],
298
+ "__v": 0,
299
+ "createdAt": "2025-08-04T16:31:00.472Z",
300
+ "updatedAt": "2025-08-04T16:31:00.472Z"
301
+ },
302
+ {
303
+ "_id": "6890e04422f0d04331e9cac3",
304
+ "segmentId": 21,
305
+ "startTime": "00:00:55,170",
306
+ "endTime": "00:00:57,820",
307
+ "duration": "2s 650ms",
308
+ "englishText": "I want to take what's yours\nand never give it back.",
309
+ "chineseTranslation": "",
310
+ "isProtected": false,
311
+ "lastModified": "2025-08-04T16:31:00.467Z",
312
+ "modificationHistory": [],
313
+ "__v": 0,
314
+ "createdAt": "2025-08-04T16:31:00.472Z",
315
+ "updatedAt": "2025-08-04T16:31:00.472Z"
316
+ },
317
+ {
318
+ "_id": "6890e04422f0d04331e9cac4",
319
+ "segmentId": 22,
320
+ "startTime": "00:00:57,840",
321
+ "endTime": "00:01:00,640",
322
+ "duration": "2s 800ms",
323
+ "englishText": "What's mine is mine\nand what's yours is mine.",
324
+ "chineseTranslation": "",
325
+ "isProtected": false,
326
+ "lastModified": "2025-08-04T16:31:00.467Z",
327
+ "modificationHistory": [],
328
+ "__v": 0,
329
+ "createdAt": "2025-08-04T16:31:00.473Z",
330
+ "updatedAt": "2025-08-04T16:31:00.473Z"
331
+ },
332
+ {
333
+ "_id": "6890e04422f0d04331e9cac5",
334
+ "segmentId": 23,
335
+ "startTime": "00:01:06,920",
336
+ "endTime": "00:01:08,290",
337
+ "duration": "1s 370ms",
338
+ "englishText": "Am I a bad person?",
339
+ "chineseTranslation": "",
340
+ "isProtected": false,
341
+ "lastModified": "2025-08-04T16:31:00.467Z",
342
+ "modificationHistory": [],
343
+ "__v": 0,
344
+ "createdAt": "2025-08-04T16:31:00.473Z",
345
+ "updatedAt": "2025-08-04T16:31:00.473Z"
346
+ },
347
+ {
348
+ "_id": "6890e04422f0d04331e9cac6",
349
+ "segmentId": 24,
350
+ "startTime": "00:01:08,840",
351
+ "endTime": "00:01:10,420",
352
+ "duration": "1s 580ms",
353
+ "englishText": "Tell me. Am I?",
354
+ "chineseTranslation": "",
355
+ "isProtected": false,
356
+ "lastModified": "2025-08-04T16:31:00.468Z",
357
+ "modificationHistory": [],
358
+ "__v": 0,
359
+ "createdAt": "2025-08-04T16:31:00.473Z",
360
+ "updatedAt": "2025-08-04T16:31:00.473Z"
361
+ },
362
+ {
363
+ "_id": "6890e04422f0d04331e9cac7",
364
+ "segmentId": 25,
365
+ "startTime": "00:01:21,500",
366
+ "endTime": "00:01:23,650",
367
+ "duration": "2s 150ms",
368
+ "englishText": "Does that make me a bad person?",
369
+ "chineseTranslation": "",
370
+ "isProtected": false,
371
+ "lastModified": "2025-08-04T16:31:00.468Z",
372
+ "modificationHistory": [],
373
+ "__v": 0,
374
+ "createdAt": "2025-08-04T16:31:00.473Z",
375
+ "updatedAt": "2025-08-04T16:31:00.473Z"
376
+ },
377
+ {
378
+ "_id": "6890e04422f0d04331e9cac8",
379
+ "segmentId": 26,
380
+ "startTime": "00:01:25,060",
381
+ "endTime": "00:01:26,900",
382
+ "duration": "1s 840ms",
383
+ "englishText": "Tell me. Does it?",
384
+ "chineseTranslation": "",
385
+ "isProtected": false,
386
+ "lastModified": "2025-08-04T16:31:00.468Z",
387
+ "modificationHistory": [],
388
+ "__v": 0,
389
+ "createdAt": "2025-08-04T16:31:00.473Z",
390
+ "updatedAt": "2025-08-04T16:31:00.473Z"
391
+ }
392
+ ]
backups/releases/release-2025-08-10T10-33-12-791Z/db/subtitlesubmissions.json ADDED
@@ -0,0 +1 @@
 
 
1
+ []
backups/releases/release-2025-08-10T10-33-12-791Z/db/users.json ADDED
@@ -0,0 +1,24 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ [
2
+ {
3
+ "_id": "688eae46478f8e712972629a",
4
+ "email": "admin@example.com",
5
+ "__v": 0,
6
+ "createdAt": "2025-08-03T00:33:10.838Z",
7
+ "lastActive": "2025-08-03T00:33:10.838Z",
8
+ "password": "$2a$10$IDr1Oi6YJA2EaiT6VrFM3OB07m.H0wVypplqv9QSnODe4oz3uvMwm",
9
+ "role": "admin",
10
+ "targetCultures": [],
11
+ "username": "admin"
12
+ },
13
+ {
14
+ "_id": "688eae46478f8e712972629b",
15
+ "email": "student@example.com",
16
+ "__v": 0,
17
+ "createdAt": "2025-08-03T00:33:10.944Z",
18
+ "lastActive": "2025-08-03T00:33:10.944Z",
19
+ "password": "$2a$10$S1.tbsA2lEQMtFjWzkBsLuOdllAGvWVSQB6O.FcudbMReHoVTXR/G",
20
+ "role": "student",
21
+ "targetCultures": [],
22
+ "username": "student"
23
+ }
24
+ ]
backups/releases/release-2025-08-10T10-33-12-791Z/frontend-6d6d7c2.tar.gz ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:d681c1aa709ca7e6283dc4f33915c3446c21b226915d9c5c2026b4ee92a60f39
3
+ size 133
backups/releases/release-2025-08-10T10-33-12-791Z/manifest.json ADDED
@@ -0,0 +1,26 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "createdAt": "2025-08-10T10:33:56.735Z",
3
+ "tag": "prod-2025-08-10T10-33-12-791Z",
4
+ "frontend": {
5
+ "sha": "6d6d7c21d4add826d755ad927b93cb3a4998620f",
6
+ "branch": "main"
7
+ },
8
+ "backend": {
9
+ "sha": "a2c8b994b45c5578c6d745e9ae63c974ec317bc0",
10
+ "branch": "main"
11
+ },
12
+ "archives": {
13
+ "frontend": "frontend-6d6d7c2.tar.gz",
14
+ "backend": "backend-a2c8b99.tar.gz"
15
+ },
16
+ "db": {
17
+ "path": "db",
18
+ "counts": {
19
+ "users": 2,
20
+ "sourcetexts": 21,
21
+ "submissions": 3,
22
+ "subtitles": 26,
23
+ "subtitlesubmissions": 0
24
+ }
25
+ }
26
+ }
comprehensive-backup.js ADDED
@@ -0,0 +1,350 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ const mongoose = require('mongoose');
2
+ const fs = require('fs').promises;
3
+ const path = require('path');
4
+ const { exec } = require('child_process');
5
+ const { promisify } = require('util');
6
+
7
+ const execAsync = promisify(exec);
8
+
9
+ // Atlas MongoDB connection string
10
+ const MONGODB_URI = 'mongodb+srv://nothingyu:wSg3lbO1PkHiRMq9@sandbox.ecysggv.mongodb.net/test?retryWrites=true&w=majority&appName=sandbox';
11
+
12
+ // Connect to MongoDB Atlas
13
+ const connectDB = async () => {
14
+ try {
15
+ await mongoose.connect(MONGODB_URI);
16
+ console.log('✅ Connected to MongoDB Atlas');
17
+ } catch (error) {
18
+ console.error('❌ MongoDB connection error:', error);
19
+ process.exit(1);
20
+ }
21
+ };
22
+
23
+ // Comprehensive Backup System (Data + Code)
24
+ const comprehensiveBackup = {
25
+ // Create comprehensive backup
26
+ async createComprehensiveBackup() {
27
+ try {
28
+ const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
29
+ const backupName = `comprehensive-backup-${timestamp}`;
30
+
31
+ console.log(`🚀 Creating comprehensive backup: ${backupName}`);
32
+
33
+ // Create backup directory
34
+ const backupDir = path.join(__dirname, 'backups', backupName);
35
+ await fs.mkdir(backupDir, { recursive: true });
36
+
37
+ // 1. BACKUP DATABASE DATA
38
+ console.log('📊 Backing up database data...');
39
+ const dbBackup = await this.backupDatabase(backupDir);
40
+
41
+ // 2. BACKUP CODE FILES
42
+ console.log('💻 Backing up code files...');
43
+ const codeBackup = await this.backupCodeFiles(backupDir);
44
+
45
+ // 3. BACKUP CONFIGURATION
46
+ console.log('⚙️ Backing up configuration...');
47
+ const configBackup = await this.backupConfiguration(backupDir);
48
+
49
+ // 4. CREATE BACKUP MANIFEST
50
+ console.log('📋 Creating backup manifest...');
51
+ const manifest = {
52
+ backupName,
53
+ timestamp: new Date(),
54
+ type: 'comprehensive',
55
+ data: {
56
+ database: dbBackup,
57
+ code: codeBackup,
58
+ configuration: configBackup
59
+ },
60
+ totalSize: await this.calculateBackupSize(backupDir),
61
+ version: '1.0'
62
+ };
63
+
64
+ await fs.writeFile(path.join(backupDir, 'manifest.json'), JSON.stringify(manifest, null, 2));
65
+
66
+ // 5. SAVE BACKUP RECORD TO DATABASE
67
+ const backupRecord = {
68
+ backupName,
69
+ timestamp: new Date(),
70
+ type: 'comprehensive',
71
+ location: backupDir,
72
+ size: manifest.totalSize,
73
+ status: 'created'
74
+ };
75
+
76
+ await mongoose.connection.db.collection('backups').insertOne(backupRecord);
77
+
78
+ console.log(`✅ Comprehensive backup created: ${backupName}`);
79
+ console.log(`📊 Database records: ${dbBackup.totalRecords}`);
80
+ console.log(`💻 Code files: ${codeBackup.fileCount}`);
81
+ console.log(`💾 Total size: ${(manifest.totalSize / 1024 / 1024).toFixed(2)} MB`);
82
+
83
+ return backupName;
84
+
85
+ } catch (error) {
86
+ console.error('❌ Error creating comprehensive backup:', error);
87
+ throw error;
88
+ }
89
+ },
90
+
91
+ // Backup database data
92
+ async backupDatabase(backupDir) {
93
+ const collections = ['subtitles', 'sourcetexts', 'submissions', 'users', 'backups'];
94
+ const dbData = {};
95
+ let totalRecords = 0;
96
+
97
+ for (const collection of collections) {
98
+ try {
99
+ const data = await mongoose.connection.db.collection(collection).find({}).toArray();
100
+ dbData[collection] = data;
101
+ totalRecords += data.length;
102
+ console.log(` 📦 Exported ${data.length} records from ${collection}`);
103
+ } catch (error) {
104
+ console.warn(` ⚠️ Could not export ${collection}:`, error.message);
105
+ }
106
+ }
107
+
108
+ const dbBackupPath = path.join(backupDir, 'database.json');
109
+ await fs.writeFile(dbBackupPath, JSON.stringify(dbData, null, 2));
110
+
111
+ return {
112
+ totalRecords,
113
+ collections: Object.keys(dbData),
114
+ filePath: dbBackupPath
115
+ };
116
+ },
117
+
118
+ // Backup code files
119
+ async backupCodeFiles(backupDir) {
120
+ const codeDir = path.join(backupDir, 'code');
121
+ await fs.mkdir(codeDir, { recursive: true });
122
+
123
+ // Define important code directories and files
124
+ const codePaths = [
125
+ // Backend code
126
+ { src: path.join(__dirname), dest: 'backend' },
127
+ // Frontend code (relative to backend)
128
+ { src: path.join(__dirname, '../frontend'), dest: 'frontend' },
129
+ // Root configuration
130
+ { src: path.join(__dirname, '../../'), dest: 'root' }
131
+ ];
132
+
133
+ let fileCount = 0;
134
+
135
+ for (const codePath of codePaths) {
136
+ try {
137
+ if (await this.pathExists(codePath.src)) {
138
+ await this.copyDirectory(codePath.src, path.join(codeDir, codePath.dest));
139
+ const count = await this.countFiles(codePath.src);
140
+ fileCount += count;
141
+ console.log(` 💻 Copied ${count} files from ${codePath.dest}`);
142
+ }
143
+ } catch (error) {
144
+ console.warn(` ⚠️ Could not backup ${codePath.dest}:`, error.message);
145
+ }
146
+ }
147
+
148
+ return {
149
+ fileCount,
150
+ directories: codePaths.map(p => p.dest),
151
+ location: codeDir
152
+ };
153
+ },
154
+
155
+ // Backup configuration
156
+ async backupConfiguration(backupDir) {
157
+ const configDir = path.join(backupDir, 'config');
158
+ await fs.mkdir(configDir, { recursive: true });
159
+
160
+ const configFiles = [
161
+ 'package.json',
162
+ 'package-lock.json',
163
+ 'Dockerfile',
164
+ 'docker-compose.yml',
165
+ 'nginx.conf',
166
+ '.gitignore'
167
+ ];
168
+
169
+ let configCount = 0;
170
+
171
+ for (const configFile of configFiles) {
172
+ try {
173
+ const srcPath = path.join(__dirname, configFile);
174
+ if (await this.pathExists(srcPath)) {
175
+ const destPath = path.join(configDir, configFile);
176
+ await fs.copyFile(srcPath, destPath);
177
+ configCount++;
178
+ console.log(` ⚙️ Copied ${configFile}`);
179
+ }
180
+ } catch (error) {
181
+ console.warn(` ⚠️ Could not copy ${configFile}:`, error.message);
182
+ }
183
+ }
184
+
185
+ return {
186
+ fileCount: configCount,
187
+ files: configFiles,
188
+ location: configDir
189
+ };
190
+ },
191
+
192
+ // Helper functions
193
+ async pathExists(path) {
194
+ try {
195
+ await fs.access(path);
196
+ return true;
197
+ } catch {
198
+ return false;
199
+ }
200
+ },
201
+
202
+ async copyDirectory(src, dest) {
203
+ await fs.mkdir(dest, { recursive: true });
204
+ const entries = await fs.readdir(src, { withFileTypes: true });
205
+
206
+ for (const entry of entries) {
207
+ const srcPath = path.join(src, entry.name);
208
+ const destPath = path.join(dest, entry.name);
209
+
210
+ if (entry.isDirectory()) {
211
+ await this.copyDirectory(srcPath, destPath);
212
+ } else {
213
+ await fs.copyFile(srcPath, destPath);
214
+ }
215
+ }
216
+ },
217
+
218
+ async countFiles(dir) {
219
+ let count = 0;
220
+ const entries = await fs.readdir(dir, { withFileTypes: true });
221
+
222
+ for (const entry of entries) {
223
+ const fullPath = path.join(dir, entry.name);
224
+
225
+ if (entry.isDirectory()) {
226
+ count += await this.countFiles(fullPath);
227
+ } else {
228
+ count++;
229
+ }
230
+ }
231
+
232
+ return count;
233
+ },
234
+
235
+ async calculateBackupSize(backupDir) {
236
+ const { stdout } = await execAsync(`du -sb "${backupDir}" | cut -f1`);
237
+ return parseInt(stdout.trim());
238
+ },
239
+
240
+ // List comprehensive backups
241
+ async listComprehensiveBackups() {
242
+ try {
243
+ console.log('📋 Available comprehensive backups:');
244
+
245
+ const backupCollection = mongoose.connection.db.collection('backups');
246
+ const backups = await backupCollection.find({ type: 'comprehensive' }).sort({ timestamp: -1 }).toArray();
247
+
248
+ if (backups.length === 0) {
249
+ console.log(' No comprehensive backups found');
250
+ } else {
251
+ backups.forEach(backup => {
252
+ const date = new Date(backup.timestamp).toLocaleString();
253
+ const size = (backup.size / 1024 / 1024).toFixed(2);
254
+ console.log(` 📦 ${backup.backupName} (${size} MB, ${date})`);
255
+ });
256
+ }
257
+
258
+ } catch (error) {
259
+ console.error('❌ Error listing backups:', error);
260
+ }
261
+ },
262
+
263
+ // Restore from comprehensive backup
264
+ async restoreFromBackup(backupName) {
265
+ try {
266
+ console.log(`🔄 Restoring from comprehensive backup: ${backupName}`);
267
+
268
+ const backupDir = path.join(__dirname, 'backups', backupName);
269
+ const manifestPath = path.join(backupDir, 'manifest.json');
270
+
271
+ if (!await this.pathExists(manifestPath)) {
272
+ throw new Error('Backup manifest not found');
273
+ }
274
+
275
+ const manifest = JSON.parse(await fs.readFile(manifestPath, 'utf8'));
276
+ console.log('📋 Backup manifest:', manifest);
277
+
278
+ // Restore database
279
+ console.log('📊 Restoring database...');
280
+ await this.restoreDatabase(backupDir);
281
+
282
+ // Restore code (optional - user confirmation)
283
+ console.log('💻 Code restoration available');
284
+ console.log('⚠️ Code restoration will overwrite existing files');
285
+ console.log(' Run: node restore-code.js <backup-name> to restore code');
286
+
287
+ console.log(`✅ Database restoration completed: ${backupName}`);
288
+
289
+ } catch (error) {
290
+ console.error('❌ Error restoring from backup:', error);
291
+ }
292
+ },
293
+
294
+ // Restore database only
295
+ async restoreDatabase(backupDir) {
296
+ const dbBackupPath = path.join(backupDir, 'database.json');
297
+ const dbData = JSON.parse(await fs.readFile(dbBackupPath, 'utf8'));
298
+
299
+ for (const [collection, data] of Object.entries(dbData)) {
300
+ try {
301
+ // Clear existing data
302
+ await mongoose.connection.db.collection(collection).deleteMany({});
303
+
304
+ // Insert backup data
305
+ if (data.length > 0) {
306
+ await mongoose.connection.db.collection(collection).insertMany(data);
307
+ }
308
+
309
+ console.log(` ✅ Restored ${data.length} records to ${collection}`);
310
+ } catch (error) {
311
+ console.error(` ❌ Error restoring ${collection}:`, error.message);
312
+ }
313
+ }
314
+ }
315
+ };
316
+
317
+ // Main function
318
+ const main = async () => {
319
+ try {
320
+ console.log('🚀 Starting comprehensive backup system...');
321
+
322
+ // Create comprehensive backup
323
+ const backupName = await comprehensiveBackup.createComprehensiveBackup();
324
+
325
+ // List backups
326
+ await comprehensiveBackup.listComprehensiveBackups();
327
+
328
+ console.log('\n🎉 Comprehensive backup system ready!');
329
+ console.log('\n📋 Available functions:');
330
+ console.log(' - createComprehensiveBackup(): Backup data + code');
331
+ console.log(' - listComprehensiveBackups(): List all backups');
332
+ console.log(' - restoreFromBackup(name): Restore database');
333
+ console.log(' - restoreCode(name): Restore code files (separate script)');
334
+
335
+ console.log('\n⏰ To set up automated comprehensive backups:');
336
+ console.log(' 1. Add to crontab: 0 2 * * * cd /path/to/backend && node comprehensive-backup.js');
337
+ console.log(' 2. Or run manually: node comprehensive-backup.js');
338
+
339
+ } catch (error) {
340
+ console.error('❌ Error in comprehensive backup system:', error);
341
+ } finally {
342
+ await mongoose.disconnect();
343
+ console.log('🔌 Disconnected from MongoDB');
344
+ }
345
+ };
346
+
347
+ // Run the system
348
+ connectDB().then(() => {
349
+ main();
350
+ });
create-complete-backup.js ADDED
@@ -0,0 +1,192 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ const mongoose = require('mongoose');
2
+ const fs = require('fs');
3
+ const path = require('path');
4
+
5
+ // MongoDB Atlas connection
6
+ const MONGODB_URI = 'mongodb+srv://nothingyu:wSg3lbO1PkHiRMq9@sandbox.ecysggv.mongodb.net/test?retryWrites=true&w=majority&appName=sandbox';
7
+
8
+ const createCompleteBackup = async () => {
9
+ try {
10
+ console.log('🔒 Creating complete backup...');
11
+
12
+ // Connect to MongoDB
13
+ await mongoose.connect(MONGODB_URI);
14
+ console.log('✅ Connected to MongoDB Atlas');
15
+
16
+ // Create backup directory with timestamp
17
+ const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
18
+ const backupDir = path.join(__dirname, 'backups', `complete-backup-${timestamp}`);
19
+
20
+ if (!fs.existsSync(path.dirname(backupDir))) {
21
+ fs.mkdirSync(path.dirname(backupDir), { recursive: true });
22
+ }
23
+ fs.mkdirSync(backupDir, { recursive: true });
24
+
25
+ console.log(`📁 Backup directory: ${backupDir}`);
26
+
27
+ // Collections to backup
28
+ const collections = [
29
+ 'users',
30
+ 'sourcetexts',
31
+ 'submissions',
32
+ 'subtitles',
33
+ 'subtitlesubmissions'
34
+ ];
35
+
36
+ const backupData = {};
37
+
38
+ // Backup each collection
39
+ for (const collectionName of collections) {
40
+ console.log(`📦 Backing up ${collectionName}...`);
41
+
42
+ try {
43
+ const collection = mongoose.connection.db.collection(collectionName);
44
+ const documents = await collection.find({}).toArray();
45
+
46
+ backupData[collectionName] = documents;
47
+
48
+ // Save to JSON file
49
+ const filePath = path.join(backupDir, `${collectionName}.json`);
50
+ fs.writeFileSync(filePath, JSON.stringify(documents, null, 2));
51
+
52
+ console.log(`✅ ${collectionName}: ${documents.length} documents`);
53
+ } catch (error) {
54
+ console.log(`⚠️ Warning: Could not backup ${collectionName}: ${error.message}`);
55
+ }
56
+ }
57
+
58
+ // Create code backup directory
59
+ const codeBackupDir = path.join(backupDir, 'code');
60
+ fs.mkdirSync(codeBackupDir, { recursive: true });
61
+
62
+ // Copy key frontend files
63
+ const frontendDir = path.join(codeBackupDir, 'frontend');
64
+ fs.mkdirSync(frontendDir, { recursive: true });
65
+
66
+ const frontendFiles = [
67
+ '../frontend/client/src/pages/WeeklyPractice.tsx',
68
+ '../frontend/client/src/pages/TutorialTasks.tsx',
69
+ '../frontend/client/src/components/Layout.tsx',
70
+ '../frontend/client/src/services/api.ts'
71
+ ];
72
+
73
+ for (const file of frontendFiles) {
74
+ try {
75
+ const sourcePath = path.join(__dirname, file);
76
+ const fileName = path.basename(file);
77
+ const destPath = path.join(frontendDir, fileName);
78
+
79
+ if (fs.existsSync(sourcePath)) {
80
+ fs.copyFileSync(sourcePath, destPath);
81
+ console.log(`📄 Copied frontend: ${fileName}`);
82
+ }
83
+ } catch (error) {
84
+ console.log(`⚠️ Warning: Could not copy ${file}: ${error.message}`);
85
+ }
86
+ }
87
+
88
+ // Copy key backend files
89
+ const backendDir = path.join(codeBackupDir, 'backend');
90
+ fs.mkdirSync(backendDir, { recursive: true });
91
+
92
+ const backendFiles = [
93
+ 'index.js',
94
+ 'routes/auth.js',
95
+ 'routes/subtitles.js',
96
+ 'routes/subtitleSubmissions.js',
97
+ 'models/SourceText.js',
98
+ 'models/Subtitle.js',
99
+ 'models/SubtitleSubmission.js',
100
+ 'seed-atlas-subtitles.js',
101
+ 'seed-subtitle-submissions.js'
102
+ ];
103
+
104
+ for (const file of backendFiles) {
105
+ try {
106
+ const sourcePath = path.join(__dirname, file);
107
+ const destPath = path.join(backendDir, file);
108
+
109
+ if (fs.existsSync(sourcePath)) {
110
+ // Create subdirectories if needed
111
+ const destDir = path.dirname(destPath);
112
+ if (!fs.existsSync(destDir)) {
113
+ fs.mkdirSync(destDir, { recursive: true });
114
+ }
115
+
116
+ fs.copyFileSync(sourcePath, destPath);
117
+ console.log(`📄 Copied backend: ${file}`);
118
+ }
119
+ } catch (error) {
120
+ console.log(`⚠️ Warning: Could not copy ${file}: ${error.message}`);
121
+ }
122
+ }
123
+
124
+ // Create manifest
125
+ const manifest = {
126
+ backupInfo: {
127
+ timestamp: new Date().toISOString(),
128
+ backupType: 'Complete Backup',
129
+ description: 'Full backup including database collections and key code files',
130
+ version: '1.0'
131
+ },
132
+ database: {
133
+ collections: collections,
134
+ totalDocuments: Object.values(backupData).reduce((sum, docs) => sum + docs.length, 0)
135
+ },
136
+ codeFiles: {
137
+ frontend: frontendFiles.filter(f => fs.existsSync(path.join(__dirname, f))),
138
+ backend: backendFiles.filter(f => fs.existsSync(path.join(__dirname, f)))
139
+ },
140
+ features: [
141
+ 'Database collections backup',
142
+ 'Key frontend components backup',
143
+ 'Backend routes and models backup',
144
+ 'Seed data scripts backup',
145
+ 'Manifest with backup details'
146
+ ],
147
+ restoreInstructions: {
148
+ database: 'Use the JSON files to restore collections to MongoDB',
149
+ code: 'Copy the code files back to their original locations',
150
+ verification: 'Check manifest.json for backup contents and details'
151
+ }
152
+ };
153
+
154
+ const manifestPath = path.join(backupDir, 'manifest.json');
155
+ fs.writeFileSync(manifestPath, JSON.stringify(manifest, null, 2));
156
+
157
+ console.log('\n🎉 Complete backup created successfully!');
158
+ console.log(`📁 Location: ${backupDir}`);
159
+ console.log(`📋 Manifest: ${manifestPath}`);
160
+ console.log(`📊 Total documents: ${manifest.database.totalDocuments}`);
161
+ console.log(`📄 Code files: ${manifest.codeFiles.frontend.length + manifest.codeFiles.backend.length}`);
162
+
163
+ // List backup contents
164
+ console.log('\n📋 Backup Contents:');
165
+ console.log('Database Collections:');
166
+ collections.forEach(col => {
167
+ const count = backupData[col] ? backupData[col].length : 0;
168
+ console.log(` - ${col}: ${count} documents`);
169
+ });
170
+
171
+ console.log('\nCode Files:');
172
+ console.log('Frontend:');
173
+ manifest.codeFiles.frontend.forEach(file => {
174
+ const fileName = path.basename(file);
175
+ console.log(` - ${fileName}`);
176
+ });
177
+
178
+ console.log('Backend:');
179
+ manifest.codeFiles.backend.forEach(file => {
180
+ console.log(` - ${file}`);
181
+ });
182
+
183
+ } catch (error) {
184
+ console.error('❌ Backup failed:', error);
185
+ } finally {
186
+ await mongoose.disconnect();
187
+ console.log('🔌 Disconnected from MongoDB');
188
+ }
189
+ };
190
+
191
+ // Run backup
192
+ createCompleteBackup();
create-release-bundle.js ADDED
@@ -0,0 +1,121 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /*
2
+ Creates a complete release bundle for fast restore:
3
+ - Tags frontend and backend repos with an annotated tag
4
+ - Copies code snapshots (tar.gz) for frontend and backend
5
+ - Dumps MongoDB collections to JSON (users, sourcetexts, submissions, subtitles, subtitlesubmissions)
6
+ - Writes a manifest.json with commit SHAs, tag, counts, and timestamps
7
+
8
+ Usage: node create-release-bundle.js [--tag TAG_NAME]
9
+ Requires: MONGODB_URI env (or falls back to local), git available
10
+ */
11
+ const fs = require('fs');
12
+ const path = require('path');
13
+ const { execSync } = require('child_process');
14
+ const mongoose = require('mongoose');
15
+
16
+ const ROOT = path.resolve(__dirname, '..', '..');
17
+ const BACKEND_DIR = path.resolve(__dirname);
18
+ const FRONTEND_DIR = path.resolve(__dirname, '../frontend');
19
+ const BACKUPS_DIR = path.join(BACKEND_DIR, 'backups');
20
+ const RELEASES_DIR = path.join(BACKUPS_DIR, 'releases');
21
+
22
+ const argTag = process.argv.includes('--tag') ? process.argv[process.argv.indexOf('--tag') + 1] : null;
23
+ const ts = new Date().toISOString().replace(/[:.]/g, '-');
24
+ const tagName = argTag || `prod-${ts}`;
25
+
26
+ function ensureDir(p) {
27
+ fs.mkdirSync(p, { recursive: true });
28
+ }
29
+
30
+ function getGitInfo(repoDir) {
31
+ const sha = execSync('git rev-parse HEAD', { cwd: repoDir }).toString().trim();
32
+ const branch = execSync('git rev-parse --abbrev-ref HEAD', { cwd: repoDir }).toString().trim();
33
+ return { sha, branch };
34
+ }
35
+
36
+ function tagAndPush(repoDir, tag) {
37
+ try {
38
+ execSync(`git tag -a ${tag} -m "Release ${tag}"`, { cwd: repoDir, stdio: 'inherit' });
39
+ } catch (e) {
40
+ // if tag exists, continue
41
+ }
42
+ // Prefer pushing to 'huggingface' remote if present, else default
43
+ try {
44
+ const remotes = execSync('git remote', { cwd: repoDir }).toString().split('\n').map(r => r.trim()).filter(Boolean);
45
+ if (remotes.includes('huggingface')) {
46
+ execSync('git push --tags huggingface', { cwd: repoDir, stdio: 'inherit' });
47
+ } else {
48
+ execSync('git push --tags', { cwd: repoDir, stdio: 'inherit' });
49
+ }
50
+ } catch (e) {
51
+ console.warn('⚠️ Warning: failed to push tags for', repoDir, '- continuing');
52
+ }
53
+ }
54
+
55
+ async function dumpCollections(outDir) {
56
+ const uri = process.env.MONGODB_URI || 'mongodb://localhost:27017/transcreation-sandbox';
57
+ await mongoose.connect(uri, { serverSelectionTimeoutMS: 5000 });
58
+ const conn = mongoose.connection;
59
+ const collections = ['users', 'sourcetexts', 'submissions', 'subtitles', 'subtitlesubmissions'];
60
+ const counts = {};
61
+ for (const name of collections) {
62
+ try {
63
+ const docs = await conn.db.collection(name).find({}).toArray();
64
+ fs.writeFileSync(path.join(outDir, `${name}.json`), JSON.stringify(docs, null, 2));
65
+ counts[name] = docs.length;
66
+ } catch (e) {
67
+ counts[name] = 0;
68
+ }
69
+ }
70
+ await mongoose.disconnect();
71
+ return counts;
72
+ }
73
+
74
+ (async () => {
75
+ console.log('📦 Creating release bundle...');
76
+ ensureDir(RELEASES_DIR);
77
+ const bundleDir = path.join(RELEASES_DIR, `release-${ts}`);
78
+ ensureDir(bundleDir);
79
+
80
+ // Git info and tags
81
+ console.log('🔖 Tagging repositories...');
82
+ const feInfo = getGitInfo(FRONTEND_DIR);
83
+ const beInfo = getGitInfo(BACKEND_DIR);
84
+ tagAndPush(FRONTEND_DIR, tagName);
85
+ tagAndPush(BACKEND_DIR, tagName);
86
+
87
+ // Code archives
88
+ console.log('🗜️ Archiving code...');
89
+ const feTar = path.join(bundleDir, `frontend-${feInfo.sha.slice(0,7)}.tar.gz`);
90
+ const beTar = path.join(bundleDir, `backend-${beInfo.sha.slice(0,7)}.tar.gz`);
91
+ execSync(`tar -czf "${feTar}" -C "${FRONTEND_DIR}" .`);
92
+ execSync(`tar -czf "${beTar}" -C "${BACKEND_DIR}" .`);
93
+
94
+ // DB dump
95
+ console.log('💾 Dumping database...');
96
+ const dbDir = path.join(bundleDir, 'db');
97
+ ensureDir(dbDir);
98
+ const counts = await dumpCollections(dbDir);
99
+
100
+ // Manifest
101
+ const manifest = {
102
+ createdAt: new Date().toISOString(),
103
+ tag: tagName,
104
+ frontend: feInfo,
105
+ backend: beInfo,
106
+ archives: {
107
+ frontend: path.basename(feTar),
108
+ backend: path.basename(beTar)
109
+ },
110
+ db: {
111
+ path: 'db',
112
+ counts
113
+ }
114
+ };
115
+ fs.writeFileSync(path.join(bundleDir, 'manifest.json'), JSON.stringify(manifest, null, 2));
116
+
117
+ console.log('\n🎉 Release bundle created');
118
+ console.log('📁 Location:', bundleDir);
119
+ console.log('🔖 Tag:', tagName);
120
+ })();
121
+
create-working-backup.js ADDED
@@ -0,0 +1,80 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+ const mongoose = require('mongoose');
4
+
5
+ // Atlas MongoDB connection string
6
+ const MONGODB_URI = 'mongodb+srv://nothingyu:wSg3lbO1PkHiRMq9@sandbox.ecysggv.mongodb.net/test?retryWrites=true&w=majority&appName=sandbox';
7
+
8
+ // Create backup with timestamp
9
+ const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
10
+ const backupName = `working-version-backup-${timestamp}`;
11
+ const backupDir = path.join(__dirname, 'backups', backupName);
12
+
13
+ async function createWorkingBackup() {
14
+ try {
15
+ console.log('🔄 Creating backup of current working version...');
16
+ console.log('📁 Backup name:', backupName);
17
+
18
+ // Create backup directory
19
+ if (!fs.existsSync(path.join(__dirname, 'backups'))) {
20
+ fs.mkdirSync(path.join(__dirname, 'backups'));
21
+ }
22
+ fs.mkdirSync(backupDir);
23
+
24
+ // Connect to MongoDB
25
+ console.log('🔗 Connecting to MongoDB...');
26
+ await mongoose.connect(MONGODB_URI);
27
+
28
+ // Backup database collections
29
+ console.log('📊 Backing up database collections...');
30
+ const collections = ['users', 'sourcetexts', 'submissions', 'subtitles', 'subtitlesubmissions'];
31
+
32
+ for (const collectionName of collections) {
33
+ try {
34
+ const collection = mongoose.connection.db.collection(collectionName);
35
+ const data = await collection.find({}).toArray();
36
+
37
+ const backupPath = path.join(backupDir, `${collectionName}.json`);
38
+ fs.writeFileSync(backupPath, JSON.stringify(data, null, 2));
39
+ console.log(`✅ Backed up ${collectionName}: ${data.length} documents`);
40
+ } catch (error) {
41
+ console.log(`⚠️ Could not backup ${collectionName}:`, error.message);
42
+ }
43
+ }
44
+
45
+ // Create backup manifest
46
+ const manifest = {
47
+ backupName,
48
+ timestamp: new Date().toISOString(),
49
+ description: 'Working version backup - subtitle system working correctly',
50
+ features: [
51
+ 'Fixed subtitle display logic with 0.5s buffer',
52
+ 'Fixed segment 21 and 22 display (60 char limit)',
53
+ 'Simplified subtitle timing logic',
54
+ 'Working subtitle submissions system',
55
+ 'Proper text formatting and line wrapping'
56
+ ],
57
+ collections: collections,
58
+ version: '1.0.0'
59
+ };
60
+
61
+ fs.writeFileSync(path.join(backupDir, 'manifest.json'), JSON.stringify(manifest, null, 2));
62
+
63
+ console.log('✅ Backup completed successfully!');
64
+ console.log('📁 Backup location:', backupDir);
65
+ console.log('📋 Manifest created with feature list');
66
+
67
+ } catch (error) {
68
+ console.error('❌ Backup failed:', error);
69
+ } finally {
70
+ await mongoose.disconnect();
71
+ console.log('🔌 Disconnected from MongoDB');
72
+ }
73
+ }
74
+
75
+ // Run the backup
76
+ if (require.main === module) {
77
+ createWorkingBackup();
78
+ }
79
+
80
+ module.exports = { createWorkingBackup };
cron-setup-guide.js ADDED
@@ -0,0 +1,200 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env node
2
+
3
+ const fs = require('fs').promises;
4
+ const path = require('path');
5
+
6
+ // Cron Setup and Backup Usage Guide
7
+ const cronGuide = {
8
+ // Show cron setup instructions
9
+ showCronSetup() {
10
+ console.log('⏰ CRON JOB SETUP GUIDE');
11
+ console.log('=======================\n');
12
+
13
+ console.log('1. 📝 EDIT CRONTAB:');
14
+ console.log(' crontab -e');
15
+ console.log('');
16
+
17
+ console.log('2. 📋 ADD BACKUP JOB (choose one):');
18
+ console.log('');
19
+ console.log(' # Daily backup at 2 AM');
20
+ console.log(' 0 2 * * * cd /Users/hongchangyu/Downloads/App\\ Projects/Cultural\\ Shift\\ Sandbox/deploy/backend && node comprehensive-backup.js');
21
+ console.log('');
22
+ console.log(' # Weekly backup on Sunday at 3 AM');
23
+ console.log(' 0 3 * * 0 cd /Users/hongchangyu/Downloads/App\\ Projects/Cultural\\ Shift\\ Sandbox/deploy/backend && node comprehensive-backup.js');
24
+ console.log('');
25
+ console.log(' # Every 6 hours');
26
+ console.log(' 0 */6 * * * cd /Users/hongchangyu/Downloads/App\\ Projects/Cultural\\ Shift\\ Sandbox/deploy/backend && node comprehensive-backup.js');
27
+ console.log('');
28
+
29
+ console.log('3. ✅ VERIFY CRON JOBS:');
30
+ console.log(' crontab -l');
31
+ console.log('');
32
+
33
+ console.log('4. 📊 CHECK CRON LOGS:');
34
+ console.log(' tail -f /var/log/cron');
35
+ console.log(' or');
36
+ console.log(' grep CRON /var/log/syslog');
37
+ console.log('');
38
+ },
39
+
40
+ // Show backup usage instructions
41
+ showBackupUsage() {
42
+ console.log('💾 BACKUP USAGE GUIDE');
43
+ console.log('=====================\n');
44
+
45
+ console.log('📋 CHECK AVAILABLE BACKUPS:');
46
+ console.log(' node simple-automated-backup.js');
47
+ console.log(' # Lists all backups with details');
48
+ console.log('');
49
+
50
+ console.log('🔍 VERIFY BACKUP INTEGRITY:');
51
+ console.log(' node comprehensive-backup.js');
52
+ console.log(' # Creates new backup and verifies integrity');
53
+ console.log('');
54
+
55
+ console.log('📊 BACKUP CONTENTS:');
56
+ console.log(' - Database: All collections (subtitles, source texts, users)');
57
+ console.log(' - Code: Frontend and backend files');
58
+ console.log(' - Configuration: package.json, Dockerfile, etc.');
59
+ console.log(' - Manifest: Backup metadata and verification info');
60
+ console.log('');
61
+
62
+ console.log('🔄 RESTORE FROM BACKUP:');
63
+ console.log(' # Database only (safe)');
64
+ console.log(' node restore-database.js <backup-name>');
65
+ console.log('');
66
+ console.log(' # Full restore (including code)');
67
+ console.log(' node restore-comprehensive.js <backup-name>');
68
+ console.log('');
69
+ },
70
+
71
+ // Show cron job examples
72
+ showCronExamples() {
73
+ console.log('📝 CRON JOB EXAMPLES');
74
+ console.log('====================\n');
75
+
76
+ console.log('⏰ TIME FORMAT: minute hour day month day-of-week command');
77
+ console.log('');
78
+
79
+ console.log('📅 COMMON SCHEDULES:');
80
+ console.log(' 0 2 * * * # Daily at 2 AM');
81
+ console.log(' 0 */6 * * * # Every 6 hours');
82
+ console.log(' 0 3 * * 0 # Weekly on Sunday at 3 AM');
83
+ console.log(' 0 1 1 * * # Monthly on 1st at 1 AM');
84
+ console.log(' */30 * * * * # Every 30 minutes');
85
+ console.log('');
86
+
87
+ console.log('🔧 BACKUP COMMANDS:');
88
+ console.log(' # Simple data backup');
89
+ console.log(' node simple-automated-backup.js');
90
+ console.log('');
91
+ console.log(' # Comprehensive backup (data + code)');
92
+ console.log(' node comprehensive-backup.js');
93
+ console.log('');
94
+ console.log(' # With logging');
95
+ console.log(' node comprehensive-backup.js >> backup.log 2>&1');
96
+ console.log('');
97
+ },
98
+
99
+ // Show backup locations
100
+ showBackupLocations() {
101
+ console.log('📁 BACKUP LOCATIONS');
102
+ console.log('===================\n');
103
+
104
+ const backupDir = path.join(__dirname, 'backups');
105
+ console.log(`📂 Local backups: ${backupDir}`);
106
+ console.log('');
107
+
108
+ console.log('📋 BACKUP STRUCTURE:');
109
+ console.log(' backups/');
110
+ console.log(' ├── comprehensive-backup-2025-08-05T.../');
111
+ console.log(' │ ├── database.json # All database data');
112
+ console.log(' │ ├── code/ # All source code');
113
+ console.log(' │ │ ├── backend/ # Backend files');
114
+ console.log(' │ │ ├── frontend/ # Frontend files');
115
+ console.log(' │ │ └── root/ # Root config');
116
+ console.log(' │ ├── config/ # Configuration files');
117
+ console.log(' │ └── manifest.json # Backup metadata');
118
+ console.log(' └── auto-backup-2025-08-05T.../');
119
+ console.log(' └── database.json # Data only');
120
+ console.log('');
121
+
122
+ console.log('💾 BACKUP TYPES:');
123
+ console.log(' - comprehensive-backup-* : Data + Code + Config');
124
+ console.log(' - auto-backup-* : Data only (faster)');
125
+ console.log('');
126
+ },
127
+
128
+ // Show how to check backup contents
129
+ async showBackupContents() {
130
+ console.log('🔍 CHECKING BACKUP CONTENTS');
131
+ console.log('===========================\n');
132
+
133
+ const backupDir = path.join(__dirname, 'backups');
134
+
135
+ try {
136
+ const backups = await fs.readdir(backupDir);
137
+
138
+ if (backups.length === 0) {
139
+ console.log('❌ No backups found');
140
+ return;
141
+ }
142
+
143
+ console.log('📋 Available backups:');
144
+ for (const backup of backups) {
145
+ const backupPath = path.join(backupDir, backup);
146
+ const stats = await fs.stat(backupPath);
147
+
148
+ if (stats.isDirectory()) {
149
+ console.log(` 📦 ${backup} (${stats.mtime.toLocaleString()})`);
150
+
151
+ // Check if it's a comprehensive backup
152
+ const manifestPath = path.join(backupPath, 'manifest.json');
153
+ try {
154
+ await fs.access(manifestPath);
155
+ console.log(` ✅ Comprehensive backup (data + code)`);
156
+ } catch {
157
+ console.log(` 📊 Data backup only`);
158
+ }
159
+ }
160
+ }
161
+
162
+ } catch (error) {
163
+ console.log('❌ Could not read backup directory:', error.message);
164
+ }
165
+
166
+ console.log('');
167
+ console.log('🔍 TO EXAMINE A SPECIFIC BACKUP:');
168
+ console.log(' ls -la backups/comprehensive-backup-*/');
169
+ console.log(' cat backups/comprehensive-backup-*/manifest.json');
170
+ console.log('');
171
+ }
172
+ };
173
+
174
+ // Main function
175
+ const main = async () => {
176
+ console.log('🚀 CRON & BACKUP GUIDE');
177
+ console.log('======================\n');
178
+
179
+ // Show all guides
180
+ cronGuide.showCronSetup();
181
+ cronGuide.showBackupUsage();
182
+ cronGuide.showCronExamples();
183
+ cronGuide.showBackupLocations();
184
+ await cronGuide.showBackupContents();
185
+
186
+ console.log('🎯 QUICK START:');
187
+ console.log('1. Set up cron: crontab -e');
188
+ console.log('2. Add: 0 2 * * * cd /path/to/backend && node comprehensive-backup.js');
189
+ console.log('3. Check backups: node simple-automated-backup.js');
190
+ console.log('4. Restore if needed: node restore-database.js <backup-name>');
191
+ console.log('');
192
+
193
+ console.log('📞 NEED HELP?');
194
+ console.log('- Check cron logs: tail -f /var/log/cron');
195
+ console.log('- Test backup: node comprehensive-backup.js');
196
+ console.log('- List backups: node simple-automated-backup.js');
197
+ };
198
+
199
+ // Run the guide
200
+ main();
enhanced-protection-system.js ADDED
@@ -0,0 +1,270 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ const mongoose = require('mongoose');
2
+ const SourceText = require('./models/SourceText');
3
+
4
+ // MongoDB connection
5
+ const MONGODB_URI = process.env.MONGODB_URI || 'mongodb+srv://nothingyu:wSg3lbO1PkHiRMq9@sandbox.ecysggv.mongodb.net/?retryWrites=true&w=majority&appName=sandbox';
6
+
7
+ // ENHANCED PROTECTION FUNCTIONS
8
+
9
+ async function safeUpdateTask(taskId, updates, reason = 'No reason provided') {
10
+ try {
11
+ console.log('🌐 Connecting to MongoDB...');
12
+ await mongoose.connect(MONGODB_URI);
13
+ console.log('✅ Connected to MongoDB');
14
+
15
+ const task = await SourceText.findById(taskId);
16
+
17
+ if (!task) {
18
+ console.log(`❌ Task with ID ${taskId} not found`);
19
+ return false;
20
+ }
21
+
22
+ if (task.isProtected) {
23
+ console.log(`🚫 CANNOT UPDATE: Task "${task.title}" is PROTECTED`);
24
+ console.log(` Reason: ${task.protectedReason}`);
25
+ console.log(` To update this task, you must first unlock it with the special key`);
26
+ return false;
27
+ }
28
+
29
+ // Safe to update
30
+ const updatedTask = await SourceText.findByIdAndUpdate(
31
+ taskId,
32
+ {
33
+ ...updates,
34
+ lastModified: new Date(),
35
+ modificationHistory: [
36
+ ...(task.modificationHistory || []),
37
+ {
38
+ action: 'UPDATED',
39
+ timestamp: new Date(),
40
+ reason: reason
41
+ }
42
+ ]
43
+ },
44
+ { new: true }
45
+ );
46
+
47
+ console.log(`✅ Updated task: ${updatedTask.title}`);
48
+ return true;
49
+
50
+ } catch (error) {
51
+ console.error('❌ Error updating task:', error);
52
+ return false;
53
+ } finally {
54
+ await mongoose.disconnect();
55
+ console.log('🔌 Disconnected from MongoDB');
56
+ }
57
+ }
58
+
59
+ async function safeDeleteTask(taskId, reason = 'No reason provided') {
60
+ try {
61
+ console.log('🌐 Connecting to MongoDB...');
62
+ await mongoose.connect(MONGODB_URI);
63
+ console.log('✅ Connected to MongoDB');
64
+
65
+ const task = await SourceText.findById(taskId);
66
+
67
+ if (!task) {
68
+ console.log(`❌ Task with ID ${taskId} not found`);
69
+ return false;
70
+ }
71
+
72
+ if (task.isProtected) {
73
+ console.log(`🚫 CANNOT DELETE: Task "${task.title}" is PROTECTED`);
74
+ console.log(` Reason: ${task.protectedReason}`);
75
+ console.log(` To delete this task, you must first unlock it with the special key`);
76
+ return false;
77
+ }
78
+
79
+ // Safe to delete
80
+ const deletedTask = await SourceText.findByIdAndDelete(taskId);
81
+ console.log(`✅ Deleted task: ${deletedTask.title}`);
82
+ return true;
83
+
84
+ } catch (error) {
85
+ console.error('❌ Error deleting task:', error);
86
+ return false;
87
+ } finally {
88
+ await mongoose.disconnect();
89
+ console.log('🔌 Disconnected from MongoDB');
90
+ }
91
+ }
92
+
93
+ async function unlockProtectedTask(taskId, unlockKey) {
94
+ try {
95
+ console.log('🌐 Connecting to MongoDB...');
96
+ await mongoose.connect(MONGODB_URI);
97
+ console.log('✅ Connected to MongoDB');
98
+
99
+ // Simple unlock key - in production, use a more secure method
100
+ const VALID_UNLOCK_KEY = 'UNLOCK_WEEK1_WEEK2_2024';
101
+
102
+ if (unlockKey !== VALID_UNLOCK_KEY) {
103
+ console.log('❌ Invalid unlock key. Protected tasks cannot be unlocked.');
104
+ return false;
105
+ }
106
+
107
+ const task = await SourceText.findById(taskId);
108
+
109
+ if (!task) {
110
+ console.log(`❌ Task with ID ${taskId} not found`);
111
+ return false;
112
+ }
113
+
114
+ if (!task.isProtected) {
115
+ console.log(`⚠️ Task "${task.title}" is not protected`);
116
+ return false;
117
+ }
118
+
119
+ const updatedTask = await SourceText.findByIdAndUpdate(
120
+ taskId,
121
+ {
122
+ isProtected: false,
123
+ protectedReason: null,
124
+ lastModified: new Date(),
125
+ modificationHistory: [
126
+ ...(task.modificationHistory || []),
127
+ {
128
+ action: 'UNLOCKED',
129
+ timestamp: new Date(),
130
+ reason: 'Task unlocked with valid key'
131
+ }
132
+ ]
133
+ },
134
+ { new: true }
135
+ );
136
+
137
+ console.log(`✅ Unlocked task: ${updatedTask.title}`);
138
+ return true;
139
+
140
+ } catch (error) {
141
+ console.error('❌ Error unlocking task:', error);
142
+ return false;
143
+ } finally {
144
+ await mongoose.disconnect();
145
+ console.log('🔌 Disconnected from MongoDB');
146
+ }
147
+ }
148
+
149
+ async function showProtectedTasks() {
150
+ try {
151
+ console.log('🌐 Connecting to MongoDB...');
152
+ await mongoose.connect(MONGODB_URI);
153
+ console.log('✅ Connected to MongoDB');
154
+
155
+ const protectedTasks = await SourceText.find({
156
+ isProtected: true
157
+ }).sort({ weekNumber: 1, title: 1 });
158
+
159
+ console.log(`\n🔒 PROTECTED TASKS (${protectedTasks.length} total):`);
160
+
161
+ protectedTasks.forEach((task, index) => {
162
+ console.log(`${index + 1}. Week ${task.weekNumber} - ${task.title}`);
163
+ console.log(` ID: ${task._id}`);
164
+ console.log(` Protected: ${task.isProtected ? 'YES' : 'NO'}`);
165
+ console.log(` Reason: ${task.protectedReason}`);
166
+ console.log(` Last Modified: ${task.lastModified}`);
167
+ console.log('---');
168
+ });
169
+
170
+ } catch (error) {
171
+ console.error('❌ Error showing protected tasks:', error);
172
+ } finally {
173
+ await mongoose.disconnect();
174
+ console.log('🔌 Disconnected from MongoDB');
175
+ }
176
+ }
177
+
178
+ async function testEnhancedProtection() {
179
+ try {
180
+ console.log('🌐 Connecting to MongoDB...');
181
+ await mongoose.connect(MONGODB_URI);
182
+ console.log('✅ Connected to MongoDB');
183
+
184
+ // Find a protected task to test
185
+ const protectedTask = await SourceText.findOne({
186
+ isProtected: true,
187
+ weekNumber: 1
188
+ });
189
+
190
+ if (!protectedTask) {
191
+ console.log('❌ No protected tasks found');
192
+ return;
193
+ }
194
+
195
+ console.log(`\n🧪 TESTING ENHANCED PROTECTION SYSTEM:`);
196
+ console.log(` Task: ${protectedTask.title}`);
197
+ console.log(` ID: ${protectedTask._id}`);
198
+ console.log(` Protected: ${protectedTask.isProtected ? 'YES' : 'NO'}`);
199
+ console.log(` Reason: ${protectedTask.protectedReason}`);
200
+
201
+ // Test 1: Try to update a protected task using safe function
202
+ console.log('\n🔒 TEST 1: Attempting to update protected task using safe function...');
203
+ const updateResult = await safeUpdateTask(
204
+ protectedTask._id,
205
+ { content: 'This should not work!' },
206
+ 'Test update'
207
+ );
208
+
209
+ if (!updateResult) {
210
+ console.log('✅ PROTECTION WORKING: Update was prevented');
211
+ } else {
212
+ console.log('❌ PROTECTION FAILED: Task was updated despite being protected!');
213
+ }
214
+
215
+ // Test 2: Try to delete a protected task using safe function
216
+ console.log('\n🔒 TEST 2: Attempting to delete protected task using safe function...');
217
+ const deleteResult = await safeDeleteTask(
218
+ protectedTask._id,
219
+ 'Test deletion'
220
+ );
221
+
222
+ if (!deleteResult) {
223
+ console.log('✅ PROTECTION WORKING: Deletion was prevented');
224
+ } else {
225
+ console.log('❌ PROTECTION FAILED: Task was deleted despite being protected!');
226
+ }
227
+
228
+ // Test 3: Show how to safely unlock a task
229
+ console.log('\n🔓 TEST 3: Demonstrating unlock process...');
230
+ console.log(' To unlock a protected task, you need the special key:');
231
+ console.log(' Key: UNLOCK_WEEK1_WEEK2_2024');
232
+ console.log(' Usage: unlockProtectedTask(taskId, key)');
233
+
234
+ // Test 4: Show current protection status
235
+ console.log('\n📊 PROTECTION STATUS:');
236
+ const allProtectedTasks = await SourceText.find({ isProtected: true });
237
+ const week1Protected = allProtectedTasks.filter(t => t.weekNumber === 1).length;
238
+ const week2Protected = allProtectedTasks.filter(t => t.weekNumber === 2).length;
239
+
240
+ console.log(` Week 1 protected tasks: ${week1Protected}`);
241
+ console.log(` Week 2 protected tasks: ${week2Protected}`);
242
+ console.log(` Total protected tasks: ${allProtectedTasks.length}`);
243
+
244
+ console.log('\n🎉 ENHANCED PROTECTION SYSTEM TEST COMPLETE');
245
+ console.log('\n📋 PROTECTION SUMMARY:');
246
+ console.log(' ✅ Week 1 and Week 2 tutorial tasks are LOCKED');
247
+ console.log(' ✅ Protected tasks cannot be accidentally modified');
248
+ console.log(' ✅ Special key required to unlock tasks');
249
+ console.log(' ✅ Modification history is tracked');
250
+ console.log(' ✅ Use safeUpdateTask() and safeDeleteTask() functions');
251
+
252
+ } catch (error) {
253
+ console.error('❌ Error testing enhanced protection system:', error);
254
+ } finally {
255
+ await mongoose.disconnect();
256
+ console.log('🔌 Disconnected from MongoDB');
257
+ }
258
+ }
259
+
260
+ // Export functions for safe operations
261
+ module.exports = {
262
+ safeUpdateTask,
263
+ safeDeleteTask,
264
+ unlockProtectedTask,
265
+ showProtectedTasks,
266
+ testEnhancedProtection
267
+ };
268
+
269
+ // Run the enhanced test
270
+ testEnhancedProtection();
implement-security-enhancements.js ADDED
@@ -0,0 +1,315 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ const mongoose = require('mongoose');
2
+ const crypto = require('crypto');
3
+
4
+ // Atlas MongoDB connection string
5
+ const MONGODB_URI = 'mongodb+srv://nothingyu:wSg3lbO1PkHiRMq9@sandbox.ecysggv.mongodb.net/test?retryWrites=true&w=majority&appName=sandbox';
6
+
7
+ // Connect to MongoDB Atlas
8
+ const connectDB = async () => {
9
+ try {
10
+ await mongoose.connect(MONGODB_URI);
11
+ console.log('✅ Connected to MongoDB Atlas');
12
+ } catch (error) {
13
+ console.error('❌ MongoDB connection error:', error);
14
+ process.exit(1);
15
+ }
16
+ };
17
+
18
+ // Enhanced Subtitle Schema with security features
19
+ const subtitleSchema = new mongoose.Schema({
20
+ segmentId: { type: Number, required: true, unique: true },
21
+ startTime: { type: String, required: true },
22
+ endTime: { type: String, required: true },
23
+ duration: { type: String, required: true },
24
+ englishText: { type: String, required: true },
25
+ chineseTranslation: { type: String, default: '' },
26
+ isProtected: { type: Boolean, default: false },
27
+ protectedReason: { type: String, default: '' },
28
+ lastModified: { type: Date, default: Date.now },
29
+ modificationHistory: [{
30
+ timestamp: { type: Date, default: Date.now },
31
+ action: String,
32
+ previousValue: String,
33
+ newValue: String,
34
+ modifiedBy: String
35
+ }],
36
+ // New security fields
37
+ contentChecksum: { type: String, required: true },
38
+ lastVerified: { type: Date, default: Date.now },
39
+ verificationHistory: [{
40
+ timestamp: Date,
41
+ checksum: String,
42
+ verifiedBy: String,
43
+ status: String
44
+ }],
45
+ watermark: { type: String, default: '' },
46
+ accessLog: [{
47
+ timestamp: Date,
48
+ userId: String,
49
+ action: String,
50
+ ip: String,
51
+ userAgent: String
52
+ }]
53
+ });
54
+
55
+ const Subtitle = mongoose.model('Subtitle', subtitleSchema);
56
+
57
+ // Security utilities
58
+ const securityUtils = {
59
+ generateChecksum: (content) => {
60
+ return crypto.createHash('sha256').update(content).digest('hex');
61
+ },
62
+
63
+ addWatermark: (text, userId) => {
64
+ const watermark = Buffer.from(userId || 'system').toString('base64').slice(0, 8);
65
+ return text + '\u200B' + watermark; // Zero-width space + watermark
66
+ },
67
+
68
+ extractWatermark: (text) => {
69
+ const parts = text.split('\u200B');
70
+ return parts.length > 1 ? parts[1] : null;
71
+ },
72
+
73
+ validateTimeFormat: (timeString) => {
74
+ const timePattern = /^\d{2}:\d{2}:\d{2},\d{3}$/;
75
+ return timePattern.test(timeString);
76
+ },
77
+
78
+ sanitizeInput: (text) => {
79
+ // Basic XSS prevention
80
+ return text
81
+ .replace(/</g, '&lt;')
82
+ .replace(/>/g, '&gt;')
83
+ .replace(/"/g, '&quot;')
84
+ .replace(/'/g, '&#x27;')
85
+ .replace(/\//g, '&#x2F;');
86
+ }
87
+ };
88
+
89
+ // Enhanced protection system
90
+ const enhancedProtection = {
91
+ async addSecurityFeatures() {
92
+ try {
93
+ console.log('🔒 Adding security features to existing subtitles...');
94
+
95
+ const subtitles = await Subtitle.find({});
96
+ console.log(`📊 Found ${subtitles.length} subtitle segments`);
97
+
98
+ let updatedCount = 0;
99
+
100
+ for (const subtitle of subtitles) {
101
+ const updates = {};
102
+
103
+ // Generate checksum if not exists
104
+ if (!subtitle.contentChecksum) {
105
+ const content = subtitle.englishText + subtitle.startTime + subtitle.endTime;
106
+ updates.contentChecksum = securityUtils.generateChecksum(content);
107
+ }
108
+
109
+ // Add watermark if not exists
110
+ if (!subtitle.watermark) {
111
+ updates.watermark = securityUtils.addWatermark(subtitle.englishText, 'system');
112
+ }
113
+
114
+ // Add verification history if empty
115
+ if (!subtitle.verificationHistory || subtitle.verificationHistory.length === 0) {
116
+ updates.verificationHistory = [{
117
+ timestamp: new Date(),
118
+ checksum: subtitle.contentChecksum || securityUtils.generateChecksum(subtitle.englishText + subtitle.startTime + subtitle.endTime),
119
+ verifiedBy: 'system',
120
+ status: 'verified'
121
+ }];
122
+ }
123
+
124
+ // Update if any changes needed
125
+ if (Object.keys(updates).length > 0) {
126
+ await Subtitle.findByIdAndUpdate(subtitle._id, {
127
+ ...updates,
128
+ lastModified: new Date(),
129
+ $push: {
130
+ modificationHistory: {
131
+ timestamp: new Date(),
132
+ action: 'SECURITY_ENHANCEMENT',
133
+ previousValue: 'none',
134
+ newValue: 'security_features_added',
135
+ modifiedBy: 'system'
136
+ }
137
+ }
138
+ });
139
+ updatedCount++;
140
+ }
141
+ }
142
+
143
+ console.log(`✅ Enhanced ${updatedCount} subtitle segments with security features`);
144
+
145
+ // Verify integrity
146
+ await this.verifyAllIntegrity();
147
+
148
+ } catch (error) {
149
+ console.error('❌ Error adding security features:', error);
150
+ }
151
+ },
152
+
153
+ async verifyAllIntegrity() {
154
+ try {
155
+ console.log('🔍 Verifying integrity of all subtitle segments...');
156
+
157
+ const subtitles = await Subtitle.find({});
158
+ let verifiedCount = 0;
159
+ let failedCount = 0;
160
+
161
+ for (const subtitle of subtitles) {
162
+ const currentChecksum = securityUtils.generateChecksum(
163
+ subtitle.englishText + subtitle.startTime + subtitle.endTime
164
+ );
165
+
166
+ if (currentChecksum === subtitle.contentChecksum) {
167
+ verifiedCount++;
168
+ } else {
169
+ failedCount++;
170
+ console.warn(`⚠️ Integrity check failed for segment ${subtitle.segmentId}`);
171
+ }
172
+ }
173
+
174
+ console.log(`✅ Integrity verification complete:`);
175
+ console.log(` - Verified: ${verifiedCount} segments`);
176
+ console.log(` - Failed: ${failedCount} segments`);
177
+
178
+ if (failedCount > 0) {
179
+ console.log('⚠️ Some segments failed integrity checks. Consider investigation.');
180
+ }
181
+
182
+ } catch (error) {
183
+ console.error('❌ Error during integrity verification:', error);
184
+ }
185
+ },
186
+
187
+ async createSecurityLogsCollection() {
188
+ try {
189
+ console.log('📝 Creating security logs collection...');
190
+
191
+ const securityLogSchema = new mongoose.Schema({
192
+ timestamp: { type: Date, default: Date.now },
193
+ event: { type: String, required: true },
194
+ details: mongoose.Schema.Types.Mixed,
195
+ ip: String,
196
+ userAgent: String,
197
+ userId: String,
198
+ severity: { type: String, enum: ['low', 'medium', 'high', 'critical'], default: 'medium' }
199
+ });
200
+
201
+ const SecurityLog = mongoose.model('SecurityLog', securityLogSchema);
202
+
203
+ // Create index for efficient querying
204
+ await SecurityLog.createIndexes();
205
+
206
+ console.log('✅ Security logs collection created');
207
+
208
+ // Log initial security enhancement
209
+ await SecurityLog.create({
210
+ event: 'SECURITY_ENHANCEMENT_IMPLEMENTED',
211
+ details: {
212
+ action: 'Enhanced subtitle protection system',
213
+ features: ['checksums', 'watermarks', 'integrity_verification'],
214
+ timestamp: new Date()
215
+ },
216
+ severity: 'high'
217
+ });
218
+
219
+ } catch (error) {
220
+ console.error('❌ Error creating security logs:', error);
221
+ }
222
+ },
223
+
224
+ async createBackupVerification() {
225
+ try {
226
+ console.log('💾 Creating backup verification system...');
227
+
228
+ const backupSchema = new mongoose.Schema({
229
+ backupName: { type: String, required: true, unique: true },
230
+ timestamp: { type: Date, default: Date.now },
231
+ collections: [String],
232
+ totalRecords: Number,
233
+ checksum: String,
234
+ status: { type: String, enum: ['created', 'verified', 'restored'], default: 'created' },
235
+ createdBy: String
236
+ });
237
+
238
+ const Backup = mongoose.model('Backup', backupSchema);
239
+
240
+ // Create initial backup record
241
+ const collections = ['subtitles', 'sourcetexts', 'submissions', 'users'];
242
+ let totalRecords = 0;
243
+
244
+ for (const collection of collections) {
245
+ const count = await mongoose.connection.db.collection(collection).countDocuments();
246
+ totalRecords += count;
247
+ }
248
+
249
+ const backupData = {
250
+ collections,
251
+ totalRecords,
252
+ checksum: securityUtils.generateChecksum(`backup-${Date.now()}`)
253
+ };
254
+
255
+ await Backup.create({
256
+ backupName: `security-enhanced-backup-${new Date().toISOString()}`,
257
+ collections,
258
+ totalRecords,
259
+ checksum: backupData.checksum,
260
+ createdBy: 'system'
261
+ });
262
+
263
+ console.log('✅ Backup verification system created');
264
+ console.log(`📊 Backup created with ${totalRecords} total records`);
265
+
266
+ } catch (error) {
267
+ console.error('❌ Error creating backup verification:', error);
268
+ }
269
+ }
270
+ };
271
+
272
+ // Main implementation function
273
+ const implementSecurityEnhancements = async () => {
274
+ try {
275
+ console.log('🚀 Starting security enhancement implementation...');
276
+
277
+ // Step 1: Add security features to existing subtitles
278
+ await enhancedProtection.addSecurityFeatures();
279
+
280
+ // Step 2: Verify integrity
281
+ await enhancedProtection.verifyAllIntegrity();
282
+
283
+ // Step 3: Create security logs collection
284
+ await enhancedProtection.createSecurityLogsCollection();
285
+
286
+ // Step 4: Create backup verification system
287
+ await enhancedProtection.createBackupVerification();
288
+
289
+ console.log('\n🎉 Security enhancements implemented successfully!');
290
+ console.log('\n📋 Implemented features:');
291
+ console.log(' ✅ Content checksums for integrity verification');
292
+ console.log(' ✅ Invisible watermarks for content tracking');
293
+ console.log(' ✅ Security logging system');
294
+ console.log(' ✅ Backup verification system');
295
+ console.log(' ✅ Input sanitization utilities');
296
+ console.log(' ✅ Time format validation');
297
+
298
+ console.log('\n🔒 Next steps:');
299
+ console.log(' 1. Implement JWT authentication');
300
+ console.log(' 2. Add rate limiting per user');
301
+ console.log(' 3. Set up monitoring and alerting');
302
+ console.log(' 4. Configure automated security testing');
303
+
304
+ } catch (error) {
305
+ console.error('❌ Error implementing security enhancements:', error);
306
+ } finally {
307
+ await mongoose.disconnect();
308
+ console.log('🔌 Disconnected from MongoDB');
309
+ }
310
+ };
311
+
312
+ // Run the implementation
313
+ connectDB().then(() => {
314
+ implementSecurityEnhancements();
315
+ });
index.js ADDED
@@ -0,0 +1,289 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ const express = require('express');
2
+ const cors = require('cors');
3
+ const mongoose = require('mongoose');
4
+ const dotenv = require('dotenv');
5
+ const rateLimit = require('express-rate-limit');
6
+
7
+ // Import routes
8
+ const { router: authRoutes } = require('./routes/auth');
9
+ const sourceTextRoutes = require('./routes/sourceTexts');
10
+ const submissionRoutes = require('./routes/submissions');
11
+ const searchRoutes = require('./routes/search');
12
+ const subtitleRoutes = require('./routes/subtitles');
13
+ const subtitleSubmissionRoutes = require('./routes/subtitleSubmissions');
14
+ const weeklyPracticeFilesRoutes = require('./routes/weeklyPracticeFiles');
15
+ const dictionaryRoutes = require('./routes/dictionary');
16
+ const icibaRoutes = require('./routes/iciba');
17
+ const oneRoutes = require('./routes/one');
18
+ const mtRoutes = require('./routes/mt');
19
+ const slidesRoutes = require('./routes/slides');
20
+ const messagesRoutes = require('./routes/messages');
21
+ const linksRoutes = require('./routes/links');
22
+ const docsRoutes = require('./routes/docs');
23
+
24
+ dotenv.config();
25
+
26
+ // Global error handlers to prevent crashes
27
+ process.on('uncaughtException', (error) => {
28
+ console.error('Uncaught Exception:', error);
29
+ // Don't exit immediately, try to log and continue
30
+ console.error('Stack trace:', error.stack);
31
+ });
32
+
33
+ process.on('unhandledRejection', (reason, promise) => {
34
+ console.error('Unhandled Rejection at:', promise, 'reason:', reason);
35
+ // Don't exit immediately, try to log and continue
36
+ console.error('Stack trace:', reason?.stack);
37
+ });
38
+
39
+ // Memory leak prevention
40
+ process.on('warning', (warning) => {
41
+ console.warn('Node.js warning:', warning.name, warning.message);
42
+ });
43
+
44
+ const app = express();
45
+ const PORT = process.env.PORT || 5000;
46
+
47
+ // Trust proxy for rate limiting
48
+ app.set('trust proxy', 1);
49
+
50
+ // Rate limiting - Increased limits to prevent 429 errors
51
+ const limiter = rateLimit({
52
+ windowMs: 15 * 60 * 1000, // 15 minutes
53
+ max: 1000, // Increased from 100 to 1000 requests per windowMs
54
+ message: { error: 'Too many requests, please try again later.' },
55
+ standardHeaders: true,
56
+ legacyHeaders: false,
57
+ skip: (req) => {
58
+ // Skip rate limiting for health checks
59
+ return req.path === '/health' || req.path === '/api/health';
60
+ }
61
+ });
62
+
63
+ // Middleware
64
+ app.use(cors());
65
+ app.use(express.json({ limit: '10mb' }));
66
+ app.use(limiter);
67
+
68
+ // Database connection with better error handling
69
+ mongoose.connect(process.env.MONGODB_URI || 'mongodb://localhost:27017/transcreation-sandbox', {
70
+ maxPoolSize: 10,
71
+ serverSelectionTimeoutMS: 5000,
72
+ socketTimeoutMS: 45000,
73
+ })
74
+ .then(() => {
75
+ console.log('Connected to MongoDB');
76
+ })
77
+ .catch(err => {
78
+ console.error('MongoDB connection error:', err);
79
+ // Don't exit immediately, try to reconnect
80
+ setTimeout(() => {
81
+ console.log('Attempting to reconnect to MongoDB...');
82
+ mongoose.connect(process.env.MONGODB_URI || 'mongodb://localhost:27017/transcreation-sandbox');
83
+ }, 5000);
84
+ });
85
+
86
+ // Handle MongoDB connection errors
87
+ mongoose.connection.on('error', (err) => {
88
+ console.error('MongoDB connection error:', err);
89
+ });
90
+
91
+ mongoose.connection.on('disconnected', () => {
92
+ console.log('MongoDB disconnected');
93
+ });
94
+
95
+ // Routes
96
+ app.use('/api/auth', authRoutes);
97
+ app.use('/api/source-texts', sourceTextRoutes);
98
+ app.use('/api/submissions', submissionRoutes);
99
+ app.use('/api/search', searchRoutes);
100
+ app.use('/api/subtitles', subtitleRoutes);
101
+ app.use('/api/subtitle-submissions', subtitleSubmissionRoutes);
102
+ app.use('/api/weekly-practice-files', weeklyPracticeFilesRoutes);
103
+ app.use('/api/dictionary', dictionaryRoutes);
104
+ app.use('/api/iciba', icibaRoutes);
105
+ app.use('/api/one', oneRoutes);
106
+ app.use('/api/mt', mtRoutes);
107
+ app.use('/api/slides', slidesRoutes);
108
+ app.use('/api/messages', messagesRoutes);
109
+ app.use('/api/links', linksRoutes);
110
+ app.use('/api/docs', docsRoutes);
111
+
112
+ // Health check endpoint
113
+ app.get('/api/health', (req, res) => {
114
+ res.json({ status: 'OK', message: 'Transcreation Sandbox API is running - Auth middleware fixed for time code editing' });
115
+ });
116
+
117
+ // Simple health check for Hugging Face Spaces
118
+ app.get('/health', (req, res) => {
119
+ res.status(200).send('OK');
120
+ });
121
+
122
+ // Error handling middleware
123
+ app.use((err, req, res, next) => {
124
+ console.error(err.stack);
125
+ res.status(500).json({ error: 'Something went wrong!' });
126
+ });
127
+
128
+ app.listen(PORT, () => {
129
+ console.log(`Server running on port ${PORT}`);
130
+ // Seed default links if empty (best-effort)
131
+ (async () => {
132
+ try {
133
+ const Link = require('./models/Link');
134
+ const count = await Link.countDocuments();
135
+ if (count === 0) {
136
+ await Link.insertMany([
137
+ { title: 'AI工具集', url: 'https://ai-bot.cn', desc: 'AI tools directory and learning resources.' },
138
+ { title: 'Proz.com', url: 'https://www.proz.com', desc: 'Translators community and job marketplace.' },
139
+ { title: 'Translators without Borders', url: 'https://translatorswithoutborders.org', desc: 'Volunteer translation for humanitarian causes.' },
140
+ { title: 'TED Translator', url: 'https://www.ted.com/participate/translate', desc: 'Volunteer to translate TED talks.' },
141
+ { title: 'Matecat', url: 'https://www.matecat.com', desc: 'Free web-based CAT tool with TM/MT support.' }
142
+ ]);
143
+ console.log('Seeded default Useful Links');
144
+ }
145
+ } catch (e) {
146
+ console.warn('Unable to seed Useful Links:', e?.message);
147
+ }
148
+ })();
149
+
150
+ // Initialize week 1 tutorial tasks and weekly practice by default
151
+ const initializeWeek1 = async () => {
152
+ try {
153
+ const SourceText = require('./models/SourceText');
154
+
155
+ // Check if week 1 tutorial tasks exist
156
+ const existingTutorialTasks = await SourceText.find({
157
+ category: 'tutorial',
158
+ weekNumber: 1
159
+ });
160
+
161
+ if (existingTutorialTasks.length === 0) {
162
+ console.log('Initializing week 1 tutorial tasks...');
163
+ const tutorialTasks = [
164
+ {
165
+ title: 'Tutorial Task 1 - Introduction',
166
+ content: '欢迎来到我们的翻译课程。今天我们将学习如何翻译产品介绍。',
167
+ category: 'tutorial',
168
+ weekNumber: 1,
169
+ sourceLanguage: 'Chinese',
170
+ sourceCulture: 'Chinese',
171
+ translationBrief: 'You have been asked to translate the following for marketing the product in a country where your LOTE is spoken.'
172
+ },
173
+ {
174
+ title: 'Tutorial Task 2 - Development',
175
+ content: '这个产品具有独特的设计理念,融合了传统与现代元素。',
176
+ category: 'tutorial',
177
+ weekNumber: 1,
178
+ sourceLanguage: 'Chinese',
179
+ sourceCulture: 'Chinese',
180
+ translationBrief: 'You have been asked to translate the following for marketing the product in a country where your LOTE is spoken.'
181
+ },
182
+ {
183
+ title: 'Tutorial Task 3 - Conclusion',
184
+ content: '我们相信这个产品能够满足您的所有需求,为您提供最佳体验。',
185
+ category: 'tutorial',
186
+ weekNumber: 1,
187
+ sourceLanguage: 'Chinese',
188
+ sourceCulture: 'Chinese',
189
+ translationBrief: 'You have been asked to translate the following for marketing the product in a country where your LOTE is spoken.'
190
+ }
191
+ ];
192
+ await SourceText.insertMany(tutorialTasks);
193
+ console.log('Week 1 tutorial tasks initialized successfully');
194
+ }
195
+
196
+ // Check if week 1 weekly practice exists
197
+ const existingWeeklyPractice = await SourceText.find({
198
+ category: 'weekly-practice',
199
+ weekNumber: 1
200
+ });
201
+
202
+ if (existingWeeklyPractice.length === 0) {
203
+ console.log('Initializing week 1 weekly practice...');
204
+ const weeklyPractice = [
205
+ {
206
+ title: 'Chinese Pun 1',
207
+ content: '为什么睡前一定要吃夜宵?因为这样才不会做饿梦。',
208
+ category: 'weekly-practice',
209
+ weekNumber: 1,
210
+ sourceLanguage: 'Chinese',
211
+ sourceCulture: 'Chinese'
212
+ },
213
+ {
214
+ title: 'Chinese Pun 2',
215
+ content: '女娲用什么补天?强扭的瓜。',
216
+ category: 'weekly-practice',
217
+ weekNumber: 1,
218
+ sourceLanguage: 'Chinese',
219
+ sourceCulture: 'Chinese'
220
+ },
221
+ {
222
+ title: 'Chinese Pun 3',
223
+ content: '你知道如何区分真假大象吗?把他们仍进水中,真相会浮出水面的。',
224
+ category: 'weekly-practice',
225
+ weekNumber: 1,
226
+ sourceLanguage: 'Chinese',
227
+ sourceCulture: 'Chinese'
228
+ },
229
+ {
230
+ title: 'English Pun 1',
231
+ content: 'What if Soy milk is just regular milk introducing itself in Spanish.',
232
+ category: 'weekly-practice',
233
+ weekNumber: 1,
234
+ sourceLanguage: 'English',
235
+ sourceCulture: 'Western'
236
+ },
237
+ {
238
+ title: 'English Pun 2',
239
+ content: 'I can\'t believe I got fired from the calendar factory. All I did was take a day off.',
240
+ category: 'weekly-practice',
241
+ weekNumber: 1,
242
+ sourceLanguage: 'English',
243
+ sourceCulture: 'Western'
244
+ },
245
+ {
246
+ title: 'English Pun 3',
247
+ content: 'When life gives you melons, you might be dyslexic.',
248
+ category: 'weekly-practice',
249
+ weekNumber: 1,
250
+ sourceLanguage: 'English',
251
+ sourceCulture: 'Western'
252
+ }
253
+ ];
254
+ await SourceText.insertMany(weeklyPractice);
255
+ console.log('Week 1 weekly practice initialized successfully');
256
+ }
257
+ } catch (error) {
258
+ console.error('Error initializing week 1 data:', error);
259
+ }
260
+ };
261
+
262
+ // Auto-initialization disabled to prevent overwriting definitive data
263
+ // initializeWeek1();
264
+ });
265
+
266
+ // Graceful shutdown
267
+ process.on('SIGTERM', async () => {
268
+ console.log('SIGTERM received, shutting down gracefully');
269
+ try {
270
+ await mongoose.connection.close();
271
+ console.log('MongoDB connection closed');
272
+ process.exit(0);
273
+ } catch (error) {
274
+ console.error('Error closing MongoDB connection:', error);
275
+ process.exit(1);
276
+ }
277
+ });
278
+
279
+ process.on('SIGINT', async () => {
280
+ console.log('SIGINT received, shutting down gracefully');
281
+ try {
282
+ await mongoose.connection.close();
283
+ console.log('MongoDB connection closed');
284
+ process.exit(0);
285
+ } catch (error) {
286
+ console.error('Error closing MongoDB connection:', error);
287
+ process.exit(1);
288
+ }
289
+ });
lock-subtitles.js ADDED
@@ -0,0 +1,107 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ const mongoose = require('mongoose');
2
+
3
+ // Atlas MongoDB connection string
4
+ const MONGODB_URI = 'mongodb+srv://nothingyu:wSg3lbO1PkHiRMq9@sandbox.ecysggv.mongodb.net/test?retryWrites=true&w=majority&appName=sandbox';
5
+
6
+ // Connect to MongoDB Atlas
7
+ const connectDB = async () => {
8
+ try {
9
+ await mongoose.connect(MONGODB_URI);
10
+ console.log('✅ Connected to MongoDB Atlas');
11
+ } catch (error) {
12
+ console.error('❌ MongoDB connection error:', error);
13
+ process.exit(1);
14
+ }
15
+ };
16
+
17
+ // Subtitle Schema (same as in models/Subtitle.js)
18
+ const subtitleSchema = new mongoose.Schema({
19
+ segmentId: { type: Number, required: true, unique: true },
20
+ startTime: { type: String, required: true },
21
+ endTime: { type: String, required: true },
22
+ duration: { type: String, required: true },
23
+ englishText: { type: String, required: true },
24
+ chineseTranslation: { type: String, default: '' },
25
+ isProtected: { type: Boolean, default: false },
26
+ protectedReason: { type: String, default: '' },
27
+ lastModified: { type: Date, default: Date.now },
28
+ modificationHistory: [{
29
+ timestamp: { type: Date, default: Date.now },
30
+ action: String,
31
+ previousValue: String,
32
+ newValue: String,
33
+ modifiedBy: String
34
+ }]
35
+ });
36
+
37
+ const Subtitle = mongoose.model('Subtitle', subtitleSchema);
38
+
39
+ const lockSubtitles = async () => {
40
+ try {
41
+ console.log('🔒 Starting subtitle protection process...');
42
+
43
+ // Find all subtitle segments
44
+ const subtitles = await Subtitle.find({});
45
+ console.log(`📊 Found ${subtitles.length} subtitle segments`);
46
+
47
+ if (subtitles.length === 0) {
48
+ console.log('❌ No subtitle segments found. Please seed the database first.');
49
+ return;
50
+ }
51
+
52
+ // Lock all subtitle segments
53
+ const updatePromises = subtitles.map(subtitle =>
54
+ Subtitle.findByIdAndUpdate(
55
+ subtitle._id,
56
+ {
57
+ isProtected: true,
58
+ protectedReason: 'Original English subtitles and timecodes - critical for course content',
59
+ lastModified: new Date(),
60
+ $push: {
61
+ modificationHistory: {
62
+ timestamp: new Date(),
63
+ action: 'PROTECTED',
64
+ previousValue: 'false',
65
+ newValue: 'true',
66
+ modifiedBy: 'system'
67
+ }
68
+ }
69
+ },
70
+ { new: true }
71
+ )
72
+ );
73
+
74
+ const updatedSubtitles = await Promise.all(updatePromises);
75
+ console.log(`✅ Successfully protected ${updatedSubtitles.length} subtitle segments`);
76
+
77
+ // Verify protection
78
+ const protectedCount = await Subtitle.countDocuments({ isProtected: true });
79
+ console.log(`🔒 Verification: ${protectedCount} segments are now protected`);
80
+
81
+ // Show sample of protected segments
82
+ const sampleProtected = await Subtitle.find({ isProtected: true }).limit(5);
83
+ console.log('\n📋 Sample of protected segments:');
84
+ sampleProtected.forEach(subtitle => {
85
+ console.log(` Segment ${subtitle.segmentId}: "${subtitle.englishText.substring(0, 50)}..."`);
86
+ });
87
+
88
+ console.log('\n🎯 Subtitle protection complete!');
89
+ console.log('📝 Protection details:');
90
+ console.log(' - All 26 subtitle segments are now protected');
91
+ console.log(' - Original English text cannot be modified');
92
+ console.log(' - Timecodes cannot be changed');
93
+ console.log(' - Chinese translations can still be added/updated');
94
+ console.log(' - Use unlock-subtitles.js to temporarily disable protection');
95
+
96
+ } catch (error) {
97
+ console.error('❌ Error protecting subtitles:', error);
98
+ } finally {
99
+ await mongoose.disconnect();
100
+ console.log('🔌 Disconnected from MongoDB');
101
+ }
102
+ };
103
+
104
+ // Run the protection script
105
+ connectDB().then(() => {
106
+ lockSubtitles();
107
+ });
lock-week1-week2-tasks.js ADDED
@@ -0,0 +1,244 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ const mongoose = require('mongoose');
2
+ const SourceText = require('./models/SourceText');
3
+
4
+ // MongoDB connection
5
+ const MONGODB_URI = process.env.MONGODB_URI || 'mongodb+srv://nothingyu:wSg3lbO1PkHiRMq9@sandbox.ecysggv.mongodb.net/?retryWrites=true&w=majority&appName=sandbox';
6
+
7
+ async function lockWeek1Week2Tasks() {
8
+ try {
9
+ console.log('🌐 Connecting to MongoDB...');
10
+ await mongoose.connect(MONGODB_URI);
11
+ console.log('✅ Connected to MongoDB');
12
+
13
+ // Find all Week 1 and Week 2 tutorial tasks
14
+ const week1Tasks = await SourceText.find({
15
+ category: 'tutorial',
16
+ weekNumber: 1
17
+ }).sort({ title: 1 });
18
+
19
+ const week2Tasks = await SourceText.find({
20
+ category: 'tutorial',
21
+ weekNumber: 2
22
+ }).sort({ title: 1 });
23
+
24
+ console.log(`📋 Found ${week1Tasks.length} Week 1 tutorial tasks`);
25
+ console.log(`📋 Found ${week2Tasks.length} Week 2 tutorial tasks`);
26
+
27
+ // Lock Week 1 tasks
28
+ console.log('\n🔒 Locking Week 1 tutorial tasks...');
29
+ let lockedWeek1Count = 0;
30
+ for (const task of week1Tasks) {
31
+ const updatedTask = await SourceText.findByIdAndUpdate(
32
+ task._id,
33
+ {
34
+ isProtected: true,
35
+ protectedReason: 'Week 1 tutorial tasks are locked to prevent accidental changes',
36
+ lastModified: new Date(),
37
+ modificationHistory: [
38
+ {
39
+ action: 'LOCKED',
40
+ timestamp: new Date(),
41
+ reason: 'Week 1 tutorial tasks locked for protection'
42
+ }
43
+ ]
44
+ },
45
+ { new: true }
46
+ );
47
+ console.log(`✅ Locked: ${updatedTask.title}`);
48
+ lockedWeek1Count++;
49
+ }
50
+
51
+ // Lock Week 2 tasks
52
+ console.log('\n🔒 Locking Week 2 tutorial tasks...');
53
+ let lockedWeek2Count = 0;
54
+ for (const task of week2Tasks) {
55
+ const updatedTask = await SourceText.findByIdAndUpdate(
56
+ task._id,
57
+ {
58
+ isProtected: true,
59
+ protectedReason: 'Week 2 tutorial tasks are locked to prevent accidental changes',
60
+ lastModified: new Date(),
61
+ modificationHistory: [
62
+ {
63
+ action: 'LOCKED',
64
+ timestamp: new Date(),
65
+ reason: 'Week 2 tutorial tasks locked for protection'
66
+ }
67
+ ]
68
+ },
69
+ { new: true }
70
+ );
71
+ console.log(`✅ Locked: ${updatedTask.title}`);
72
+ lockedWeek2Count++;
73
+ }
74
+
75
+ console.log(`\n🎉 LOCKING COMPLETE:`);
76
+ console.log(` Week 1: ${lockedWeek1Count} tasks locked`);
77
+ console.log(` Week 2: ${lockedWeek2Count} tasks locked`);
78
+
79
+ // Verify locked tasks
80
+ console.log('\n📋 Verification - Locked tasks:');
81
+ const allLockedTasks = await SourceText.find({
82
+ isProtected: true
83
+ }).sort({ weekNumber: 1, title: 1 });
84
+
85
+ allLockedTasks.forEach((task, index) => {
86
+ console.log(`${index + 1}. Week ${task.weekNumber} - ${task.title}`);
87
+ console.log(` Protected: ${task.isProtected ? 'YES' : 'NO'}`);
88
+ console.log(` Reason: ${task.protectedReason}`);
89
+ });
90
+
91
+ console.log(`\n📊 Total locked tasks: ${allLockedTasks.length}`);
92
+
93
+ } catch (error) {
94
+ console.error('❌ Error locking tasks:', error);
95
+ process.exit(1);
96
+ } finally {
97
+ await mongoose.disconnect();
98
+ console.log('🔌 Disconnected from MongoDB');
99
+ }
100
+ }
101
+
102
+ // SAFE UPDATE FUNCTIONS WITH PROTECTION CHECKS
103
+
104
+ async function safeUpdateProtectedTask(taskId, updates, reason = 'No reason provided') {
105
+ try {
106
+ console.log('🌐 Connecting to MongoDB...');
107
+ await mongoose.connect(MONGODB_URI);
108
+ console.log('✅ Connected to MongoDB');
109
+
110
+ const task = await SourceText.findById(taskId);
111
+
112
+ if (!task) {
113
+ console.log(`❌ Task with ID ${taskId} not found`);
114
+ return;
115
+ }
116
+
117
+ if (task.isProtected) {
118
+ console.log(`🚫 CANNOT UPDATE: Task "${task.title}" is PROTECTED`);
119
+ console.log(` Reason: ${task.protectedReason}`);
120
+ console.log(` To update this task, you must first unlock it with a special key`);
121
+ return;
122
+ }
123
+
124
+ // Safe to update
125
+ const updatedTask = await SourceText.findByIdAndUpdate(
126
+ taskId,
127
+ {
128
+ ...updates,
129
+ lastModified: new Date(),
130
+ modificationHistory: [
131
+ ...(task.modificationHistory || []),
132
+ {
133
+ action: 'UPDATED',
134
+ timestamp: new Date(),
135
+ reason: reason
136
+ }
137
+ ]
138
+ },
139
+ { new: true }
140
+ );
141
+
142
+ console.log(`✅ Updated task: ${updatedTask.title}`);
143
+
144
+ } catch (error) {
145
+ console.error('❌ Error updating task:', error);
146
+ } finally {
147
+ await mongoose.disconnect();
148
+ console.log('🔌 Disconnected from MongoDB');
149
+ }
150
+ }
151
+
152
+ async function unlockProtectedTask(taskId, unlockKey) {
153
+ try {
154
+ console.log('🌐 Connecting to MongoDB...');
155
+ await mongoose.connect(MONGODB_URI);
156
+ console.log('✅ Connected to MongoDB');
157
+
158
+ // Simple unlock key - in production, use a more secure method
159
+ const VALID_UNLOCK_KEY = 'UNLOCK_WEEK1_WEEK2_2024';
160
+
161
+ if (unlockKey !== VALID_UNLOCK_KEY) {
162
+ console.log('❌ Invalid unlock key. Protected tasks cannot be unlocked.');
163
+ return;
164
+ }
165
+
166
+ const task = await SourceText.findById(taskId);
167
+
168
+ if (!task) {
169
+ console.log(`❌ Task with ID ${taskId} not found`);
170
+ return;
171
+ }
172
+
173
+ if (!task.isProtected) {
174
+ console.log(`⚠️ Task "${task.title}" is not protected`);
175
+ return;
176
+ }
177
+
178
+ const updatedTask = await SourceText.findByIdAndUpdate(
179
+ taskId,
180
+ {
181
+ isProtected: false,
182
+ protectedReason: null,
183
+ lastModified: new Date(),
184
+ modificationHistory: [
185
+ ...(task.modificationHistory || []),
186
+ {
187
+ action: 'UNLOCKED',
188
+ timestamp: new Date(),
189
+ reason: 'Task unlocked with valid key'
190
+ }
191
+ ]
192
+ },
193
+ { new: true }
194
+ );
195
+
196
+ console.log(`✅ Unlocked task: ${updatedTask.title}`);
197
+
198
+ } catch (error) {
199
+ console.error('❌ Error unlocking task:', error);
200
+ } finally {
201
+ await mongoose.disconnect();
202
+ console.log('🔌 Disconnected from MongoDB');
203
+ }
204
+ }
205
+
206
+ async function showProtectedTasks() {
207
+ try {
208
+ console.log('🌐 Connecting to MongoDB...');
209
+ await mongoose.connect(MONGODB_URI);
210
+ console.log('✅ Connected to MongoDB');
211
+
212
+ const protectedTasks = await SourceText.find({
213
+ isProtected: true
214
+ }).sort({ weekNumber: 1, title: 1 });
215
+
216
+ console.log(`\n🔒 PROTECTED TASKS (${protectedTasks.length} total):`);
217
+
218
+ protectedTasks.forEach((task, index) => {
219
+ console.log(`${index + 1}. Week ${task.weekNumber} - ${task.title}`);
220
+ console.log(` ID: ${task._id}`);
221
+ console.log(` Protected: ${task.isProtected ? 'YES' : 'NO'}`);
222
+ console.log(` Reason: ${task.protectedReason}`);
223
+ console.log(` Last Modified: ${task.lastModified}`);
224
+ console.log('---');
225
+ });
226
+
227
+ } catch (error) {
228
+ console.error('❌ Error showing protected tasks:', error);
229
+ } finally {
230
+ await mongoose.disconnect();
231
+ console.log('🔌 Disconnected from MongoDB');
232
+ }
233
+ }
234
+
235
+ // Export functions for safe operations
236
+ module.exports = {
237
+ lockWeek1Week2Tasks,
238
+ safeUpdateProtectedTask,
239
+ unlockProtectedTask,
240
+ showProtectedTasks
241
+ };
242
+
243
+ // Run the locking
244
+ lockWeek1Week2Tasks();
middleware/auth.js ADDED
@@ -0,0 +1,38 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ const auth = (req, res, next) => {
2
+ try {
3
+ const token = req.header('Authorization')?.replace('Bearer ', '');
4
+ const userRole = req.header('user-role');
5
+
6
+ if (!token) {
7
+ return res.status(401).json({
8
+ success: false,
9
+ message: 'No token provided'
10
+ });
11
+ }
12
+
13
+ // Check if token is in the simplified format (user_ or visitor_)
14
+ if (token.startsWith('user_') || token.startsWith('visitor_')) {
15
+ // For simplified system, include user role from header
16
+ const userInfo = req.header('user-info');
17
+ req.user = {
18
+ token,
19
+ role: userRole || 'visitor', // Default to visitor if no role provided
20
+ userInfo: userInfo ? JSON.parse(userInfo) : {}
21
+ };
22
+ next();
23
+ } else {
24
+ return res.status(401).json({
25
+ success: false,
26
+ message: 'Invalid token format'
27
+ });
28
+ }
29
+ } catch (error) {
30
+ console.error('Auth middleware error:', error);
31
+ res.status(401).json({
32
+ success: false,
33
+ message: 'Invalid token'
34
+ });
35
+ }
36
+ };
37
+
38
+ module.exports = auth;
models/AccessSession.js ADDED
@@ -0,0 +1,18 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ const mongoose = require('mongoose');
2
+
3
+ const accessSessionSchema = new mongoose.Schema({
4
+ email: { type: String, index: true },
5
+ role: { type: String, enum: ['student', 'instructor', 'admin', 'visitor'], index: true },
6
+ startAt: { type: Date, default: Date.now, index: true },
7
+ lastSeen: { type: Date, default: Date.now, index: true },
8
+ userAgent: { type: String },
9
+ ip: { type: String },
10
+ path: { type: String }
11
+ }, { timestamps: true });
12
+
13
+ // Compound index for querying active sessions by email + startAt
14
+ accessSessionSchema.index({ email: 1, startAt: -1 });
15
+
16
+ module.exports = mongoose.model('AccessSession', accessSessionSchema);
17
+
18
+
models/GroupDoc.js ADDED
@@ -0,0 +1,17 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ const mongoose = require('mongoose');
2
+
3
+ const groupDocSchema = new mongoose.Schema({
4
+ weekNumber: { type: Number, required: true, index: true },
5
+ groupNumber: { type: Number, required: true, index: true },
6
+ docId: { type: String, required: true, unique: true },
7
+ docUrl: { type: String, required: true },
8
+ title: { type: String },
9
+ createdBy: { type: String },
10
+ createdAt: { type: Date, default: Date.now }
11
+ }, { timestamps: true });
12
+
13
+ groupDocSchema.index({ weekNumber: 1, groupNumber: 1 }, { unique: true });
14
+
15
+ module.exports = mongoose.model('GroupDoc', groupDocSchema);
16
+
17
+
models/Link.js ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ const mongoose = require('mongoose');
2
+
3
+ const LinkSchema = new mongoose.Schema({
4
+ title: { type: String, required: true },
5
+ url: { type: String, required: true },
6
+ desc: { type: String, default: '' },
7
+ category: { type: String, default: '' },
8
+ order: { type: Number, default: 0 }
9
+ }, { timestamps: true });
10
+
11
+ module.exports = mongoose.model('Link', LinkSchema);
12
+
13
+