linguabot commited on
Commit
0ca84af
·
verified ·
1 Parent(s): a2c8b99

Upload folder using huggingface_hub

Browse files
Files changed (39) hide show
  1. .gitattributes +2 -0
  2. SECURITY_ENHANCEMENT_PLAN.md +433 -0
  3. backup-version-control.js +364 -0
  4. backups/complete-backup-2025-08-10T07-59-20-407Z/code/backend/index.js +252 -0
  5. backups/complete-backup-2025-08-10T07-59-20-407Z/code/backend/models/SourceText.js +75 -0
  6. backups/complete-backup-2025-08-10T07-59-20-407Z/code/backend/models/Subtitle.js +168 -0
  7. backups/complete-backup-2025-08-10T07-59-20-407Z/code/backend/models/SubtitleSubmission.js +102 -0
  8. backups/complete-backup-2025-08-10T07-59-20-407Z/code/backend/routes/auth.js +354 -0
  9. backups/complete-backup-2025-08-10T07-59-20-407Z/code/backend/routes/subtitleSubmissions.js +287 -0
  10. backups/complete-backup-2025-08-10T07-59-20-407Z/code/backend/routes/subtitles.js +343 -0
  11. backups/complete-backup-2025-08-10T07-59-20-407Z/code/backend/seed-atlas-subtitles.js +87 -0
  12. backups/complete-backup-2025-08-10T07-59-20-407Z/code/backend/seed-subtitle-submissions.js +178 -0
  13. backups/complete-backup-2025-08-10T07-59-20-407Z/code/frontend/Layout.tsx +191 -0
  14. backups/complete-backup-2025-08-10T07-59-20-407Z/code/frontend/TutorialTasks.tsx +1724 -0
  15. backups/complete-backup-2025-08-10T07-59-20-407Z/code/frontend/WeeklyPractice.tsx +0 -0
  16. backups/complete-backup-2025-08-10T07-59-20-407Z/code/frontend/api.ts +91 -0
  17. backups/complete-backup-2025-08-10T07-59-20-407Z/manifest.json +49 -0
  18. backups/complete-backup-2025-08-10T07-59-20-407Z/sourcetexts.json +0 -0
  19. backups/complete-backup-2025-08-10T07-59-20-407Z/submissions.json +1361 -0
  20. backups/complete-backup-2025-08-10T07-59-20-407Z/subtitles.json +608 -0
  21. backups/complete-backup-2025-08-10T07-59-20-407Z/subtitlesubmissions.json +1 -0
  22. backups/complete-backup-2025-08-10T07-59-20-407Z/users.json +24 -0
  23. backups/releases/release-2025-08-10T10-33-12-791Z/backend-a2c8b99.tar.gz +3 -0
  24. backups/releases/release-2025-08-10T10-33-12-791Z/db/sourcetexts.json +449 -0
  25. backups/releases/release-2025-08-10T10-33-12-791Z/db/submissions.json +56 -0
  26. backups/releases/release-2025-08-10T10-33-12-791Z/db/subtitles.json +392 -0
  27. backups/releases/release-2025-08-10T10-33-12-791Z/db/subtitlesubmissions.json +1 -0
  28. backups/releases/release-2025-08-10T10-33-12-791Z/db/users.json +24 -0
  29. backups/releases/release-2025-08-10T10-33-12-791Z/frontend-6d6d7c2.tar.gz +3 -0
  30. backups/releases/release-2025-08-10T10-33-12-791Z/manifest.json +26 -0
  31. comprehensive-backup.js +350 -0
  32. create-complete-backup.js +192 -0
  33. create-release-bundle.js +121 -0
  34. create-working-backup.js +80 -0
  35. cron-setup-guide.js +200 -0
  36. implement-security-enhancements.js +315 -0
  37. lock-subtitles.js +107 -0
  38. simple-automated-backup.js +244 -0
  39. unlock-subtitles.js +100 -0
