Tristan Yu commited on
Commit
70ed4fd
·
1 Parent(s): 341919b

Deploy Transcreation Explorer with full-stack React + Node.js app

Browse files
.dockerignore ADDED
@@ -0,0 +1,18 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ node_modules
2
+ npm-debug.log
3
+ .git
4
+ .gitignore
5
+ README.md
6
+ DEPLOYMENT.md
7
+ .env
8
+ .DS_Store
9
+ .vscode
10
+ .idea
11
+ *.log
12
+ client/build
13
+ server/cached-examples.json
14
+ .nyc_output
15
+ coverage
16
+ .nyc_output
17
+ .coverage
18
+ *.lcov
.gitignore ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ node_modules/
2
+ .env
3
+ .DS_Store
4
+ */node_modules/
5
+ client/build/
6
+ server/cached-examples.json
7
+ *.log
8
+ .vscode/
9
+ .idea/
10
+ dist/
11
+ build/
client/package-lock.json ADDED
The diff for this file is too large to render. See raw diff
 
client/package.json ADDED
@@ -0,0 +1,47 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "transcreation-explorer-client",
3
+ "version": "1.0.0",
4
+ "private": true,
5
+ "dependencies": {
6
+ "@testing-library/jest-dom": "^5.17.0",
7
+ "@testing-library/react": "^13.4.0",
8
+ "@testing-library/user-event": "^13.5.0",
9
+ "axios": "^1.6.7",
10
+ "lucide-react": "^0.323.0",
11
+ "react": "^18.2.0",
12
+ "react-dom": "^18.2.0",
13
+ "react-hot-toast": "^2.4.1",
14
+ "react-router-dom": "^6.22.0",
15
+ "react-scripts": "5.0.1",
16
+ "web-vitals": "^2.1.4"
17
+ },
18
+ "scripts": {
19
+ "start": "react-scripts start",
20
+ "build": "react-scripts build",
21
+ "test": "react-scripts test",
22
+ "eject": "react-scripts eject"
23
+ },
24
+ "homepage": ".",
25
+ "eslintConfig": {
26
+ "extends": [
27
+ "react-app",
28
+ "react-app/jest"
29
+ ]
30
+ },
31
+ "browserslist": {
32
+ "production": [
33
+ ">0.2%",
34
+ "not dead",
35
+ "not op_mini all"
36
+ ],
37
+ "development": [
38
+ "last 1 chrome version",
39
+ "last 1 firefox version",
40
+ "last 1 safari version"
41
+ ]
42
+ },
43
+ "devDependencies": {
44
+ "tailwindcss": "^3.4.1"
45
+ },
46
+ "proxy": "http://localhost:3001"
47
+ }
client/public/favicon.ico ADDED
client/public/index.html ADDED
@@ -0,0 +1,23 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8" />
5
+ <link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
6
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
7
+ <meta name="theme-color" content="#000000" />
8
+ <meta
9
+ name="description"
10
+ content="Transcreation Explorer - Discover and learn about English-Chinese marketing adaptations"
11
+ />
12
+ <link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
13
+ <link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
14
+ <link rel="preconnect" href="https://fonts.googleapis.com">
15
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
16
+ <link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&family=Noto+Sans+SC:wght@300;400;500;600;700&display=swap" rel="stylesheet">
17
+ <title>Transcreation Explorer</title>
18
+ </head>
19
+ <body>
20
+ <noscript>You need to enable JavaScript to run this app.</noscript>
21
+ <div id="root"></div>
22
+ </body>
23
+ </html>
client/public/manifest.json ADDED
@@ -0,0 +1,25 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "short_name": "Transcreation Explorer",
3
+ "name": "Transcreation Explorer - English-Chinese Marketing Adaptations",
4
+ "icons": [
5
+ {
6
+ "src": "favicon.ico",
7
+ "sizes": "64x64 32x32 24x24 16x16",
8
+ "type": "image/x-icon"
9
+ },
10
+ {
11
+ "src": "logo192.png",
12
+ "type": "image/png",
13
+ "sizes": "192x192"
14
+ },
15
+ {
16
+ "src": "logo512.png",
17
+ "type": "image/png",
18
+ "sizes": "512x512"
19
+ }
20
+ ],
21
+ "start_url": ".",
22
+ "display": "standalone",
23
+ "theme_color": "#667eea",
24
+ "background_color": "#ffffff"
25
+ }
client/src/App.css ADDED
@@ -0,0 +1,451 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ * {
2
+ margin: 0;
3
+ padding: 0;
4
+ box-sizing: border-box;
5
+ }
6
+
7
+ body {
8
+ font-family: 'Inter', 'Noto Sans SC', sans-serif;
9
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
10
+ min-height: 100vh;
11
+ color: #333;
12
+ line-height: 1.6;
13
+ }
14
+
15
+ .App {
16
+ min-height: 100vh;
17
+ display: flex;
18
+ flex-direction: column;
19
+ }
20
+
21
+ .main-content {
22
+ flex: 1;
23
+ padding: 2rem;
24
+ max-width: 1400px;
25
+ margin: 0 auto;
26
+ width: 100%;
27
+ }
28
+
29
+ /* Header Styles */
30
+ .header {
31
+ background: rgba(255, 255, 255, 0.95);
32
+ backdrop-filter: blur(10px);
33
+ padding: 1rem 2rem;
34
+ box-shadow: 0 2px 20px rgba(0, 0, 0, 0.1);
35
+ border-bottom: 1px solid rgba(255, 255, 255, 0.2);
36
+ }
37
+
38
+ .header-content {
39
+ max-width: 1400px;
40
+ margin: 0 auto;
41
+ display: flex;
42
+ justify-content: space-between;
43
+ align-items: center;
44
+ }
45
+
46
+ .logo {
47
+ display: flex;
48
+ align-items: center;
49
+ gap: 0.5rem;
50
+ text-decoration: none;
51
+ color: #333;
52
+ }
53
+
54
+ .logo h1 {
55
+ font-size: 1.5rem;
56
+ font-weight: 700;
57
+ background: linear-gradient(135deg, #667eea, #764ba2);
58
+ -webkit-background-clip: text;
59
+ -webkit-text-fill-color: transparent;
60
+ background-clip: text;
61
+ }
62
+
63
+ .nav {
64
+ display: flex;
65
+ gap: 2rem;
66
+ align-items: center;
67
+ }
68
+
69
+ .nav a {
70
+ text-decoration: none;
71
+ color: #666;
72
+ font-weight: 500;
73
+ padding: 0.5rem 1rem;
74
+ border-radius: 8px;
75
+ transition: all 0.3s ease;
76
+ }
77
+
78
+ .nav a:hover,
79
+ .nav a.active {
80
+ color: #667eea;
81
+ background: rgba(102, 126, 234, 0.1);
82
+ }
83
+
84
+ /* Card Styles */
85
+ .card {
86
+ background: rgba(255, 255, 255, 0.95);
87
+ backdrop-filter: blur(10px);
88
+ border-radius: 20px;
89
+ padding: 2rem;
90
+ box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1);
91
+ border: 1px solid rgba(255, 255, 255, 0.2);
92
+ transition: all 0.3s ease;
93
+ }
94
+
95
+ .card:hover {
96
+ transform: translateY(-5px);
97
+ box-shadow: 0 12px 40px rgba(0, 0, 0, 0.15);
98
+ }
99
+
100
+ .example-card {
101
+ background: rgba(255, 255, 255, 0.95);
102
+ backdrop-filter: blur(10px);
103
+ border-radius: 16px;
104
+ padding: 1.5rem;
105
+ margin-bottom: 1rem;
106
+ box-shadow: 0 4px 20px rgba(0, 0, 0, 0.1);
107
+ border: 1px solid rgba(255, 255, 255, 0.2);
108
+ transition: all 0.3s ease;
109
+ }
110
+
111
+ .example-card:hover {
112
+ transform: translateY(-2px);
113
+ box-shadow: 0 8px 30px rgba(0, 0, 0, 0.15);
114
+ }
115
+
116
+ .example-header {
117
+ display: flex;
118
+ justify-content: between;
119
+ align-items: flex-start;
120
+ margin-bottom: 1rem;
121
+ }
122
+
123
+ .example-brand {
124
+ font-size: 0.9rem;
125
+ color: #667eea;
126
+ font-weight: 600;
127
+ background: rgba(102, 126, 234, 0.1);
128
+ padding: 0.25rem 0.75rem;
129
+ border-radius: 20px;
130
+ }
131
+
132
+ .example-category {
133
+ font-size: 0.8rem;
134
+ color: #888;
135
+ background: rgba(136, 136, 136, 0.1);
136
+ padding: 0.25rem 0.75rem;
137
+ border-radius: 20px;
138
+ margin-left: auto;
139
+ }
140
+
141
+ .language-section {
142
+ margin: 1rem 0;
143
+ }
144
+
145
+ .language-label {
146
+ font-size: 0.9rem;
147
+ color: #666;
148
+ font-weight: 600;
149
+ margin-bottom: 0.5rem;
150
+ display: flex;
151
+ align-items: center;
152
+ gap: 0.5rem;
153
+ }
154
+
155
+ .language-text {
156
+ font-size: 1.1rem;
157
+ font-weight: 500;
158
+ color: #333;
159
+ padding: 0.75rem;
160
+ background: rgba(102, 126, 234, 0.05);
161
+ border-radius: 8px;
162
+ border-left: 3px solid #667eea;
163
+ }
164
+
165
+ .chinese-text {
166
+ font-family: 'Noto Sans SC', sans-serif;
167
+ }
168
+
169
+ .example-description {
170
+ margin-top: 1rem;
171
+ padding-top: 1rem;
172
+ border-top: 1px solid rgba(0, 0, 0, 0.1);
173
+ font-size: 0.9rem;
174
+ color: #666;
175
+ line-height: 1.5;
176
+ }
177
+
178
+ /* Button Styles */
179
+ .btn {
180
+ background: linear-gradient(135deg, #667eea, #764ba2);
181
+ color: white;
182
+ border: none;
183
+ padding: 0.75rem 1.5rem;
184
+ border-radius: 10px;
185
+ font-size: 1rem;
186
+ font-weight: 600;
187
+ cursor: pointer;
188
+ transition: all 0.3s ease;
189
+ display: inline-flex;
190
+ align-items: center;
191
+ gap: 0.5rem;
192
+ text-decoration: none;
193
+ }
194
+
195
+ .btn:hover {
196
+ transform: translateY(-2px);
197
+ box-shadow: 0 6px 20px rgba(102, 126, 234, 0.4);
198
+ }
199
+
200
+ .btn-secondary {
201
+ background: rgba(255, 255, 255, 0.2);
202
+ color: #333;
203
+ border: 1px solid rgba(255, 255, 255, 0.3);
204
+ }
205
+
206
+ .btn-secondary:hover {
207
+ background: rgba(255, 255, 255, 0.3);
208
+ box-shadow: 0 4px 15px rgba(0, 0, 0, 0.1);
209
+ }
210
+
211
+ .btn-large {
212
+ padding: 1rem 2rem;
213
+ font-size: 1.1rem;
214
+ }
215
+
216
+ /* Search Styles */
217
+ .search-container {
218
+ margin-bottom: 2rem;
219
+ }
220
+
221
+ .search-input {
222
+ width: 100%;
223
+ padding: 1rem 1.5rem;
224
+ border: 1px solid rgba(255, 255, 255, 0.3);
225
+ border-radius: 12px;
226
+ font-size: 1rem;
227
+ background: rgba(255, 255, 255, 0.9);
228
+ backdrop-filter: blur(10px);
229
+ color: #333;
230
+ transition: all 0.3s ease;
231
+ }
232
+
233
+ .search-input:focus {
234
+ outline: none;
235
+ border-color: #667eea;
236
+ box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1);
237
+ }
238
+
239
+ .search-input::placeholder {
240
+ color: #999;
241
+ }
242
+
243
+ /* Filter Styles */
244
+ .filters {
245
+ display: flex;
246
+ gap: 1rem;
247
+ margin-bottom: 2rem;
248
+ flex-wrap: wrap;
249
+ }
250
+
251
+ .filter-select {
252
+ padding: 0.5rem 1rem;
253
+ border: 1px solid rgba(255, 255, 255, 0.3);
254
+ border-radius: 8px;
255
+ background: rgba(255, 255, 255, 0.9);
256
+ color: #333;
257
+ font-size: 0.9rem;
258
+ min-width: 150px;
259
+ }
260
+
261
+ .filter-select:focus {
262
+ outline: none;
263
+ border-color: #667eea;
264
+ }
265
+
266
+ /* Grid Layouts */
267
+ .grid {
268
+ display: grid;
269
+ gap: 1.5rem;
270
+ }
271
+
272
+ .grid-1 {
273
+ grid-template-columns: 1fr;
274
+ }
275
+
276
+ .grid-2 {
277
+ grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
278
+ }
279
+
280
+ .grid-3 {
281
+ grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
282
+ }
283
+
284
+ /* Home Page Styles */
285
+ .hero {
286
+ text-align: center;
287
+ padding: 4rem 0;
288
+ }
289
+
290
+ .hero h1 {
291
+ font-size: 3rem;
292
+ font-weight: 700;
293
+ color: white;
294
+ margin-bottom: 1rem;
295
+ text-shadow: 0 2px 10px rgba(0, 0, 0, 0.2);
296
+ }
297
+
298
+ .hero p {
299
+ font-size: 1.2rem;
300
+ color: rgba(255, 255, 255, 0.9);
301
+ margin-bottom: 2rem;
302
+ max-width: 600px;
303
+ margin-left: auto;
304
+ margin-right: auto;
305
+ }
306
+
307
+ .hero-actions {
308
+ display: flex;
309
+ gap: 1rem;
310
+ justify-content: center;
311
+ flex-wrap: wrap;
312
+ }
313
+
314
+ .feature-grid {
315
+ margin-top: 4rem;
316
+ }
317
+
318
+ .feature-card {
319
+ text-align: center;
320
+ padding: 2rem;
321
+ }
322
+
323
+ .feature-icon {
324
+ width: 60px;
325
+ height: 60px;
326
+ margin: 0 auto 1rem;
327
+ background: linear-gradient(135deg, #667eea, #764ba2);
328
+ border-radius: 50%;
329
+ display: flex;
330
+ align-items: center;
331
+ justify-content: center;
332
+ color: white;
333
+ }
334
+
335
+ .feature-card h3 {
336
+ font-size: 1.3rem;
337
+ margin-bottom: 0.5rem;
338
+ color: #333;
339
+ }
340
+
341
+ .feature-card p {
342
+ color: #666;
343
+ }
344
+
345
+ /* Loading States */
346
+ .loading {
347
+ display: flex;
348
+ justify-content: center;
349
+ align-items: center;
350
+ padding: 2rem;
351
+ }
352
+
353
+ .spinner {
354
+ width: 40px;
355
+ height: 40px;
356
+ border: 3px solid rgba(102, 126, 234, 0.3);
357
+ border-top: 3px solid #667eea;
358
+ border-radius: 50%;
359
+ animation: spin 1s linear infinite;
360
+ }
361
+
362
+ @keyframes spin {
363
+ 0% { transform: rotate(0deg); }
364
+ 100% { transform: rotate(360deg); }
365
+ }
366
+
367
+ /* Error States */
368
+ .error {
369
+ background: rgba(239, 68, 68, 0.1);
370
+ color: #dc2626;
371
+ padding: 1rem;
372
+ border-radius: 8px;
373
+ border: 1px solid rgba(239, 68, 68, 0.2);
374
+ margin: 1rem 0;
375
+ }
376
+
377
+ /* Success States */
378
+ .success {
379
+ background: rgba(34, 197, 94, 0.1);
380
+ color: #059669;
381
+ padding: 1rem;
382
+ border-radius: 8px;
383
+ border: 1px solid rgba(34, 197, 94, 0.2);
384
+ margin: 1rem 0;
385
+ }
386
+
387
+ /* Responsive Design */
388
+ @media (max-width: 768px) {
389
+ .main-content {
390
+ padding: 1rem;
391
+ }
392
+
393
+ .header {
394
+ padding: 1rem;
395
+ }
396
+
397
+ .header-content {
398
+ flex-direction: column;
399
+ gap: 1rem;
400
+ }
401
+
402
+ .nav {
403
+ gap: 1rem;
404
+ }
405
+
406
+ .hero h1 {
407
+ font-size: 2rem;
408
+ }
409
+
410
+ .hero p {
411
+ font-size: 1rem;
412
+ }
413
+
414
+ .filters {
415
+ flex-direction: column;
416
+ }
417
+
418
+ .filter-select {
419
+ min-width: auto;
420
+ }
421
+
422
+ .hero-actions {
423
+ flex-direction: column;
424
+ align-items: center;
425
+ }
426
+
427
+ .btn {
428
+ width: 100%;
429
+ max-width: 300px;
430
+ justify-content: center;
431
+ }
432
+ }
433
+
434
+ @media (max-width: 480px) {
435
+ .card {
436
+ padding: 1.5rem;
437
+ }
438
+
439
+ .example-card {
440
+ padding: 1rem;
441
+ }
442
+
443
+ .hero h1 {
444
+ font-size: 1.8rem;
445
+ }
446
+
447
+ .nav {
448
+ flex-direction: column;
449
+ gap: 0.5rem;
450
+ }
451
+ }
client/src/App.js ADDED
@@ -0,0 +1,41 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React from 'react';
2
+ import { BrowserRouter as Router, Routes, Route } from 'react-router-dom';
3
+ import { Toaster } from 'react-hot-toast';
4
+ import Header from './components/Header';
5
+ import Home from './pages/Home';
6
+ import Browse from './pages/Browse';
7
+ import Random from './pages/Random';
8
+ import Search from './pages/Search';
9
+ import Manage from './pages/Manage';
10
+ import './App.css';
11
+
12
+ function App() {
13
+ return (
14
+ <Router future={{ v7_startTransition: true, v7_relativeSplatPath: true }}>
15
+ <div className="App">
16
+ <Header />
17
+ <main className="main-content">
18
+ <Routes>
19
+ <Route path="/" element={<Home />} />
20
+ <Route path="/browse" element={<Browse />} />
21
+ <Route path="/random" element={<Random />} />
22
+ <Route path="/search" element={<Search />} />
23
+ <Route path="/manage" element={<Manage />} />
24
+ </Routes>
25
+ </main>
26
+ <Toaster
27
+ position="top-right"
28
+ toastOptions={{
29
+ duration: 4000,
30
+ style: {
31
+ background: '#363636',
32
+ color: '#fff',
33
+ },
34
+ }}
35
+ />
36
+ </div>
37
+ </Router>
38
+ );
39
+ }
40
+
41
+ export default App;
client/src/components/ExampleCard.js ADDED
@@ -0,0 +1,159 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React from 'react';
2
+ import { motion } from 'framer-motion';
3
+ import { Flag, Globe, Info, MapPin, CheckCircle, PenLine, User } from 'lucide-react';
4
+
5
+ const ExampleCard = ({ example, index = 0 }) => {
6
+ const cardVariants = {
7
+ hidden: { opacity: 0, y: 10 },
8
+ visible: {
9
+ opacity: 1,
10
+ y: 0,
11
+ transition: {
12
+ duration: 0.3,
13
+ delay: index * 0.05,
14
+ },
15
+ },
16
+ };
17
+
18
+ const StatusIcon = example.status === 'verified' ? CheckCircle : PenLine;
19
+
20
+ // Detect if this is a Chinese-to-English example (no taiwan field or isChineseToEnglish flag)
21
+ const isChineseToEnglish = example.isChineseToEnglish || !example.taiwan;
22
+
23
+ return (
24
+ <motion.div
25
+ className="example-card"
26
+ variants={cardVariants}
27
+ initial="visible"
28
+ animate="visible"
29
+ whileHover={{ scale: 1.02 }}
30
+ >
31
+ <div className="example-header">
32
+ <div className="example-header-left">
33
+ <span className="example-brand">{example.brand}</span>
34
+ <span className="example-category">{example.category}</span>
35
+ </div>
36
+ <div className="example-header-right">
37
+ {example.contributor && (
38
+ <div className="contributor" title={`Added by ${example.contributor}`}>
39
+ <User size={14} />
40
+ <span>{example.contributor}</span>
41
+ </div>
42
+ )}
43
+ <StatusIcon
44
+ size={16}
45
+ className={example.status === 'verified' ? 'status-verified' : 'status-pending'}
46
+ title={example.status === 'verified' ? 'Verified Example' : 'Pending Review'}
47
+ />
48
+ </div>
49
+ </div>
50
+
51
+ {isChineseToEnglish ? (
52
+ <>
53
+ {/* Chinese to English layout */}
54
+ <div className="language-section">
55
+ <div className="language-label">
56
+ <Flag size={16} />
57
+ Chinese (Original)
58
+ </div>
59
+ <div className="language-text chinese-text">
60
+ {example.mainland}
61
+ </div>
62
+ </div>
63
+
64
+ <div className="language-section">
65
+ <div className="language-label">
66
+ <Globe size={16} />
67
+ English Translation
68
+ </div>
69
+ <div className="language-text">
70
+ {example.english}
71
+ </div>
72
+ </div>
73
+ </>
74
+ ) : (
75
+ <>
76
+ {/* English to Chinese layout */}
77
+ <div className="language-section">
78
+ <div className="language-label">
79
+ <Flag size={16} />
80
+ English (Original)
81
+ </div>
82
+ <div className="language-text">
83
+ {example.english}
84
+ </div>
85
+ </div>
86
+
87
+ <div className="language-section">
88
+ <div className="language-label">
89
+ <MapPin size={16} />
90
+ <span>Mainland China</span>
91
+ </div>
92
+ <div className="language-text chinese-text">
93
+ {example.mainland}
94
+ </div>
95
+ </div>
96
+
97
+ <div className="language-section">
98
+ <div className="language-label">
99
+ <Globe size={16} />
100
+ <span>Taiwan</span>
101
+ </div>
102
+ <div className="language-text chinese-text">
103
+ {example.taiwan}
104
+ </div>
105
+ </div>
106
+ </>
107
+ )}
108
+
109
+ {example.description && (
110
+ <div className="example-description">
111
+ <div className="language-label">
112
+ <Info size={16} />
113
+ Description
114
+ </div>
115
+ {example.description}
116
+ </div>
117
+ )}
118
+
119
+ <style jsx>{`
120
+ .example-header {
121
+ display: flex;
122
+ justify-content: space-between;
123
+ align-items: center;
124
+ margin-bottom: 1.5rem;
125
+ }
126
+
127
+ .example-header-left {
128
+ display: flex;
129
+ align-items: center;
130
+ gap: 0.75rem;
131
+ }
132
+
133
+ .example-header-right {
134
+ display: flex;
135
+ align-items: center;
136
+ gap: 1rem;
137
+ }
138
+
139
+ .contributor {
140
+ display: flex;
141
+ align-items: center;
142
+ gap: 0.25rem;
143
+ font-size: 0.85rem;
144
+ color: #666;
145
+ }
146
+
147
+ .status-verified {
148
+ color: #10b981;
149
+ }
150
+
151
+ .status-pending {
152
+ color: #6b7280;
153
+ }
154
+ `}</style>
155
+ </motion.div>
156
+ );
157
+ };
158
+
159
+ export default ExampleCard;
client/src/components/Header.js ADDED
@@ -0,0 +1,42 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React from 'react';
2
+ import { Link, useLocation } from 'react-router-dom';
3
+ import { Languages } from 'lucide-react';
4
+
5
+ const Header = () => {
6
+ const location = useLocation();
7
+
8
+ const isActive = (path) => {
9
+ return location.pathname === path ? 'active' : '';
10
+ };
11
+
12
+ return (
13
+ <header className="header">
14
+ <div className="header-content">
15
+ <Link to="/" className="logo">
16
+ <Languages size={32} />
17
+ <h1>Transcreation Explorer</h1>
18
+ </Link>
19
+
20
+ <nav className="nav">
21
+ <Link to="/" className={isActive('/')}>
22
+ Home
23
+ </Link>
24
+ <Link to="/browse" className={isActive('/browse')}>
25
+ Browse
26
+ </Link>
27
+ <Link to="/random" className={isActive('/random')}>
28
+ Random
29
+ </Link>
30
+ <Link to="/search" className={isActive('/search')}>
31
+ Search
32
+ </Link>
33
+ <Link to="/manage" className={isActive('/manage')}>
34
+ Manage
35
+ </Link>
36
+ </nav>
37
+ </div>
38
+ </header>
39
+ );
40
+ };
41
+
42
+ export default Header;
client/src/index.css ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ @tailwind base;
2
+ @tailwind components;
3
+ @tailwind utilities;
client/src/index.js ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ import React from 'react';
2
+ import ReactDOM from 'react-dom/client';
3
+ import App from './App';
4
+
5
+ const root = ReactDOM.createRoot(document.getElementById('root'));
6
+ root.render(
7
+ <React.StrictMode>
8
+ <App />
9
+ </React.StrictMode>
10
+ );
client/src/pages/Browse.js ADDED
@@ -0,0 +1,163 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React, { useState, useEffect } from 'react';
2
+ import { motion } from 'framer-motion';
3
+ import axios from 'axios';
4
+ import toast from 'react-hot-toast';
5
+ import ExampleCard from '../components/ExampleCard';
6
+ import { Filter, Grid } from 'lucide-react';
7
+
8
+ const Browse = () => {
9
+ const [examples, setExamples] = useState([]);
10
+ const [filteredExamples, setFilteredExamples] = useState([]);
11
+ const [categories, setCategories] = useState([]);
12
+ const [loading, setLoading] = useState(true);
13
+ const [selectedCategory, setSelectedCategory] = useState('');
14
+
15
+ const fetchData = async () => {
16
+ try {
17
+ const [examplesRes, categoriesRes] = await Promise.all([
18
+ axios.get('/api/examples'),
19
+ axios.get('/api/categories')
20
+ ]);
21
+
22
+ setExamples(examplesRes.data.data);
23
+ setFilteredExamples(examplesRes.data.data);
24
+ setCategories(categoriesRes.data.data);
25
+ } catch (error) {
26
+ toast.error('Failed to load examples');
27
+ console.error('Error fetching data:', error);
28
+ } finally {
29
+ setLoading(false);
30
+ }
31
+ };
32
+
33
+ useEffect(() => {
34
+ fetchData();
35
+ }, []);
36
+
37
+ useEffect(() => {
38
+ let filtered = examples;
39
+
40
+ if (selectedCategory) {
41
+ filtered = filtered.filter(example =>
42
+ example.category === selectedCategory
43
+ );
44
+ }
45
+
46
+ setFilteredExamples(filtered);
47
+ }, [examples, selectedCategory]);
48
+
49
+ const handleCategoryChange = (e) => {
50
+ setSelectedCategory(e.target.value);
51
+ };
52
+
53
+ const clearFilters = () => {
54
+ setSelectedCategory('');
55
+ };
56
+
57
+ if (loading) {
58
+ return (
59
+ <div className="loading">
60
+ <div className="spinner"></div>
61
+ </div>
62
+ );
63
+ }
64
+
65
+ return (
66
+ <motion.div
67
+ initial={{ opacity: 0, y: 20 }}
68
+ animate={{ opacity: 1, y: 0 }}
69
+ transition={{ duration: 0.6 }}
70
+ >
71
+ <div className="card" style={{ marginBottom: '2rem' }}>
72
+ <h1 style={{ marginBottom: '1rem', display: 'flex', alignItems: 'center', gap: '0.5rem' }}>
73
+ <Grid size={24} />
74
+ Browse Examples
75
+ </h1>
76
+ <p style={{ color: '#666', marginBottom: '1.5rem' }}>
77
+ Explore our collection of {examples.length} transcreation examples from
78
+ leading global brands. Use the filter below to find specific examples.
79
+ </p>
80
+
81
+ <div className="filters">
82
+ <div style={{ display: 'flex', alignItems: 'center', gap: '0.5rem' }}>
83
+ <Filter size={16} />
84
+ <span style={{ fontWeight: '600' }}>Filter:</span>
85
+ </div>
86
+
87
+ <select
88
+ className="filter-select"
89
+ value={selectedCategory}
90
+ onChange={handleCategoryChange}
91
+ >
92
+ <option value="">All Categories</option>
93
+ {categories.map(category => (
94
+ <option key={category} value={category}>
95
+ {category}
96
+ </option>
97
+ ))}
98
+ </select>
99
+
100
+ {selectedCategory && (
101
+ <button
102
+ className="btn btn-secondary"
103
+ onClick={clearFilters}
104
+ style={{ padding: '0.5rem 1rem' }}
105
+ >
106
+ Clear Filter
107
+ </button>
108
+ )}
109
+ </div>
110
+
111
+ <div style={{ color: '#666', fontSize: '0.9rem' }}>
112
+ Showing {filteredExamples.length} of {examples.length} examples
113
+ </div>
114
+ </div>
115
+
116
+ {examples.length === 0 ? (
117
+ <div className="card" style={{ textAlign: 'center', padding: '3rem' }}>
118
+ <Grid size={48} style={{ color: '#ccc', marginBottom: '1rem' }} />
119
+ <h3 style={{ marginBottom: '1rem' }}>No Examples Available</h3>
120
+ <p style={{ color: '#666', marginBottom: '1.5rem' }}>
121
+ There are currently no examples in the database.
122
+ </p>
123
+ </div>
124
+ ) : filteredExamples.length === 0 ? (
125
+ <div className="card" style={{ textAlign: 'center', padding: '3rem' }}>
126
+ <h3 style={{ marginBottom: '1rem' }}>No examples found</h3>
127
+ <p style={{ color: '#666', marginBottom: '1.5rem' }}>
128
+ No examples match your current filter setting. Try adjusting your filter.
129
+ </p>
130
+ <button
131
+ className="btn btn-secondary"
132
+ onClick={clearFilters}
133
+ >
134
+ Clear Filter
135
+ </button>
136
+ </div>
137
+ ) : (
138
+ <div className="grid grid-2">
139
+ {filteredExamples.map((example, index) => (
140
+ <ExampleCard
141
+ key={example.id}
142
+ example={example}
143
+ index={index}
144
+ />
145
+ ))}
146
+ </div>
147
+ )}
148
+
149
+ <style jsx>{`
150
+ .spin {
151
+ animation: spin 1s linear infinite;
152
+ }
153
+
154
+ @keyframes spin {
155
+ from { transform: rotate(0deg); }
156
+ to { transform: rotate(360deg); }
157
+ }
158
+ `}</style>
159
+ </motion.div>
160
+ );
161
+ };
162
+
163
+ export default Browse;
client/src/pages/Home.js ADDED
@@ -0,0 +1,59 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React from 'react';
2
+ import { Link } from 'react-router-dom';
3
+ import { motion } from 'framer-motion';
4
+ import { Shuffle, List } from 'lucide-react';
5
+
6
+ const Home = () => {
7
+ const containerVariants = {
8
+ hidden: { opacity: 0 },
9
+ visible: {
10
+ opacity: 1,
11
+ transition: {
12
+ staggerChildren: 0.2,
13
+ },
14
+ },
15
+ };
16
+
17
+ const itemVariants = {
18
+ hidden: { opacity: 0, y: 20 },
19
+ visible: {
20
+ opacity: 1,
21
+ y: 0,
22
+ transition: {
23
+ duration: 0.6,
24
+ },
25
+ },
26
+ };
27
+
28
+ return (
29
+ <motion.div
30
+ variants={containerVariants}
31
+ initial="hidden"
32
+ animate="visible"
33
+ >
34
+ <section className="hero">
35
+ <motion.h1 variants={itemVariants}>
36
+ Transcreation Explorer
37
+ </motion.h1>
38
+ <motion.p variants={itemVariants}>
39
+ Discover the art of transcreation through real-world examples of English-Chinese
40
+ marketing adaptations. See how global brands craft different messages for
41
+ mainland China and Taiwan markets.
42
+ </motion.p>
43
+
44
+ <motion.div className="hero-actions" variants={itemVariants}>
45
+ <Link to="/random" className="btn btn-large">
46
+ <Shuffle size={20} />
47
+ Discover Examples
48
+ </Link>
49
+ <Link to="/browse" className="btn btn-secondary btn-large">
50
+ <List size={20} />
51
+ Browse Collection
52
+ </Link>
53
+ </motion.div>
54
+ </section>
55
+ </motion.div>
56
+ );
57
+ };
58
+
59
+ export default Home;
client/src/pages/Manage.js ADDED
@@ -0,0 +1,834 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React, { useState, useEffect } from 'react';
2
+ import axios from 'axios';
3
+ import { toast } from 'react-hot-toast';
4
+ import { motion } from 'framer-motion';
5
+ import { Settings, Plus, CheckCircle, PenLine } from 'lucide-react';
6
+
7
+ const Manage = () => {
8
+ const [examples, setExamples] = useState([]);
9
+ const [selectedId, setSelectedId] = useState('');
10
+ const [loading, setLoading] = useState(true);
11
+ const [showForm, setShowForm] = useState(false);
12
+ const [isOtherCategory, setIsOtherCategory] = useState(false);
13
+ const [isChineseToEnglish, setIsChineseToEnglish] = useState(false);
14
+ const [formData, setFormData] = useState({
15
+ english: '',
16
+ mainland: '',
17
+ taiwan: '',
18
+ brand: '',
19
+ category: '',
20
+ type: 'slogan',
21
+ description: '',
22
+ status: 'pending',
23
+ contributor: ''
24
+ });
25
+
26
+ // Get unique categories from examples
27
+ const getUniqueCategories = () => {
28
+ const categories = new Set(examples.map(ex => ex.category));
29
+ return Array.from(categories).sort();
30
+ };
31
+
32
+ // eslint-disable-next-line react-hooks/exhaustive-deps
33
+ const loadExamples = async () => {
34
+ try {
35
+ const response = await axios.get('/api/examples');
36
+ if (response.data.success) {
37
+ setExamples(response.data.data);
38
+ }
39
+ } catch (error) {
40
+ console.error('Load error:', error);
41
+ toast.error('Failed to load examples');
42
+ } finally {
43
+ setLoading(false);
44
+ }
45
+ };
46
+
47
+ useEffect(() => {
48
+ loadExamples();
49
+ }, [loadExamples]);
50
+
51
+ const handleExampleSelect = (e) => {
52
+ const id = e.target.value;
53
+ setSelectedId(id);
54
+
55
+ if (id === 'new') {
56
+ // Adding new example
57
+ setFormData({
58
+ english: '',
59
+ mainland: '',
60
+ taiwan: '',
61
+ brand: '',
62
+ category: '',
63
+ type: 'slogan',
64
+ description: '',
65
+ status: 'pending',
66
+ contributor: ''
67
+ });
68
+ setIsOtherCategory(false);
69
+ setShowForm(true);
70
+ } else if (id) {
71
+ // Editing existing example
72
+ const selected = examples.find(ex => ex.id === id);
73
+ if (selected) {
74
+ setFormData({
75
+ english: selected.english || '',
76
+ mainland: selected.mainland || '',
77
+ taiwan: selected.taiwan || '',
78
+ brand: selected.brand || '',
79
+ category: selected.category || '',
80
+ type: selected.type || 'slogan',
81
+ description: selected.description || '',
82
+ status: selected.status || 'pending',
83
+ contributor: selected.contributor || ''
84
+ });
85
+ setIsOtherCategory(false);
86
+ setShowForm(true);
87
+ }
88
+ } else {
89
+ // No selection
90
+ setShowForm(false);
91
+ setIsOtherCategory(false);
92
+ }
93
+ };
94
+
95
+ const handleChange = (e) => {
96
+ const { name, value } = e.target;
97
+
98
+ if (name === 'category' && value === 'other') {
99
+ setIsOtherCategory(true);
100
+ setFormData(prev => ({
101
+ ...prev,
102
+ category: ''
103
+ }));
104
+ } else if (name === 'category' && isOtherCategory) {
105
+ // When typing in the custom category input, just update the value
106
+ setFormData(prev => ({
107
+ ...prev,
108
+ [name]: value
109
+ }));
110
+ } else if (name === 'category') {
111
+ // When selecting from dropdown
112
+ setIsOtherCategory(false);
113
+ setFormData(prev => ({
114
+ ...prev,
115
+ [name]: value
116
+ }));
117
+ } else {
118
+ setFormData(prev => ({
119
+ ...prev,
120
+ [name]: value
121
+ }));
122
+ }
123
+ };
124
+
125
+ const handleSubmit = async (e) => {
126
+ e.preventDefault();
127
+ try {
128
+ // Validate required fields based on direction
129
+ const requiredFields = isChineseToEnglish
130
+ ? ['english', 'mainland', 'brand', 'category', 'type']
131
+ : ['english', 'mainland', 'taiwan', 'brand', 'category', 'type'];
132
+
133
+ const missingFields = requiredFields.filter(field => !formData[field] || formData[field].trim() === '');
134
+
135
+ if (missingFields.length > 0) {
136
+ console.error('Missing required fields:', missingFields);
137
+ toast.error(`Please fill in all required fields: ${missingFields.map(field => field.charAt(0).toUpperCase() + field.slice(1)).join(', ')}`);
138
+ return;
139
+ }
140
+
141
+ // Clean and prepare data
142
+ const dataToSubmit = {
143
+ ...formData,
144
+ // Ensure all fields are strings and trimmed
145
+ english: formData.english.trim(),
146
+ mainland: formData.mainland.trim(),
147
+ taiwan: isChineseToEnglish ? undefined : formData.taiwan?.trim(),
148
+ brand: formData.brand.trim(),
149
+ category: formData.category.trim(),
150
+ type: formData.type || 'slogan',
151
+ description: formData.description?.trim() ?? '',
152
+ status: formData.status || 'pending',
153
+ contributor: formData.contributor?.trim(), // Allow empty string
154
+ isChineseToEnglish // Add direction flag
155
+ };
156
+
157
+ console.log('Submitting form data:', dataToSubmit);
158
+ let response;
159
+
160
+ if (selectedId && selectedId !== 'new') {
161
+ console.log('Updating existing example:', selectedId);
162
+ response = await axios.put(`/api/examples/${selectedId}`, dataToSubmit);
163
+ } else {
164
+ console.log('Adding new example');
165
+ response = await axios.post('/api/examples/add', dataToSubmit);
166
+ }
167
+
168
+ if (response.data.success) {
169
+ console.log('Save successful:', response.data);
170
+ toast.success(selectedId && selectedId !== 'new' ? 'Example updated successfully!' : 'Example added successfully!');
171
+ await loadExamples();
172
+ // Reset form
173
+ setSelectedId('');
174
+ setShowForm(false);
175
+ setFormData({
176
+ english: '',
177
+ mainland: '',
178
+ taiwan: '',
179
+ brand: '',
180
+ category: '',
181
+ type: 'slogan',
182
+ description: '',
183
+ status: 'pending',
184
+ contributor: ''
185
+ });
186
+ setIsChineseToEnglish(false);
187
+ } else {
188
+ console.error('Save failed:', response.data);
189
+ toast.error(response.data.message || 'Failed to save example');
190
+ }
191
+ } catch (error) {
192
+ console.error('Save error:', error);
193
+ console.error('Error details:', {
194
+ message: error.message,
195
+ response: error.response?.data,
196
+ status: error.response?.status,
197
+ data: formData
198
+ });
199
+ toast.error(error.response?.data?.message || 'Failed to save example');
200
+ }
201
+ };
202
+
203
+ const handleDelete = async () => {
204
+ if (!selectedId || selectedId === 'new') return;
205
+
206
+ if (window.confirm('Are you sure you want to delete this example?')) {
207
+ try {
208
+ const response = await axios.delete(`/api/examples/${selectedId}`);
209
+ if (response.data.success) {
210
+ toast.success('Example deleted successfully!');
211
+ setSelectedId('');
212
+ setShowForm(false);
213
+ setFormData({
214
+ english: '',
215
+ mainland: '',
216
+ taiwan: '',
217
+ brand: '',
218
+ category: '',
219
+ type: 'slogan',
220
+ description: '',
221
+ status: 'pending',
222
+ contributor: ''
223
+ });
224
+ await loadExamples();
225
+ }
226
+ } catch (error) {
227
+ console.error('Delete error:', error);
228
+ toast.error('Failed to delete example');
229
+ }
230
+ }
231
+ };
232
+
233
+ const handleCancel = () => {
234
+ setSelectedId('');
235
+ setShowForm(false);
236
+ setFormData({
237
+ english: '',
238
+ mainland: '',
239
+ taiwan: '',
240
+ brand: '',
241
+ category: '',
242
+ type: 'slogan',
243
+ description: '',
244
+ status: 'pending',
245
+ contributor: ''
246
+ });
247
+ };
248
+
249
+ const getStatusIcon = (status) => {
250
+ return status === 'verified' ? <CheckCircle size={16} className="status-icon verified" /> : <PenLine size={16} className="status-icon pending" />;
251
+ };
252
+
253
+ if (loading) {
254
+ return (
255
+ <div className="page-container">
256
+ <div className="loading">
257
+ <div className="spinner"></div>
258
+ </div>
259
+ </div>
260
+ );
261
+ }
262
+
263
+ return (
264
+ <div className="page-container">
265
+ <motion.div
266
+ initial={{ opacity: 0, y: 20 }}
267
+ animate={{ opacity: 1, y: 0 }}
268
+ transition={{ duration: 0.6 }}
269
+ className="content-wrapper"
270
+ >
271
+ <div className="card" style={{ marginBottom: '2rem' }}>
272
+ <h1 style={{ marginBottom: '1rem', display: 'flex', alignItems: 'center', gap: '0.5rem' }}>
273
+ <Settings size={24} />
274
+ Manage Examples
275
+ </h1>
276
+ <p style={{ color: '#666', marginBottom: '1.5rem' }}>
277
+ Add new transcreation examples or edit existing ones. Select an option below to get started.
278
+ </p>
279
+
280
+ <div className="action-bar">
281
+ <div className="select-wrapper">
282
+ <select
283
+ value={selectedId}
284
+ onChange={handleExampleSelect}
285
+ className="action-select"
286
+ >
287
+ <option value="">Choose an action...</option>
288
+ <option value="new">➕ Add new example</option>
289
+ <optgroup label="Edit existing examples:">
290
+ {examples
291
+ .sort((a, b) => a.brand.localeCompare(b.brand))
292
+ .map(ex => (
293
+ <option key={ex.id} value={ex.id}>
294
+ {ex.status === 'verified' ? '✓' : '✏️'} {ex.brand} - {ex.english}
295
+ </option>
296
+ ))}
297
+ </optgroup>
298
+ </select>
299
+ </div>
300
+
301
+ {!showForm && (
302
+ <button
303
+ onClick={() => {
304
+ setSelectedId('new');
305
+ setShowForm(true);
306
+ }}
307
+ className="btn quick-add-btn"
308
+ >
309
+ <Plus size={18} />
310
+ Quick Add
311
+ </button>
312
+ )}
313
+ </div>
314
+ </div>
315
+
316
+ {showForm && (
317
+ <motion.div
318
+ className="card form-card"
319
+ initial={{ opacity: 0, y: 20 }}
320
+ animate={{ opacity: 1, y: 0 }}
321
+ transition={{ duration: 0.3 }}
322
+ >
323
+ <div className="form-header">
324
+ <h2 className="form-title">
325
+ {selectedId === 'new' ? '➕ Add New Example' : (
326
+ <span className="title-with-status">
327
+ {getStatusIcon(formData.status)}
328
+ Edit Example
329
+ </span>
330
+ )}
331
+ </h2>
332
+ <button
333
+ onClick={handleCancel}
334
+ className="close-btn"
335
+ >
336
+
337
+ </button>
338
+ </div>
339
+
340
+ <form onSubmit={handleSubmit} className="form-content">
341
+ <div className="form-row">
342
+ <div className="form-field">
343
+ <label className="field-label">Brand Name</label>
344
+ <input
345
+ type="text"
346
+ name="brand"
347
+ value={formData.brand}
348
+ onChange={handleChange}
349
+ className="field-input"
350
+ required
351
+ />
352
+ </div>
353
+ <div className="form-field">
354
+ <label className="field-label">Category</label>
355
+ {isOtherCategory ? (
356
+ <input
357
+ type="text"
358
+ name="category"
359
+ value={formData.category}
360
+ onChange={handleChange}
361
+ className="field-input"
362
+ placeholder="Enter custom category"
363
+ required
364
+ />
365
+ ) : (
366
+ <select
367
+ name="category"
368
+ value={formData.category}
369
+ onChange={handleChange}
370
+ className="field-select"
371
+ required
372
+ >
373
+ <option value="">Select a category...</option>
374
+ {getUniqueCategories().map(category => (
375
+ <option key={category} value={category}>
376
+ {category}
377
+ </option>
378
+ ))}
379
+ <option value="other">Other (specify)</option>
380
+ </select>
381
+ )}
382
+ {isOtherCategory && (
383
+ <button
384
+ type="button"
385
+ onClick={() => {
386
+ setIsOtherCategory(false);
387
+ setFormData(prev => ({
388
+ ...prev,
389
+ category: ''
390
+ }));
391
+ }}
392
+ className="btn btn-small btn-secondary"
393
+ style={{ marginTop: '0.5rem' }}
394
+ >
395
+ Back to list
396
+ </button>
397
+ )}
398
+ </div>
399
+ </div>
400
+
401
+ <div className="form-row">
402
+ <div className="form-field">
403
+ <label className="field-label">Status</label>
404
+ <select
405
+ name="status"
406
+ value={formData.status || 'pending'}
407
+ onChange={handleChange}
408
+ className="field-select"
409
+ >
410
+ <option value="pending">
411
+ Pending Review ✏️
412
+ </option>
413
+ <option value="verified">
414
+ Verified ✓
415
+ </option>
416
+ </select>
417
+ </div>
418
+ <div className="form-field">
419
+ <label className="field-label">Contributor</label>
420
+ <input
421
+ type="text"
422
+ name="contributor"
423
+ value={formData.contributor || ''}
424
+ onChange={handleChange}
425
+ className="field-input"
426
+ placeholder="e.g., Student Name"
427
+ />
428
+ </div>
429
+ </div>
430
+
431
+ <div className="translation-section">
432
+ <h3 className="section-title">Translations</h3>
433
+ <div className="direction-toggle-wrapper">
434
+ <button
435
+ type="button"
436
+ onClick={() => setIsChineseToEnglish(!isChineseToEnglish)}
437
+ className="btn btn-small btn-secondary direction-toggle"
438
+ title={isChineseToEnglish ? "Switch to English → Chinese" : "Switch to Chinese → English"}
439
+ >
440
+ {isChineseToEnglish ? "🇨🇳 → 🇬🇧" : "🇬🇧 → 🇨🇳"}
441
+ </button>
442
+ </div>
443
+ <div className="form-row">
444
+ <div className="form-field">
445
+ <label className="field-label">
446
+ {isChineseToEnglish ? "Chinese Original" : "English Original"}
447
+ </label>
448
+ <input
449
+ type="text"
450
+ name={isChineseToEnglish ? "mainland" : "english"}
451
+ value={isChineseToEnglish ? formData.mainland : formData.english}
452
+ onChange={handleChange}
453
+ className="field-input"
454
+ required
455
+ />
456
+ </div>
457
+ </div>
458
+ <div className="form-row">
459
+ <div className="form-field">
460
+ <label className="field-label">
461
+ {isChineseToEnglish ? "English Translation" : "Mainland China Version"}
462
+ </label>
463
+ <input
464
+ type="text"
465
+ name={isChineseToEnglish ? "english" : "mainland"}
466
+ value={isChineseToEnglish ? formData.english : formData.mainland}
467
+ onChange={handleChange}
468
+ className="field-input"
469
+ required
470
+ />
471
+ </div>
472
+ {!isChineseToEnglish && (
473
+ <div className="form-field">
474
+ <label className="field-label">Taiwan Version</label>
475
+ <input
476
+ type="text"
477
+ name="taiwan"
478
+ value={formData.taiwan}
479
+ onChange={handleChange}
480
+ className="field-input"
481
+ required
482
+ />
483
+ </div>
484
+ )}
485
+ </div>
486
+ </div>
487
+
488
+ <div className="form-field">
489
+ <label className="field-label">Description (Optional)</label>
490
+ <textarea
491
+ name="description"
492
+ value={formData.description || ''}
493
+ onChange={handleChange}
494
+ className="field-textarea"
495
+ rows="3"
496
+ />
497
+ </div>
498
+
499
+ <div className="form-actions">
500
+ <div className="action-group">
501
+ <button type="submit" className="btn btn-primary">
502
+ {selectedId === 'new' ? 'Add Example' : 'Save Changes'}
503
+ </button>
504
+ <button type="button" className="btn btn-secondary" onClick={handleCancel}>
505
+ Cancel
506
+ </button>
507
+ </div>
508
+ {selectedId !== 'new' && (
509
+ <button type="button" className="btn btn-danger" onClick={handleDelete}>
510
+ Delete Example
511
+ </button>
512
+ )}
513
+ </div>
514
+ </form>
515
+ </motion.div>
516
+ )}
517
+
518
+ <style jsx="true">{`
519
+ .page-container {
520
+ min-height: 100vh;
521
+ padding: 2rem 1rem;
522
+ }
523
+
524
+ .content-wrapper {
525
+ max-width: 800px;
526
+ margin: 0 auto;
527
+ }
528
+
529
+ .card {
530
+ background: white;
531
+ border-radius: 16px;
532
+ box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1);
533
+ margin-bottom: 2rem;
534
+ overflow: hidden;
535
+ }
536
+
537
+
538
+
539
+ .action-bar {
540
+ display: flex;
541
+ gap: 1rem;
542
+ align-items: center;
543
+ }
544
+
545
+ .select-wrapper {
546
+ flex: 1;
547
+ }
548
+
549
+ .action-select {
550
+ width: 100%;
551
+ padding: 1rem;
552
+ font-size: 1rem;
553
+ border: 2px solid #e2e8f0;
554
+ border-radius: 12px;
555
+ background: white;
556
+ transition: all 0.2s;
557
+ }
558
+
559
+ .action-select:focus {
560
+ outline: none;
561
+ border-color: #667eea;
562
+ box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1);
563
+ }
564
+
565
+ .quick-add-btn {
566
+ display: flex;
567
+ align-items: center;
568
+ gap: 0.5rem;
569
+ white-space: nowrap;
570
+ padding: 1rem 1.5rem;
571
+ }
572
+
573
+ .form-card {
574
+ padding: 0;
575
+ }
576
+
577
+ .form-header {
578
+ display: flex;
579
+ justify-content: space-between;
580
+ align-items: center;
581
+ padding: 2rem 2.5rem;
582
+ border-bottom: 1px solid #e2e8f0;
583
+ background: #f8fafc;
584
+ }
585
+
586
+ .form-title {
587
+ margin: 0;
588
+ font-size: 1.5rem;
589
+ font-weight: 600;
590
+ color: #2d3748;
591
+ }
592
+
593
+ .close-btn {
594
+ background: none;
595
+ border: none;
596
+ color: #718096;
597
+ cursor: pointer;
598
+ font-size: 1.5rem;
599
+ padding: 0.5rem;
600
+ border-radius: 50%;
601
+ transition: all 0.2s;
602
+ }
603
+
604
+ .close-btn:hover {
605
+ background: #e2e8f0;
606
+ color: #2d3748;
607
+ }
608
+
609
+ .form-content {
610
+ padding: 2.5rem;
611
+ display: flex;
612
+ flex-direction: column;
613
+ gap: 2rem;
614
+ }
615
+
616
+ .form-row {
617
+ display: grid;
618
+ grid-template-columns: 1fr 1fr;
619
+ gap: 1.5rem;
620
+ }
621
+
622
+ .form-field {
623
+ display: flex;
624
+ flex-direction: column;
625
+ gap: 0.5rem;
626
+ }
627
+
628
+ .field-label {
629
+ font-weight: 600;
630
+ color: #2d3748;
631
+ font-size: 0.95rem;
632
+ }
633
+
634
+ .field-input, .field-textarea, .field-select {
635
+ padding: 0.875rem;
636
+ font-size: 1rem;
637
+ border: 2px solid #e2e8f0;
638
+ border-radius: 8px;
639
+ transition: all 0.2s;
640
+ background: white;
641
+ }
642
+
643
+ .field-input:focus, .field-textarea:focus, .field-select:focus {
644
+ outline: none;
645
+ border-color: #667eea;
646
+ box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1);
647
+ }
648
+
649
+ .field-textarea {
650
+ min-height: 80px;
651
+ resize: vertical;
652
+ font-family: inherit;
653
+ }
654
+
655
+ .field-textarea.small {
656
+ min-height: 60px;
657
+ }
658
+
659
+ .translation-section {
660
+ background: #f8fafc;
661
+ padding: 1.5rem;
662
+ border-radius: 12px;
663
+ border: 1px solid #e2e8f0;
664
+ }
665
+
666
+ .section-title {
667
+ margin: 0 0 1rem 0;
668
+ font-size: 1.1rem;
669
+ font-weight: 600;
670
+ color: #2d3748;
671
+ text-align: center;
672
+ }
673
+
674
+ .form-actions {
675
+ display: flex;
676
+ justify-content: space-between;
677
+ align-items: center;
678
+ padding-top: 1.5rem;
679
+ border-top: 1px solid #e2e8f0;
680
+ }
681
+
682
+ .action-group {
683
+ display: flex;
684
+ gap: 1rem;
685
+ }
686
+
687
+ .btn {
688
+ padding: 0.875rem 1.5rem;
689
+ border: none;
690
+ border-radius: 8px;
691
+ font-weight: 600;
692
+ cursor: pointer;
693
+ transition: all 0.2s;
694
+ font-size: 1rem;
695
+ display: flex;
696
+ align-items: center;
697
+ gap: 0.5rem;
698
+ }
699
+
700
+ .btn-primary {
701
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
702
+ color: white;
703
+ }
704
+
705
+ .btn-primary:hover {
706
+ transform: translateY(-1px);
707
+ box-shadow: 0 8px 20px rgba(102, 126, 234, 0.3);
708
+ }
709
+
710
+ .btn-secondary {
711
+ background: #f7fafc;
712
+ color: #2d3748;
713
+ border: 2px solid #e2e8f0;
714
+ }
715
+
716
+ .btn-secondary:hover {
717
+ background: #edf2f7;
718
+ border-color: #cbd5e0;
719
+ }
720
+
721
+ .btn-danger {
722
+ background: #fed7d7;
723
+ color: #c53030;
724
+ border: 2px solid #feb2b2;
725
+ }
726
+
727
+ .btn-danger:hover {
728
+ background: #fbb6b6;
729
+ border-color: #f56565;
730
+ }
731
+
732
+ .btn-small {
733
+ padding: 0.5rem 1rem;
734
+ font-size: 0.875rem;
735
+ height: 32px;
736
+ }
737
+
738
+ .loading {
739
+ display: flex;
740
+ justify-content: center;
741
+ align-items: center;
742
+ min-height: 400px;
743
+ }
744
+
745
+ .spinner {
746
+ width: 40px;
747
+ height: 40px;
748
+ border: 4px solid #f3f3f3;
749
+ border-top: 4px solid #667eea;
750
+ border-radius: 50%;
751
+ animation: spin 1s linear infinite;
752
+ }
753
+
754
+ @keyframes spin {
755
+ 0% { transform: rotate(0deg); }
756
+ 100% { transform: rotate(360deg); }
757
+ }
758
+
759
+ @media (max-width: 768px) {
760
+ .content-wrapper {
761
+ max-width: 100%;
762
+ }
763
+
764
+ .form-row {
765
+ grid-template-columns: 1fr;
766
+ }
767
+
768
+ .action-bar {
769
+ flex-direction: column;
770
+ gap: 1rem;
771
+ }
772
+
773
+ .form-actions {
774
+ flex-direction: column;
775
+ gap: 1rem;
776
+ align-items: stretch;
777
+ }
778
+
779
+ .action-group {
780
+ justify-content: center;
781
+ }
782
+ }
783
+
784
+ .title-with-status {
785
+ display: flex;
786
+ align-items: center;
787
+ gap: 0.5rem;
788
+ }
789
+
790
+ .status-icon {
791
+ width: 16px;
792
+ height: 16px;
793
+ }
794
+
795
+ .status-icon.verified {
796
+ color: #48bb78;
797
+ }
798
+
799
+ .status-icon.pending {
800
+ color: #718096;
801
+ }
802
+
803
+ .status-option {
804
+ display: flex;
805
+ align-items: center;
806
+ gap: 0.5rem;
807
+ }
808
+
809
+ .direction-toggle-wrapper {
810
+ margin-bottom: 1rem;
811
+ display: flex;
812
+ justify-content: center;
813
+ align-items: center;
814
+ }
815
+
816
+ .direction-toggle {
817
+ padding: 0.5rem 1rem;
818
+ font-size: 0.875rem;
819
+ height: auto;
820
+ border-radius: 20px;
821
+ transition: all 0.2s;
822
+ }
823
+
824
+ .direction-toggle:hover {
825
+ transform: scale(1.05);
826
+ background: #edf2f7;
827
+ }
828
+ `}</style>
829
+ </motion.div>
830
+ </div>
831
+ );
832
+ };
833
+
834
+ export default Manage;
client/src/pages/Random.js ADDED
@@ -0,0 +1,191 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React, { useState, useEffect } from 'react';
2
+ import { motion, AnimatePresence } from 'framer-motion';
3
+ import axios from 'axios';
4
+ import toast from 'react-hot-toast';
5
+ import ExampleCard from '../components/ExampleCard';
6
+ import { Shuffle, RefreshCw, Database } from 'lucide-react';
7
+
8
+ const Random = () => {
9
+ const [currentExample, setCurrentExample] = useState(null);
10
+ const [loading, setLoading] = useState(true);
11
+ const [refreshing, setRefreshing] = useState(false);
12
+ const [stats, setStats] = useState({ totalExamples: 0 });
13
+
14
+ const fetchRandomExample = async (showLoadingState = true) => {
15
+ if (showLoadingState) {
16
+ setRefreshing(true);
17
+ }
18
+
19
+ try {
20
+ const response = await axios.get('/api/examples/random');
21
+ setCurrentExample(response.data.data);
22
+ } catch (error) {
23
+ toast.error('Failed to load random example');
24
+ console.error('Error fetching random example:', error);
25
+ } finally {
26
+ setLoading(false);
27
+ setRefreshing(false);
28
+ }
29
+ };
30
+
31
+ const fetchStats = async () => {
32
+ try {
33
+ const response = await axios.get('/api/stats');
34
+ setStats(response.data.data);
35
+ } catch (error) {
36
+ console.error('Error fetching stats:', error);
37
+ }
38
+ };
39
+
40
+ useEffect(() => {
41
+ fetchRandomExample();
42
+ fetchStats();
43
+ }, []);
44
+
45
+ const handleNewExample = () => {
46
+ fetchRandomExample(true);
47
+ };
48
+
49
+ if (loading) {
50
+ return (
51
+ <div className="loading">
52
+ <div className="spinner"></div>
53
+ </div>
54
+ );
55
+ }
56
+
57
+ return (
58
+ <motion.div
59
+ initial={{ opacity: 0, y: 20 }}
60
+ animate={{ opacity: 1, y: 0 }}
61
+ transition={{ duration: 0.6 }}
62
+ >
63
+ <div className="card" style={{
64
+ marginBottom: '2rem',
65
+ textAlign: 'center',
66
+ maxWidth: '800px',
67
+ margin: '0 auto 2rem auto'
68
+ }}>
69
+ <h1 style={{
70
+ marginBottom: '1rem',
71
+ display: 'flex',
72
+ alignItems: 'center',
73
+ justifyContent: 'center',
74
+ gap: '0.5rem'
75
+ }}>
76
+ <Shuffle size={24} />
77
+ Random Discovery
78
+ </h1>
79
+ <p style={{ color: '#666', marginBottom: '1.5rem' }}>
80
+ Discover transcreation examples through serendipitous exploration.
81
+ Each click reveals a new example to inspire and educate.
82
+ </p>
83
+
84
+ <div style={{
85
+ display: 'flex',
86
+ gap: '1rem',
87
+ justifyContent: 'center',
88
+ flexWrap: 'wrap',
89
+ marginBottom: '1rem'
90
+ }}>
91
+ <button
92
+ className="btn"
93
+ onClick={handleNewExample}
94
+ disabled={refreshing}
95
+ style={{
96
+ display: 'inline-flex',
97
+ alignItems: 'center',
98
+ gap: '0.5rem'
99
+ }}
100
+ >
101
+ {refreshing ? (
102
+ <>
103
+ <RefreshCw size={20} className="spin" />
104
+ Loading...
105
+ </>
106
+ ) : (
107
+ <>
108
+ <Shuffle size={20} />
109
+ {currentExample ? 'Get Another Example' : 'Try Again'}
110
+ </>
111
+ )}
112
+ </button>
113
+ </div>
114
+
115
+ <div style={{
116
+ display: 'flex',
117
+ alignItems: 'center',
118
+ justifyContent: 'center',
119
+ gap: '1rem',
120
+ fontSize: '0.9rem',
121
+ color: '#666'
122
+ }}>
123
+ <div style={{ display: 'flex', alignItems: 'center', gap: '0.25rem' }}>
124
+ <Database size={14} />
125
+ <span>{stats.totalExamples} examples available</span>
126
+ </div>
127
+ </div>
128
+ </div>
129
+
130
+ {currentExample ? (
131
+ <AnimatePresence mode="wait">
132
+ <motion.div
133
+ key={currentExample.id}
134
+ initial={{ opacity: 0, scale: 0.95 }}
135
+ animate={{ opacity: 1, scale: 1 }}
136
+ exit={{ opacity: 0, scale: 0.95 }}
137
+ transition={{ duration: 0.3 }}
138
+ style={{
139
+ maxWidth: '800px',
140
+ margin: '0 auto'
141
+ }}
142
+ >
143
+ <ExampleCard example={currentExample} />
144
+ </motion.div>
145
+ </AnimatePresence>
146
+ ) : (
147
+ <div className="card" style={{ textAlign: 'center', padding: '3rem' }}>
148
+ <h3 style={{ marginBottom: '1rem' }}>No examples available</h3>
149
+ <p style={{ color: '#666', marginBottom: '1.5rem' }}>
150
+ Please try again later.
151
+ </p>
152
+ <button
153
+ className="btn"
154
+ onClick={handleNewExample}
155
+ disabled={refreshing}
156
+ style={{
157
+ display: 'inline-flex',
158
+ alignItems: 'center',
159
+ gap: '0.5rem'
160
+ }}
161
+ >
162
+ {refreshing ? (
163
+ <>
164
+ <RefreshCw size={20} className="spin" />
165
+ Loading...
166
+ </>
167
+ ) : (
168
+ <>
169
+ <Shuffle size={20} />
170
+ Try Again
171
+ </>
172
+ )}
173
+ </button>
174
+ </div>
175
+ )}
176
+
177
+ <style jsx>{`
178
+ .spin {
179
+ animation: spin 1s linear infinite;
180
+ }
181
+
182
+ @keyframes spin {
183
+ from { transform: rotate(0deg); }
184
+ to { transform: rotate(360deg); }
185
+ }
186
+ `}</style>
187
+ </motion.div>
188
+ );
189
+ };
190
+
191
+ export default Random;
client/src/pages/Search.js ADDED
@@ -0,0 +1,265 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React, { useState, useEffect, useMemo } from 'react';
2
+ import { motion } from 'framer-motion';
3
+ import axios from 'axios';
4
+ import toast from 'react-hot-toast';
5
+ import ExampleCard from '../components/ExampleCard';
6
+ import { Search as SearchIcon, X, Zap } from 'lucide-react';
7
+
8
+ const Search = () => {
9
+ const [searchTerm, setSearchTerm] = useState('');
10
+ const [results, setResults] = useState([]);
11
+ const [loading, setLoading] = useState(false);
12
+ const [hasSearched, setHasSearched] = useState(false);
13
+ const [allExamples, setAllExamples] = useState([]);
14
+
15
+ useEffect(() => {
16
+ // Load all examples for suggestions
17
+ const fetchAllExamples = async () => {
18
+ try {
19
+ const response = await axios.get('/api/examples');
20
+ setAllExamples(response.data.data);
21
+ } catch (error) {
22
+ console.error('Error fetching examples:', error);
23
+ }
24
+ };
25
+
26
+ fetchAllExamples();
27
+ }, []);
28
+
29
+ const performSearch = async (term) => {
30
+ if (!term.trim()) {
31
+ setResults([]);
32
+ setHasSearched(false);
33
+ return;
34
+ }
35
+
36
+ setLoading(true);
37
+ setHasSearched(true);
38
+
39
+ try {
40
+ const response = await axios.get(`/api/search?q=${encodeURIComponent(term)}`);
41
+ setResults(response.data.data);
42
+ } catch (error) {
43
+ toast.error('Search failed');
44
+ console.error('Search error:', error);
45
+ setResults([]);
46
+ } finally {
47
+ setLoading(false);
48
+ }
49
+ };
50
+
51
+ const handleSearchSubmit = (e) => {
52
+ e.preventDefault();
53
+ performSearch(searchTerm);
54
+ };
55
+
56
+ const handleClearSearch = () => {
57
+ setSearchTerm('');
58
+ setResults([]);
59
+ setHasSearched(false);
60
+ };
61
+
62
+ // Debounced search
63
+ useEffect(() => {
64
+ const delayedSearch = setTimeout(() => {
65
+ if (searchTerm.length > 2) {
66
+ performSearch(searchTerm);
67
+ } else if (searchTerm.length === 0) {
68
+ setResults([]);
69
+ setHasSearched(false);
70
+ }
71
+ }, 300);
72
+
73
+ return () => clearTimeout(delayedSearch);
74
+ }, [searchTerm]);
75
+
76
+ // Generate search suggestions
77
+ const suggestions = useMemo(() => {
78
+ if (searchTerm.length < 2) return [];
79
+
80
+ const brands = [...new Set(allExamples.map(ex => ex.brand))];
81
+ const categories = [...new Set(allExamples.map(ex => ex.category))];
82
+
83
+ const allSuggestions = [...brands, ...categories];
84
+
85
+ return allSuggestions
86
+ .filter(item =>
87
+ item.toLowerCase().includes(searchTerm.toLowerCase()) &&
88
+ item.toLowerCase() !== searchTerm.toLowerCase()
89
+ )
90
+ .slice(0, 5);
91
+ }, [searchTerm, allExamples]);
92
+
93
+ const handleSuggestionClick = (suggestion) => {
94
+ setSearchTerm(suggestion);
95
+ performSearch(suggestion);
96
+ };
97
+
98
+ return (
99
+ <motion.div
100
+ initial={{ opacity: 0, y: 20 }}
101
+ animate={{ opacity: 1, y: 0 }}
102
+ transition={{ duration: 0.6 }}
103
+ >
104
+ <div className="card" style={{ marginBottom: '2rem' }}>
105
+ <h1 style={{
106
+ marginBottom: '1rem',
107
+ display: 'flex',
108
+ alignItems: 'center',
109
+ gap: '0.5rem'
110
+ }}>
111
+ <SearchIcon size={24} />
112
+ Search Examples
113
+ </h1>
114
+ <p style={{ color: '#666', marginBottom: '1.5rem' }}>
115
+ Search through our collection of transcreation examples by brand name,
116
+ category, English text, Chinese text, or keywords in descriptions.
117
+ </p>
118
+
119
+ <form onSubmit={handleSearchSubmit} className="search-container">
120
+ <div style={{ position: 'relative' }}>
121
+ <input
122
+ type="text"
123
+ placeholder="Search by brand, category, or content..."
124
+ value={searchTerm}
125
+ onChange={(e) => setSearchTerm(e.target.value)}
126
+ className="search-input"
127
+ style={{ paddingRight: searchTerm ? '3rem' : '1.5rem' }}
128
+ />
129
+ {searchTerm && (
130
+ <button
131
+ type="button"
132
+ onClick={handleClearSearch}
133
+ style={{
134
+ position: 'absolute',
135
+ right: '1rem',
136
+ top: '50%',
137
+ transform: 'translateY(-50%)',
138
+ background: 'none',
139
+ border: 'none',
140
+ color: '#999',
141
+ cursor: 'pointer',
142
+ padding: '0.25rem'
143
+ }}
144
+ >
145
+ <X size={16} />
146
+ </button>
147
+ )}
148
+ </div>
149
+
150
+ {suggestions.length > 0 && (
151
+ <div style={{
152
+ position: 'absolute',
153
+ top: '100%',
154
+ left: 0,
155
+ right: 0,
156
+ background: 'white',
157
+ border: '1px solid rgba(0,0,0,0.1)',
158
+ borderRadius: '8px',
159
+ boxShadow: '0 4px 20px rgba(0,0,0,0.1)',
160
+ zIndex: 10,
161
+ marginTop: '0.5rem'
162
+ }}>
163
+ {suggestions.map((suggestion, index) => (
164
+ <div
165
+ key={index}
166
+ onClick={() => handleSuggestionClick(suggestion)}
167
+ style={{
168
+ padding: '0.75rem 1rem',
169
+ cursor: 'pointer',
170
+ borderBottom: index < suggestions.length - 1 ? '1px solid rgba(0,0,0,0.05)' : 'none',
171
+ transition: 'background-color 0.2s'
172
+ }}
173
+ onMouseEnter={(e) => e.target.style.backgroundColor = 'rgba(102, 126, 234, 0.05)'}
174
+ onMouseLeave={(e) => e.target.style.backgroundColor = 'transparent'}
175
+ >
176
+ <SearchIcon size={14} style={{ marginRight: '0.5rem', color: '#999' }} />
177
+ {suggestion}
178
+ </div>
179
+ ))}
180
+ </div>
181
+ )}
182
+ </form>
183
+
184
+ {hasSearched && (
185
+ <div style={{
186
+ marginTop: '1rem',
187
+ color: '#666',
188
+ fontSize: '0.9rem',
189
+ display: 'flex',
190
+ alignItems: 'center',
191
+ gap: '0.5rem'
192
+ }}>
193
+ {loading ? (
194
+ <>
195
+ <div className="spinner" style={{ width: '16px', height: '16px' }}></div>
196
+ Searching...
197
+ </>
198
+ ) : (
199
+ `Found ${results.length} result${results.length !== 1 ? 's' : ''} for "${searchTerm}"`
200
+ )}
201
+ </div>
202
+ )}
203
+ </div>
204
+
205
+ {!hasSearched && !loading && (
206
+ <div className="card" style={{ textAlign: 'center', padding: '3rem' }}>
207
+ <SearchIcon size={48} style={{ color: '#ccc', marginBottom: '1rem' }} />
208
+ <h3 style={{ marginBottom: '1rem', color: '#666' }}>Start Searching</h3>
209
+ <p style={{ color: '#999', marginBottom: '1.5rem' }}>
210
+ Try searching for brands like "Nike", "McDonald's", or categories like "Technology", "Food".
211
+ </p>
212
+
213
+ <div style={{ display: 'flex', gap: '0.5rem', justifyContent: 'center', flexWrap: 'wrap' }}>
214
+ {['Nike', 'Apple', 'Technology', 'Food & Beverage'].map(term => (
215
+ <button
216
+ key={term}
217
+ className="btn btn-secondary"
218
+ onClick={() => handleSuggestionClick(term)}
219
+ style={{
220
+ padding: '0.5rem 1rem',
221
+ fontSize: '0.9rem',
222
+ display: 'flex',
223
+ alignItems: 'center',
224
+ gap: '0.25rem'
225
+ }}
226
+ >
227
+ <Zap size={14} />
228
+ {term}
229
+ </button>
230
+ ))}
231
+ </div>
232
+ </div>
233
+ )}
234
+
235
+ {hasSearched && !loading && results.length === 0 && (
236
+ <div className="card" style={{ textAlign: 'center', padding: '3rem' }}>
237
+ <h3 style={{ marginBottom: '1rem' }}>No results found</h3>
238
+ <p style={{ color: '#666', marginBottom: '1.5rem' }}>
239
+ Try different keywords or browse all examples instead.
240
+ </p>
241
+ <button
242
+ className="btn"
243
+ onClick={handleClearSearch}
244
+ >
245
+ Clear Search
246
+ </button>
247
+ </div>
248
+ )}
249
+
250
+ {results.length > 0 && (
251
+ <div className="grid grid-2">
252
+ {results.map((example, index) => (
253
+ <ExampleCard
254
+ key={example.id}
255
+ example={example}
256
+ index={index}
257
+ />
258
+ ))}
259
+ </div>
260
+ )}
261
+ </motion.div>
262
+ );
263
+ };
264
+
265
+ export default Search;
package-lock.json DELETED
@@ -1,345 +0,0 @@
1
- {
2
- "name": "transcreation-explorer",
3
- "version": "1.0.0",
4
- "lockfileVersion": 3,
5
- "requires": true,
6
- "packages": {
7
- "": {
8
- "name": "transcreation-explorer",
9
- "version": "1.0.0",
10
- "license": "MIT",
11
- "dependencies": {
12
- "concurrently": "^8.2.2"
13
- },
14
- "devDependencies": {}
15
- },
16
- "node_modules/@babel/runtime": {
17
- "version": "7.27.6",
18
- "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.27.6.tgz",
19
- "integrity": "sha512-vbavdySgbTTrmFE+EsiqUTzlOr5bzlnJtUv9PynGCAKvfQqjIXbvFdumPM/GxMDfyuGMJaJAU6TO4zc1Jf1i8Q==",
20
- "license": "MIT",
21
- "engines": {
22
- "node": ">=6.9.0"
23
- }
24
- },
25
- "node_modules/ansi-regex": {
26
- "version": "5.0.1",
27
- "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
28
- "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
29
- "license": "MIT",
30
- "engines": {
31
- "node": ">=8"
32
- }
33
- },
34
- "node_modules/ansi-styles": {
35
- "version": "4.3.0",
36
- "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
37
- "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
38
- "license": "MIT",
39
- "dependencies": {
40
- "color-convert": "^2.0.1"
41
- },
42
- "engines": {
43
- "node": ">=8"
44
- },
45
- "funding": {
46
- "url": "https://github.com/chalk/ansi-styles?sponsor=1"
47
- }
48
- },
49
- "node_modules/chalk": {
50
- "version": "4.1.2",
51
- "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
52
- "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
53
- "license": "MIT",
54
- "dependencies": {
55
- "ansi-styles": "^4.1.0",
56
- "supports-color": "^7.1.0"
57
- },
58
- "engines": {
59
- "node": ">=10"
60
- },
61
- "funding": {
62
- "url": "https://github.com/chalk/chalk?sponsor=1"
63
- }
64
- },
65
- "node_modules/chalk/node_modules/supports-color": {
66
- "version": "7.2.0",
67
- "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
68
- "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
69
- "license": "MIT",
70
- "dependencies": {
71
- "has-flag": "^4.0.0"
72
- },
73
- "engines": {
74
- "node": ">=8"
75
- }
76
- },
77
- "node_modules/cliui": {
78
- "version": "8.0.1",
79
- "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz",
80
- "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==",
81
- "license": "ISC",
82
- "dependencies": {
83
- "string-width": "^4.2.0",
84
- "strip-ansi": "^6.0.1",
85
- "wrap-ansi": "^7.0.0"
86
- },
87
- "engines": {
88
- "node": ">=12"
89
- }
90
- },
91
- "node_modules/color-convert": {
92
- "version": "2.0.1",
93
- "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
94
- "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
95
- "license": "MIT",
96
- "dependencies": {
97
- "color-name": "~1.1.4"
98
- },
99
- "engines": {
100
- "node": ">=7.0.0"
101
- }
102
- },
103
- "node_modules/color-name": {
104
- "version": "1.1.4",
105
- "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
106
- "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
107
- "license": "MIT"
108
- },
109
- "node_modules/concurrently": {
110
- "version": "8.2.2",
111
- "resolved": "https://registry.npmjs.org/concurrently/-/concurrently-8.2.2.tgz",
112
- "integrity": "sha512-1dP4gpXFhei8IOtlXRE/T/4H88ElHgTiUzh71YUmtjTEHMSRS2Z/fgOxHSxxusGHogsRfxNq1vyAwxSC+EVyDg==",
113
- "license": "MIT",
114
- "dependencies": {
115
- "chalk": "^4.1.2",
116
- "date-fns": "^2.30.0",
117
- "lodash": "^4.17.21",
118
- "rxjs": "^7.8.1",
119
- "shell-quote": "^1.8.1",
120
- "spawn-command": "0.0.2",
121
- "supports-color": "^8.1.1",
122
- "tree-kill": "^1.2.2",
123
- "yargs": "^17.7.2"
124
- },
125
- "bin": {
126
- "conc": "dist/bin/concurrently.js",
127
- "concurrently": "dist/bin/concurrently.js"
128
- },
129
- "engines": {
130
- "node": "^14.13.0 || >=16.0.0"
131
- },
132
- "funding": {
133
- "url": "https://github.com/open-cli-tools/concurrently?sponsor=1"
134
- }
135
- },
136
- "node_modules/date-fns": {
137
- "version": "2.30.0",
138
- "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.30.0.tgz",
139
- "integrity": "sha512-fnULvOpxnC5/Vg3NCiWelDsLiUc9bRwAPs/+LfTLNvetFCtCTN+yQz15C/fs4AwX1R9K5GLtLfn8QW+dWisaAw==",
140
- "license": "MIT",
141
- "dependencies": {
142
- "@babel/runtime": "^7.21.0"
143
- },
144
- "engines": {
145
- "node": ">=0.11"
146
- },
147
- "funding": {
148
- "type": "opencollective",
149
- "url": "https://opencollective.com/date-fns"
150
- }
151
- },
152
- "node_modules/emoji-regex": {
153
- "version": "8.0.0",
154
- "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
155
- "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
156
- "license": "MIT"
157
- },
158
- "node_modules/escalade": {
159
- "version": "3.2.0",
160
- "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz",
161
- "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==",
162
- "license": "MIT",
163
- "engines": {
164
- "node": ">=6"
165
- }
166
- },
167
- "node_modules/get-caller-file": {
168
- "version": "2.0.5",
169
- "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz",
170
- "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==",
171
- "license": "ISC",
172
- "engines": {
173
- "node": "6.* || 8.* || >= 10.*"
174
- }
175
- },
176
- "node_modules/has-flag": {
177
- "version": "4.0.0",
178
- "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
179
- "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
180
- "license": "MIT",
181
- "engines": {
182
- "node": ">=8"
183
- }
184
- },
185
- "node_modules/is-fullwidth-code-point": {
186
- "version": "3.0.0",
187
- "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
188
- "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
189
- "license": "MIT",
190
- "engines": {
191
- "node": ">=8"
192
- }
193
- },
194
- "node_modules/lodash": {
195
- "version": "4.17.21",
196
- "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
197
- "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
198
- "license": "MIT"
199
- },
200
- "node_modules/require-directory": {
201
- "version": "2.1.1",
202
- "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
203
- "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==",
204
- "license": "MIT",
205
- "engines": {
206
- "node": ">=0.10.0"
207
- }
208
- },
209
- "node_modules/rxjs": {
210
- "version": "7.8.2",
211
- "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz",
212
- "integrity": "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==",
213
- "license": "Apache-2.0",
214
- "dependencies": {
215
- "tslib": "^2.1.0"
216
- }
217
- },
218
- "node_modules/shell-quote": {
219
- "version": "1.8.3",
220
- "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.3.tgz",
221
- "integrity": "sha512-ObmnIF4hXNg1BqhnHmgbDETF8dLPCggZWBjkQfhZpbszZnYur5DUljTcCHii5LC3J5E0yeO/1LIMyH+UvHQgyw==",
222
- "license": "MIT",
223
- "engines": {
224
- "node": ">= 0.4"
225
- },
226
- "funding": {
227
- "url": "https://github.com/sponsors/ljharb"
228
- }
229
- },
230
- "node_modules/spawn-command": {
231
- "version": "0.0.2",
232
- "resolved": "https://registry.npmjs.org/spawn-command/-/spawn-command-0.0.2.tgz",
233
- "integrity": "sha512-zC8zGoGkmc8J9ndvml8Xksr1Amk9qBujgbF0JAIWO7kXr43w0h/0GJNM/Vustixu+YE8N/MTrQ7N31FvHUACxQ=="
234
- },
235
- "node_modules/string-width": {
236
- "version": "4.2.3",
237
- "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
238
- "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
239
- "license": "MIT",
240
- "dependencies": {
241
- "emoji-regex": "^8.0.0",
242
- "is-fullwidth-code-point": "^3.0.0",
243
- "strip-ansi": "^6.0.1"
244
- },
245
- "engines": {
246
- "node": ">=8"
247
- }
248
- },
249
- "node_modules/strip-ansi": {
250
- "version": "6.0.1",
251
- "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
252
- "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
253
- "license": "MIT",
254
- "dependencies": {
255
- "ansi-regex": "^5.0.1"
256
- },
257
- "engines": {
258
- "node": ">=8"
259
- }
260
- },
261
- "node_modules/supports-color": {
262
- "version": "8.1.1",
263
- "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz",
264
- "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==",
265
- "license": "MIT",
266
- "dependencies": {
267
- "has-flag": "^4.0.0"
268
- },
269
- "engines": {
270
- "node": ">=10"
271
- },
272
- "funding": {
273
- "url": "https://github.com/chalk/supports-color?sponsor=1"
274
- }
275
- },
276
- "node_modules/tree-kill": {
277
- "version": "1.2.2",
278
- "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz",
279
- "integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==",
280
- "license": "MIT",
281
- "bin": {
282
- "tree-kill": "cli.js"
283
- }
284
- },
285
- "node_modules/tslib": {
286
- "version": "2.8.1",
287
- "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
288
- "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
289
- "license": "0BSD"
290
- },
291
- "node_modules/wrap-ansi": {
292
- "version": "7.0.0",
293
- "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
294
- "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==",
295
- "license": "MIT",
296
- "dependencies": {
297
- "ansi-styles": "^4.0.0",
298
- "string-width": "^4.1.0",
299
- "strip-ansi": "^6.0.0"
300
- },
301
- "engines": {
302
- "node": ">=10"
303
- },
304
- "funding": {
305
- "url": "https://github.com/chalk/wrap-ansi?sponsor=1"
306
- }
307
- },
308
- "node_modules/y18n": {
309
- "version": "5.0.8",
310
- "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz",
311
- "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==",
312
- "license": "ISC",
313
- "engines": {
314
- "node": ">=10"
315
- }
316
- },
317
- "node_modules/yargs": {
318
- "version": "17.7.2",
319
- "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz",
320
- "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==",
321
- "license": "MIT",
322
- "dependencies": {
323
- "cliui": "^8.0.1",
324
- "escalade": "^3.1.1",
325
- "get-caller-file": "^2.0.5",
326
- "require-directory": "^2.1.1",
327
- "string-width": "^4.2.3",
328
- "y18n": "^5.0.5",
329
- "yargs-parser": "^21.1.1"
330
- },
331
- "engines": {
332
- "node": ">=12"
333
- }
334
- },
335
- "node_modules/yargs-parser": {
336
- "version": "21.1.1",
337
- "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz",
338
- "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==",
339
- "license": "ISC",
340
- "engines": {
341
- "node": ">=12"
342
- }
343
- }
344
- }
345
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
package.json CHANGED
@@ -1,27 +1,29 @@
1
  {
2
- "name": "transcreation-explorer",
3
  "version": "1.0.0",
4
- "description": "A tool for exploring English-Chinese transcreation examples",
5
  "main": "index.js",
6
  "scripts": {
7
- "start": "npm run dev",
8
- "start:prod": "cd server && node index.js",
9
- "dev": "concurrently \"npm run server\" \"npm run client\"",
10
- "server": "cd server && node index.js",
11
- "client": "cd client && npm start",
12
- "build": "cd client && npm run build",
13
  "test": "echo \"Error: no test specified\" && exit 1"
14
  },
15
  "keywords": [
16
  "transcreation",
17
- "translation",
18
- "chinese",
19
- "english",
20
- "examples"
21
  ],
22
  "author": "",
23
- "license": "MIT",
24
  "dependencies": {
25
- "concurrently": "^8.2.2"
 
 
 
 
 
 
 
 
26
  }
27
  }
 
1
  {
2
+ "name": "transcreation-explorer-server",
3
  "version": "1.0.0",
4
+ "description": "Server for Transcreation Explorer",
5
  "main": "index.js",
6
  "scripts": {
7
+ "start": "node index.js",
8
+ "dev": "nodemon index.js",
 
 
 
 
9
  "test": "echo \"Error: no test specified\" && exit 1"
10
  },
11
  "keywords": [
12
  "transcreation",
13
+ "server",
14
+ "api"
 
 
15
  ],
16
  "author": "",
17
+ "license": "ISC",
18
  "dependencies": {
19
+ "axios": "^1.6.7",
20
+ "cheerio": "^1.0.0-rc.12",
21
+ "cors": "^2.8.5",
22
+ "express": "^4.18.2",
23
+ "node-cron": "^3.0.3",
24
+ "uuid": "^9.0.1"
25
+ },
26
+ "devDependencies": {
27
+ "nodemon": "^3.0.3"
28
  }
29
  }
server/index.js ADDED
@@ -0,0 +1,1145 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ const express = require('express');
2
+ const cors = require('cors');
3
+ const axios = require('axios');
4
+ const cheerio = require('cheerio');
5
+ const path = require('path');
6
+ const fs = require('fs').promises;
7
+ const cron = require('node-cron');
8
+ const { v4: uuidv4 } = require('uuid');
9
+ require('dotenv').config();
10
+
11
+ const app = express();
12
+ const PORT = process.env.PORT || 3001;
13
+
14
+ app.use(cors());
15
+ app.use(express.json());
16
+
17
+ // Serve static files from React build in production
18
+ if (process.env.NODE_ENV === 'production') {
19
+ app.use(express.static(path.join(__dirname, '../client/build')));
20
+ }
21
+
22
+ // In-memory storage for examples (in production, you'd use a database)
23
+ let transcreationExamples = [];
24
+
25
+ // Load cached examples from file on startup
26
+ const loadCachedExamples = async () => {
27
+ try {
28
+ const data = await fs.readFile(path.join(__dirname, 'cached-examples.json'), 'utf8');
29
+ transcreationExamples = JSON.parse(data);
30
+ console.log(`📚 Loaded ${transcreationExamples.length} cached examples`);
31
+ } catch (error) {
32
+ console.log('📝 No cached examples found, starting fresh');
33
+ transcreationExamples = [];
34
+ }
35
+ };
36
+
37
+ // Save examples to file
38
+ const saveCachedExamples = async () => {
39
+ try {
40
+ // Sort examples by dateAdded to maintain consistent order
41
+ const sortedExamples = [...transcreationExamples].sort((a, b) =>
42
+ new Date(a.dateAdded) - new Date(b.dateAdded)
43
+ );
44
+
45
+ // Ensure all examples have consistent structure
46
+ const cleanedExamples = sortedExamples.map(example => ({
47
+ ...example,
48
+ // Ensure optional fields are properly handled
49
+ status: example.status ?? 'pending',
50
+ contributor: example.contributor === undefined ? null : example.contributor,
51
+ type: example.type ?? 'slogan',
52
+ description: example.description ?? '',
53
+ lastModified: example.lastModified ?? example.dateAdded
54
+ }));
55
+
56
+ await fs.writeFile(
57
+ path.join(__dirname, 'cached-examples.json'),
58
+ JSON.stringify(cleanedExamples, null, 2)
59
+ );
60
+ console.log(`💾 Saved ${cleanedExamples.length} examples to cache`);
61
+ } catch (error) {
62
+ console.error('❌ Failed to save examples:', error);
63
+ }
64
+ };
65
+
66
+ // Search for transcreation examples online
67
+ const searchTranscreationExamples = async (category = '', maxResults = 5) => {
68
+ console.log(`🔍 Searching for transcreation examples...`);
69
+
70
+ try {
71
+ // Simulate online search with curated examples
72
+ // In a real implementation, this would scrape marketing sites, case studies, etc.
73
+ const simulatedResults = await simulateOnlineSearch(category, maxResults);
74
+
75
+ // Add to our cache
76
+ for (const example of simulatedResults) {
77
+ // Check if already exists
78
+ const exists = transcreationExamples.find(ex =>
79
+ ex.english.toLowerCase() === example.english.toLowerCase() &&
80
+ ex.brand.toLowerCase() === example.brand.toLowerCase()
81
+ );
82
+
83
+ if (!exists) {
84
+ example.id = Date.now().toString() + Math.random().toString(36).substr(2, 9);
85
+ example.dateAdded = new Date().toISOString();
86
+ example.source = 'Online Search';
87
+ transcreationExamples.push(example);
88
+ console.log(`✅ Added new example: ${example.brand} - ${example.english}`);
89
+ }
90
+ }
91
+
92
+ // Save to cache
93
+ await saveCachedExamples();
94
+
95
+ return simulatedResults;
96
+ } catch (error) {
97
+ console.error('❌ Search failed:', error);
98
+ return [];
99
+ }
100
+ };
101
+
102
+ // Simulate online search with realistic examples
103
+ const simulateOnlineSearch = async (category = '', maxResults = 5) => {
104
+ // This simulates what would be scraped from marketing sites
105
+ const searchResults = [
106
+ // Brand Names
107
+ {
108
+ english: 'MasterCard',
109
+ mainland: '万事达卡',
110
+ taiwan: '萬事達卡',
111
+ brand: 'MasterCard',
112
+ category: 'Brand Names',
113
+ description: 'Global payment brand name adaptation emphasizing universal capability.',
114
+ type: 'brand_name',
115
+ culturalNote: 'Both versions use characters meaning "everything achievable", with traditional vs simplified characters being the main difference.'
116
+ },
117
+ {
118
+ english: 'IKEA',
119
+ mainland: '宜家',
120
+ taiwan: '宜家家居',
121
+ brand: 'IKEA',
122
+ category: 'Brand Names',
123
+ description: 'Swedish furniture retailer\'s name adapted to reflect affordability and home focus.',
124
+ type: 'brand_name',
125
+ culturalNote: 'Mainland uses shorter version meaning "suitable home", Taiwan adds "home furnishings" for clarity.'
126
+ },
127
+ {
128
+ english: 'Bing',
129
+ mainland: '必应',
130
+ taiwan: '必應',
131
+ brand: 'Microsoft',
132
+ category: 'Brand Names',
133
+ description: 'Microsoft\'s search engine name adapted to reflect responsiveness.',
134
+ type: 'brand_name',
135
+ culturalNote: 'Both versions use characters meaning "must respond", maintaining the concept of getting answers.'
136
+ },
137
+ {
138
+ english: 'Subway',
139
+ mainland: '赛百味',
140
+ taiwan: '賽百味',
141
+ brand: 'Subway',
142
+ category: 'Brand Names',
143
+ description: 'Restaurant chain name adapted to focus on taste rather than transportation meaning.',
144
+ type: 'brand_name',
145
+ culturalNote: 'Both use characters meaning "competing hundred tastes", completely departing from subway/metro meaning.'
146
+ },
147
+ {
148
+ english: 'Mercedes-Benz',
149
+ mainland: '奔驰',
150
+ taiwan: '賓士',
151
+ brand: 'Mercedes-Benz',
152
+ category: 'Brand Names',
153
+ description: 'Luxury car manufacturer name adapted differently in each region.',
154
+ type: 'brand_name',
155
+ culturalNote: 'Mainland emphasizes speed ("galloping"), Taiwan uses phonetic translation of "Benz".'
156
+ },
157
+ {
158
+ english: 'BMW',
159
+ mainland: '宝马',
160
+ taiwan: '寶馬',
161
+ brand: 'BMW',
162
+ category: 'Brand Names',
163
+ description: 'German automaker name adapted using traditional lucky symbolism.',
164
+ type: 'brand_name',
165
+ culturalNote: 'Both use "precious horse", a culturally auspicious symbol, differing only in traditional vs simplified characters.'
166
+ },
167
+ {
168
+ english: 'Revlon',
169
+ mainland: '露华浓',
170
+ taiwan: '露華濃',
171
+ brand: 'Revlon',
172
+ category: 'Brand Names',
173
+ description: 'Cosmetics brand name adapted using poetic beauty references.',
174
+ type: 'brand_name',
175
+ culturalNote: 'Both use poetic phrase meaning "concentrated dew", evoking natural beauty and luxury.'
176
+ },
177
+ // Food & Beverage
178
+ {
179
+ english: 'Open Happiness',
180
+ mainland: '畅爽开怀',
181
+ taiwan: '分享快樂',
182
+ brand: 'Coca-Cola',
183
+ category: 'Food & Beverage',
184
+ description: 'Coca-Cola\'s global campaign adapted differently for mainland China (emphasizing refreshment) vs Taiwan (emphasizing sharing joy).',
185
+ type: 'slogan',
186
+ culturalNote: 'Mainland version focuses on personal refreshing experience, Taiwan version emphasizes social sharing aspect valued in Taiwanese culture.'
187
+ },
188
+ {
189
+ english: 'Taste the Feeling',
190
+ mainland: '可口可乐 就是要这个味',
191
+ taiwan: '就是這個味道',
192
+ brand: 'Coca-Cola',
193
+ category: 'Food & Beverage',
194
+ description: 'Another Coca-Cola campaign focusing on the sensory experience of the product.',
195
+ type: 'slogan',
196
+ culturalNote: 'Mainland version includes brand name and emphasizes specificity, Taiwan version is more poetic.'
197
+ },
198
+ {
199
+ english: 'Have a Break, Have a Kit Kat',
200
+ mainland: '休息一下,来根奇巧',
201
+ taiwan: '休息一下,吃個Kit Kat',
202
+ brand: 'Kit Kat',
203
+ category: 'Food & Beverage',
204
+ description: 'Kit Kat\'s break concept maintaining rhythm while using different approaches to product naming.',
205
+ type: 'slogan',
206
+ culturalNote: 'Mainland uses localized brand name, Taiwan keeps original brand name with local language structure.'
207
+ },
208
+ {
209
+ english: 'I\'m Lovin\' It',
210
+ mainland: '我就喜欢',
211
+ taiwan: '我愛死了',
212
+ brand: 'McDonald\'s',
213
+ category: 'Food & Beverage',
214
+ description: 'McDonald\'s global jingle adapted for regional expressions of enjoyment and enthusiasm.',
215
+ type: 'slogan',
216
+ culturalNote: 'Mainland uses softer expression of liking, Taiwan uses more enthusiastic colloquial expression.'
217
+ },
218
+ {
219
+ english: 'Finger Lickin\' Good',
220
+ mainland: '美味到舔手指',
221
+ taiwan: '好吃到舔手指',
222
+ brand: 'KFC',
223
+ category: 'Food & Beverage',
224
+ description: 'KFC\'s sensory appeal slogan with regional preferences for describing taste intensity.',
225
+ type: 'slogan',
226
+ culturalNote: 'Both maintain the playful imagery but use different intensifiers for taste description.'
227
+ },
228
+ {
229
+ english: 'Every Bubble\'s Different',
230
+ mainland: '每个泡泡都不同',
231
+ taiwan: '每個泡泡都是獨特的',
232
+ brand: 'Sprite',
233
+ category: 'Food & Beverage',
234
+ description: 'Sprite\'s campaign celebrating uniqueness through bubble metaphor.',
235
+ type: 'slogan',
236
+ culturalNote: 'Taiwan version adds "unique" character to emphasize individuality more strongly.'
237
+ },
238
+ {
239
+ english: 'Red Bull Gives You Wings',
240
+ mainland: '红牛让你展翅飞翔',
241
+ taiwan: '紅牛給你一對翅膀',
242
+ brand: 'Red Bull',
243
+ category: 'Food & Beverage',
244
+ description: 'Red Bull\'s energetic message adapted for different markets.',
245
+ type: 'slogan',
246
+ culturalNote: 'Mainland emphasizes the action of flying, Taiwan emphasizes the gift of wings.'
247
+ },
248
+ {
249
+ english: 'Melts in Your Mouth, Not in Your Hands',
250
+ mainland: '入口即化,不粘手',
251
+ taiwan: '入口即化,不沾手',
252
+ brand: 'M&M\'s',
253
+ category: 'Food & Beverage',
254
+ description: 'M&M\'s unique selling proposition translated with slight regional variations.',
255
+ type: 'slogan',
256
+ culturalNote: 'Both versions maintain the dual benefit message with minor character variations.'
257
+ },
258
+ {
259
+ english: 'Share a Coke',
260
+ mainland: '把快乐分享给你',
261
+ taiwan: '分享一瓶可口可樂',
262
+ brand: 'Coca-Cola',
263
+ category: 'Food & Beverage',
264
+ description: 'Coca-Cola\'s sharing campaign adapted to different social contexts.',
265
+ type: 'campaign',
266
+ culturalNote: 'Mainland emphasizes sharing happiness, Taiwan emphasizes sharing the product.'
267
+ },
268
+
269
+ // Technology
270
+ {
271
+ english: 'Think Different',
272
+ mainland: '非同凡想',
273
+ taiwan: '不同凡想',
274
+ brand: 'Apple',
275
+ category: 'Technology',
276
+ description: 'Apple\'s famous campaign with slight variations - mainland uses more contemporary language, Taiwan preserves traditional elements.',
277
+ type: 'slogan',
278
+ culturalNote: 'Both versions maintain the creative rebellion message but adapt to local linguistic preferences.'
279
+ },
280
+ {
281
+ english: 'Connecting People',
282
+ mainland: '科技以人为本',
283
+ taiwan: '串聯你我',
284
+ brand: 'Nokia',
285
+ category: 'Technology',
286
+ description: 'Nokia\'s brand promise adapted to different cultural values about technology\'s role in society.',
287
+ type: 'slogan',
288
+ culturalNote: 'Mainland emphasizes human-centered technology philosophy, Taiwan emphasizes personal connection.'
289
+ },
290
+ {
291
+ english: 'Innovation for All',
292
+ mainland: '创新科技,为你所用',
293
+ taiwan: '科技創新,人人共享',
294
+ brand: 'Samsung',
295
+ category: 'Technology',
296
+ description: 'Samsung\'s inclusive innovation message adapted for different market perspectives.',
297
+ type: 'slogan',
298
+ culturalNote: 'Mainland emphasizes personal utility, Taiwan emphasizes communal sharing of benefits.'
299
+ },
300
+ {
301
+ english: 'Power to You',
302
+ mainland: '为你而来',
303
+ taiwan: '為你而強',
304
+ brand: 'Motorola',
305
+ category: 'Technology',
306
+ description: 'Motorola\'s empowerment message adapted to local expressions of personal capability.',
307
+ type: 'slogan',
308
+ culturalNote: 'Mainland emphasizes service aspect, Taiwan emphasizes strength/empowerment.'
309
+ },
310
+ {
311
+ english: 'Life\'s Good',
312
+ mainland: '让生活更美好',
313
+ taiwan: '人生就是要精彩',
314
+ brand: 'LG',
315
+ category: 'Technology',
316
+ description: 'LG\'s positive lifestyle message adapted to different cultural perspectives on good life.',
317
+ type: 'slogan',
318
+ culturalNote: 'Mainland suggests improvement, Taiwan emphasizes living life to the fullest.'
319
+ },
320
+ {
321
+ english: 'Do What You Can\'t',
322
+ mainland: '突破不可能',
323
+ taiwan: '挑戰你的極限',
324
+ brand: 'Samsung',
325
+ category: 'Technology',
326
+ description: 'Samsung\'s challenge-focused campaign adapted for different markets.',
327
+ type: 'slogan',
328
+ culturalNote: 'Mainland emphasizes breaking through impossibility, Taiwan emphasizes personal limits.'
329
+ },
330
+ {
331
+ english: 'Make.Believe',
332
+ mainland: '创造梦想',
333
+ taiwan: '夢想無限',
334
+ brand: 'Sony',
335
+ category: 'Technology',
336
+ description: 'Sony\'s creative vision slogan adapted to different dream concepts.',
337
+ type: 'slogan',
338
+ culturalNote: 'Mainland emphasizes creating dreams, Taiwan emphasizes limitless dreams.'
339
+ },
340
+ {
341
+ english: 'Together We Make the Difference',
342
+ mainland: '技术改变生活',
343
+ taiwan: '攜手創造精彩',
344
+ brand: 'Lenovo',
345
+ category: 'Technology',
346
+ description: 'Lenovo\'s collaborative message adapted to different market focuses.',
347
+ type: 'slogan',
348
+ culturalNote: 'Mainland emphasizes technological impact, Taiwan emphasizes collaboration.'
349
+ },
350
+
351
+ // Automotive
352
+ {
353
+ english: 'The Ultimate Driving Machine',
354
+ mainland: '终极驾驶机器',
355
+ taiwan: '終極駕馭樂趣',
356
+ brand: 'BMW',
357
+ category: 'Automotive',
358
+ description: 'BMW\'s precision-focused tagline adapted to emphasize different aspects of the driving experience.',
359
+ type: 'slogan',
360
+ culturalNote: 'Mainland emphasizes mechanical excellence, Taiwan emphasizes the joy and control of driving.'
361
+ },
362
+ {
363
+ english: 'The Best or Nothing',
364
+ mainland: '专注完美,不负期待',
365
+ taiwan: '至善至美,不容妥協',
366
+ brand: 'Mercedes-Benz',
367
+ category: 'Automotive',
368
+ description: 'Mercedes\' commitment to excellence expressed through different cultural values.',
369
+ type: 'slogan',
370
+ culturalNote: 'Mainland emphasizes meeting expectations, Taiwan emphasizes uncompromising standards.'
371
+ },
372
+ {
373
+ english: 'Way of Life',
374
+ mainland: '引领生活方式',
375
+ taiwan: '為你預見精彩生活',
376
+ brand: 'Suzuki',
377
+ category: 'Automotive',
378
+ description: 'Suzuki\'s lifestyle proposition adapted to different cultural aspirations.',
379
+ type: 'slogan',
380
+ culturalNote: 'Mainland emphasizes leadership, Taiwan emphasizes anticipating excitement.'
381
+ },
382
+ {
383
+ english: 'Drive Together',
384
+ mainland: '驾驭���享',
385
+ taiwan: '心心相驅',
386
+ brand: 'Mazda',
387
+ category: 'Automotive',
388
+ description: 'Mazda\'s community-focused message adapted to different social values.',
389
+ type: 'slogan',
390
+ culturalNote: 'Mainland emphasizes shared experience, Taiwan uses wordplay connecting hearts and driving.'
391
+ },
392
+ {
393
+ english: 'The Power of Dreams',
394
+ mainland: '梦想的力量',
395
+ taiwan: '夢想驅動力',
396
+ brand: 'Honda',
397
+ category: 'Automotive',
398
+ description: 'Honda\'s aspirational message adapted to different cultural perspectives on dreams.',
399
+ type: 'slogan',
400
+ culturalNote: 'Mainland uses literal translation, Taiwan adds dynamic element with "driving force".'
401
+ },
402
+ {
403
+ english: 'Vorsprung durch Technik',
404
+ mainland: '进取科技,启迪未来',
405
+ taiwan: '科技創新,定義未來',
406
+ brand: 'Audi',
407
+ category: 'Automotive',
408
+ description: 'Audi\'s technical advancement slogan adapted for Chinese markets.',
409
+ type: 'slogan',
410
+ culturalNote: 'Both versions emphasize future-oriented technology but with different focuses.'
411
+ },
412
+ {
413
+ english: 'Motion & Emotion',
414
+ mainland: '激情驾驭',
415
+ taiwan: '感動啟程',
416
+ brand: 'Peugeot',
417
+ category: 'Automotive',
418
+ description: 'Peugeot\'s emotional driving experience adapted to regional preferences.',
419
+ type: 'slogan',
420
+ culturalNote: 'Mainland emphasizes passionate control, Taiwan emphasizes emotional journey.'
421
+ },
422
+ {
423
+ english: 'Drive to Delight',
424
+ mainland: '悦享驾驭',
425
+ taiwan: '樂在駕馭',
426
+ brand: 'Toyota',
427
+ category: 'Automotive',
428
+ description: 'Toyota\'s enjoyable driving experience message adapted for different markets.',
429
+ type: 'slogan',
430
+ culturalNote: 'Both versions emphasize enjoyment but with different linguistic approaches.'
431
+ },
432
+
433
+ // Sports & Lifestyle
434
+ {
435
+ english: 'Just Do It',
436
+ mainland: '想做就做',
437
+ taiwan: '做就對了',
438
+ brand: 'Nike',
439
+ category: 'Sports & Lifestyle',
440
+ description: 'Nike\'s motivational slogan adapted to resonate with different cultural attitudes toward action and decision-making.',
441
+ type: 'slogan',
442
+ culturalNote: 'Mainland version emphasizes desire and action, Taiwan version emphasizes confidence and correctness.'
443
+ },
444
+ {
445
+ english: 'Impossible is Nothing',
446
+ mainland: '没有不可能',
447
+ taiwan: '無所不能',
448
+ brand: 'Adidas',
449
+ category: 'Sports & Lifestyle',
450
+ description: 'Adidas\' motivational message adapted to local expressions of possibility.',
451
+ type: 'slogan',
452
+ culturalNote: 'Mainland uses direct negation of impossibility, Taiwan emphasizes unlimited ability.'
453
+ },
454
+ {
455
+ english: 'Forever Faster',
456
+ mainland: '永远更快',
457
+ taiwan: '極速進化',
458
+ brand: 'Puma',
459
+ category: 'Sports & Lifestyle',
460
+ description: 'Puma\'s speed-focused message adapted to different cultural perspectives on progress.',
461
+ type: 'slogan',
462
+ culturalNote: 'Mainland emphasizes perpetual improvement, Taiwan emphasizes evolutionary speed.'
463
+ },
464
+ {
465
+ english: 'Through this together',
466
+ mainland: '一起向前',
467
+ taiwan: '攜手共進',
468
+ brand: 'Under Armour',
469
+ category: 'Sports & Lifestyle',
470
+ description: 'Under Armour\'s unity message adapted during global challenges.',
471
+ type: 'slogan',
472
+ culturalNote: 'Mainland emphasizes forward movement, Taiwan emphasizes hand-in-hand progress.'
473
+ },
474
+ {
475
+ english: 'I Am What I Am',
476
+ mainland: '我就是我',
477
+ taiwan: '展現自我',
478
+ brand: 'Reebok',
479
+ category: 'Sports & Lifestyle',
480
+ description: 'Reebok\'s self-expression message adapted to different cultural contexts.',
481
+ type: 'slogan',
482
+ culturalNote: 'Mainland uses direct translation, Taiwan emphasizes showing true self.'
483
+ },
484
+ {
485
+ english: 'Play to Rise',
486
+ mainland: '玩出巅峰',
487
+ taiwan: '玩出高峰',
488
+ brand: 'Nike',
489
+ category: 'Sports & Lifestyle',
490
+ description: 'Nike\'s play-focused campaign adapted for different markets.',
491
+ type: 'campaign',
492
+ culturalNote: 'Both versions maintain the concept of achievement through play.'
493
+ },
494
+ {
495
+ english: 'Live Without Limits',
496
+ mainland: '突破极限',
497
+ taiwan: '無限可能',
498
+ brand: 'The North Face',
499
+ category: 'Sports & Lifestyle',
500
+ description: 'The North Face\'s boundless potential message adapted regionally.',
501
+ type: 'slogan',
502
+ culturalNote: 'Mainland emphasizes breaking through limits, Taiwan emphasizes endless possibilities.'
503
+ },
504
+
505
+ // Beauty & Cosmetics
506
+ {
507
+ english: 'Because You\'re Worth It',
508
+ mainland: '你值得拥有',
509
+ taiwan: '因為你值得',
510
+ brand: 'L\'Oréal',
511
+ category: 'Beauty & Cosmetics',
512
+ description: 'L\'Oréal\'s empowerment message adapted for different concepts of self-worth and beauty standards.',
513
+ type: 'slogan',
514
+ culturalNote: 'Mainland focuses on possession/ownership, Taiwan emphasizes inherent worthiness.'
515
+ },
516
+ {
517
+ english: 'Maybe she\'s born with it',
518
+ mainland: '美来自天生',
519
+ taiwan: '天生美麗',
520
+ brand: 'Maybelline',
521
+ category: 'Beauty & Cosmetics',
522
+ description: 'Maybelline\'s natural beauty message adapted to different beauty standards.',
523
+ type: 'slogan',
524
+ culturalNote: 'Both versions emphasize natural beauty but with different linguistic approaches.'
525
+ },
526
+ {
527
+ english: 'Ageless Beauty',
528
+ mainland: '永驻美丽',
529
+ taiwan: '凍齡美麗',
530
+ brand: 'SK-II',
531
+ category: 'Beauty & Cosmetics',
532
+ description: 'SK-II\'s anti-aging message adapted to different cultural beauty concepts.',
533
+ type: 'slogan',
534
+ culturalNote: 'Mainland emphasizes lasting beauty, Taiwan uses popular "age-freezing" concept.'
535
+ },
536
+ {
537
+ english: 'The Art of Beauty',
538
+ mainland: '美的艺术',
539
+ taiwan: '美麗的藝術',
540
+ brand: 'Shiseido',
541
+ category: 'Beauty & Cosmetics',
542
+ description: 'Shiseido\'s artistic approach to beauty adapted for different markets.',
543
+ type: 'slogan',
544
+ culturalNote: 'Both versions maintain artistic element with slight linguistic variations.'
545
+ },
546
+ {
547
+ english: 'Beauty with a Purpose',
548
+ mainland: '美丽有意义',
549
+ taiwan: '讓美麗更有意義',
550
+ brand: 'Estée Lauder',
551
+ category: 'Beauty & Cosmetics',
552
+ description: 'Estée Lauder\'s purposeful beauty message adapted regionally.',
553
+ type: 'slogan',
554
+ culturalNote: 'Taiwan version adds active verb to emphasize making beauty meaningful.'
555
+ },
556
+ {
557
+ english: 'Pure. Proven. Performance.',
558
+ mainland: '纯净 • 科研 • 功效',
559
+ taiwan: '純淨 • 實證 • 效果',
560
+ brand: 'Clinique',
561
+ category: 'Beauty & Cosmetics',
562
+ description: 'Clinique\'s scientific approach adapted with different emphasis.',
563
+ type: 'slogan',
564
+ culturalNote: 'Mainland emphasizes research, Taiwan emphasizes proof and results.'
565
+ },
566
+
567
+ // Retail & Shopping
568
+ {
569
+ english: 'Black Friday',
570
+ mainland: '黑色星期五',
571
+ taiwan: '黑色購物節',
572
+ brand: 'Various Retailers',
573
+ category: 'Retail',
574
+ description: 'Western shopping event adapted with different naming conventions for local market familiarity.',
575
+ type: 'event',
576
+ culturalNote: 'Mainland keeps literal translation, Taiwan adds "shopping festival" for clarity and appeal.'
577
+ },
578
+ {
579
+ english: 'Cyber Monday',
580
+ mainland: '网购星期一',
581
+ taiwan: '網購狂歡節',
582
+ brand: 'Various Retailers',
583
+ category: 'Retail',
584
+ description: 'Western online shopping event adapted for local markets.',
585
+ type: 'event',
586
+ culturalNote: 'Mainland uses literal translation, Taiwan adds festive element for appeal.'
587
+ },
588
+ {
589
+ english: 'Singles\' Day',
590
+ mainland: '双十一',
591
+ taiwan: '光棍節',
592
+ brand: 'Various Retailers',
593
+ category: 'Retail',
594
+ description: 'Chinese shopping festival with different regional interpretations.',
595
+ type: 'event',
596
+ culturalNote: 'Mainland uses date (11.11), Taiwan keeps traditional festival name.'
597
+ },
598
+ {
599
+ english: 'Shop Smart, Live Better',
600
+ mainland: '精明购物,品质生活',
601
+ taiwan: '聰明購物,樂活人生',
602
+ brand: 'Walmart',
603
+ category: 'Retail',
604
+ description: 'Walmart\'s value proposition adapted for different markets.',
605
+ type: 'slogan',
606
+ culturalNote: 'Mainland emphasizes quality life, Taiwan emphasizes happy living.'
607
+ },
608
+ {
609
+ english: 'Save Money. Live Better.',
610
+ mainland: '省钱,让生活更美好',
611
+ taiwan: '省錢,享受更好生活',
612
+ brand: 'Walmart',
613
+ category: 'Retail',
614
+ description: 'Another Walmart campaign adapted to different value perceptions.',
615
+ type: 'slogan',
616
+ culturalNote: 'Both maintain saving concept but with different emphasis on lifestyle benefits.'
617
+ },
618
+ {
619
+ english: 'Everything\'s Better with a Bit of What You Fancy',
620
+ mainland: '选你所爱,美好生活',
621
+ taiwan: '寵愛自己,享受生活',
622
+ brand: 'Marks & Spencer',
623
+ category: 'Retail',
624
+ description: 'M&S\'s indulgence message adapted to different cultural contexts.',
625
+ type: 'slogan',
626
+ culturalNote: 'Mainland focuses on choice, Taiwan emphasizes self-pampering.'
627
+ },
628
+
629
+ // Entertainment & Gaming
630
+ {
631
+ english: 'Power Your Dreams',
632
+ mainland: '激发你的梦想',
633
+ taiwan: '引爆你的夢想',
634
+ brand: 'Xbox',
635
+ category: 'Entertainment',
636
+ description: 'Xbox\'s aspirational message adapted for different gaming markets.',
637
+ type: 'slogan',
638
+ culturalNote: 'Mainland uses softer "inspire", Taiwan uses more dynamic "ignite".'
639
+ },
640
+ {
641
+ english: 'Play Has No Limits',
642
+ mainland: '游戏无界',
643
+ taiwan: '遊戲無限',
644
+ brand: 'PlayStation',
645
+ category: 'Entertainment',
646
+ description: 'PlayStation\'s boundless gaming message adapted regionally.',
647
+ type: 'slogan',
648
+ culturalNote: 'Mainland emphasizes boundlessness, Taiwan emphasizes infinity.'
649
+ },
650
+ {
651
+ english: 'Everyone\'s Game',
652
+ mainland: '人人都是玩家',
653
+ taiwan: '遊戲屬於每個人',
654
+ brand: 'Nintendo',
655
+ category: 'Entertainment',
656
+ description: 'Nintendo\'s inclusive gaming message adapted for different markets.',
657
+ type: 'slogan',
658
+ culturalNote: 'Mainland emphasizes player identity, Taiwan emphasizes ownership.'
659
+ },
660
+
661
+ // Financial Services
662
+ {
663
+ english: 'The World\'s Local Bank',
664
+ mainland: '全球本地银行',
665
+ taiwan: '在地的國際銀行',
666
+ brand: 'HSBC',
667
+ category: 'Financial',
668
+ description: 'HSBC\'s global-local positioning adapted for different markets.',
669
+ type: 'slogan',
670
+ culturalNote: 'Different approaches to expressing the global-local balance.'
671
+ },
672
+ {
673
+ english: 'Your Financial GPS',
674
+ mainland: '您的财富向导',
675
+ taiwan: '您的理財指南',
676
+ brand: 'American Express',
677
+ category: 'Financial',
678
+ description: 'AmEx\'s guidance concept adapted to different financial cultures.',
679
+ type: 'slogan',
680
+ culturalNote: 'Mainland emphasizes wealth, Taiwan emphasizes financial management.'
681
+ },
682
+ {
683
+ english: 'Bank for a Changing World',
684
+ mainland: '助您把握变化中的世界',
685
+ taiwan: '為變革世界提供新選擇',
686
+ brand: 'BNP Paribas',
687
+ category: 'Financial',
688
+ description: 'BNP\'s change-focused message adapted regionally.',
689
+ type: 'slogan',
690
+ culturalNote: 'Mainland emphasizes guidance, Taiwan emphasizes providing choices.'
691
+ }
692
+ ];
693
+
694
+ // Filter by category if specified
695
+ let results = searchResults;
696
+ if (category) {
697
+ results = searchResults.filter(ex =>
698
+ ex.category.toLowerCase().includes(category.toLowerCase())
699
+ );
700
+ }
701
+
702
+ // Simulate network delay
703
+ await new Promise(resolve => setTimeout(resolve, 1500 + Math.random() * 2000));
704
+
705
+ // Return random subset
706
+ const shuffled = results.sort(() => Math.random() - 0.5);
707
+ return shuffled.slice(0, Math.min(maxResults, shuffled.length));
708
+ };
709
+
710
+ // Initialize cache on startup
711
+ loadCachedExamples();
712
+
713
+ // API Routes
714
+
715
+ // Get all examples
716
+ app.get('/api/examples', (req, res) => {
717
+ const { category, type, random } = req.query;
718
+ let examples = [...transcreationExamples];
719
+
720
+ // Filter by category
721
+ if (category) {
722
+ examples = examples.filter(ex =>
723
+ ex.category.toLowerCase().includes(category.toLowerCase())
724
+ );
725
+ }
726
+
727
+ // Filter by type
728
+ if (type) {
729
+ examples = examples.filter(ex => ex.type === type);
730
+ }
731
+
732
+ // Randomize if requested
733
+ if (random === 'true') {
734
+ examples = examples.sort(() => Math.random() - 0.5);
735
+ }
736
+
737
+ res.json({
738
+ success: true,
739
+ data: examples,
740
+ total: examples.length
741
+ });
742
+ });
743
+
744
+ // Get random example (or search for new one if cache is low)
745
+ app.get('/api/examples/random', async (req, res) => {
746
+ try {
747
+ // If we have few examples, try to find more
748
+ if (transcreationExamples.length < 5) {
749
+ console.log('🔍 Cache low, searching for new examples...');
750
+ await searchTranscreationExamples('', 3);
751
+ }
752
+
753
+ if (transcreationExamples.length === 0) {
754
+ return res.json({
755
+ success: true,
756
+ data: null,
757
+ message: 'No examples available yet. Try searching to discover new ones!'
758
+ });
759
+ }
760
+
761
+ const randomIndex = Math.floor(Math.random() * transcreationExamples.length);
762
+ const example = transcreationExamples[randomIndex];
763
+
764
+ res.json({
765
+ success: true,
766
+ data: example
767
+ });
768
+ } catch (error) {
769
+ res.status(500).json({
770
+ success: false,
771
+ error: 'Failed to get random example'
772
+ });
773
+ }
774
+ });
775
+
776
+ // Search for new examples online
777
+ app.post('/api/examples/search-online', async (req, res) => {
778
+ try {
779
+ const { category } = req.body;
780
+ console.log(`🔍 Online search requested for category: ${category || 'all'}`);
781
+
782
+ const newExamples = await searchTranscreationExamples(category, 5);
783
+
784
+ res.json({
785
+ success: true,
786
+ data: newExamples,
787
+ message: `Found ${newExamples.length} new example${newExamples.length !== 1 ? 's' : ''}`,
788
+ totalCached: transcreationExamples.length
789
+ });
790
+ } catch (error) {
791
+ console.error('Search error:', error);
792
+ res.status(500).json({
793
+ success: false,
794
+ error: 'Online search failed'
795
+ });
796
+ }
797
+ });
798
+
799
+ // Get example by ID
800
+ app.get('/api/examples/:id', (req, res) => {
801
+ const example = transcreationExamples.find(ex => ex.id === req.params.id);
802
+
803
+ if (!example) {
804
+ return res.status(404).json({
805
+ success: false,
806
+ error: 'Example not found'
807
+ });
808
+ }
809
+
810
+ res.json({
811
+ success: true,
812
+ data: example
813
+ });
814
+ });
815
+
816
+ // Get categories
817
+ app.get('/api/categories', (req, res) => {
818
+ const categories = [...new Set(transcreationExamples.map(ex => ex.category))];
819
+
820
+ res.json({
821
+ success: true,
822
+ data: categories
823
+ });
824
+ });
825
+
826
+ // Get types
827
+ app.get('/api/types', (req, res) => {
828
+ const types = [...new Set(transcreationExamples.map(ex => ex.type))];
829
+
830
+ res.json({
831
+ success: true,
832
+ data: types
833
+ });
834
+ });
835
+
836
+ // Search examples
837
+ app.get('/api/search', (req, res) => {
838
+ const { q } = req.query;
839
+
840
+ if (!q) {
841
+ return res.json({
842
+ success: true,
843
+ data: [],
844
+ total: 0
845
+ });
846
+ }
847
+
848
+ const searchTerm = q.toLowerCase();
849
+ const results = transcreationExamples.filter(ex =>
850
+ ex.english.toLowerCase().includes(searchTerm) ||
851
+ ex.mainland.toLowerCase().includes(searchTerm) ||
852
+ ex.taiwan.toLowerCase().includes(searchTerm) ||
853
+ ex.brand.toLowerCase().includes(searchTerm) ||
854
+ ex.category.toLowerCase().includes(searchTerm) ||
855
+ ex.description.toLowerCase().includes(searchTerm)
856
+ );
857
+
858
+ res.json({
859
+ success: true,
860
+ data: results,
861
+ total: results.length
862
+ });
863
+ });
864
+
865
+ // Get database stats
866
+ app.get('/api/stats', (req, res) => {
867
+ const stats = {
868
+ totalExamples: transcreationExamples.length,
869
+ categories: [...new Set(transcreationExamples.map(ex => ex.category))].length,
870
+ types: [...new Set(transcreationExamples.map(ex => ex.type))].length,
871
+ lastUpdated: transcreationExamples.length > 0 ?
872
+ Math.max(...transcreationExamples.map(ex => new Date(ex.dateAdded || Date.now()).getTime())) : null
873
+ };
874
+
875
+ res.json({
876
+ success: true,
877
+ data: stats
878
+ });
879
+ });
880
+
881
+ // Health check
882
+ app.get('/api/health', (req, res) => {
883
+ res.json({
884
+ success: true,
885
+ message: 'Transcreation Explorer API is running',
886
+ timestamp: new Date().toISOString(),
887
+ cachedExamples: transcreationExamples.length
888
+ });
889
+ });
890
+
891
+ // Manual Edit API Endpoints
892
+
893
+ // Add new example
894
+ app.post('/api/examples/add', async (req, res) => {
895
+ try {
896
+ console.log('📝 Received new example request:', JSON.stringify(req.body, null, 2));
897
+
898
+ // Determine if this is a Chinese to English entry (no taiwan field)
899
+ const isChineseToEnglish = req.body.hasOwnProperty('isChineseToEnglish') && req.body.isChineseToEnglish;
900
+
901
+ // Validate required fields based on direction
902
+ const requiredFields = isChineseToEnglish
903
+ ? ['english', 'mainland', 'brand', 'category', 'type']
904
+ : ['english', 'mainland', 'taiwan', 'brand', 'category', 'type'];
905
+
906
+ const missingFields = requiredFields.filter(field => !req.body[field]);
907
+
908
+ if (missingFields.length > 0) {
909
+ console.error('❌ Missing required fields:', {
910
+ missingFields,
911
+ receivedFields: Object.keys(req.body),
912
+ receivedValues: req.body
913
+ });
914
+ return res.status(400).json({
915
+ success: false,
916
+ message: `Missing required fields: ${missingFields.join(', ')}`,
917
+ details: {
918
+ missingFields,
919
+ receivedFields: Object.keys(req.body)
920
+ }
921
+ });
922
+ }
923
+
924
+ // Create new example with cleaned data
925
+ const newExample = {
926
+ ...req.body,
927
+ // Clean strings and handle optional fields
928
+ english: req.body.english.trim(),
929
+ mainland: req.body.mainland.trim(),
930
+ taiwan: isChineseToEnglish ? undefined : req.body.taiwan?.trim(),
931
+ brand: req.body.brand.trim(),
932
+ category: req.body.category.trim(),
933
+ type: req.body.type || 'slogan',
934
+ description: req.body.description?.trim() ?? '',
935
+ status: req.body.status || 'pending',
936
+ contributor: req.body.contributor === undefined ? null : req.body.contributor.trim(),
937
+ id: Date.now().toString() + Math.random().toString(36).substring(2),
938
+ dateAdded: new Date().toISOString()
939
+ };
940
+
941
+ console.log('✨ Created new example:', JSON.stringify(newExample, null, 2));
942
+ transcreationExamples.push(newExample);
943
+ await saveCachedExamples();
944
+
945
+ res.json({
946
+ success: true,
947
+ message: 'Example added successfully',
948
+ example: newExample
949
+ });
950
+ } catch (error) {
951
+ console.error('❌ Error adding example:', error);
952
+ res.status(500).json({
953
+ success: false,
954
+ message: 'Error adding example',
955
+ error: error.message
956
+ });
957
+ }
958
+ });
959
+
960
+ // Update existing example
961
+ app.put('/api/examples/:id', async (req, res) => {
962
+ try {
963
+ const { id } = req.params;
964
+ console.log(`📝 UPDATE REQUEST for ID: ${id}`);
965
+ console.log(`📝 REQUEST BODY:`, JSON.stringify(req.body, null, 2));
966
+
967
+ const index = transcreationExamples.findIndex(ex => ex.id === id);
968
+
969
+ if (index === -1) {
970
+ console.log(`❌ Example with ID ${id} not found`);
971
+ return res.status(404).json({
972
+ success: false,
973
+ message: 'Example not found'
974
+ });
975
+ }
976
+
977
+ console.log(`📝 FOUND EXAMPLE at index ${index}:`, JSON.stringify(transcreationExamples[index], null, 2));
978
+
979
+ // Determine if this is a Chinese to English entry (no taiwan field)
980
+ const isChineseToEnglish = !req.body.hasOwnProperty('taiwan');
981
+
982
+ // Validate required fields based on direction
983
+ const requiredFields = isChineseToEnglish
984
+ ? ['english', 'mainland', 'brand', 'category', 'type']
985
+ : ['english', 'mainland', 'taiwan', 'brand', 'category', 'type'];
986
+
987
+ const missingFields = requiredFields.filter(field => !req.body[field]);
988
+
989
+ if (missingFields.length > 0) {
990
+ console.error('❌ Missing required fields:', {
991
+ missingFields,
992
+ receivedFields: Object.keys(req.body),
993
+ receivedValues: req.body
994
+ });
995
+ return res.status(400).json({
996
+ success: false,
997
+ message: `Missing required fields: ${missingFields.join(', ')}`,
998
+ details: {
999
+ missingFields,
1000
+ receivedFields: Object.keys(req.body)
1001
+ }
1002
+ });
1003
+ }
1004
+
1005
+ // Preserve original dateAdded and merge updates
1006
+ const updatedExample = {
1007
+ ...transcreationExamples[index], // Start with existing data
1008
+ ...req.body, // Merge in updates
1009
+ id, // Ensure ID doesn't change
1010
+ dateAdded: transcreationExamples[index].dateAdded, // Preserve original date
1011
+ lastModified: new Date().toISOString(), // Add last modified timestamp
1012
+ // Handle optional fields - if not provided in request, keep existing value
1013
+ status: req.body.status ?? transcreationExamples[index].status ?? 'pending',
1014
+ contributor: req.body.contributor === undefined ? transcreationExamples[index].contributor : req.body.contributor,
1015
+ type: req.body.type ?? transcreationExamples[index].type ?? 'slogan',
1016
+ description: req.body.description ?? transcreationExamples[index].description ?? ''
1017
+ };
1018
+
1019
+ // Update the example in the array
1020
+ transcreationExamples[index] = updatedExample;
1021
+
1022
+ console.log(`📝 UPDATED EXAMPLE:`, JSON.stringify(updatedExample, null, 2));
1023
+ console.log(`📝 EXAMPLE IN ARRAY AFTER UPDATE:`, JSON.stringify(transcreationExamples[index], null, 2));
1024
+
1025
+ // Save to cache file
1026
+ await saveCachedExamples();
1027
+
1028
+ console.log(`✅ UPDATE COMPLETED for ID: ${id}`);
1029
+
1030
+ res.json({
1031
+ success: true,
1032
+ message: 'Example updated successfully',
1033
+ data: updatedExample
1034
+ });
1035
+ } catch (error) {
1036
+ console.error('❌ Error updating example:', error);
1037
+ res.status(500).json({
1038
+ success: false,
1039
+ message: 'Failed to update example',
1040
+ error: error.message
1041
+ });
1042
+ }
1043
+ });
1044
+
1045
+ // Delete example
1046
+ app.delete('/api/examples/:id', (req, res) => {
1047
+ try {
1048
+ const { id } = req.params;
1049
+
1050
+ // Find and remove example
1051
+ const index = transcreationExamples.findIndex(ex => ex.id === id);
1052
+ if (index === -1) {
1053
+ return res.status(404).json({
1054
+ success: false,
1055
+ error: 'Example not found'
1056
+ });
1057
+ }
1058
+
1059
+ transcreationExamples.splice(index, 1);
1060
+ saveCachedExamples();
1061
+
1062
+ res.json({
1063
+ success: true,
1064
+ message: 'Example deleted successfully'
1065
+ });
1066
+ } catch (error) {
1067
+ res.status(500).json({
1068
+ success: false,
1069
+ error: 'Failed to delete example'
1070
+ });
1071
+ }
1072
+ });
1073
+
1074
+ // Mark example as containing intentional errors (for teaching)
1075
+ app.post('/api/examples/:id/mark-educational', (req, res) => {
1076
+ try {
1077
+ const { id } = req.params;
1078
+ const { hasIntentionalErrors, errorNotes } = req.body;
1079
+
1080
+ // Find example
1081
+ const index = transcreationExamples.findIndex(ex => ex.id === id);
1082
+ if (index === -1) {
1083
+ return res.status(404).json({
1084
+ success: false,
1085
+ error: 'Example not found'
1086
+ });
1087
+ }
1088
+
1089
+ // Update educational metadata
1090
+ transcreationExamples[index] = {
1091
+ ...transcreationExamples[index],
1092
+ isEducational: true,
1093
+ hasIntentionalErrors: hasIntentionalErrors || false,
1094
+ errorNotes: errorNotes || '',
1095
+ lastModified: new Date().toISOString()
1096
+ };
1097
+
1098
+ saveCachedExamples();
1099
+
1100
+ res.json({
1101
+ success: true,
1102
+ data: transcreationExamples[index],
1103
+ message: 'Example marked as educational'
1104
+ });
1105
+ } catch (error) {
1106
+ res.status(500).json({
1107
+ success: false,
1108
+ error: 'Failed to mark example as educational'
1109
+ });
1110
+ }
1111
+ });
1112
+
1113
+ // Get all educational examples
1114
+ app.get('/api/examples/educational', (req, res) => {
1115
+ try {
1116
+ const educationalExamples = transcreationExamples.filter(ex => ex.isEducational);
1117
+
1118
+ res.json({
1119
+ success: true,
1120
+ data: educationalExamples,
1121
+ total: educationalExamples.length
1122
+ });
1123
+ } catch (error) {
1124
+ res.status(500).json({
1125
+ success: false,
1126
+ error: 'Failed to fetch educational examples'
1127
+ });
1128
+ }
1129
+ });
1130
+
1131
+ // Serve static files from React build (for production)
1132
+ if (process.env.NODE_ENV === 'production') {
1133
+ app.use(express.static(path.join(__dirname, '../client/build')));
1134
+
1135
+ app.get('*', (req, res) => {
1136
+ res.sendFile(path.join(__dirname, '../client/build', 'index.html'));
1137
+ });
1138
+ }
1139
+
1140
+ app.listen(PORT, () => {
1141
+ console.log(`🚀 Transcreation Explorer API running on port ${PORT}`);
1142
+ console.log(`📊 Cached examples: ${transcreationExamples.length}`);
1143
+ });
1144
+
1145
+ module.exports = app;
server/package-lock.json ADDED
@@ -0,0 +1,1680 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "transcreation-explorer-server",
3
+ "version": "1.0.0",
4
+ "lockfileVersion": 3,
5
+ "requires": true,
6
+ "packages": {
7
+ "": {
8
+ "name": "transcreation-explorer-server",
9
+ "version": "1.0.0",
10
+ "dependencies": {
11
+ "axios": "^1.6.2",
12
+ "cheerio": "^1.0.0-rc.12",
13
+ "cors": "^2.8.5",
14
+ "dotenv": "^16.3.1",
15
+ "express": "^4.18.2",
16
+ "node-cron": "^3.0.3"
17
+ },
18
+ "devDependencies": {
19
+ "nodemon": "^3.0.2"
20
+ }
21
+ },
22
+ "node_modules/accepts": {
23
+ "version": "1.3.8",
24
+ "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz",
25
+ "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==",
26
+ "license": "MIT",
27
+ "dependencies": {
28
+ "mime-types": "~2.1.34",
29
+ "negotiator": "0.6.3"
30
+ },
31
+ "engines": {
32
+ "node": ">= 0.6"
33
+ }
34
+ },
35
+ "node_modules/anymatch": {
36
+ "version": "3.1.3",
37
+ "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz",
38
+ "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==",
39
+ "dev": true,
40
+ "license": "ISC",
41
+ "dependencies": {
42
+ "normalize-path": "^3.0.0",
43
+ "picomatch": "^2.0.4"
44
+ },
45
+ "engines": {
46
+ "node": ">= 8"
47
+ }
48
+ },
49
+ "node_modules/array-flatten": {
50
+ "version": "1.1.1",
51
+ "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz",
52
+ "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==",
53
+ "license": "MIT"
54
+ },
55
+ "node_modules/asynckit": {
56
+ "version": "0.4.0",
57
+ "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
58
+ "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
59
+ "license": "MIT"
60
+ },
61
+ "node_modules/axios": {
62
+ "version": "1.10.0",
63
+ "resolved": "https://registry.npmjs.org/axios/-/axios-1.10.0.tgz",
64
+ "integrity": "sha512-/1xYAC4MP/HEG+3duIhFr4ZQXR4sQXOIe+o6sdqzeykGLx6Upp/1p8MHqhINOvGeP7xyNHe7tsiJByc4SSVUxw==",
65
+ "license": "MIT",
66
+ "dependencies": {
67
+ "follow-redirects": "^1.15.6",
68
+ "form-data": "^4.0.0",
69
+ "proxy-from-env": "^1.1.0"
70
+ }
71
+ },
72
+ "node_modules/balanced-match": {
73
+ "version": "1.0.2",
74
+ "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
75
+ "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
76
+ "dev": true,
77
+ "license": "MIT"
78
+ },
79
+ "node_modules/binary-extensions": {
80
+ "version": "2.3.0",
81
+ "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz",
82
+ "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==",
83
+ "dev": true,
84
+ "license": "MIT",
85
+ "engines": {
86
+ "node": ">=8"
87
+ },
88
+ "funding": {
89
+ "url": "https://github.com/sponsors/sindresorhus"
90
+ }
91
+ },
92
+ "node_modules/body-parser": {
93
+ "version": "1.20.3",
94
+ "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz",
95
+ "integrity": "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==",
96
+ "license": "MIT",
97
+ "dependencies": {
98
+ "bytes": "3.1.2",
99
+ "content-type": "~1.0.5",
100
+ "debug": "2.6.9",
101
+ "depd": "2.0.0",
102
+ "destroy": "1.2.0",
103
+ "http-errors": "2.0.0",
104
+ "iconv-lite": "0.4.24",
105
+ "on-finished": "2.4.1",
106
+ "qs": "6.13.0",
107
+ "raw-body": "2.5.2",
108
+ "type-is": "~1.6.18",
109
+ "unpipe": "1.0.0"
110
+ },
111
+ "engines": {
112
+ "node": ">= 0.8",
113
+ "npm": "1.2.8000 || >= 1.4.16"
114
+ }
115
+ },
116
+ "node_modules/body-parser/node_modules/iconv-lite": {
117
+ "version": "0.4.24",
118
+ "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
119
+ "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==",
120
+ "license": "MIT",
121
+ "dependencies": {
122
+ "safer-buffer": ">= 2.1.2 < 3"
123
+ },
124
+ "engines": {
125
+ "node": ">=0.10.0"
126
+ }
127
+ },
128
+ "node_modules/boolbase": {
129
+ "version": "1.0.0",
130
+ "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz",
131
+ "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==",
132
+ "license": "ISC"
133
+ },
134
+ "node_modules/brace-expansion": {
135
+ "version": "1.1.12",
136
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
137
+ "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
138
+ "dev": true,
139
+ "license": "MIT",
140
+ "dependencies": {
141
+ "balanced-match": "^1.0.0",
142
+ "concat-map": "0.0.1"
143
+ }
144
+ },
145
+ "node_modules/braces": {
146
+ "version": "3.0.3",
147
+ "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz",
148
+ "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==",
149
+ "dev": true,
150
+ "license": "MIT",
151
+ "dependencies": {
152
+ "fill-range": "^7.1.1"
153
+ },
154
+ "engines": {
155
+ "node": ">=8"
156
+ }
157
+ },
158
+ "node_modules/bytes": {
159
+ "version": "3.1.2",
160
+ "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
161
+ "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==",
162
+ "license": "MIT",
163
+ "engines": {
164
+ "node": ">= 0.8"
165
+ }
166
+ },
167
+ "node_modules/call-bind-apply-helpers": {
168
+ "version": "1.0.2",
169
+ "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
170
+ "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
171
+ "license": "MIT",
172
+ "dependencies": {
173
+ "es-errors": "^1.3.0",
174
+ "function-bind": "^1.1.2"
175
+ },
176
+ "engines": {
177
+ "node": ">= 0.4"
178
+ }
179
+ },
180
+ "node_modules/call-bound": {
181
+ "version": "1.0.4",
182
+ "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz",
183
+ "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==",
184
+ "license": "MIT",
185
+ "dependencies": {
186
+ "call-bind-apply-helpers": "^1.0.2",
187
+ "get-intrinsic": "^1.3.0"
188
+ },
189
+ "engines": {
190
+ "node": ">= 0.4"
191
+ },
192
+ "funding": {
193
+ "url": "https://github.com/sponsors/ljharb"
194
+ }
195
+ },
196
+ "node_modules/cheerio": {
197
+ "version": "1.1.0",
198
+ "resolved": "https://registry.npmjs.org/cheerio/-/cheerio-1.1.0.tgz",
199
+ "integrity": "sha512-+0hMx9eYhJvWbgpKV9hN7jg0JcwydpopZE4hgi+KvQtByZXPp04NiCWU0LzcAbP63abZckIHkTQaXVF52mX3xQ==",
200
+ "license": "MIT",
201
+ "dependencies": {
202
+ "cheerio-select": "^2.1.0",
203
+ "dom-serializer": "^2.0.0",
204
+ "domhandler": "^5.0.3",
205
+ "domutils": "^3.2.2",
206
+ "encoding-sniffer": "^0.2.0",
207
+ "htmlparser2": "^10.0.0",
208
+ "parse5": "^7.3.0",
209
+ "parse5-htmlparser2-tree-adapter": "^7.1.0",
210
+ "parse5-parser-stream": "^7.1.2",
211
+ "undici": "^7.10.0",
212
+ "whatwg-mimetype": "^4.0.0"
213
+ },
214
+ "engines": {
215
+ "node": ">=18.17"
216
+ },
217
+ "funding": {
218
+ "url": "https://github.com/cheeriojs/cheerio?sponsor=1"
219
+ }
220
+ },
221
+ "node_modules/cheerio-select": {
222
+ "version": "2.1.0",
223
+ "resolved": "https://registry.npmjs.org/cheerio-select/-/cheerio-select-2.1.0.tgz",
224
+ "integrity": "sha512-9v9kG0LvzrlcungtnJtpGNxY+fzECQKhK4EGJX2vByejiMX84MFNQw4UxPJl3bFbTMw+Dfs37XaIkCwTZfLh4g==",
225
+ "license": "BSD-2-Clause",
226
+ "dependencies": {
227
+ "boolbase": "^1.0.0",
228
+ "css-select": "^5.1.0",
229
+ "css-what": "^6.1.0",
230
+ "domelementtype": "^2.3.0",
231
+ "domhandler": "^5.0.3",
232
+ "domutils": "^3.0.1"
233
+ },
234
+ "funding": {
235
+ "url": "https://github.com/sponsors/fb55"
236
+ }
237
+ },
238
+ "node_modules/chokidar": {
239
+ "version": "3.6.0",
240
+ "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz",
241
+ "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==",
242
+ "dev": true,
243
+ "license": "MIT",
244
+ "dependencies": {
245
+ "anymatch": "~3.1.2",
246
+ "braces": "~3.0.2",
247
+ "glob-parent": "~5.1.2",
248
+ "is-binary-path": "~2.1.0",
249
+ "is-glob": "~4.0.1",
250
+ "normalize-path": "~3.0.0",
251
+ "readdirp": "~3.6.0"
252
+ },
253
+ "engines": {
254
+ "node": ">= 8.10.0"
255
+ },
256
+ "funding": {
257
+ "url": "https://paulmillr.com/funding/"
258
+ },
259
+ "optionalDependencies": {
260
+ "fsevents": "~2.3.2"
261
+ }
262
+ },
263
+ "node_modules/combined-stream": {
264
+ "version": "1.0.8",
265
+ "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
266
+ "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
267
+ "license": "MIT",
268
+ "dependencies": {
269
+ "delayed-stream": "~1.0.0"
270
+ },
271
+ "engines": {
272
+ "node": ">= 0.8"
273
+ }
274
+ },
275
+ "node_modules/concat-map": {
276
+ "version": "0.0.1",
277
+ "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
278
+ "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==",
279
+ "dev": true,
280
+ "license": "MIT"
281
+ },
282
+ "node_modules/content-disposition": {
283
+ "version": "0.5.4",
284
+ "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz",
285
+ "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==",
286
+ "license": "MIT",
287
+ "dependencies": {
288
+ "safe-buffer": "5.2.1"
289
+ },
290
+ "engines": {
291
+ "node": ">= 0.6"
292
+ }
293
+ },
294
+ "node_modules/content-type": {
295
+ "version": "1.0.5",
296
+ "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz",
297
+ "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==",
298
+ "license": "MIT",
299
+ "engines": {
300
+ "node": ">= 0.6"
301
+ }
302
+ },
303
+ "node_modules/cookie": {
304
+ "version": "0.7.1",
305
+ "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz",
306
+ "integrity": "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==",
307
+ "license": "MIT",
308
+ "engines": {
309
+ "node": ">= 0.6"
310
+ }
311
+ },
312
+ "node_modules/cookie-signature": {
313
+ "version": "1.0.6",
314
+ "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz",
315
+ "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==",
316
+ "license": "MIT"
317
+ },
318
+ "node_modules/cors": {
319
+ "version": "2.8.5",
320
+ "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz",
321
+ "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==",
322
+ "license": "MIT",
323
+ "dependencies": {
324
+ "object-assign": "^4",
325
+ "vary": "^1"
326
+ },
327
+ "engines": {
328
+ "node": ">= 0.10"
329
+ }
330
+ },
331
+ "node_modules/css-select": {
332
+ "version": "5.2.2",
333
+ "resolved": "https://registry.npmjs.org/css-select/-/css-select-5.2.2.tgz",
334
+ "integrity": "sha512-TizTzUddG/xYLA3NXodFM0fSbNizXjOKhqiQQwvhlspadZokn1KDy0NZFS0wuEubIYAV5/c1/lAr0TaaFXEXzw==",
335
+ "license": "BSD-2-Clause",
336
+ "dependencies": {
337
+ "boolbase": "^1.0.0",
338
+ "css-what": "^6.1.0",
339
+ "domhandler": "^5.0.2",
340
+ "domutils": "^3.0.1",
341
+ "nth-check": "^2.0.1"
342
+ },
343
+ "funding": {
344
+ "url": "https://github.com/sponsors/fb55"
345
+ }
346
+ },
347
+ "node_modules/css-what": {
348
+ "version": "6.2.2",
349
+ "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.2.2.tgz",
350
+ "integrity": "sha512-u/O3vwbptzhMs3L1fQE82ZSLHQQfto5gyZzwteVIEyeaY5Fc7R4dapF/BvRoSYFeqfBk4m0V1Vafq5Pjv25wvA==",
351
+ "license": "BSD-2-Clause",
352
+ "engines": {
353
+ "node": ">= 6"
354
+ },
355
+ "funding": {
356
+ "url": "https://github.com/sponsors/fb55"
357
+ }
358
+ },
359
+ "node_modules/debug": {
360
+ "version": "2.6.9",
361
+ "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
362
+ "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
363
+ "license": "MIT",
364
+ "dependencies": {
365
+ "ms": "2.0.0"
366
+ }
367
+ },
368
+ "node_modules/delayed-stream": {
369
+ "version": "1.0.0",
370
+ "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
371
+ "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
372
+ "license": "MIT",
373
+ "engines": {
374
+ "node": ">=0.4.0"
375
+ }
376
+ },
377
+ "node_modules/depd": {
378
+ "version": "2.0.0",
379
+ "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
380
+ "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==",
381
+ "license": "MIT",
382
+ "engines": {
383
+ "node": ">= 0.8"
384
+ }
385
+ },
386
+ "node_modules/destroy": {
387
+ "version": "1.2.0",
388
+ "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz",
389
+ "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==",
390
+ "license": "MIT",
391
+ "engines": {
392
+ "node": ">= 0.8",
393
+ "npm": "1.2.8000 || >= 1.4.16"
394
+ }
395
+ },
396
+ "node_modules/dom-serializer": {
397
+ "version": "2.0.0",
398
+ "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz",
399
+ "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==",
400
+ "license": "MIT",
401
+ "dependencies": {
402
+ "domelementtype": "^2.3.0",
403
+ "domhandler": "^5.0.2",
404
+ "entities": "^4.2.0"
405
+ },
406
+ "funding": {
407
+ "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1"
408
+ }
409
+ },
410
+ "node_modules/domelementtype": {
411
+ "version": "2.3.0",
412
+ "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz",
413
+ "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==",
414
+ "funding": [
415
+ {
416
+ "type": "github",
417
+ "url": "https://github.com/sponsors/fb55"
418
+ }
419
+ ],
420
+ "license": "BSD-2-Clause"
421
+ },
422
+ "node_modules/domhandler": {
423
+ "version": "5.0.3",
424
+ "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz",
425
+ "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==",
426
+ "license": "BSD-2-Clause",
427
+ "dependencies": {
428
+ "domelementtype": "^2.3.0"
429
+ },
430
+ "engines": {
431
+ "node": ">= 4"
432
+ },
433
+ "funding": {
434
+ "url": "https://github.com/fb55/domhandler?sponsor=1"
435
+ }
436
+ },
437
+ "node_modules/domutils": {
438
+ "version": "3.2.2",
439
+ "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.2.2.tgz",
440
+ "integrity": "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==",
441
+ "license": "BSD-2-Clause",
442
+ "dependencies": {
443
+ "dom-serializer": "^2.0.0",
444
+ "domelementtype": "^2.3.0",
445
+ "domhandler": "^5.0.3"
446
+ },
447
+ "funding": {
448
+ "url": "https://github.com/fb55/domutils?sponsor=1"
449
+ }
450
+ },
451
+ "node_modules/dotenv": {
452
+ "version": "16.6.1",
453
+ "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz",
454
+ "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==",
455
+ "license": "BSD-2-Clause",
456
+ "engines": {
457
+ "node": ">=12"
458
+ },
459
+ "funding": {
460
+ "url": "https://dotenvx.com"
461
+ }
462
+ },
463
+ "node_modules/dunder-proto": {
464
+ "version": "1.0.1",
465
+ "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
466
+ "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
467
+ "license": "MIT",
468
+ "dependencies": {
469
+ "call-bind-apply-helpers": "^1.0.1",
470
+ "es-errors": "^1.3.0",
471
+ "gopd": "^1.2.0"
472
+ },
473
+ "engines": {
474
+ "node": ">= 0.4"
475
+ }
476
+ },
477
+ "node_modules/ee-first": {
478
+ "version": "1.1.1",
479
+ "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
480
+ "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==",
481
+ "license": "MIT"
482
+ },
483
+ "node_modules/encodeurl": {
484
+ "version": "2.0.0",
485
+ "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz",
486
+ "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==",
487
+ "license": "MIT",
488
+ "engines": {
489
+ "node": ">= 0.8"
490
+ }
491
+ },
492
+ "node_modules/encoding-sniffer": {
493
+ "version": "0.2.1",
494
+ "resolved": "https://registry.npmjs.org/encoding-sniffer/-/encoding-sniffer-0.2.1.tgz",
495
+ "integrity": "sha512-5gvq20T6vfpekVtqrYQsSCFZ1wEg5+wW0/QaZMWkFr6BqD3NfKs0rLCx4rrVlSWJeZb5NBJgVLswK/w2MWU+Gw==",
496
+ "license": "MIT",
497
+ "dependencies": {
498
+ "iconv-lite": "^0.6.3",
499
+ "whatwg-encoding": "^3.1.1"
500
+ },
501
+ "funding": {
502
+ "url": "https://github.com/fb55/encoding-sniffer?sponsor=1"
503
+ }
504
+ },
505
+ "node_modules/entities": {
506
+ "version": "4.5.0",
507
+ "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz",
508
+ "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==",
509
+ "license": "BSD-2-Clause",
510
+ "engines": {
511
+ "node": ">=0.12"
512
+ },
513
+ "funding": {
514
+ "url": "https://github.com/fb55/entities?sponsor=1"
515
+ }
516
+ },
517
+ "node_modules/es-define-property": {
518
+ "version": "1.0.1",
519
+ "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
520
+ "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
521
+ "license": "MIT",
522
+ "engines": {
523
+ "node": ">= 0.4"
524
+ }
525
+ },
526
+ "node_modules/es-errors": {
527
+ "version": "1.3.0",
528
+ "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
529
+ "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
530
+ "license": "MIT",
531
+ "engines": {
532
+ "node": ">= 0.4"
533
+ }
534
+ },
535
+ "node_modules/es-object-atoms": {
536
+ "version": "1.1.1",
537
+ "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
538
+ "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
539
+ "license": "MIT",
540
+ "dependencies": {
541
+ "es-errors": "^1.3.0"
542
+ },
543
+ "engines": {
544
+ "node": ">= 0.4"
545
+ }
546
+ },
547
+ "node_modules/es-set-tostringtag": {
548
+ "version": "2.1.0",
549
+ "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz",
550
+ "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==",
551
+ "license": "MIT",
552
+ "dependencies": {
553
+ "es-errors": "^1.3.0",
554
+ "get-intrinsic": "^1.2.6",
555
+ "has-tostringtag": "^1.0.2",
556
+ "hasown": "^2.0.2"
557
+ },
558
+ "engines": {
559
+ "node": ">= 0.4"
560
+ }
561
+ },
562
+ "node_modules/escape-html": {
563
+ "version": "1.0.3",
564
+ "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
565
+ "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==",
566
+ "license": "MIT"
567
+ },
568
+ "node_modules/etag": {
569
+ "version": "1.8.1",
570
+ "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz",
571
+ "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==",
572
+ "license": "MIT",
573
+ "engines": {
574
+ "node": ">= 0.6"
575
+ }
576
+ },
577
+ "node_modules/express": {
578
+ "version": "4.21.2",
579
+ "resolved": "https://registry.npmjs.org/express/-/express-4.21.2.tgz",
580
+ "integrity": "sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==",
581
+ "license": "MIT",
582
+ "dependencies": {
583
+ "accepts": "~1.3.8",
584
+ "array-flatten": "1.1.1",
585
+ "body-parser": "1.20.3",
586
+ "content-disposition": "0.5.4",
587
+ "content-type": "~1.0.4",
588
+ "cookie": "0.7.1",
589
+ "cookie-signature": "1.0.6",
590
+ "debug": "2.6.9",
591
+ "depd": "2.0.0",
592
+ "encodeurl": "~2.0.0",
593
+ "escape-html": "~1.0.3",
594
+ "etag": "~1.8.1",
595
+ "finalhandler": "1.3.1",
596
+ "fresh": "0.5.2",
597
+ "http-errors": "2.0.0",
598
+ "merge-descriptors": "1.0.3",
599
+ "methods": "~1.1.2",
600
+ "on-finished": "2.4.1",
601
+ "parseurl": "~1.3.3",
602
+ "path-to-regexp": "0.1.12",
603
+ "proxy-addr": "~2.0.7",
604
+ "qs": "6.13.0",
605
+ "range-parser": "~1.2.1",
606
+ "safe-buffer": "5.2.1",
607
+ "send": "0.19.0",
608
+ "serve-static": "1.16.2",
609
+ "setprototypeof": "1.2.0",
610
+ "statuses": "2.0.1",
611
+ "type-is": "~1.6.18",
612
+ "utils-merge": "1.0.1",
613
+ "vary": "~1.1.2"
614
+ },
615
+ "engines": {
616
+ "node": ">= 0.10.0"
617
+ },
618
+ "funding": {
619
+ "type": "opencollective",
620
+ "url": "https://opencollective.com/express"
621
+ }
622
+ },
623
+ "node_modules/fill-range": {
624
+ "version": "7.1.1",
625
+ "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
626
+ "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==",
627
+ "dev": true,
628
+ "license": "MIT",
629
+ "dependencies": {
630
+ "to-regex-range": "^5.0.1"
631
+ },
632
+ "engines": {
633
+ "node": ">=8"
634
+ }
635
+ },
636
+ "node_modules/finalhandler": {
637
+ "version": "1.3.1",
638
+ "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.1.tgz",
639
+ "integrity": "sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==",
640
+ "license": "MIT",
641
+ "dependencies": {
642
+ "debug": "2.6.9",
643
+ "encodeurl": "~2.0.0",
644
+ "escape-html": "~1.0.3",
645
+ "on-finished": "2.4.1",
646
+ "parseurl": "~1.3.3",
647
+ "statuses": "2.0.1",
648
+ "unpipe": "~1.0.0"
649
+ },
650
+ "engines": {
651
+ "node": ">= 0.8"
652
+ }
653
+ },
654
+ "node_modules/follow-redirects": {
655
+ "version": "1.15.9",
656
+ "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz",
657
+ "integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==",
658
+ "funding": [
659
+ {
660
+ "type": "individual",
661
+ "url": "https://github.com/sponsors/RubenVerborgh"
662
+ }
663
+ ],
664
+ "license": "MIT",
665
+ "engines": {
666
+ "node": ">=4.0"
667
+ },
668
+ "peerDependenciesMeta": {
669
+ "debug": {
670
+ "optional": true
671
+ }
672
+ }
673
+ },
674
+ "node_modules/form-data": {
675
+ "version": "4.0.4",
676
+ "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz",
677
+ "integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==",
678
+ "license": "MIT",
679
+ "dependencies": {
680
+ "asynckit": "^0.4.0",
681
+ "combined-stream": "^1.0.8",
682
+ "es-set-tostringtag": "^2.1.0",
683
+ "hasown": "^2.0.2",
684
+ "mime-types": "^2.1.12"
685
+ },
686
+ "engines": {
687
+ "node": ">= 6"
688
+ }
689
+ },
690
+ "node_modules/forwarded": {
691
+ "version": "0.2.0",
692
+ "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
693
+ "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==",
694
+ "license": "MIT",
695
+ "engines": {
696
+ "node": ">= 0.6"
697
+ }
698
+ },
699
+ "node_modules/fresh": {
700
+ "version": "0.5.2",
701
+ "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz",
702
+ "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==",
703
+ "license": "MIT",
704
+ "engines": {
705
+ "node": ">= 0.6"
706
+ }
707
+ },
708
+ "node_modules/fsevents": {
709
+ "version": "2.3.3",
710
+ "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
711
+ "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
712
+ "dev": true,
713
+ "hasInstallScript": true,
714
+ "license": "MIT",
715
+ "optional": true,
716
+ "os": [
717
+ "darwin"
718
+ ],
719
+ "engines": {
720
+ "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
721
+ }
722
+ },
723
+ "node_modules/function-bind": {
724
+ "version": "1.1.2",
725
+ "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
726
+ "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
727
+ "license": "MIT",
728
+ "funding": {
729
+ "url": "https://github.com/sponsors/ljharb"
730
+ }
731
+ },
732
+ "node_modules/get-intrinsic": {
733
+ "version": "1.3.0",
734
+ "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
735
+ "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
736
+ "license": "MIT",
737
+ "dependencies": {
738
+ "call-bind-apply-helpers": "^1.0.2",
739
+ "es-define-property": "^1.0.1",
740
+ "es-errors": "^1.3.0",
741
+ "es-object-atoms": "^1.1.1",
742
+ "function-bind": "^1.1.2",
743
+ "get-proto": "^1.0.1",
744
+ "gopd": "^1.2.0",
745
+ "has-symbols": "^1.1.0",
746
+ "hasown": "^2.0.2",
747
+ "math-intrinsics": "^1.1.0"
748
+ },
749
+ "engines": {
750
+ "node": ">= 0.4"
751
+ },
752
+ "funding": {
753
+ "url": "https://github.com/sponsors/ljharb"
754
+ }
755
+ },
756
+ "node_modules/get-proto": {
757
+ "version": "1.0.1",
758
+ "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
759
+ "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
760
+ "license": "MIT",
761
+ "dependencies": {
762
+ "dunder-proto": "^1.0.1",
763
+ "es-object-atoms": "^1.0.0"
764
+ },
765
+ "engines": {
766
+ "node": ">= 0.4"
767
+ }
768
+ },
769
+ "node_modules/glob-parent": {
770
+ "version": "5.1.2",
771
+ "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
772
+ "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
773
+ "dev": true,
774
+ "license": "ISC",
775
+ "dependencies": {
776
+ "is-glob": "^4.0.1"
777
+ },
778
+ "engines": {
779
+ "node": ">= 6"
780
+ }
781
+ },
782
+ "node_modules/gopd": {
783
+ "version": "1.2.0",
784
+ "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
785
+ "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
786
+ "license": "MIT",
787
+ "engines": {
788
+ "node": ">= 0.4"
789
+ },
790
+ "funding": {
791
+ "url": "https://github.com/sponsors/ljharb"
792
+ }
793
+ },
794
+ "node_modules/has-flag": {
795
+ "version": "3.0.0",
796
+ "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz",
797
+ "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==",
798
+ "dev": true,
799
+ "license": "MIT",
800
+ "engines": {
801
+ "node": ">=4"
802
+ }
803
+ },
804
+ "node_modules/has-symbols": {
805
+ "version": "1.1.0",
806
+ "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
807
+ "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
808
+ "license": "MIT",
809
+ "engines": {
810
+ "node": ">= 0.4"
811
+ },
812
+ "funding": {
813
+ "url": "https://github.com/sponsors/ljharb"
814
+ }
815
+ },
816
+ "node_modules/has-tostringtag": {
817
+ "version": "1.0.2",
818
+ "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
819
+ "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
820
+ "license": "MIT",
821
+ "dependencies": {
822
+ "has-symbols": "^1.0.3"
823
+ },
824
+ "engines": {
825
+ "node": ">= 0.4"
826
+ },
827
+ "funding": {
828
+ "url": "https://github.com/sponsors/ljharb"
829
+ }
830
+ },
831
+ "node_modules/hasown": {
832
+ "version": "2.0.2",
833
+ "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
834
+ "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
835
+ "license": "MIT",
836
+ "dependencies": {
837
+ "function-bind": "^1.1.2"
838
+ },
839
+ "engines": {
840
+ "node": ">= 0.4"
841
+ }
842
+ },
843
+ "node_modules/htmlparser2": {
844
+ "version": "10.0.0",
845
+ "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-10.0.0.tgz",
846
+ "integrity": "sha512-TwAZM+zE5Tq3lrEHvOlvwgj1XLWQCtaaibSN11Q+gGBAS7Y1uZSWwXXRe4iF6OXnaq1riyQAPFOBtYc77Mxq0g==",
847
+ "funding": [
848
+ "https://github.com/fb55/htmlparser2?sponsor=1",
849
+ {
850
+ "type": "github",
851
+ "url": "https://github.com/sponsors/fb55"
852
+ }
853
+ ],
854
+ "license": "MIT",
855
+ "dependencies": {
856
+ "domelementtype": "^2.3.0",
857
+ "domhandler": "^5.0.3",
858
+ "domutils": "^3.2.1",
859
+ "entities": "^6.0.0"
860
+ }
861
+ },
862
+ "node_modules/htmlparser2/node_modules/entities": {
863
+ "version": "6.0.1",
864
+ "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz",
865
+ "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==",
866
+ "license": "BSD-2-Clause",
867
+ "engines": {
868
+ "node": ">=0.12"
869
+ },
870
+ "funding": {
871
+ "url": "https://github.com/fb55/entities?sponsor=1"
872
+ }
873
+ },
874
+ "node_modules/http-errors": {
875
+ "version": "2.0.0",
876
+ "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz",
877
+ "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==",
878
+ "license": "MIT",
879
+ "dependencies": {
880
+ "depd": "2.0.0",
881
+ "inherits": "2.0.4",
882
+ "setprototypeof": "1.2.0",
883
+ "statuses": "2.0.1",
884
+ "toidentifier": "1.0.1"
885
+ },
886
+ "engines": {
887
+ "node": ">= 0.8"
888
+ }
889
+ },
890
+ "node_modules/iconv-lite": {
891
+ "version": "0.6.3",
892
+ "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz",
893
+ "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==",
894
+ "license": "MIT",
895
+ "dependencies": {
896
+ "safer-buffer": ">= 2.1.2 < 3.0.0"
897
+ },
898
+ "engines": {
899
+ "node": ">=0.10.0"
900
+ }
901
+ },
902
+ "node_modules/ignore-by-default": {
903
+ "version": "1.0.1",
904
+ "resolved": "https://registry.npmjs.org/ignore-by-default/-/ignore-by-default-1.0.1.tgz",
905
+ "integrity": "sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA==",
906
+ "dev": true,
907
+ "license": "ISC"
908
+ },
909
+ "node_modules/inherits": {
910
+ "version": "2.0.4",
911
+ "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
912
+ "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
913
+ "license": "ISC"
914
+ },
915
+ "node_modules/ipaddr.js": {
916
+ "version": "1.9.1",
917
+ "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
918
+ "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==",
919
+ "license": "MIT",
920
+ "engines": {
921
+ "node": ">= 0.10"
922
+ }
923
+ },
924
+ "node_modules/is-binary-path": {
925
+ "version": "2.1.0",
926
+ "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz",
927
+ "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==",
928
+ "dev": true,
929
+ "license": "MIT",
930
+ "dependencies": {
931
+ "binary-extensions": "^2.0.0"
932
+ },
933
+ "engines": {
934
+ "node": ">=8"
935
+ }
936
+ },
937
+ "node_modules/is-extglob": {
938
+ "version": "2.1.1",
939
+ "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
940
+ "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==",
941
+ "dev": true,
942
+ "license": "MIT",
943
+ "engines": {
944
+ "node": ">=0.10.0"
945
+ }
946
+ },
947
+ "node_modules/is-glob": {
948
+ "version": "4.0.3",
949
+ "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
950
+ "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==",
951
+ "dev": true,
952
+ "license": "MIT",
953
+ "dependencies": {
954
+ "is-extglob": "^2.1.1"
955
+ },
956
+ "engines": {
957
+ "node": ">=0.10.0"
958
+ }
959
+ },
960
+ "node_modules/is-number": {
961
+ "version": "7.0.0",
962
+ "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
963
+ "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
964
+ "dev": true,
965
+ "license": "MIT",
966
+ "engines": {
967
+ "node": ">=0.12.0"
968
+ }
969
+ },
970
+ "node_modules/math-intrinsics": {
971
+ "version": "1.1.0",
972
+ "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
973
+ "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
974
+ "license": "MIT",
975
+ "engines": {
976
+ "node": ">= 0.4"
977
+ }
978
+ },
979
+ "node_modules/media-typer": {
980
+ "version": "0.3.0",
981
+ "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz",
982
+ "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==",
983
+ "license": "MIT",
984
+ "engines": {
985
+ "node": ">= 0.6"
986
+ }
987
+ },
988
+ "node_modules/merge-descriptors": {
989
+ "version": "1.0.3",
990
+ "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz",
991
+ "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==",
992
+ "license": "MIT",
993
+ "funding": {
994
+ "url": "https://github.com/sponsors/sindresorhus"
995
+ }
996
+ },
997
+ "node_modules/methods": {
998
+ "version": "1.1.2",
999
+ "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz",
1000
+ "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==",
1001
+ "license": "MIT",
1002
+ "engines": {
1003
+ "node": ">= 0.6"
1004
+ }
1005
+ },
1006
+ "node_modules/mime": {
1007
+ "version": "1.6.0",
1008
+ "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz",
1009
+ "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==",
1010
+ "license": "MIT",
1011
+ "bin": {
1012
+ "mime": "cli.js"
1013
+ },
1014
+ "engines": {
1015
+ "node": ">=4"
1016
+ }
1017
+ },
1018
+ "node_modules/mime-db": {
1019
+ "version": "1.52.0",
1020
+ "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
1021
+ "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
1022
+ "license": "MIT",
1023
+ "engines": {
1024
+ "node": ">= 0.6"
1025
+ }
1026
+ },
1027
+ "node_modules/mime-types": {
1028
+ "version": "2.1.35",
1029
+ "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
1030
+ "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
1031
+ "license": "MIT",
1032
+ "dependencies": {
1033
+ "mime-db": "1.52.0"
1034
+ },
1035
+ "engines": {
1036
+ "node": ">= 0.6"
1037
+ }
1038
+ },
1039
+ "node_modules/minimatch": {
1040
+ "version": "3.1.2",
1041
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
1042
+ "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
1043
+ "dev": true,
1044
+ "license": "ISC",
1045
+ "dependencies": {
1046
+ "brace-expansion": "^1.1.7"
1047
+ },
1048
+ "engines": {
1049
+ "node": "*"
1050
+ }
1051
+ },
1052
+ "node_modules/ms": {
1053
+ "version": "2.0.0",
1054
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
1055
+ "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
1056
+ "license": "MIT"
1057
+ },
1058
+ "node_modules/negotiator": {
1059
+ "version": "0.6.3",
1060
+ "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz",
1061
+ "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==",
1062
+ "license": "MIT",
1063
+ "engines": {
1064
+ "node": ">= 0.6"
1065
+ }
1066
+ },
1067
+ "node_modules/node-cron": {
1068
+ "version": "3.0.3",
1069
+ "resolved": "https://registry.npmjs.org/node-cron/-/node-cron-3.0.3.tgz",
1070
+ "integrity": "sha512-dOal67//nohNgYWb+nWmg5dkFdIwDm8EpeGYMekPMrngV3637lqnX0lbUcCtgibHTz6SEz7DAIjKvKDFYCnO1A==",
1071
+ "license": "ISC",
1072
+ "dependencies": {
1073
+ "uuid": "8.3.2"
1074
+ },
1075
+ "engines": {
1076
+ "node": ">=6.0.0"
1077
+ }
1078
+ },
1079
+ "node_modules/nodemon": {
1080
+ "version": "3.1.10",
1081
+ "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.1.10.tgz",
1082
+ "integrity": "sha512-WDjw3pJ0/0jMFmyNDp3gvY2YizjLmmOUQo6DEBY+JgdvW/yQ9mEeSw6H5ythl5Ny2ytb7f9C2nIbjSxMNzbJXw==",
1083
+ "dev": true,
1084
+ "license": "MIT",
1085
+ "dependencies": {
1086
+ "chokidar": "^3.5.2",
1087
+ "debug": "^4",
1088
+ "ignore-by-default": "^1.0.1",
1089
+ "minimatch": "^3.1.2",
1090
+ "pstree.remy": "^1.1.8",
1091
+ "semver": "^7.5.3",
1092
+ "simple-update-notifier": "^2.0.0",
1093
+ "supports-color": "^5.5.0",
1094
+ "touch": "^3.1.0",
1095
+ "undefsafe": "^2.0.5"
1096
+ },
1097
+ "bin": {
1098
+ "nodemon": "bin/nodemon.js"
1099
+ },
1100
+ "engines": {
1101
+ "node": ">=10"
1102
+ },
1103
+ "funding": {
1104
+ "type": "opencollective",
1105
+ "url": "https://opencollective.com/nodemon"
1106
+ }
1107
+ },
1108
+ "node_modules/nodemon/node_modules/debug": {
1109
+ "version": "4.4.1",
1110
+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz",
1111
+ "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==",
1112
+ "dev": true,
1113
+ "license": "MIT",
1114
+ "dependencies": {
1115
+ "ms": "^2.1.3"
1116
+ },
1117
+ "engines": {
1118
+ "node": ">=6.0"
1119
+ },
1120
+ "peerDependenciesMeta": {
1121
+ "supports-color": {
1122
+ "optional": true
1123
+ }
1124
+ }
1125
+ },
1126
+ "node_modules/nodemon/node_modules/ms": {
1127
+ "version": "2.1.3",
1128
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
1129
+ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
1130
+ "dev": true,
1131
+ "license": "MIT"
1132
+ },
1133
+ "node_modules/normalize-path": {
1134
+ "version": "3.0.0",
1135
+ "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
1136
+ "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==",
1137
+ "dev": true,
1138
+ "license": "MIT",
1139
+ "engines": {
1140
+ "node": ">=0.10.0"
1141
+ }
1142
+ },
1143
+ "node_modules/nth-check": {
1144
+ "version": "2.1.1",
1145
+ "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz",
1146
+ "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==",
1147
+ "license": "BSD-2-Clause",
1148
+ "dependencies": {
1149
+ "boolbase": "^1.0.0"
1150
+ },
1151
+ "funding": {
1152
+ "url": "https://github.com/fb55/nth-check?sponsor=1"
1153
+ }
1154
+ },
1155
+ "node_modules/object-assign": {
1156
+ "version": "4.1.1",
1157
+ "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
1158
+ "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==",
1159
+ "license": "MIT",
1160
+ "engines": {
1161
+ "node": ">=0.10.0"
1162
+ }
1163
+ },
1164
+ "node_modules/object-inspect": {
1165
+ "version": "1.13.4",
1166
+ "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz",
1167
+ "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==",
1168
+ "license": "MIT",
1169
+ "engines": {
1170
+ "node": ">= 0.4"
1171
+ },
1172
+ "funding": {
1173
+ "url": "https://github.com/sponsors/ljharb"
1174
+ }
1175
+ },
1176
+ "node_modules/on-finished": {
1177
+ "version": "2.4.1",
1178
+ "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz",
1179
+ "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==",
1180
+ "license": "MIT",
1181
+ "dependencies": {
1182
+ "ee-first": "1.1.1"
1183
+ },
1184
+ "engines": {
1185
+ "node": ">= 0.8"
1186
+ }
1187
+ },
1188
+ "node_modules/parse5": {
1189
+ "version": "7.3.0",
1190
+ "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz",
1191
+ "integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==",
1192
+ "license": "MIT",
1193
+ "dependencies": {
1194
+ "entities": "^6.0.0"
1195
+ },
1196
+ "funding": {
1197
+ "url": "https://github.com/inikulin/parse5?sponsor=1"
1198
+ }
1199
+ },
1200
+ "node_modules/parse5-htmlparser2-tree-adapter": {
1201
+ "version": "7.1.0",
1202
+ "resolved": "https://registry.npmjs.org/parse5-htmlparser2-tree-adapter/-/parse5-htmlparser2-tree-adapter-7.1.0.tgz",
1203
+ "integrity": "sha512-ruw5xyKs6lrpo9x9rCZqZZnIUntICjQAd0Wsmp396Ul9lN/h+ifgVV1x1gZHi8euej6wTfpqX8j+BFQxF0NS/g==",
1204
+ "license": "MIT",
1205
+ "dependencies": {
1206
+ "domhandler": "^5.0.3",
1207
+ "parse5": "^7.0.0"
1208
+ },
1209
+ "funding": {
1210
+ "url": "https://github.com/inikulin/parse5?sponsor=1"
1211
+ }
1212
+ },
1213
+ "node_modules/parse5-parser-stream": {
1214
+ "version": "7.1.2",
1215
+ "resolved": "https://registry.npmjs.org/parse5-parser-stream/-/parse5-parser-stream-7.1.2.tgz",
1216
+ "integrity": "sha512-JyeQc9iwFLn5TbvvqACIF/VXG6abODeB3Fwmv/TGdLk2LfbWkaySGY72at4+Ty7EkPZj854u4CrICqNk2qIbow==",
1217
+ "license": "MIT",
1218
+ "dependencies": {
1219
+ "parse5": "^7.0.0"
1220
+ },
1221
+ "funding": {
1222
+ "url": "https://github.com/inikulin/parse5?sponsor=1"
1223
+ }
1224
+ },
1225
+ "node_modules/parse5/node_modules/entities": {
1226
+ "version": "6.0.1",
1227
+ "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz",
1228
+ "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==",
1229
+ "license": "BSD-2-Clause",
1230
+ "engines": {
1231
+ "node": ">=0.12"
1232
+ },
1233
+ "funding": {
1234
+ "url": "https://github.com/fb55/entities?sponsor=1"
1235
+ }
1236
+ },
1237
+ "node_modules/parseurl": {
1238
+ "version": "1.3.3",
1239
+ "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
1240
+ "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==",
1241
+ "license": "MIT",
1242
+ "engines": {
1243
+ "node": ">= 0.8"
1244
+ }
1245
+ },
1246
+ "node_modules/path-to-regexp": {
1247
+ "version": "0.1.12",
1248
+ "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz",
1249
+ "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==",
1250
+ "license": "MIT"
1251
+ },
1252
+ "node_modules/picomatch": {
1253
+ "version": "2.3.1",
1254
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
1255
+ "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
1256
+ "dev": true,
1257
+ "license": "MIT",
1258
+ "engines": {
1259
+ "node": ">=8.6"
1260
+ },
1261
+ "funding": {
1262
+ "url": "https://github.com/sponsors/jonschlinkert"
1263
+ }
1264
+ },
1265
+ "node_modules/proxy-addr": {
1266
+ "version": "2.0.7",
1267
+ "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
1268
+ "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==",
1269
+ "license": "MIT",
1270
+ "dependencies": {
1271
+ "forwarded": "0.2.0",
1272
+ "ipaddr.js": "1.9.1"
1273
+ },
1274
+ "engines": {
1275
+ "node": ">= 0.10"
1276
+ }
1277
+ },
1278
+ "node_modules/proxy-from-env": {
1279
+ "version": "1.1.0",
1280
+ "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
1281
+ "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==",
1282
+ "license": "MIT"
1283
+ },
1284
+ "node_modules/pstree.remy": {
1285
+ "version": "1.1.8",
1286
+ "resolved": "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.8.tgz",
1287
+ "integrity": "sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==",
1288
+ "dev": true,
1289
+ "license": "MIT"
1290
+ },
1291
+ "node_modules/qs": {
1292
+ "version": "6.13.0",
1293
+ "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz",
1294
+ "integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==",
1295
+ "license": "BSD-3-Clause",
1296
+ "dependencies": {
1297
+ "side-channel": "^1.0.6"
1298
+ },
1299
+ "engines": {
1300
+ "node": ">=0.6"
1301
+ },
1302
+ "funding": {
1303
+ "url": "https://github.com/sponsors/ljharb"
1304
+ }
1305
+ },
1306
+ "node_modules/range-parser": {
1307
+ "version": "1.2.1",
1308
+ "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz",
1309
+ "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==",
1310
+ "license": "MIT",
1311
+ "engines": {
1312
+ "node": ">= 0.6"
1313
+ }
1314
+ },
1315
+ "node_modules/raw-body": {
1316
+ "version": "2.5.2",
1317
+ "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz",
1318
+ "integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==",
1319
+ "license": "MIT",
1320
+ "dependencies": {
1321
+ "bytes": "3.1.2",
1322
+ "http-errors": "2.0.0",
1323
+ "iconv-lite": "0.4.24",
1324
+ "unpipe": "1.0.0"
1325
+ },
1326
+ "engines": {
1327
+ "node": ">= 0.8"
1328
+ }
1329
+ },
1330
+ "node_modules/raw-body/node_modules/iconv-lite": {
1331
+ "version": "0.4.24",
1332
+ "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
1333
+ "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==",
1334
+ "license": "MIT",
1335
+ "dependencies": {
1336
+ "safer-buffer": ">= 2.1.2 < 3"
1337
+ },
1338
+ "engines": {
1339
+ "node": ">=0.10.0"
1340
+ }
1341
+ },
1342
+ "node_modules/readdirp": {
1343
+ "version": "3.6.0",
1344
+ "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
1345
+ "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==",
1346
+ "dev": true,
1347
+ "license": "MIT",
1348
+ "dependencies": {
1349
+ "picomatch": "^2.2.1"
1350
+ },
1351
+ "engines": {
1352
+ "node": ">=8.10.0"
1353
+ }
1354
+ },
1355
+ "node_modules/safe-buffer": {
1356
+ "version": "5.2.1",
1357
+ "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
1358
+ "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
1359
+ "funding": [
1360
+ {
1361
+ "type": "github",
1362
+ "url": "https://github.com/sponsors/feross"
1363
+ },
1364
+ {
1365
+ "type": "patreon",
1366
+ "url": "https://www.patreon.com/feross"
1367
+ },
1368
+ {
1369
+ "type": "consulting",
1370
+ "url": "https://feross.org/support"
1371
+ }
1372
+ ],
1373
+ "license": "MIT"
1374
+ },
1375
+ "node_modules/safer-buffer": {
1376
+ "version": "2.1.2",
1377
+ "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
1378
+ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
1379
+ "license": "MIT"
1380
+ },
1381
+ "node_modules/semver": {
1382
+ "version": "7.7.2",
1383
+ "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz",
1384
+ "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==",
1385
+ "dev": true,
1386
+ "license": "ISC",
1387
+ "bin": {
1388
+ "semver": "bin/semver.js"
1389
+ },
1390
+ "engines": {
1391
+ "node": ">=10"
1392
+ }
1393
+ },
1394
+ "node_modules/send": {
1395
+ "version": "0.19.0",
1396
+ "resolved": "https://registry.npmjs.org/send/-/send-0.19.0.tgz",
1397
+ "integrity": "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==",
1398
+ "license": "MIT",
1399
+ "dependencies": {
1400
+ "debug": "2.6.9",
1401
+ "depd": "2.0.0",
1402
+ "destroy": "1.2.0",
1403
+ "encodeurl": "~1.0.2",
1404
+ "escape-html": "~1.0.3",
1405
+ "etag": "~1.8.1",
1406
+ "fresh": "0.5.2",
1407
+ "http-errors": "2.0.0",
1408
+ "mime": "1.6.0",
1409
+ "ms": "2.1.3",
1410
+ "on-finished": "2.4.1",
1411
+ "range-parser": "~1.2.1",
1412
+ "statuses": "2.0.1"
1413
+ },
1414
+ "engines": {
1415
+ "node": ">= 0.8.0"
1416
+ }
1417
+ },
1418
+ "node_modules/send/node_modules/encodeurl": {
1419
+ "version": "1.0.2",
1420
+ "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz",
1421
+ "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==",
1422
+ "license": "MIT",
1423
+ "engines": {
1424
+ "node": ">= 0.8"
1425
+ }
1426
+ },
1427
+ "node_modules/send/node_modules/ms": {
1428
+ "version": "2.1.3",
1429
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
1430
+ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
1431
+ "license": "MIT"
1432
+ },
1433
+ "node_modules/serve-static": {
1434
+ "version": "1.16.2",
1435
+ "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.2.tgz",
1436
+ "integrity": "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==",
1437
+ "license": "MIT",
1438
+ "dependencies": {
1439
+ "encodeurl": "~2.0.0",
1440
+ "escape-html": "~1.0.3",
1441
+ "parseurl": "~1.3.3",
1442
+ "send": "0.19.0"
1443
+ },
1444
+ "engines": {
1445
+ "node": ">= 0.8.0"
1446
+ }
1447
+ },
1448
+ "node_modules/setprototypeof": {
1449
+ "version": "1.2.0",
1450
+ "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz",
1451
+ "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==",
1452
+ "license": "ISC"
1453
+ },
1454
+ "node_modules/side-channel": {
1455
+ "version": "1.1.0",
1456
+ "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz",
1457
+ "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==",
1458
+ "license": "MIT",
1459
+ "dependencies": {
1460
+ "es-errors": "^1.3.0",
1461
+ "object-inspect": "^1.13.3",
1462
+ "side-channel-list": "^1.0.0",
1463
+ "side-channel-map": "^1.0.1",
1464
+ "side-channel-weakmap": "^1.0.2"
1465
+ },
1466
+ "engines": {
1467
+ "node": ">= 0.4"
1468
+ },
1469
+ "funding": {
1470
+ "url": "https://github.com/sponsors/ljharb"
1471
+ }
1472
+ },
1473
+ "node_modules/side-channel-list": {
1474
+ "version": "1.0.0",
1475
+ "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz",
1476
+ "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==",
1477
+ "license": "MIT",
1478
+ "dependencies": {
1479
+ "es-errors": "^1.3.0",
1480
+ "object-inspect": "^1.13.3"
1481
+ },
1482
+ "engines": {
1483
+ "node": ">= 0.4"
1484
+ },
1485
+ "funding": {
1486
+ "url": "https://github.com/sponsors/ljharb"
1487
+ }
1488
+ },
1489
+ "node_modules/side-channel-map": {
1490
+ "version": "1.0.1",
1491
+ "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz",
1492
+ "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==",
1493
+ "license": "MIT",
1494
+ "dependencies": {
1495
+ "call-bound": "^1.0.2",
1496
+ "es-errors": "^1.3.0",
1497
+ "get-intrinsic": "^1.2.5",
1498
+ "object-inspect": "^1.13.3"
1499
+ },
1500
+ "engines": {
1501
+ "node": ">= 0.4"
1502
+ },
1503
+ "funding": {
1504
+ "url": "https://github.com/sponsors/ljharb"
1505
+ }
1506
+ },
1507
+ "node_modules/side-channel-weakmap": {
1508
+ "version": "1.0.2",
1509
+ "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz",
1510
+ "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==",
1511
+ "license": "MIT",
1512
+ "dependencies": {
1513
+ "call-bound": "^1.0.2",
1514
+ "es-errors": "^1.3.0",
1515
+ "get-intrinsic": "^1.2.5",
1516
+ "object-inspect": "^1.13.3",
1517
+ "side-channel-map": "^1.0.1"
1518
+ },
1519
+ "engines": {
1520
+ "node": ">= 0.4"
1521
+ },
1522
+ "funding": {
1523
+ "url": "https://github.com/sponsors/ljharb"
1524
+ }
1525
+ },
1526
+ "node_modules/simple-update-notifier": {
1527
+ "version": "2.0.0",
1528
+ "resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-2.0.0.tgz",
1529
+ "integrity": "sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w==",
1530
+ "dev": true,
1531
+ "license": "MIT",
1532
+ "dependencies": {
1533
+ "semver": "^7.5.3"
1534
+ },
1535
+ "engines": {
1536
+ "node": ">=10"
1537
+ }
1538
+ },
1539
+ "node_modules/statuses": {
1540
+ "version": "2.0.1",
1541
+ "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz",
1542
+ "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==",
1543
+ "license": "MIT",
1544
+ "engines": {
1545
+ "node": ">= 0.8"
1546
+ }
1547
+ },
1548
+ "node_modules/supports-color": {
1549
+ "version": "5.5.0",
1550
+ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz",
1551
+ "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==",
1552
+ "dev": true,
1553
+ "license": "MIT",
1554
+ "dependencies": {
1555
+ "has-flag": "^3.0.0"
1556
+ },
1557
+ "engines": {
1558
+ "node": ">=4"
1559
+ }
1560
+ },
1561
+ "node_modules/to-regex-range": {
1562
+ "version": "5.0.1",
1563
+ "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
1564
+ "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
1565
+ "dev": true,
1566
+ "license": "MIT",
1567
+ "dependencies": {
1568
+ "is-number": "^7.0.0"
1569
+ },
1570
+ "engines": {
1571
+ "node": ">=8.0"
1572
+ }
1573
+ },
1574
+ "node_modules/toidentifier": {
1575
+ "version": "1.0.1",
1576
+ "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz",
1577
+ "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==",
1578
+ "license": "MIT",
1579
+ "engines": {
1580
+ "node": ">=0.6"
1581
+ }
1582
+ },
1583
+ "node_modules/touch": {
1584
+ "version": "3.1.1",
1585
+ "resolved": "https://registry.npmjs.org/touch/-/touch-3.1.1.tgz",
1586
+ "integrity": "sha512-r0eojU4bI8MnHr8c5bNo7lJDdI2qXlWWJk6a9EAFG7vbhTjElYhBVS3/miuE0uOuoLdb8Mc/rVfsmm6eo5o9GA==",
1587
+ "dev": true,
1588
+ "license": "ISC",
1589
+ "bin": {
1590
+ "nodetouch": "bin/nodetouch.js"
1591
+ }
1592
+ },
1593
+ "node_modules/type-is": {
1594
+ "version": "1.6.18",
1595
+ "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz",
1596
+ "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==",
1597
+ "license": "MIT",
1598
+ "dependencies": {
1599
+ "media-typer": "0.3.0",
1600
+ "mime-types": "~2.1.24"
1601
+ },
1602
+ "engines": {
1603
+ "node": ">= 0.6"
1604
+ }
1605
+ },
1606
+ "node_modules/undefsafe": {
1607
+ "version": "2.0.5",
1608
+ "resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.5.tgz",
1609
+ "integrity": "sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==",
1610
+ "dev": true,
1611
+ "license": "MIT"
1612
+ },
1613
+ "node_modules/undici": {
1614
+ "version": "7.12.0",
1615
+ "resolved": "https://registry.npmjs.org/undici/-/undici-7.12.0.tgz",
1616
+ "integrity": "sha512-GrKEsc3ughskmGA9jevVlIOPMiiAHJ4OFUtaAH+NhfTUSiZ1wMPIQqQvAJUrJspFXJt3EBWgpAeoHEDVT1IBug==",
1617
+ "license": "MIT",
1618
+ "engines": {
1619
+ "node": ">=20.18.1"
1620
+ }
1621
+ },
1622
+ "node_modules/unpipe": {
1623
+ "version": "1.0.0",
1624
+ "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
1625
+ "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==",
1626
+ "license": "MIT",
1627
+ "engines": {
1628
+ "node": ">= 0.8"
1629
+ }
1630
+ },
1631
+ "node_modules/utils-merge": {
1632
+ "version": "1.0.1",
1633
+ "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz",
1634
+ "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==",
1635
+ "license": "MIT",
1636
+ "engines": {
1637
+ "node": ">= 0.4.0"
1638
+ }
1639
+ },
1640
+ "node_modules/uuid": {
1641
+ "version": "8.3.2",
1642
+ "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz",
1643
+ "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==",
1644
+ "license": "MIT",
1645
+ "bin": {
1646
+ "uuid": "dist/bin/uuid"
1647
+ }
1648
+ },
1649
+ "node_modules/vary": {
1650
+ "version": "1.1.2",
1651
+ "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
1652
+ "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==",
1653
+ "license": "MIT",
1654
+ "engines": {
1655
+ "node": ">= 0.8"
1656
+ }
1657
+ },
1658
+ "node_modules/whatwg-encoding": {
1659
+ "version": "3.1.1",
1660
+ "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz",
1661
+ "integrity": "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==",
1662
+ "license": "MIT",
1663
+ "dependencies": {
1664
+ "iconv-lite": "0.6.3"
1665
+ },
1666
+ "engines": {
1667
+ "node": ">=18"
1668
+ }
1669
+ },
1670
+ "node_modules/whatwg-mimetype": {
1671
+ "version": "4.0.0",
1672
+ "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz",
1673
+ "integrity": "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==",
1674
+ "license": "MIT",
1675
+ "engines": {
1676
+ "node": ">=18"
1677
+ }
1678
+ }
1679
+ }
1680
+ }
server/package.json ADDED
@@ -0,0 +1,29 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "transcreation-explorer-server",
3
+ "version": "1.0.0",
4
+ "description": "Server for Transcreation Explorer",
5
+ "main": "index.js",
6
+ "scripts": {
7
+ "start": "node index.js",
8
+ "dev": "nodemon index.js",
9
+ "test": "echo \"Error: no test specified\" && exit 1"
10
+ },
11
+ "keywords": [
12
+ "transcreation",
13
+ "server",
14
+ "api"
15
+ ],
16
+ "author": "",
17
+ "license": "ISC",
18
+ "dependencies": {
19
+ "axios": "^1.6.7",
20
+ "cheerio": "^1.0.0-rc.12",
21
+ "cors": "^2.8.5",
22
+ "express": "^4.18.2",
23
+ "node-cron": "^3.0.3",
24
+ "uuid": "^9.0.1"
25
+ },
26
+ "devDependencies": {
27
+ "nodemon": "^3.0.3"
28
+ }
29
+ }