Spaces:
Sleeping
Sleeping
Tristan Yu commited on
Commit ·
a54a085
1
Parent(s): 14af9d9
Fix UI issues and add example data
Browse files- Update Home page to match local version (remove unnecessary features section)
- Update Browse page with proper styling and functionality
- Update server with comprehensive example data and all endpoints
- Add missing dependencies (axios, cheerio, node-cron, uuid, dotenv)
- Fix Random page crash by providing /api/stats endpoint and example data
- client/package-lock.json +83 -80
- client/src/pages/Browse.js +117 -70
- client/src/pages/Home.js +41 -27
- server/index.js +548 -20
- server/package.json +20 -2
client/package-lock.json
CHANGED
|
@@ -1,25 +1,28 @@
|
|
| 1 |
{
|
| 2 |
"name": "transcreation-explorer-client",
|
| 3 |
-
"version": "
|
| 4 |
"lockfileVersion": 3,
|
| 5 |
"requires": true,
|
| 6 |
"packages": {
|
| 7 |
"": {
|
| 8 |
"name": "transcreation-explorer-client",
|
| 9 |
-
"version": "
|
| 10 |
"dependencies": {
|
| 11 |
"@testing-library/jest-dom": "^5.17.0",
|
| 12 |
"@testing-library/react": "^13.4.0",
|
| 13 |
"@testing-library/user-event": "^13.5.0",
|
| 14 |
-
"axios": "^1.6.
|
| 15 |
-
"framer-motion": "^10.16.
|
| 16 |
-
"lucide-react": "^0.
|
| 17 |
"react": "^18.2.0",
|
| 18 |
"react-dom": "^18.2.0",
|
| 19 |
"react-hot-toast": "^2.4.1",
|
| 20 |
-
"react-router-dom": "^6.
|
| 21 |
"react-scripts": "5.0.1",
|
| 22 |
"web-vitals": "^2.1.4"
|
|
|
|
|
|
|
|
|
|
| 23 |
}
|
| 24 |
},
|
| 25 |
"node_modules/@adobe/css-tools": {
|
|
@@ -2950,9 +2953,9 @@
|
|
| 2950 |
}
|
| 2951 |
},
|
| 2952 |
"node_modules/@jest/expect-utils": {
|
| 2953 |
-
"version": "30.0.
|
| 2954 |
-
"resolved": "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-30.0.
|
| 2955 |
-
"integrity": "sha512-
|
| 2956 |
"license": "MIT",
|
| 2957 |
"dependencies": {
|
| 2958 |
"@jest/get-type": "30.0.1"
|
|
@@ -3329,9 +3332,9 @@
|
|
| 3329 |
}
|
| 3330 |
},
|
| 3331 |
"node_modules/@jest/schemas": {
|
| 3332 |
-
"version": "30.0.
|
| 3333 |
-
"resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-30.0.
|
| 3334 |
-
"integrity": "sha512-
|
| 3335 |
"license": "MIT",
|
| 3336 |
"dependencies": {
|
| 3337 |
"@sinclair/typebox": "^0.34.0"
|
|
@@ -3538,13 +3541,13 @@
|
|
| 3538 |
}
|
| 3539 |
},
|
| 3540 |
"node_modules/@jest/types": {
|
| 3541 |
-
"version": "30.0.
|
| 3542 |
-
"resolved": "https://registry.npmjs.org/@jest/types/-/types-30.0.
|
| 3543 |
-
"integrity": "sha512-
|
| 3544 |
"license": "MIT",
|
| 3545 |
"dependencies": {
|
| 3546 |
"@jest/pattern": "30.0.1",
|
| 3547 |
-
"@jest/schemas": "30.0.
|
| 3548 |
"@types/istanbul-lib-coverage": "^2.0.6",
|
| 3549 |
"@types/istanbul-reports": "^3.0.4",
|
| 3550 |
"@types/node": "*",
|
|
@@ -4458,12 +4461,12 @@
|
|
| 4458 |
}
|
| 4459 |
},
|
| 4460 |
"node_modules/@types/jest/node_modules/pretty-format": {
|
| 4461 |
-
"version": "30.0.
|
| 4462 |
-
"resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.0.
|
| 4463 |
-
"integrity": "sha512-
|
| 4464 |
"license": "MIT",
|
| 4465 |
"dependencies": {
|
| 4466 |
-
"@jest/schemas": "30.0.
|
| 4467 |
"ansi-styles": "^5.2.0",
|
| 4468 |
"react-is": "^18.3.1"
|
| 4469 |
},
|
|
@@ -4496,9 +4499,9 @@
|
|
| 4496 |
"license": "MIT"
|
| 4497 |
},
|
| 4498 |
"node_modules/@types/node": {
|
| 4499 |
-
"version": "24.
|
| 4500 |
-
"resolved": "https://registry.npmjs.org/@types/node/-/node-24.0.
|
| 4501 |
-
"integrity": "sha512-
|
| 4502 |
"license": "MIT",
|
| 4503 |
"dependencies": {
|
| 4504 |
"undici-types": "~7.8.0"
|
|
@@ -5682,13 +5685,13 @@
|
|
| 5682 |
}
|
| 5683 |
},
|
| 5684 |
"node_modules/axios": {
|
| 5685 |
-
"version": "1.
|
| 5686 |
-
"resolved": "https://registry.npmjs.org/axios/-/axios-1.
|
| 5687 |
-
"integrity": "sha512-
|
| 5688 |
"license": "MIT",
|
| 5689 |
"dependencies": {
|
| 5690 |
"follow-redirects": "^1.15.6",
|
| 5691 |
-
"form-data": "^4.0.
|
| 5692 |
"proxy-from-env": "^1.1.0"
|
| 5693 |
}
|
| 5694 |
},
|
|
@@ -7699,9 +7702,9 @@
|
|
| 7699 |
}
|
| 7700 |
},
|
| 7701 |
"node_modules/electron-to-chromium": {
|
| 7702 |
-
"version": "1.5.
|
| 7703 |
-
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.
|
| 7704 |
-
"integrity": "sha512-
|
| 7705 |
"license": "ISC"
|
| 7706 |
},
|
| 7707 |
"node_modules/emittery": {
|
|
@@ -8704,17 +8707,17 @@
|
|
| 8704 |
}
|
| 8705 |
},
|
| 8706 |
"node_modules/expect": {
|
| 8707 |
-
"version": "30.0.
|
| 8708 |
-
"resolved": "https://registry.npmjs.org/expect/-/expect-30.0.
|
| 8709 |
-
"integrity": "sha512-
|
| 8710 |
"license": "MIT",
|
| 8711 |
"dependencies": {
|
| 8712 |
-
"@jest/expect-utils": "30.0.
|
| 8713 |
"@jest/get-type": "30.0.1",
|
| 8714 |
-
"jest-matcher-utils": "30.0.
|
| 8715 |
-
"jest-message-util": "30.0.
|
| 8716 |
-
"jest-mock": "30.0.
|
| 8717 |
-
"jest-util": "30.0.
|
| 8718 |
},
|
| 8719 |
"engines": {
|
| 8720 |
"node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0"
|
|
@@ -11320,15 +11323,15 @@
|
|
| 11320 |
}
|
| 11321 |
},
|
| 11322 |
"node_modules/jest-diff": {
|
| 11323 |
-
"version": "30.0.
|
| 11324 |
-
"resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-30.0.
|
| 11325 |
-
"integrity": "sha512-
|
| 11326 |
"license": "MIT",
|
| 11327 |
"dependencies": {
|
| 11328 |
"@jest/diff-sequences": "30.0.1",
|
| 11329 |
"@jest/get-type": "30.0.1",
|
| 11330 |
"chalk": "^4.1.2",
|
| 11331 |
-
"pretty-format": "30.0.
|
| 11332 |
},
|
| 11333 |
"engines": {
|
| 11334 |
"node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0"
|
|
@@ -11347,12 +11350,12 @@
|
|
| 11347 |
}
|
| 11348 |
},
|
| 11349 |
"node_modules/jest-diff/node_modules/pretty-format": {
|
| 11350 |
-
"version": "30.0.
|
| 11351 |
-
"resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.0.
|
| 11352 |
-
"integrity": "sha512-
|
| 11353 |
"license": "MIT",
|
| 11354 |
"dependencies": {
|
| 11355 |
-
"@jest/schemas": "30.0.
|
| 11356 |
"ansi-styles": "^5.2.0",
|
| 11357 |
"react-is": "^18.3.1"
|
| 11358 |
},
|
|
@@ -11951,15 +11954,15 @@
|
|
| 11951 |
}
|
| 11952 |
},
|
| 11953 |
"node_modules/jest-matcher-utils": {
|
| 11954 |
-
"version": "30.0.
|
| 11955 |
-
"resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-30.0.
|
| 11956 |
-
"integrity": "sha512-
|
| 11957 |
"license": "MIT",
|
| 11958 |
"dependencies": {
|
| 11959 |
"@jest/get-type": "30.0.1",
|
| 11960 |
"chalk": "^4.1.2",
|
| 11961 |
-
"jest-diff": "30.0.
|
| 11962 |
-
"pretty-format": "30.0.
|
| 11963 |
},
|
| 11964 |
"engines": {
|
| 11965 |
"node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0"
|
|
@@ -11978,12 +11981,12 @@
|
|
| 11978 |
}
|
| 11979 |
},
|
| 11980 |
"node_modules/jest-matcher-utils/node_modules/pretty-format": {
|
| 11981 |
-
"version": "30.0.
|
| 11982 |
-
"resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.0.
|
| 11983 |
-
"integrity": "sha512-
|
| 11984 |
"license": "MIT",
|
| 11985 |
"dependencies": {
|
| 11986 |
-
"@jest/schemas": "30.0.
|
| 11987 |
"ansi-styles": "^5.2.0",
|
| 11988 |
"react-is": "^18.3.1"
|
| 11989 |
},
|
|
@@ -11998,18 +12001,18 @@
|
|
| 11998 |
"license": "MIT"
|
| 11999 |
},
|
| 12000 |
"node_modules/jest-message-util": {
|
| 12001 |
-
"version": "30.0.
|
| 12002 |
-
"resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-30.0.
|
| 12003 |
-
"integrity": "sha512-
|
| 12004 |
"license": "MIT",
|
| 12005 |
"dependencies": {
|
| 12006 |
"@babel/code-frame": "^7.27.1",
|
| 12007 |
-
"@jest/types": "30.0.
|
| 12008 |
"@types/stack-utils": "^2.0.3",
|
| 12009 |
"chalk": "^4.1.2",
|
| 12010 |
"graceful-fs": "^4.2.11",
|
| 12011 |
"micromatch": "^4.0.8",
|
| 12012 |
-
"pretty-format": "30.0.
|
| 12013 |
"slash": "^3.0.0",
|
| 12014 |
"stack-utils": "^2.0.6"
|
| 12015 |
},
|
|
@@ -12030,12 +12033,12 @@
|
|
| 12030 |
}
|
| 12031 |
},
|
| 12032 |
"node_modules/jest-message-util/node_modules/pretty-format": {
|
| 12033 |
-
"version": "30.0.
|
| 12034 |
-
"resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.0.
|
| 12035 |
-
"integrity": "sha512-
|
| 12036 |
"license": "MIT",
|
| 12037 |
"dependencies": {
|
| 12038 |
-
"@jest/schemas": "30.0.
|
| 12039 |
"ansi-styles": "^5.2.0",
|
| 12040 |
"react-is": "^18.3.1"
|
| 12041 |
},
|
|
@@ -12050,14 +12053,14 @@
|
|
| 12050 |
"license": "MIT"
|
| 12051 |
},
|
| 12052 |
"node_modules/jest-mock": {
|
| 12053 |
-
"version": "30.0.
|
| 12054 |
-
"resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-30.0.
|
| 12055 |
-
"integrity": "sha512-
|
| 12056 |
"license": "MIT",
|
| 12057 |
"dependencies": {
|
| 12058 |
-
"@jest/types": "30.0.
|
| 12059 |
"@types/node": "*",
|
| 12060 |
-
"jest-util": "30.0.
|
| 12061 |
},
|
| 12062 |
"engines": {
|
| 12063 |
"node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0"
|
|
@@ -12673,12 +12676,12 @@
|
|
| 12673 |
}
|
| 12674 |
},
|
| 12675 |
"node_modules/jest-util": {
|
| 12676 |
-
"version": "30.0.
|
| 12677 |
-
"resolved": "https://registry.npmjs.org/jest-util/-/jest-util-30.0.
|
| 12678 |
-
"integrity": "sha512-
|
| 12679 |
"license": "MIT",
|
| 12680 |
"dependencies": {
|
| 12681 |
-
"@jest/types": "30.0.
|
| 12682 |
"@types/node": "*",
|
| 12683 |
"chalk": "^4.1.2",
|
| 12684 |
"ci-info": "^4.2.0",
|
|
@@ -13585,9 +13588,9 @@
|
|
| 13585 |
}
|
| 13586 |
},
|
| 13587 |
"node_modules/lucide-react": {
|
| 13588 |
-
"version": "0.
|
| 13589 |
-
"resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.
|
| 13590 |
-
"integrity": "sha512-
|
| 13591 |
"license": "ISC",
|
| 13592 |
"peerDependencies": {
|
| 13593 |
"react": "^16.5.1 || ^17.0.0 || ^18.0.0"
|
|
@@ -18822,9 +18825,9 @@
|
|
| 18822 |
}
|
| 18823 |
},
|
| 18824 |
"node_modules/typescript": {
|
| 18825 |
-
"version": "
|
| 18826 |
-
"resolved": "https://registry.npmjs.org/typescript/-/typescript-
|
| 18827 |
-
"integrity": "sha512-
|
| 18828 |
"license": "Apache-2.0",
|
| 18829 |
"peer": true,
|
| 18830 |
"bin": {
|
|
@@ -18832,7 +18835,7 @@
|
|
| 18832 |
"tsserver": "bin/tsserver"
|
| 18833 |
},
|
| 18834 |
"engines": {
|
| 18835 |
-
"node": ">=
|
| 18836 |
}
|
| 18837 |
},
|
| 18838 |
"node_modules/unbox-primitive": {
|
|
|
|
| 1 |
{
|
| 2 |
"name": "transcreation-explorer-client",
|
| 3 |
+
"version": "1.0.0",
|
| 4 |
"lockfileVersion": 3,
|
| 5 |
"requires": true,
|
| 6 |
"packages": {
|
| 7 |
"": {
|
| 8 |
"name": "transcreation-explorer-client",
|
| 9 |
+
"version": "1.0.0",
|
| 10 |
"dependencies": {
|
| 11 |
"@testing-library/jest-dom": "^5.17.0",
|
| 12 |
"@testing-library/react": "^13.4.0",
|
| 13 |
"@testing-library/user-event": "^13.5.0",
|
| 14 |
+
"axios": "^1.6.7",
|
| 15 |
+
"framer-motion": "^10.16.16",
|
| 16 |
+
"lucide-react": "^0.323.0",
|
| 17 |
"react": "^18.2.0",
|
| 18 |
"react-dom": "^18.2.0",
|
| 19 |
"react-hot-toast": "^2.4.1",
|
| 20 |
+
"react-router-dom": "^6.22.0",
|
| 21 |
"react-scripts": "5.0.1",
|
| 22 |
"web-vitals": "^2.1.4"
|
| 23 |
+
},
|
| 24 |
+
"devDependencies": {
|
| 25 |
+
"tailwindcss": "^3.4.1"
|
| 26 |
}
|
| 27 |
},
|
| 28 |
"node_modules/@adobe/css-tools": {
|
|
|
|
| 2953 |
}
|
| 2954 |
},
|
| 2955 |
"node_modules/@jest/expect-utils": {
|
| 2956 |
+
"version": "30.0.5",
|
| 2957 |
+
"resolved": "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-30.0.5.tgz",
|
| 2958 |
+
"integrity": "sha512-F3lmTT7CXWYywoVUGTCmom0vXq3HTTkaZyTAzIy+bXSBizB7o5qzlC9VCtq0arOa8GqmNsbg/cE9C6HLn7Szew==",
|
| 2959 |
"license": "MIT",
|
| 2960 |
"dependencies": {
|
| 2961 |
"@jest/get-type": "30.0.1"
|
|
|
|
| 3332 |
}
|
| 3333 |
},
|
| 3334 |
"node_modules/@jest/schemas": {
|
| 3335 |
+
"version": "30.0.5",
|
| 3336 |
+
"resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-30.0.5.tgz",
|
| 3337 |
+
"integrity": "sha512-DmdYgtezMkh3cpU8/1uyXakv3tJRcmcXxBOcO0tbaozPwpmh4YMsnWrQm9ZmZMfa5ocbxzbFk6O4bDPEc/iAnA==",
|
| 3338 |
"license": "MIT",
|
| 3339 |
"dependencies": {
|
| 3340 |
"@sinclair/typebox": "^0.34.0"
|
|
|
|
| 3541 |
}
|
| 3542 |
},
|
| 3543 |
"node_modules/@jest/types": {
|
| 3544 |
+
"version": "30.0.5",
|
| 3545 |
+
"resolved": "https://registry.npmjs.org/@jest/types/-/types-30.0.5.tgz",
|
| 3546 |
+
"integrity": "sha512-aREYa3aku9SSnea4aX6bhKn4bgv3AXkgijoQgbYV3yvbiGt6z+MQ85+6mIhx9DsKW2BuB/cLR/A+tcMThx+KLQ==",
|
| 3547 |
"license": "MIT",
|
| 3548 |
"dependencies": {
|
| 3549 |
"@jest/pattern": "30.0.1",
|
| 3550 |
+
"@jest/schemas": "30.0.5",
|
| 3551 |
"@types/istanbul-lib-coverage": "^2.0.6",
|
| 3552 |
"@types/istanbul-reports": "^3.0.4",
|
| 3553 |
"@types/node": "*",
|
|
|
|
| 4461 |
}
|
| 4462 |
},
|
| 4463 |
"node_modules/@types/jest/node_modules/pretty-format": {
|
| 4464 |
+
"version": "30.0.5",
|
| 4465 |
+
"resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.0.5.tgz",
|
| 4466 |
+
"integrity": "sha512-D1tKtYvByrBkFLe2wHJl2bwMJIiT8rW+XA+TiataH79/FszLQMrpGEvzUVkzPau7OCO0Qnrhpe87PqtOAIB8Yw==",
|
| 4467 |
"license": "MIT",
|
| 4468 |
"dependencies": {
|
| 4469 |
+
"@jest/schemas": "30.0.5",
|
| 4470 |
"ansi-styles": "^5.2.0",
|
| 4471 |
"react-is": "^18.3.1"
|
| 4472 |
},
|
|
|
|
| 4499 |
"license": "MIT"
|
| 4500 |
},
|
| 4501 |
"node_modules/@types/node": {
|
| 4502 |
+
"version": "24.1.0",
|
| 4503 |
+
"resolved": "https://registry.npmjs.org/@types/node/-/node-24.1.0.tgz",
|
| 4504 |
+
"integrity": "sha512-ut5FthK5moxFKH2T1CUOC6ctR67rQRvvHdFLCD2Ql6KXmMuCrjsSsRI9UsLCm9M18BMwClv4pn327UvB7eeO1w==",
|
| 4505 |
"license": "MIT",
|
| 4506 |
"dependencies": {
|
| 4507 |
"undici-types": "~7.8.0"
|
|
|
|
| 5685 |
}
|
| 5686 |
},
|
| 5687 |
"node_modules/axios": {
|
| 5688 |
+
"version": "1.11.0",
|
| 5689 |
+
"resolved": "https://registry.npmjs.org/axios/-/axios-1.11.0.tgz",
|
| 5690 |
+
"integrity": "sha512-1Lx3WLFQWm3ooKDYZD1eXmoGO9fxYQjrycfHFC8P0sCfQVXyROp0p9PFWBehewBOdCwHc+f/b8I0fMto5eSfwA==",
|
| 5691 |
"license": "MIT",
|
| 5692 |
"dependencies": {
|
| 5693 |
"follow-redirects": "^1.15.6",
|
| 5694 |
+
"form-data": "^4.0.4",
|
| 5695 |
"proxy-from-env": "^1.1.0"
|
| 5696 |
}
|
| 5697 |
},
|
|
|
|
| 7702 |
}
|
| 7703 |
},
|
| 7704 |
"node_modules/electron-to-chromium": {
|
| 7705 |
+
"version": "1.5.190",
|
| 7706 |
+
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.190.tgz",
|
| 7707 |
+
"integrity": "sha512-k4McmnB2091YIsdCgkS0fMVMPOJgxl93ltFzaryXqwip1AaxeDqKCGLxkXODDA5Ab/D+tV5EL5+aTx76RvLRxw==",
|
| 7708 |
"license": "ISC"
|
| 7709 |
},
|
| 7710 |
"node_modules/emittery": {
|
|
|
|
| 8707 |
}
|
| 8708 |
},
|
| 8709 |
"node_modules/expect": {
|
| 8710 |
+
"version": "30.0.5",
|
| 8711 |
+
"resolved": "https://registry.npmjs.org/expect/-/expect-30.0.5.tgz",
|
| 8712 |
+
"integrity": "sha512-P0te2pt+hHI5qLJkIR+iMvS+lYUZml8rKKsohVHAGY+uClp9XVbdyYNJOIjSRpHVp8s8YqxJCiHUkSYZGr8rtQ==",
|
| 8713 |
"license": "MIT",
|
| 8714 |
"dependencies": {
|
| 8715 |
+
"@jest/expect-utils": "30.0.5",
|
| 8716 |
"@jest/get-type": "30.0.1",
|
| 8717 |
+
"jest-matcher-utils": "30.0.5",
|
| 8718 |
+
"jest-message-util": "30.0.5",
|
| 8719 |
+
"jest-mock": "30.0.5",
|
| 8720 |
+
"jest-util": "30.0.5"
|
| 8721 |
},
|
| 8722 |
"engines": {
|
| 8723 |
"node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0"
|
|
|
|
| 11323 |
}
|
| 11324 |
},
|
| 11325 |
"node_modules/jest-diff": {
|
| 11326 |
+
"version": "30.0.5",
|
| 11327 |
+
"resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-30.0.5.tgz",
|
| 11328 |
+
"integrity": "sha512-1UIqE9PoEKaHcIKvq2vbibrCog4Y8G0zmOxgQUVEiTqwR5hJVMCoDsN1vFvI5JvwD37hjueZ1C4l2FyGnfpE0A==",
|
| 11329 |
"license": "MIT",
|
| 11330 |
"dependencies": {
|
| 11331 |
"@jest/diff-sequences": "30.0.1",
|
| 11332 |
"@jest/get-type": "30.0.1",
|
| 11333 |
"chalk": "^4.1.2",
|
| 11334 |
+
"pretty-format": "30.0.5"
|
| 11335 |
},
|
| 11336 |
"engines": {
|
| 11337 |
"node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0"
|
|
|
|
| 11350 |
}
|
| 11351 |
},
|
| 11352 |
"node_modules/jest-diff/node_modules/pretty-format": {
|
| 11353 |
+
"version": "30.0.5",
|
| 11354 |
+
"resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.0.5.tgz",
|
| 11355 |
+
"integrity": "sha512-D1tKtYvByrBkFLe2wHJl2bwMJIiT8rW+XA+TiataH79/FszLQMrpGEvzUVkzPau7OCO0Qnrhpe87PqtOAIB8Yw==",
|
| 11356 |
"license": "MIT",
|
| 11357 |
"dependencies": {
|
| 11358 |
+
"@jest/schemas": "30.0.5",
|
| 11359 |
"ansi-styles": "^5.2.0",
|
| 11360 |
"react-is": "^18.3.1"
|
| 11361 |
},
|
|
|
|
| 11954 |
}
|
| 11955 |
},
|
| 11956 |
"node_modules/jest-matcher-utils": {
|
| 11957 |
+
"version": "30.0.5",
|
| 11958 |
+
"resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-30.0.5.tgz",
|
| 11959 |
+
"integrity": "sha512-uQgGWt7GOrRLP1P7IwNWwK1WAQbq+m//ZY0yXygyfWp0rJlksMSLQAA4wYQC3b6wl3zfnchyTx+k3HZ5aPtCbQ==",
|
| 11960 |
"license": "MIT",
|
| 11961 |
"dependencies": {
|
| 11962 |
"@jest/get-type": "30.0.1",
|
| 11963 |
"chalk": "^4.1.2",
|
| 11964 |
+
"jest-diff": "30.0.5",
|
| 11965 |
+
"pretty-format": "30.0.5"
|
| 11966 |
},
|
| 11967 |
"engines": {
|
| 11968 |
"node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0"
|
|
|
|
| 11981 |
}
|
| 11982 |
},
|
| 11983 |
"node_modules/jest-matcher-utils/node_modules/pretty-format": {
|
| 11984 |
+
"version": "30.0.5",
|
| 11985 |
+
"resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.0.5.tgz",
|
| 11986 |
+
"integrity": "sha512-D1tKtYvByrBkFLe2wHJl2bwMJIiT8rW+XA+TiataH79/FszLQMrpGEvzUVkzPau7OCO0Qnrhpe87PqtOAIB8Yw==",
|
| 11987 |
"license": "MIT",
|
| 11988 |
"dependencies": {
|
| 11989 |
+
"@jest/schemas": "30.0.5",
|
| 11990 |
"ansi-styles": "^5.2.0",
|
| 11991 |
"react-is": "^18.3.1"
|
| 11992 |
},
|
|
|
|
| 12001 |
"license": "MIT"
|
| 12002 |
},
|
| 12003 |
"node_modules/jest-message-util": {
|
| 12004 |
+
"version": "30.0.5",
|
| 12005 |
+
"resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-30.0.5.tgz",
|
| 12006 |
+
"integrity": "sha512-NAiDOhsK3V7RU0Aa/HnrQo+E4JlbarbmI3q6Pi4KcxicdtjV82gcIUrejOtczChtVQR4kddu1E1EJlW6EN9IyA==",
|
| 12007 |
"license": "MIT",
|
| 12008 |
"dependencies": {
|
| 12009 |
"@babel/code-frame": "^7.27.1",
|
| 12010 |
+
"@jest/types": "30.0.5",
|
| 12011 |
"@types/stack-utils": "^2.0.3",
|
| 12012 |
"chalk": "^4.1.2",
|
| 12013 |
"graceful-fs": "^4.2.11",
|
| 12014 |
"micromatch": "^4.0.8",
|
| 12015 |
+
"pretty-format": "30.0.5",
|
| 12016 |
"slash": "^3.0.0",
|
| 12017 |
"stack-utils": "^2.0.6"
|
| 12018 |
},
|
|
|
|
| 12033 |
}
|
| 12034 |
},
|
| 12035 |
"node_modules/jest-message-util/node_modules/pretty-format": {
|
| 12036 |
+
"version": "30.0.5",
|
| 12037 |
+
"resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.0.5.tgz",
|
| 12038 |
+
"integrity": "sha512-D1tKtYvByrBkFLe2wHJl2bwMJIiT8rW+XA+TiataH79/FszLQMrpGEvzUVkzPau7OCO0Qnrhpe87PqtOAIB8Yw==",
|
| 12039 |
"license": "MIT",
|
| 12040 |
"dependencies": {
|
| 12041 |
+
"@jest/schemas": "30.0.5",
|
| 12042 |
"ansi-styles": "^5.2.0",
|
| 12043 |
"react-is": "^18.3.1"
|
| 12044 |
},
|
|
|
|
| 12053 |
"license": "MIT"
|
| 12054 |
},
|
| 12055 |
"node_modules/jest-mock": {
|
| 12056 |
+
"version": "30.0.5",
|
| 12057 |
+
"resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-30.0.5.tgz",
|
| 12058 |
+
"integrity": "sha512-Od7TyasAAQX/6S+QCbN6vZoWOMwlTtzzGuxJku1GhGanAjz9y+QsQkpScDmETvdc9aSXyJ/Op4rhpMYBWW91wQ==",
|
| 12059 |
"license": "MIT",
|
| 12060 |
"dependencies": {
|
| 12061 |
+
"@jest/types": "30.0.5",
|
| 12062 |
"@types/node": "*",
|
| 12063 |
+
"jest-util": "30.0.5"
|
| 12064 |
},
|
| 12065 |
"engines": {
|
| 12066 |
"node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0"
|
|
|
|
| 12676 |
}
|
| 12677 |
},
|
| 12678 |
"node_modules/jest-util": {
|
| 12679 |
+
"version": "30.0.5",
|
| 12680 |
+
"resolved": "https://registry.npmjs.org/jest-util/-/jest-util-30.0.5.tgz",
|
| 12681 |
+
"integrity": "sha512-pvyPWssDZR0FlfMxCBoc0tvM8iUEskaRFALUtGQYzVEAqisAztmy+R8LnU14KT4XA0H/a5HMVTXat1jLne010g==",
|
| 12682 |
"license": "MIT",
|
| 12683 |
"dependencies": {
|
| 12684 |
+
"@jest/types": "30.0.5",
|
| 12685 |
"@types/node": "*",
|
| 12686 |
"chalk": "^4.1.2",
|
| 12687 |
"ci-info": "^4.2.0",
|
|
|
|
| 13588 |
}
|
| 13589 |
},
|
| 13590 |
"node_modules/lucide-react": {
|
| 13591 |
+
"version": "0.323.0",
|
| 13592 |
+
"resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.323.0.tgz",
|
| 13593 |
+
"integrity": "sha512-rTXZFILl2Y4d1SG9p1Mdcf17AcPvPvpc/egFIzUrp7IUy60MUQo3Oi1mu8LGYXUVwuRZYsSMt3csHRW5mAovJg==",
|
| 13594 |
"license": "ISC",
|
| 13595 |
"peerDependencies": {
|
| 13596 |
"react": "^16.5.1 || ^17.0.0 || ^18.0.0"
|
|
|
|
| 18825 |
}
|
| 18826 |
},
|
| 18827 |
"node_modules/typescript": {
|
| 18828 |
+
"version": "5.8.3",
|
| 18829 |
+
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz",
|
| 18830 |
+
"integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==",
|
| 18831 |
"license": "Apache-2.0",
|
| 18832 |
"peer": true,
|
| 18833 |
"bin": {
|
|
|
|
| 18835 |
"tsserver": "bin/tsserver"
|
| 18836 |
},
|
| 18837 |
"engines": {
|
| 18838 |
+
"node": ">=14.17"
|
| 18839 |
}
|
| 18840 |
},
|
| 18841 |
"node_modules/unbox-primitive": {
|
client/src/pages/Browse.js
CHANGED
|
@@ -1,35 +1,36 @@
|
|
| 1 |
import React, { useState, useEffect } from 'react';
|
|
|
|
| 2 |
import axios from 'axios';
|
|
|
|
| 3 |
import ExampleCard from '../components/ExampleCard';
|
| 4 |
-
import
|
| 5 |
|
| 6 |
const Browse = () => {
|
| 7 |
const [examples, setExamples] = useState([]);
|
| 8 |
const [filteredExamples, setFilteredExamples] = useState([]);
|
| 9 |
const [categories, setCategories] = useState([]);
|
|
|
|
| 10 |
const [selectedCategory, setSelectedCategory] = useState('');
|
| 11 |
-
const [isLoading, setIsLoading] = useState(true);
|
| 12 |
-
const [searchTerm, setSearchTerm] = useState('');
|
| 13 |
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
|
| 20 |
-
|
| 21 |
-
|
| 22 |
-
|
| 23 |
-
|
| 24 |
-
|
| 25 |
-
|
| 26 |
-
|
| 27 |
-
|
| 28 |
-
|
| 29 |
-
|
| 30 |
-
|
| 31 |
-
};
|
| 32 |
|
|
|
|
| 33 |
fetchData();
|
| 34 |
}, []);
|
| 35 |
|
|
@@ -37,52 +38,56 @@ const Browse = () => {
|
|
| 37 |
let filtered = examples;
|
| 38 |
|
| 39 |
if (selectedCategory) {
|
| 40 |
-
filtered = filtered.filter(example =>
|
| 41 |
-
example.category === selectedCategory
|
| 42 |
-
);
|
| 43 |
-
}
|
| 44 |
-
|
| 45 |
-
if (searchTerm) {
|
| 46 |
filtered = filtered.filter(example =>
|
| 47 |
-
example.
|
| 48 |
-
example.mainland.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
| 49 |
-
(example.taiwan && example.taiwan.toLowerCase().includes(searchTerm.toLowerCase())) ||
|
| 50 |
-
example.brand.toLowerCase().includes(searchTerm.toLowerCase())
|
| 51 |
);
|
| 52 |
}
|
| 53 |
|
| 54 |
setFilteredExamples(filtered);
|
| 55 |
-
}, [examples, selectedCategory
|
| 56 |
|
| 57 |
-
|
| 58 |
-
|
| 59 |
-
}
|
| 60 |
|
| 61 |
-
|
| 62 |
-
|
| 63 |
-
|
| 64 |
-
|
| 65 |
-
|
|
|
|
|
|
|
|
|
|
| 66 |
</div>
|
|
|
|
|
|
|
| 67 |
|
| 68 |
-
|
| 69 |
-
|
| 70 |
-
|
| 71 |
-
|
| 72 |
-
|
| 73 |
-
|
| 74 |
-
|
| 75 |
-
|
| 76 |
-
|
| 77 |
-
|
| 78 |
-
</
|
|
|
|
|
|
|
|
|
|
|
|
|
| 79 |
|
| 80 |
-
<div className="
|
| 81 |
-
<
|
|
|
|
|
|
|
|
|
|
|
|
|
| 82 |
<select
|
| 83 |
-
|
| 84 |
value={selectedCategory}
|
| 85 |
-
onChange={
|
| 86 |
>
|
| 87 |
<option value="">All Categories</option>
|
| 88 |
{categories.map(category => (
|
|
@@ -91,25 +96,67 @@ const Browse = () => {
|
|
| 91 |
</option>
|
| 92 |
))}
|
| 93 |
</select>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 94 |
</div>
|
| 95 |
-
</div>
|
| 96 |
|
| 97 |
-
|
| 98 |
-
|
| 99 |
-
|
| 100 |
-
<p>No examples found matching your criteria.</p>
|
| 101 |
-
</div>
|
| 102 |
-
) : (
|
| 103 |
-
filteredExamples.map(example => (
|
| 104 |
-
<ExampleCard key={example.id} example={example} />
|
| 105 |
-
))
|
| 106 |
-
)}
|
| 107 |
</div>
|
| 108 |
|
| 109 |
-
|
| 110 |
-
|
| 111 |
-
|
| 112 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 113 |
);
|
| 114 |
};
|
| 115 |
|
|
|
|
| 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 |
|
|
|
|
| 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 => (
|
|
|
|
| 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 |
|
client/src/pages/Home.js
CHANGED
|
@@ -1,44 +1,58 @@
|
|
| 1 |
import React from 'react';
|
| 2 |
import { Link } from 'react-router-dom';
|
|
|
|
|
|
|
| 3 |
|
| 4 |
const Home = () => {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 5 |
return (
|
| 6 |
-
<div
|
|
|
|
|
|
|
|
|
|
|
|
|
| 7 |
<section className="hero">
|
| 8 |
-
<h1
|
| 9 |
-
|
|
|
|
|
|
|
| 10 |
Discover the art of transcreation through real-world examples of English-Chinese
|
| 11 |
marketing adaptations. See how global brands craft different messages for
|
| 12 |
mainland China and Taiwan markets.
|
| 13 |
-
</p>
|
| 14 |
|
| 15 |
-
<div className="hero-actions">
|
| 16 |
<Link to="/random" className="btn btn-large">
|
| 17 |
-
|
|
|
|
| 18 |
</Link>
|
| 19 |
-
<Link to="/browse" className="btn btn-
|
| 20 |
-
|
|
|
|
| 21 |
</Link>
|
| 22 |
-
</div>
|
| 23 |
-
</section>
|
| 24 |
-
|
| 25 |
-
<section className="features">
|
| 26 |
-
<div className="feature-grid">
|
| 27 |
-
<div className="feature-card">
|
| 28 |
-
<h3>Real Examples</h3>
|
| 29 |
-
<p>Curated collection of actual brand transcreations</p>
|
| 30 |
-
</div>
|
| 31 |
-
<div className="feature-card">
|
| 32 |
-
<h3>Cultural Context</h3>
|
| 33 |
-
<p>Understanding why adaptations were made</p>
|
| 34 |
-
</div>
|
| 35 |
-
<div className="feature-card">
|
| 36 |
-
<h3>Regional Differences</h3>
|
| 37 |
-
<p>See how mainland China and Taiwan differ</p>
|
| 38 |
-
</div>
|
| 39 |
-
</div>
|
| 40 |
</section>
|
| 41 |
-
</div>
|
| 42 |
);
|
| 43 |
};
|
| 44 |
|
|
|
|
| 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 |
|
server/index.js
CHANGED
|
@@ -1,31 +1,25 @@
|
|
| 1 |
const express = require('express');
|
| 2 |
const cors = require('cors');
|
|
|
|
|
|
|
| 3 |
const path = require('path');
|
| 4 |
const fs = require('fs').promises;
|
|
|
|
|
|
|
|
|
|
| 5 |
|
| 6 |
const app = express();
|
| 7 |
const PORT = process.env.PORT || 7860;
|
| 8 |
|
| 9 |
-
console.log('🚀 Starting Transcreation Explorer Server...');
|
| 10 |
-
console.log('📍 Environment:', process.env.NODE_ENV);
|
| 11 |
-
console.log('🔌 Port:', PORT);
|
| 12 |
-
|
| 13 |
app.use(cors());
|
| 14 |
app.use(express.json());
|
| 15 |
|
| 16 |
// Serve static files from React build in production
|
| 17 |
if (process.env.NODE_ENV === 'production') {
|
| 18 |
-
|
| 19 |
-
console.log('📁 Serving static files from:', buildPath);
|
| 20 |
-
app.use(express.static(buildPath));
|
| 21 |
}
|
| 22 |
|
| 23 |
-
//
|
| 24 |
-
app.get('/test', (req, res) => {
|
| 25 |
-
res.json({ message: 'Transcreation Explorer API is working!', timestamp: new Date().toISOString() });
|
| 26 |
-
});
|
| 27 |
-
|
| 28 |
-
// In-memory storage for examples
|
| 29 |
let transcreationExamples = [];
|
| 30 |
|
| 31 |
// Load cached examples from file on startup
|
|
@@ -43,30 +37,300 @@ const loadCachedExamples = async () => {
|
|
| 43 |
// Save examples to file
|
| 44 |
const saveCachedExamples = async () => {
|
| 45 |
try {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 46 |
await fs.writeFile(
|
| 47 |
path.join(__dirname, 'cached-examples.json'),
|
| 48 |
-
JSON.stringify(
|
| 49 |
);
|
| 50 |
-
console.log(`💾 Saved ${
|
| 51 |
} catch (error) {
|
| 52 |
console.error('❌ Failed to save examples:', error);
|
| 53 |
}
|
| 54 |
};
|
| 55 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 56 |
// Initialize cache on startup
|
| 57 |
loadCachedExamples();
|
| 58 |
|
| 59 |
// API Routes
|
|
|
|
|
|
|
| 60 |
app.get('/api/examples', (req, res) => {
|
| 61 |
-
const { category } = req.query;
|
| 62 |
let examples = [...transcreationExamples];
|
| 63 |
|
|
|
|
| 64 |
if (category) {
|
| 65 |
examples = examples.filter(ex =>
|
| 66 |
ex.category.toLowerCase().includes(category.toLowerCase())
|
| 67 |
);
|
| 68 |
}
|
| 69 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 70 |
res.json({
|
| 71 |
success: true,
|
| 72 |
data: examples,
|
|
@@ -74,13 +338,20 @@ app.get('/api/examples', (req, res) => {
|
|
| 74 |
});
|
| 75 |
});
|
| 76 |
|
|
|
|
| 77 |
app.get('/api/examples/random', async (req, res) => {
|
| 78 |
try {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 79 |
if (transcreationExamples.length === 0) {
|
| 80 |
return res.json({
|
| 81 |
success: true,
|
| 82 |
data: null,
|
| 83 |
-
message: 'No examples available yet.'
|
| 84 |
});
|
| 85 |
}
|
| 86 |
|
|
@@ -99,14 +370,67 @@ app.get('/api/examples/random', async (req, res) => {
|
|
| 99 |
}
|
| 100 |
});
|
| 101 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 102 |
app.get('/api/categories', (req, res) => {
|
| 103 |
const categories = [...new Set(transcreationExamples.map(ex => ex.category))];
|
|
|
|
| 104 |
res.json({
|
| 105 |
success: true,
|
| 106 |
data: categories
|
| 107 |
});
|
| 108 |
});
|
| 109 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 110 |
app.get('/api/search', (req, res) => {
|
| 111 |
const { q } = req.query;
|
| 112 |
|
|
@@ -135,6 +459,23 @@ app.get('/api/search', (req, res) => {
|
|
| 135 |
});
|
| 136 |
});
|
| 137 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 138 |
app.get('/api/health', (req, res) => {
|
| 139 |
res.json({
|
| 140 |
success: true,
|
|
@@ -144,14 +485,201 @@ app.get('/api/health', (req, res) => {
|
|
| 144 |
});
|
| 145 |
});
|
| 146 |
|
| 147 |
-
//
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 148 |
if (process.env.NODE_ENV === 'production') {
|
|
|
|
|
|
|
| 149 |
app.get('*', (req, res) => {
|
| 150 |
-
res.sendFile(path.join(__dirname, '../client/build
|
| 151 |
});
|
| 152 |
}
|
| 153 |
|
| 154 |
app.listen(PORT, () => {
|
| 155 |
console.log(`🚀 Transcreation Explorer API running on port ${PORT}`);
|
| 156 |
console.log(`📊 Cached examples: ${transcreationExamples.length}`);
|
| 157 |
-
});
|
|
|
|
|
|
|
|
|
| 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 || 7860;
|
| 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
|
|
|
|
| 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: 'Financial Services',
|
| 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: 'Retail',
|
| 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: 'Technology',
|
| 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: 'Food & Beverage',
|
| 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: 'Automotive',
|
| 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: 'Automotive',
|
| 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: 'Beauty & Cosmetics',
|
| 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 |
+
// Technology
|
| 229 |
+
{
|
| 230 |
+
english: 'Think Different',
|
| 231 |
+
mainland: '非同凡想',
|
| 232 |
+
taiwan: '不同凡想',
|
| 233 |
+
brand: 'Apple',
|
| 234 |
+
category: 'Technology',
|
| 235 |
+
description: 'Apple\'s famous campaign with slight variations - mainland uses more contemporary language, Taiwan preserves traditional elements.',
|
| 236 |
+
type: 'slogan',
|
| 237 |
+
culturalNote: 'Both versions maintain the creative rebellion message but adapt to local linguistic preferences.'
|
| 238 |
+
},
|
| 239 |
+
{
|
| 240 |
+
english: 'Connecting People',
|
| 241 |
+
mainland: '科技以人为本',
|
| 242 |
+
taiwan: '串聯你我',
|
| 243 |
+
brand: 'Nokia',
|
| 244 |
+
category: 'Technology',
|
| 245 |
+
description: 'Nokia\'s brand promise adapted to different cultural values about technology\'s role in society.',
|
| 246 |
+
type: 'slogan',
|
| 247 |
+
culturalNote: 'Mainland emphasizes human-centered technology philosophy, Taiwan emphasizes personal connection.'
|
| 248 |
+
},
|
| 249 |
+
{
|
| 250 |
+
english: 'Just Do It',
|
| 251 |
+
mainland: '想做���做',
|
| 252 |
+
taiwan: '做就對了',
|
| 253 |
+
brand: 'Nike',
|
| 254 |
+
category: 'Sports & Lifestyle',
|
| 255 |
+
description: 'Nike\'s motivational slogan adapted to resonate with different cultural attitudes toward action and decision-making.',
|
| 256 |
+
type: 'slogan',
|
| 257 |
+
culturalNote: 'Mainland version emphasizes desire and action, Taiwan version emphasizes confidence and correctness.'
|
| 258 |
+
},
|
| 259 |
+
{
|
| 260 |
+
english: 'Because You\'re Worth It',
|
| 261 |
+
mainland: '你值得拥有',
|
| 262 |
+
taiwan: '因為你值得',
|
| 263 |
+
brand: 'L\'Oréal',
|
| 264 |
+
category: 'Beauty & Cosmetics',
|
| 265 |
+
description: 'L\'Oréal\'s empowerment message adapted for different concepts of self-worth and beauty standards.',
|
| 266 |
+
type: 'slogan',
|
| 267 |
+
culturalNote: 'Mainland focuses on possession/ownership, Taiwan emphasizes inherent worthiness.'
|
| 268 |
+
},
|
| 269 |
+
{
|
| 270 |
+
english: 'The Ultimate Driving Machine',
|
| 271 |
+
mainland: '终极驾驶机器',
|
| 272 |
+
taiwan: '終極駕馭樂趣',
|
| 273 |
+
brand: 'BMW',
|
| 274 |
+
category: 'Automotive',
|
| 275 |
+
description: 'BMW\'s precision-focused tagline adapted to emphasize different aspects of the driving experience.',
|
| 276 |
+
type: 'slogan',
|
| 277 |
+
culturalNote: 'Mainland emphasizes mechanical excellence, Taiwan emphasizes the joy and control of driving.'
|
| 278 |
+
},
|
| 279 |
+
{
|
| 280 |
+
english: 'The World\'s Local Bank',
|
| 281 |
+
mainland: '环球金融,地方智慧',
|
| 282 |
+
taiwan: '環球金融,地方智慧',
|
| 283 |
+
brand: 'HSBC',
|
| 284 |
+
category: 'Financial Services',
|
| 285 |
+
description: 'HSBC\'s global-local positioning adapted for different markets.',
|
| 286 |
+
type: 'slogan',
|
| 287 |
+
culturalNote: 'Different approaches to expressing the global-local balance.'
|
| 288 |
+
}
|
| 289 |
+
];
|
| 290 |
+
|
| 291 |
+
// Filter by category if specified
|
| 292 |
+
let results = searchResults;
|
| 293 |
+
if (category) {
|
| 294 |
+
results = searchResults.filter(ex =>
|
| 295 |
+
ex.category.toLowerCase().includes(category.toLowerCase())
|
| 296 |
+
);
|
| 297 |
+
}
|
| 298 |
+
|
| 299 |
+
// Simulate network delay
|
| 300 |
+
await new Promise(resolve => setTimeout(resolve, 1500 + Math.random() * 2000));
|
| 301 |
+
|
| 302 |
+
// Return random subset
|
| 303 |
+
const shuffled = results.sort(() => Math.random() - 0.5);
|
| 304 |
+
return shuffled.slice(0, Math.min(maxResults, shuffled.length));
|
| 305 |
+
};
|
| 306 |
+
|
| 307 |
// Initialize cache on startup
|
| 308 |
loadCachedExamples();
|
| 309 |
|
| 310 |
// API Routes
|
| 311 |
+
|
| 312 |
+
// Get all examples
|
| 313 |
app.get('/api/examples', (req, res) => {
|
| 314 |
+
const { category, type, random } = req.query;
|
| 315 |
let examples = [...transcreationExamples];
|
| 316 |
|
| 317 |
+
// Filter by category
|
| 318 |
if (category) {
|
| 319 |
examples = examples.filter(ex =>
|
| 320 |
ex.category.toLowerCase().includes(category.toLowerCase())
|
| 321 |
);
|
| 322 |
}
|
| 323 |
|
| 324 |
+
// Filter by type
|
| 325 |
+
if (type) {
|
| 326 |
+
examples = examples.filter(ex => ex.type === type);
|
| 327 |
+
}
|
| 328 |
+
|
| 329 |
+
// Randomize if requested
|
| 330 |
+
if (random === 'true') {
|
| 331 |
+
examples = examples.sort(() => Math.random() - 0.5);
|
| 332 |
+
}
|
| 333 |
+
|
| 334 |
res.json({
|
| 335 |
success: true,
|
| 336 |
data: examples,
|
|
|
|
| 338 |
});
|
| 339 |
});
|
| 340 |
|
| 341 |
+
// Get random example (or search for new one if cache is low)
|
| 342 |
app.get('/api/examples/random', async (req, res) => {
|
| 343 |
try {
|
| 344 |
+
// If we have few examples, try to find more
|
| 345 |
+
if (transcreationExamples.length < 5) {
|
| 346 |
+
console.log('🔍 Cache low, searching for new examples...');
|
| 347 |
+
await searchTranscreationExamples('', 10);
|
| 348 |
+
}
|
| 349 |
+
|
| 350 |
if (transcreationExamples.length === 0) {
|
| 351 |
return res.json({
|
| 352 |
success: true,
|
| 353 |
data: null,
|
| 354 |
+
message: 'No examples available yet. Try searching to discover new ones!'
|
| 355 |
});
|
| 356 |
}
|
| 357 |
|
|
|
|
| 370 |
}
|
| 371 |
});
|
| 372 |
|
| 373 |
+
// Search for new examples online
|
| 374 |
+
app.post('/api/examples/search-online', async (req, res) => {
|
| 375 |
+
try {
|
| 376 |
+
const { category } = req.body;
|
| 377 |
+
console.log(`🔍 Online search requested for category: ${category || 'all'}`);
|
| 378 |
+
|
| 379 |
+
const newExamples = await searchTranscreationExamples(category, 5);
|
| 380 |
+
|
| 381 |
+
res.json({
|
| 382 |
+
success: true,
|
| 383 |
+
data: newExamples,
|
| 384 |
+
message: `Found ${newExamples.length} new example${newExamples.length !== 1 ? 's' : ''}`,
|
| 385 |
+
totalCached: transcreationExamples.length
|
| 386 |
+
});
|
| 387 |
+
} catch (error) {
|
| 388 |
+
console.error('Search error:', error);
|
| 389 |
+
res.status(500).json({
|
| 390 |
+
success: false,
|
| 391 |
+
error: 'Online search failed'
|
| 392 |
+
});
|
| 393 |
+
}
|
| 394 |
+
});
|
| 395 |
+
|
| 396 |
+
// Get example by ID
|
| 397 |
+
app.get('/api/examples/:id', (req, res) => {
|
| 398 |
+
const example = transcreationExamples.find(ex => ex.id === req.params.id);
|
| 399 |
+
|
| 400 |
+
if (!example) {
|
| 401 |
+
return res.status(404).json({
|
| 402 |
+
success: false,
|
| 403 |
+
error: 'Example not found'
|
| 404 |
+
});
|
| 405 |
+
}
|
| 406 |
+
|
| 407 |
+
res.json({
|
| 408 |
+
success: true,
|
| 409 |
+
data: example
|
| 410 |
+
});
|
| 411 |
+
});
|
| 412 |
+
|
| 413 |
+
// Get categories
|
| 414 |
app.get('/api/categories', (req, res) => {
|
| 415 |
const categories = [...new Set(transcreationExamples.map(ex => ex.category))];
|
| 416 |
+
|
| 417 |
res.json({
|
| 418 |
success: true,
|
| 419 |
data: categories
|
| 420 |
});
|
| 421 |
});
|
| 422 |
|
| 423 |
+
// Get types
|
| 424 |
+
app.get('/api/types', (req, res) => {
|
| 425 |
+
const types = [...new Set(transcreationExamples.map(ex => ex.type))];
|
| 426 |
+
|
| 427 |
+
res.json({
|
| 428 |
+
success: true,
|
| 429 |
+
data: types
|
| 430 |
+
});
|
| 431 |
+
});
|
| 432 |
+
|
| 433 |
+
// Search examples
|
| 434 |
app.get('/api/search', (req, res) => {
|
| 435 |
const { q } = req.query;
|
| 436 |
|
|
|
|
| 459 |
});
|
| 460 |
});
|
| 461 |
|
| 462 |
+
// Get database stats
|
| 463 |
+
app.get('/api/stats', (req, res) => {
|
| 464 |
+
const stats = {
|
| 465 |
+
totalExamples: transcreationExamples.length,
|
| 466 |
+
categories: [...new Set(transcreationExamples.map(ex => ex.category))].length,
|
| 467 |
+
types: [...new Set(transcreationExamples.map(ex => ex.type))].length,
|
| 468 |
+
lastUpdated: transcreationExamples.length > 0 ?
|
| 469 |
+
Math.max(...transcreationExamples.map(ex => new Date(ex.dateAdded || Date.now()).getTime())) : null
|
| 470 |
+
};
|
| 471 |
+
|
| 472 |
+
res.json({
|
| 473 |
+
success: true,
|
| 474 |
+
data: stats
|
| 475 |
+
});
|
| 476 |
+
});
|
| 477 |
+
|
| 478 |
+
// Health check
|
| 479 |
app.get('/api/health', (req, res) => {
|
| 480 |
res.json({
|
| 481 |
success: true,
|
|
|
|
| 485 |
});
|
| 486 |
});
|
| 487 |
|
| 488 |
+
// Manual Edit API Endpoints
|
| 489 |
+
|
| 490 |
+
// Add new example
|
| 491 |
+
app.post('/api/examples/add', async (req, res) => {
|
| 492 |
+
try {
|
| 493 |
+
console.log('📝 Received new example request:', JSON.stringify(req.body, null, 2));
|
| 494 |
+
|
| 495 |
+
// Determine if this is a Chinese to English entry (no taiwan field)
|
| 496 |
+
const isChineseToEnglish = req.body.hasOwnProperty('isChineseToEnglish') && req.body.isChineseToEnglish;
|
| 497 |
+
|
| 498 |
+
// Validate required fields based on direction
|
| 499 |
+
const requiredFields = isChineseToEnglish
|
| 500 |
+
? ['english', 'mainland', 'brand', 'category', 'type']
|
| 501 |
+
: ['english', 'mainland', 'taiwan', 'brand', 'category', 'type'];
|
| 502 |
+
|
| 503 |
+
const missingFields = requiredFields.filter(field => !req.body[field]);
|
| 504 |
+
|
| 505 |
+
if (missingFields.length > 0) {
|
| 506 |
+
console.error('❌ Missing required fields:', {
|
| 507 |
+
missingFields,
|
| 508 |
+
receivedFields: Object.keys(req.body),
|
| 509 |
+
receivedValues: req.body
|
| 510 |
+
});
|
| 511 |
+
return res.status(400).json({
|
| 512 |
+
success: false,
|
| 513 |
+
message: `Missing required fields: ${missingFields.join(', ')}`,
|
| 514 |
+
details: {
|
| 515 |
+
missingFields,
|
| 516 |
+
receivedFields: Object.keys(req.body)
|
| 517 |
+
}
|
| 518 |
+
});
|
| 519 |
+
}
|
| 520 |
+
|
| 521 |
+
// Create new example with cleaned data
|
| 522 |
+
const newExample = {
|
| 523 |
+
...req.body,
|
| 524 |
+
// Clean strings and handle optional fields
|
| 525 |
+
english: req.body.english.trim(),
|
| 526 |
+
mainland: req.body.mainland.trim(),
|
| 527 |
+
taiwan: isChineseToEnglish ? undefined : req.body.taiwan?.trim(),
|
| 528 |
+
brand: req.body.brand.trim(),
|
| 529 |
+
category: req.body.category.trim(),
|
| 530 |
+
type: req.body.type || 'slogan',
|
| 531 |
+
description: req.body.description?.trim() ?? '',
|
| 532 |
+
status: req.body.status || 'pending',
|
| 533 |
+
contributor: req.body.contributor === undefined ? null : req.body.contributor.trim(),
|
| 534 |
+
id: Date.now().toString() + Math.random().toString(36).substring(2),
|
| 535 |
+
dateAdded: new Date().toISOString()
|
| 536 |
+
};
|
| 537 |
+
|
| 538 |
+
console.log('✨ Created new example:', JSON.stringify(newExample, null, 2));
|
| 539 |
+
transcreationExamples.push(newExample);
|
| 540 |
+
await saveCachedExamples();
|
| 541 |
+
|
| 542 |
+
res.json({
|
| 543 |
+
success: true,
|
| 544 |
+
message: 'Example added successfully',
|
| 545 |
+
example: newExample
|
| 546 |
+
});
|
| 547 |
+
} catch (error) {
|
| 548 |
+
console.error('❌ Error adding example:', error);
|
| 549 |
+
res.status(500).json({
|
| 550 |
+
success: false,
|
| 551 |
+
message: 'Error adding example',
|
| 552 |
+
error: error.message
|
| 553 |
+
});
|
| 554 |
+
}
|
| 555 |
+
});
|
| 556 |
+
|
| 557 |
+
// Update existing example
|
| 558 |
+
app.put('/api/examples/:id', async (req, res) => {
|
| 559 |
+
try {
|
| 560 |
+
const { id } = req.params;
|
| 561 |
+
console.log(`📝 UPDATE REQUEST for ID: ${id}`);
|
| 562 |
+
console.log(`📝 REQUEST BODY:`, JSON.stringify(req.body, null, 2));
|
| 563 |
+
|
| 564 |
+
const index = transcreationExamples.findIndex(ex => ex.id === id);
|
| 565 |
+
|
| 566 |
+
if (index === -1) {
|
| 567 |
+
console.log(`❌ Example with ID ${id} not found`);
|
| 568 |
+
return res.status(404).json({
|
| 569 |
+
success: false,
|
| 570 |
+
message: 'Example not found'
|
| 571 |
+
});
|
| 572 |
+
}
|
| 573 |
+
|
| 574 |
+
console.log(`📝 FOUND EXAMPLE at index ${index}:`, JSON.stringify(transcreationExamples[index], null, 2));
|
| 575 |
+
|
| 576 |
+
// Determine if this is a Chinese to English entry (no taiwan field)
|
| 577 |
+
const isChineseToEnglish = !req.body.hasOwnProperty('taiwan');
|
| 578 |
+
|
| 579 |
+
// Validate required fields based on direction
|
| 580 |
+
const requiredFields = isChineseToEnglish
|
| 581 |
+
? ['english', 'mainland', 'brand', 'category', 'type']
|
| 582 |
+
: ['english', 'mainland', 'taiwan', 'brand', 'category', 'type'];
|
| 583 |
+
|
| 584 |
+
const missingFields = requiredFields.filter(field => !req.body[field]);
|
| 585 |
+
|
| 586 |
+
if (missingFields.length > 0) {
|
| 587 |
+
console.error('❌ Missing required fields:', {
|
| 588 |
+
missingFields,
|
| 589 |
+
receivedFields: Object.keys(req.body),
|
| 590 |
+
receivedValues: req.body
|
| 591 |
+
});
|
| 592 |
+
return res.status(400).json({
|
| 593 |
+
success: false,
|
| 594 |
+
message: `Missing required fields: ${missingFields.join(', ')}`,
|
| 595 |
+
details: {
|
| 596 |
+
missingFields,
|
| 597 |
+
receivedFields: Object.keys(req.body)
|
| 598 |
+
}
|
| 599 |
+
});
|
| 600 |
+
}
|
| 601 |
+
|
| 602 |
+
// Preserve original dateAdded and merge updates
|
| 603 |
+
const updatedExample = {
|
| 604 |
+
...transcreationExamples[index], // Start with existing data
|
| 605 |
+
...req.body, // Merge in updates
|
| 606 |
+
id, // Ensure ID doesn't change
|
| 607 |
+
dateAdded: transcreationExamples[index].dateAdded, // Preserve original date
|
| 608 |
+
lastModified: new Date().toISOString(), // Add last modified timestamp
|
| 609 |
+
// Handle optional fields - if not provided in request, keep existing value
|
| 610 |
+
status: req.body.status ?? transcreationExamples[index].status ?? 'pending',
|
| 611 |
+
contributor: req.body.contributor === undefined ? transcreationExamples[index].contributor : req.body.contributor,
|
| 612 |
+
type: req.body.type ?? transcreationExamples[index].type ?? 'slogan',
|
| 613 |
+
description: req.body.description ?? transcreationExamples[index].description ?? ''
|
| 614 |
+
};
|
| 615 |
+
|
| 616 |
+
// Update the example in the array
|
| 617 |
+
transcreationExamples[index] = updatedExample;
|
| 618 |
+
|
| 619 |
+
console.log(`📝 UPDATED EXAMPLE:`, JSON.stringify(updatedExample, null, 2));
|
| 620 |
+
console.log(`📝 EXAMPLE IN ARRAY AFTER UPDATE:`, JSON.stringify(transcreationExamples[index], null, 2));
|
| 621 |
+
|
| 622 |
+
// Save to cache file
|
| 623 |
+
await saveCachedExamples();
|
| 624 |
+
|
| 625 |
+
console.log(`✅ UPDATE COMPLETED for ID: ${id}`);
|
| 626 |
+
|
| 627 |
+
res.json({
|
| 628 |
+
success: true,
|
| 629 |
+
message: 'Example updated successfully',
|
| 630 |
+
data: updatedExample
|
| 631 |
+
});
|
| 632 |
+
} catch (error) {
|
| 633 |
+
console.error('❌ Error updating example:', error);
|
| 634 |
+
res.status(500).json({
|
| 635 |
+
success: false,
|
| 636 |
+
message: 'Failed to update example',
|
| 637 |
+
error: error.message
|
| 638 |
+
});
|
| 639 |
+
}
|
| 640 |
+
});
|
| 641 |
+
|
| 642 |
+
// Delete example
|
| 643 |
+
app.delete('/api/examples/:id', (req, res) => {
|
| 644 |
+
try {
|
| 645 |
+
const { id } = req.params;
|
| 646 |
+
|
| 647 |
+
// Find and remove example
|
| 648 |
+
const index = transcreationExamples.findIndex(ex => ex.id === id);
|
| 649 |
+
if (index === -1) {
|
| 650 |
+
return res.status(404).json({
|
| 651 |
+
success: false,
|
| 652 |
+
error: 'Example not found'
|
| 653 |
+
});
|
| 654 |
+
}
|
| 655 |
+
|
| 656 |
+
transcreationExamples.splice(index, 1);
|
| 657 |
+
saveCachedExamples();
|
| 658 |
+
|
| 659 |
+
res.json({
|
| 660 |
+
success: true,
|
| 661 |
+
message: 'Example deleted successfully'
|
| 662 |
+
});
|
| 663 |
+
} catch (error) {
|
| 664 |
+
res.status(500).json({
|
| 665 |
+
success: false,
|
| 666 |
+
error: 'Failed to delete example'
|
| 667 |
+
});
|
| 668 |
+
}
|
| 669 |
+
});
|
| 670 |
+
|
| 671 |
+
// Serve static files from React build (for production)
|
| 672 |
if (process.env.NODE_ENV === 'production') {
|
| 673 |
+
app.use(express.static(path.join(__dirname, '../client/build')));
|
| 674 |
+
|
| 675 |
app.get('*', (req, res) => {
|
| 676 |
+
res.sendFile(path.join(__dirname, '../client/build', 'index.html'));
|
| 677 |
});
|
| 678 |
}
|
| 679 |
|
| 680 |
app.listen(PORT, () => {
|
| 681 |
console.log(`🚀 Transcreation Explorer API running on port ${PORT}`);
|
| 682 |
console.log(`📊 Cached examples: ${transcreationExamples.length}`);
|
| 683 |
+
});
|
| 684 |
+
|
| 685 |
+
module.exports = app;
|
server/package.json
CHANGED
|
@@ -1,12 +1,30 @@
|
|
| 1 |
{
|
| 2 |
"name": "transcreation-explorer-server",
|
| 3 |
"version": "1.0.0",
|
|
|
|
| 4 |
"main": "index.js",
|
| 5 |
"scripts": {
|
| 6 |
-
"start": "node index.js"
|
|
|
|
|
|
|
| 7 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 8 |
"dependencies": {
|
|
|
|
|
|
|
| 9 |
"cors": "^2.8.5",
|
| 10 |
-
"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 11 |
}
|
| 12 |
}
|
|
|
|
| 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 |
+
"dotenv": "^16.4.1",
|
| 23 |
+
"express": "^4.18.2",
|
| 24 |
+
"node-cron": "^3.0.3",
|
| 25 |
+
"uuid": "^9.0.1"
|
| 26 |
+
},
|
| 27 |
+
"devDependencies": {
|
| 28 |
+
"nodemon": "^3.0.3"
|
| 29 |
}
|
| 30 |
}
|