.gitattributes ADDED
@@ -0,0 +1,2 @@
 
 
 
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
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-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/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:d7b1392f0b7e4d5aa0f81a2b4b363392c04917473cd7ed60bccbd45643257968
3
+ size 81151584
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:ae5267f4cd981c77b481f67a05e56d4a4c4209c382ad63336b84b9df899027a5
3
+ size 69713305
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();
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
+ });
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
+ });
simple-automated-backup.js ADDED
@@ -0,0 +1,244 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+ // Simple Automated Backup System
20
+ const automatedBackup = {
21
+ // Create backup
22
+ async createBackup() {
23
+ try {
24
+ const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
25
+ const backupName = `auto-backup-${timestamp}`;
26
+
27
+ console.log(`💾 Creating automated backup: ${backupName}`);
28
+
29
+ // Collections to backup
30
+ const collections = ['subtitles', 'sourcetexts', 'submissions', 'users'];
31
+ const backupData = {
32
+ metadata: {
33
+ backupName,
34
+ timestamp: new Date(),
35
+ collections,
36
+ totalRecords: 0,
37
+ version: '1.0',
38
+ type: 'automated'
39
+ },
40
+ data: {}
41
+ };
42
+
43
+ // Export each collection
44
+ for (const collection of collections) {
45
+ try {
46
+ const data = await mongoose.connection.db.collection(collection).find({}).toArray();
47
+ backupData.data[collection] = data;
48
+ backupData.metadata.totalRecords += data.length;
49
+ console.log(` 📦 Exported ${data.length} records from ${collection}`);
50
+ } catch (error) {
51
+ console.warn(` ⚠️ Could not export ${collection}:`, error.message);
52
+ }
53
+ }
54
+
55
+ // Save to backup directory
56
+ const backupDir = path.join(__dirname, 'backups');
57
+ await fs.mkdir(backupDir, { recursive: true });
58
+
59
+ const backupPath = path.join(backupDir, `${backupName}.json`);
60
+ await fs.writeFile(backupPath, JSON.stringify(backupData, null, 2));
61
+
62
+ // Save backup record to database
63
+ const backupRecord = {
64
+ backupName,
65
+ timestamp: new Date(),
66
+ collections,
67
+ totalRecords: backupData.metadata.totalRecords,
68
+ filePath: backupPath,
69
+ status: 'created',
70
+ type: 'automated'
71
+ };
72
+
73
+ await mongoose.connection.db.collection('backups').insertOne(backupRecord);
74
+
75
+ console.log(`✅ Backup created successfully: ${backupName}`);
76
+ console.log(`📊 Total records: ${backupData.metadata.totalRecords}`);
77
+ console.log(`💾 File saved: ${backupPath}`);
78
+
79
+ return backupName;
80
+
81
+ } catch (error) {
82
+ console.error('❌ Error creating backup:', error);
83
+ throw error;
84
+ }
85
+ },
86
+
87
+ // List backups
88
+ async listBackups() {
89
+ try {
90
+ console.log('📋 Available backups:');
91
+
92
+ // List from database
93
+ const dbBackups = await mongoose.connection.db.collection('backups').find({}).sort({ timestamp: -1 }).toArray();
94
+
95
+ if (dbBackups.length === 0) {
96
+ console.log(' No backups found in database');
97
+ } else {
98
+ console.log(' Database backups:');
99
+ dbBackups.forEach(backup => {
100
+ const date = new Date(backup.timestamp).toLocaleString();
101
+ console.log(` 📦 ${backup.backupName} (${backup.totalRecords} records, ${date})`);
102
+ });
103
+ }
104
+
105
+ // List from file system
106
+ const backupDir = path.join(__dirname, 'backups');
107
+ try {
108
+ const files = await fs.readdir(backupDir);
109
+ const backupFiles = files.filter(file => file.endsWith('.json'));
110
+
111
+ if (backupFiles.length > 0) {
112
+ console.log(' File system backups:');
113
+ for (const file of backupFiles) {
114
+ const filePath = path.join(backupDir, file);
115
+ const stats = await fs.stat(filePath);
116
+ console.log(` 💾 ${file} (${(stats.size / 1024).toFixed(2)} KB, ${stats.mtime.toLocaleString()})`);
117
+ }
118
+ }
119
+ } catch (error) {
120
+ console.log(' No backup directory found');
121
+ }
122
+
123
+ } catch (error) {
124
+ console.error('❌ Error listing backups:', error);
125
+ }
126
+ },
127
+
128
+ // Clean old backups (keep last 10)
129
+ async cleanOldBackups() {
130
+ try {
131
+ console.log('🧹 Cleaning old backups...');
132
+
133
+ const backupCollection = mongoose.connection.db.collection('backups');
134
+ const allBackups = await backupCollection.find({}).sort({ timestamp: -1 }).toArray();
135
+
136
+ if (allBackups.length > 10) {
137
+ const backupsToDelete = allBackups.slice(10);
138
+
139
+ for (const backup of backupsToDelete) {
140
+ // Delete file
141
+ try {
142
+ await fs.unlink(backup.filePath);
143
+ console.log(` 🗑️ Deleted file: ${backup.backupName}`);
144
+ } catch (error) {
145
+ console.warn(` ⚠️ Could not delete file: ${backup.backupName}`);
146
+ }
147
+
148
+ // Delete database record
149
+ await backupCollection.deleteOne({ _id: backup._id });
150
+ console.log(` 🗑️ Deleted record: ${backup.backupName}`);
151
+ }
152
+
153
+ console.log(`✅ Cleaned ${backupsToDelete.length} old backups`);
154
+ } else {
155
+ console.log('✅ No old backups to clean (keeping all backups)');
156
+ }
157
+
158
+ } catch (error) {
159
+ console.error('❌ Error cleaning old backups:', error);
160
+ }
161
+ },
162
+
163
+ // Verify backup integrity
164
+ async verifyBackup(backupName) {
165
+ try {
166
+ console.log(`🔍 Verifying backup: ${backupName}`);
167
+
168
+ const backupPath = path.join(__dirname, 'backups', `${backupName}.json`);
169
+ const backupData = JSON.parse(await fs.readFile(backupPath, 'utf8'));
170
+
171
+ let verifiedCount = 0;
172
+ let failedCount = 0;
173
+
174
+ for (const [collection, data] of Object.entries(backupData.data)) {
175
+ try {
176
+ const currentCount = await mongoose.connection.db.collection(collection).countDocuments();
177
+ const backupCount = data.length;
178
+
179
+ if (currentCount === backupCount) {
180
+ verifiedCount++;
181
+ console.log(` ✅ ${collection}: ${backupCount} records verified`);
182
+ } else {
183
+ failedCount++;
184
+ console.log(` ❌ ${collection}: ${backupCount} in backup, ${currentCount} in database`);
185
+ }
186
+ } catch (error) {
187
+ failedCount++;
188
+ console.log(` ❌ ${collection}: verification failed`);
189
+ }
190
+ }
191
+
192
+ console.log(`🔍 Verification complete:`);
193
+ console.log(` - Verified: ${verifiedCount} collections`);
194
+ console.log(` - Failed: ${failedCount} collections`);
195
+
196
+ return { verifiedCount, failedCount };
197
+
198
+ } catch (error) {
199
+ console.error('❌ Error verifying backup:', error);
200
+ }
201
+ }
202
+ };
203
+
204
+ // Main function
205
+ const main = async () => {
206
+ try {
207
+ console.log('🚀 Starting automated backup system...');
208
+
209
+ // Create backup
210
+ const backupName = await automatedBackup.createBackup();
211
+
212
+ // List all backups
213
+ await automatedBackup.listBackups();
214
+
215
+ // Clean old backups
216
+ await automatedBackup.cleanOldBackups();
217
+
218
+ // Verify the backup
219
+ await automatedBackup.verifyBackup(backupName);
220
+
221
+ console.log('\n🎉 Automated backup system ready!');
222
+ console.log('\n📋 Available functions:');
223
+ console.log(' - createBackup(): Create new backup');
224
+ console.log(' - listBackups(): List available backups');
225
+ console.log(' - cleanOldBackups(): Keep only last 10 backups');
226
+ console.log(' - verifyBackup(name): Verify backup integrity');
227
+
228
+ console.log('\n⏰ To enable daily automated backups:');
229
+ console.log(' 1. Set up a cron job or scheduled task');
230
+ console.log(' 2. Run: node simple-automated-backup.js');
231
+ console.log(' 3. Or use MongoDB Atlas automatic backups (already enabled)');
232
+
233
+ } catch (error) {
234
+ console.error('❌ Error in automated backup system:', error);
235
+ } finally {
236
+ await mongoose.disconnect();
237
+ console.log('🔌 Disconnected from MongoDB');
238
+ }
239
+ };
240
+
241
+ // Run the system
242
+ connectDB().then(() => {
243
+ main();
244
+ });
unlock-subtitles.js ADDED
@@ -0,0 +1,100 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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 unlockSubtitles = async () => {
40
+ try {
41
+ console.log('🔓 Starting subtitle unlock process...');
42
+
43
+ // Find all protected subtitle segments
44
+ const protectedSubtitles = await Subtitle.find({ isProtected: true });
45
+ console.log(`📊 Found ${protectedSubtitles.length} protected subtitle segments`);
46
+
47
+ if (protectedSubtitles.length === 0) {
48
+ console.log('ℹ️ No protected subtitle segments found.');
49
+ return;
50
+ }
51
+
52
+ // Unlock all subtitle segments
53
+ const updatePromises = protectedSubtitles.map(subtitle =>
54
+ Subtitle.findByIdAndUpdate(
55
+ subtitle._id,
56
+ {
57
+ isProtected: false,
58
+ protectedReason: '',
59
+ lastModified: new Date(),
60
+ $push: {
61
+ modificationHistory: {
62
+ timestamp: new Date(),
63
+ action: 'UNLOCKED',
64
+ previousValue: 'true',
65
+ newValue: 'false',
66
+ modifiedBy: 'admin'
67
+ }
68
+ }
69
+ },
70
+ { new: true }
71
+ )
72
+ );
73
+
74
+ const updatedSubtitles = await Promise.all(updatePromises);
75
+ console.log(`✅ Successfully unlocked ${updatedSubtitles.length} subtitle segments`);
76
+
77
+ // Verify unlock
78
+ const unlockedCount = await Subtitle.countDocuments({ isProtected: false });
79
+ console.log(`🔓 Verification: ${unlockedCount} segments are now unlocked`);
80
+
81
+ console.log('\n⚠️ WARNING: Subtitles are now unlocked!');
82
+ console.log('📝 Unlock details:');
83
+ console.log(' - All subtitle segments can now be modified');
84
+ console.log(' - Original English text can be changed');
85
+ console.log(' - Timecodes can be updated');
86
+ console.log(' - Remember to re-lock after modifications');
87
+ console.log(' - Run lock-subtitles.js to re-protect');
88
+
89
+ } catch (error) {
90
+ console.error('❌ Error unlocking subtitles:', error);
91
+ } finally {
92
+ await mongoose.disconnect();
93
+ console.log('🔌 Disconnected from MongoDB');
94
+ }
95
+ };
96
+
97
+ // Run the unlock script
98
+ connectDB().then(() => {
99
+ unlockSubtitles();
100
+ });