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 CHANGED
@@ -1,25 +1,28 @@
1
  {
2
  "name": "transcreation-explorer-client",
3
- "version": "0.1.0",
4
  "lockfileVersion": 3,
5
  "requires": true,
6
  "packages": {
7
  "": {
8
  "name": "transcreation-explorer-client",
9
- "version": "0.1.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.2",
15
- "framer-motion": "^10.16.5",
16
- "lucide-react": "^0.294.0",
17
  "react": "^18.2.0",
18
  "react-dom": "^18.2.0",
19
  "react-hot-toast": "^2.4.1",
20
- "react-router-dom": "^6.8.1",
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.4",
2954
- "resolved": "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-30.0.4.tgz",
2955
- "integrity": "sha512-EgXecHDNfANeqOkcak0DxsoVI4qkDUsR7n/Lr2vtmTBjwLPBnnPOF71S11Q8IObWzxm2QgQoY6f9hzrRD3gHRA==",
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.1",
3333
- "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-30.0.1.tgz",
3334
- "integrity": "sha512-+g/1TKjFuGrf1Hh0QPCv0gISwBxJ+MQSNXmG9zjHy7BmFhtoJ9fdNhWJp3qUKRi93AOZHXtdxZgJ1vAtz6z65w==",
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.1",
3542
- "resolved": "https://registry.npmjs.org/@jest/types/-/types-30.0.1.tgz",
3543
- "integrity": "sha512-HGwoYRVF0QSKJu1ZQX0o5ZrUrrhj0aOOFA8hXrumD7SIzjouevhawbTjmXdwOmURdGluU9DM/XvGm3NyFoiQjw==",
3544
  "license": "MIT",
3545
  "dependencies": {
3546
  "@jest/pattern": "30.0.1",
3547
- "@jest/schemas": "30.0.1",
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.2",
4462
- "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.0.2.tgz",
4463
- "integrity": "sha512-yC5/EBSOrTtqhCKfLHqoUIAXVRZnukHPwWBJWR7h84Q3Be1DRQZLncwcfLoPA5RPQ65qfiCMqgYwdUuQ//eVpg==",
4464
  "license": "MIT",
4465
  "dependencies": {
4466
- "@jest/schemas": "30.0.1",
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.0.15",
4500
- "resolved": "https://registry.npmjs.org/@types/node/-/node-24.0.15.tgz",
4501
- "integrity": "sha512-oaeTSbCef7U/z7rDeJA138xpG3NuKc64/rZ2qmUFkFJmnMsAPaluIifqyWd8hSSMxyP9oie3dLAqYPblag9KgA==",
4502
  "license": "MIT",
4503
  "dependencies": {
4504
  "undici-types": "~7.8.0"
@@ -5682,13 +5685,13 @@
5682
  }
5683
  },
5684
  "node_modules/axios": {
5685
- "version": "1.10.0",
5686
- "resolved": "https://registry.npmjs.org/axios/-/axios-1.10.0.tgz",
5687
- "integrity": "sha512-/1xYAC4MP/HEG+3duIhFr4ZQXR4sQXOIe+o6sdqzeykGLx6Upp/1p8MHqhINOvGeP7xyNHe7tsiJByc4SSVUxw==",
5688
  "license": "MIT",
5689
  "dependencies": {
5690
  "follow-redirects": "^1.15.6",
5691
- "form-data": "^4.0.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.187",
7703
- "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.187.tgz",
7704
- "integrity": "sha512-cl5Jc9I0KGUoOoSbxvTywTa40uspGJt/BDBoDLoxJRSBpWh4FFXBsjNRHfQrONsV/OoEjDfHUmZQa2d6Ze4YgA==",
7705
  "license": "ISC"
7706
  },
7707
  "node_modules/emittery": {
@@ -8704,17 +8707,17 @@
8704
  }
8705
  },
8706
  "node_modules/expect": {
8707
- "version": "30.0.4",
8708
- "resolved": "https://registry.npmjs.org/expect/-/expect-30.0.4.tgz",
8709
- "integrity": "sha512-dDLGjnP2cKbEppxVICxI/Uf4YemmGMPNy0QytCbfafbpYk9AFQsxb8Uyrxii0RPK7FWgLGlSem+07WirwS3cFQ==",
8710
  "license": "MIT",
8711
  "dependencies": {
8712
- "@jest/expect-utils": "30.0.4",
8713
  "@jest/get-type": "30.0.1",
8714
- "jest-matcher-utils": "30.0.4",
8715
- "jest-message-util": "30.0.2",
8716
- "jest-mock": "30.0.2",
8717
- "jest-util": "30.0.2"
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.4",
11324
- "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-30.0.4.tgz",
11325
- "integrity": "sha512-TSjceIf6797jyd+R64NXqicttROD+Qf98fex7CowmlSn7f8+En0da1Dglwr1AXxDtVizoxXYZBlUQwNhoOXkNw==",
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.2"
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.2",
11351
- "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.0.2.tgz",
11352
- "integrity": "sha512-yC5/EBSOrTtqhCKfLHqoUIAXVRZnukHPwWBJWR7h84Q3Be1DRQZLncwcfLoPA5RPQ65qfiCMqgYwdUuQ//eVpg==",
11353
  "license": "MIT",
11354
  "dependencies": {
11355
- "@jest/schemas": "30.0.1",
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.4",
11955
- "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-30.0.4.tgz",
11956
- "integrity": "sha512-ubCewJ54YzeAZ2JeHHGVoU+eDIpQFsfPQs0xURPWoNiO42LGJ+QGgfSf+hFIRplkZDkhH5MOvuxHKXRTUU3dUQ==",
11957
  "license": "MIT",
11958
  "dependencies": {
11959
  "@jest/get-type": "30.0.1",
11960
  "chalk": "^4.1.2",
11961
- "jest-diff": "30.0.4",
11962
- "pretty-format": "30.0.2"
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.2",
11982
- "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.0.2.tgz",
11983
- "integrity": "sha512-yC5/EBSOrTtqhCKfLHqoUIAXVRZnukHPwWBJWR7h84Q3Be1DRQZLncwcfLoPA5RPQ65qfiCMqgYwdUuQ//eVpg==",
11984
  "license": "MIT",
11985
  "dependencies": {
11986
- "@jest/schemas": "30.0.1",
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.2",
12002
- "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-30.0.2.tgz",
12003
- "integrity": "sha512-vXywcxmr0SsKXF/bAD7t7nMamRvPuJkras00gqYeB1V0WllxZrbZ0paRr3XqpFU2sYYjD0qAaG2fRyn/CGZ0aw==",
12004
  "license": "MIT",
12005
  "dependencies": {
12006
  "@babel/code-frame": "^7.27.1",
12007
- "@jest/types": "30.0.1",
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.2",
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.2",
12034
- "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.0.2.tgz",
12035
- "integrity": "sha512-yC5/EBSOrTtqhCKfLHqoUIAXVRZnukHPwWBJWR7h84Q3Be1DRQZLncwcfLoPA5RPQ65qfiCMqgYwdUuQ//eVpg==",
12036
  "license": "MIT",
12037
  "dependencies": {
12038
- "@jest/schemas": "30.0.1",
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.2",
12054
- "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-30.0.2.tgz",
12055
- "integrity": "sha512-PnZOHmqup/9cT/y+pXIVbbi8ID6U1XHRmbvR7MvUy4SLqhCbwpkmXhLbsWbGewHrV5x/1bF7YDjs+x24/QSvFA==",
12056
  "license": "MIT",
12057
  "dependencies": {
12058
- "@jest/types": "30.0.1",
12059
  "@types/node": "*",
12060
- "jest-util": "30.0.2"
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.2",
12677
- "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-30.0.2.tgz",
12678
- "integrity": "sha512-8IyqfKS4MqprBuUpZNlFB5l+WFehc8bfCe1HSZFHzft2mOuND8Cvi9r1musli+u6F3TqanCZ/Ik4H4pXUolZIg==",
12679
  "license": "MIT",
12680
  "dependencies": {
12681
- "@jest/types": "30.0.1",
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.294.0",
13589
- "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.294.0.tgz",
13590
- "integrity": "sha512-V7o0/VECSGbLHn3/1O67FUgBwWB+hmzshrgDVRJQhMh8uj5D3HBuIvhuAmQTtlupILSplwIZg5FTc4tTKMA2SA==",
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": "4.9.5",
18826
- "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz",
18827
- "integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==",
18828
  "license": "Apache-2.0",
18829
  "peer": true,
18830
  "bin": {
@@ -18832,7 +18835,7 @@
18832
  "tsserver": "bin/tsserver"
18833
  },
18834
  "engines": {
18835
- "node": ">=4.2.0"
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 LoadingSpinner from '../components/LoadingSpinner';
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
- useEffect(() => {
15
- const fetchData = async () => {
16
- try {
17
- const [examplesResponse, categoriesResponse] = await Promise.all([
18
- axios.get('/api/examples'),
19
- axios.get('/api/categories')
20
- ]);
21
-
22
- setExamples(examplesResponse.data.data || []);
23
- setCategories(categoriesResponse.data.data || []);
24
- } catch (error) {
25
- console.error('Error fetching data:', error);
26
- setExamples([]);
27
- setCategories([]);
28
- } finally {
29
- setIsLoading(false);
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.english.toLowerCase().includes(searchTerm.toLowerCase()) ||
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, searchTerm]);
56
 
57
- if (isLoading) {
58
- return <LoadingSpinner />;
59
- }
60
 
61
- return (
62
- <div className="browse">
63
- <div className="browse-header">
64
- <h1>Browse Examples</h1>
65
- <p>Explore our collection of transcreation examples</p>
 
 
 
66
  </div>
 
 
67
 
68
- <div className="browse-filters">
69
- <div className="filter-group">
70
- <label htmlFor="search">Search:</label>
71
- <input
72
- id="search"
73
- type="text"
74
- placeholder="Search examples..."
75
- value={searchTerm}
76
- onChange={(e) => setSearchTerm(e.target.value)}
77
- />
78
- </div>
 
 
 
 
79
 
80
- <div className="filter-group">
81
- <label htmlFor="category">Category:</label>
 
 
 
 
82
  <select
83
- id="category"
84
  value={selectedCategory}
85
- onChange={(e) => setSelectedCategory(e.target.value)}
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
- <div className="examples-grid">
98
- {filteredExamples.length === 0 ? (
99
- <div className="no-results">
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
- <div className="results-count">
110
- Showing {filteredExamples.length} of {examples.length} examples
111
- </div>
112
- </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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 className="home">
 
 
 
 
7
  <section className="hero">
8
- <h1>Transcreation Explorer</h1>
9
- <p>
 
 
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
- Explore Random Example
 
18
  </Link>
19
- <Link to="/browse" className="btn btn-outline">
20
- Browse All Examples
 
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
- const buildPath = path.join(__dirname, '../client/build');
19
- console.log('📁 Serving static files from:', buildPath);
20
- app.use(express.static(buildPath));
21
  }
22
 
23
- // Test route to confirm our server is running
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(transcreationExamples, null, 2)
49
  );
50
- console.log(`💾 Saved ${transcreationExamples.length} examples to cache`);
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
- // Serve React app for all other routes
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
148
  if (process.env.NODE_ENV === 'production') {
 
 
149
  app.get('*', (req, res) => {
150
- res.sendFile(path.join(__dirname, '../client/build/index.html'));
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
- "express": "^4.18.2"
 
 
 
 
 
 
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
  }