lewtun HF Staff commited on
Commit
a2ef1ee
·
1 Parent(s): d917493
Files changed (12) hide show
  1. .dockerignore +7 -0
  2. .gitignore +1 -0
  3. Dockerfile +15 -0
  4. README.md +16 -3
  5. data/.gitkeep +0 -0
  6. package-lock.json +725 -188
  7. package.json +9 -0
  8. server/index.js +204 -0
  9. src/App.css +10 -0
  10. src/App.js +88 -33
  11. src/App.test.js +12 -2
  12. src/utils/storage.js +36 -10
.dockerignore ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ node_modules
2
+ build
3
+ .git
4
+ .gitignore
5
+ npm-debug.log*
6
+ yarn-debug.log*
7
+ yarn-error.log*
.gitignore CHANGED
@@ -10,6 +10,7 @@
10
 
11
  # production
12
  /build
 
13
 
14
  # misc
15
  .DS_Store
 
10
 
11
  # production
12
  /build
13
+ /data/sessions.json
14
 
15
  # misc
16
  .DS_Store
Dockerfile ADDED
@@ -0,0 +1,15 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM node:20-slim
2
+
3
+ WORKDIR /app
4
+
5
+ COPY package.json package-lock.json ./
6
+ RUN npm ci
7
+
8
+ COPY . .
9
+ RUN npm run build
10
+
11
+ ENV NODE_ENV=production
12
+ ENV PORT=7860
13
+ EXPOSE 7860
14
+
15
+ CMD ["node", "server/index.js"]
README.md CHANGED
@@ -3,14 +3,27 @@ title: Climbing Dashboard
3
  emoji: 🐠
4
  colorFrom: indigo
5
  colorTo: red
6
- sdk: static
7
  pinned: false
8
- app_build_command: npm run build
9
- app_file: build/index.html
10
  ---
11
 
12
  # Getting Started with Create React App
13
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
14
  This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app).
15
 
16
  ## Available Scripts
 
3
  emoji: 🐠
4
  colorFrom: indigo
5
  colorTo: red
6
+ sdk: docker
7
  pinned: false
 
 
8
  ---
9
 
10
  # Getting Started with Create React App
11
 
12
+ ## Shared Storage Setup
13
+
14
+ The dashboard now uses a backend API. By default it writes sessions to `data/sessions.json` locally. If you set Hugging Face dataset credentials, it stores the same `sessions.json` in a dataset repo instead.
15
+
16
+ - Run frontend + backend in development:
17
+ - `npm run dev`
18
+ - Run backend only:
19
+ - `npm run server`
20
+ - Use a dataset repo for persistent storage:
21
+ - set `HF_TOKEN`
22
+ - set `HF_DATASET_REPO` to something like `your-name/climbing-dashboard-data`
23
+ - optional: set `HF_DATASET_FILE` if you want a path other than `sessions.json`
24
+
25
+ Without `HF_TOKEN` and `HF_DATASET_REPO`, the app falls back to the local file in `data/sessions.json`.
26
+
27
  This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app).
28
 
29
  ## Available Scripts
data/.gitkeep ADDED
File without changes
package-lock.json CHANGED
@@ -8,15 +8,21 @@
8
  "name": "react-template",
9
  "version": "0.1.0",
10
  "dependencies": {
 
11
  "@testing-library/dom": "^10.4.0",
12
  "@testing-library/jest-dom": "^6.6.3",
13
  "@testing-library/react": "^16.3.0",
14
  "@testing-library/user-event": "^13.5.0",
 
 
15
  "react": "^19.1.0",
16
  "react-dom": "^19.1.0",
17
  "react-scripts": "5.0.1",
18
  "recharts": "^3.7.0",
19
  "web-vitals": "^2.1.4"
 
 
 
20
  }
21
  },
22
  "node_modules/@adobe/css-tools": {
@@ -2457,6 +2463,30 @@
2457
  "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
2458
  }
2459
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2460
  "node_modules/@humanwhocodes/config-array": {
2461
  "version": "0.13.0",
2462
  "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.13.0.tgz",
@@ -2938,6 +2968,22 @@
2938
  "node": ">= 8"
2939
  }
2940
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2941
  "node_modules/@pmmmwh/react-refresh-webpack-plugin": {
2942
  "version": "0.5.17",
2943
  "resolved": "https://registry.npmjs.org/@pmmmwh/react-refresh-webpack-plugin/-/react-refresh-webpack-plugin-0.5.17.tgz",
@@ -5249,56 +5295,45 @@
5249
  "license": "MIT"
5250
  },
5251
  "node_modules/body-parser": {
5252
- "version": "1.20.4",
5253
- "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.4.tgz",
5254
- "integrity": "sha512-ZTgYYLMOXY9qKU/57FAo8F+HA2dGX7bqGc71txDRC1rS4frdFI5R7NhluHxH6M0YItAP0sHB4uqAOcYKxO6uGA==",
5255
  "license": "MIT",
5256
  "dependencies": {
5257
- "bytes": "~3.1.2",
5258
- "content-type": "~1.0.5",
5259
- "debug": "2.6.9",
5260
- "depd": "2.0.0",
5261
- "destroy": "~1.2.0",
5262
- "http-errors": "~2.0.1",
5263
- "iconv-lite": "~0.4.24",
5264
- "on-finished": "~2.4.1",
5265
- "qs": "~6.14.0",
5266
- "raw-body": "~2.5.3",
5267
- "type-is": "~1.6.18",
5268
- "unpipe": "~1.0.0"
5269
  },
5270
  "engines": {
5271
- "node": ">= 0.8",
5272
- "npm": "1.2.8000 || >= 1.4.16"
5273
- }
5274
- },
5275
- "node_modules/body-parser/node_modules/debug": {
5276
- "version": "2.6.9",
5277
- "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
5278
- "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
5279
- "license": "MIT",
5280
- "dependencies": {
5281
- "ms": "2.0.0"
5282
  }
5283
  },
5284
  "node_modules/body-parser/node_modules/iconv-lite": {
5285
- "version": "0.4.24",
5286
- "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
5287
- "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==",
5288
  "license": "MIT",
5289
  "dependencies": {
5290
- "safer-buffer": ">= 2.1.2 < 3"
5291
  },
5292
  "engines": {
5293
  "node": ">=0.10.0"
 
 
 
 
5294
  }
5295
  },
5296
- "node_modules/body-parser/node_modules/ms": {
5297
- "version": "2.0.0",
5298
- "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
5299
- "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
5300
- "license": "MIT"
5301
- },
5302
  "node_modules/bonjour-service": {
5303
  "version": "1.3.0",
5304
  "resolved": "https://registry.npmjs.org/bonjour-service/-/bonjour-service-1.3.0.tgz",
@@ -5673,6 +5708,19 @@
5673
  "node": ">=0.10.0"
5674
  }
5675
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
5676
  "node_modules/cliui": {
5677
  "version": "7.0.4",
5678
  "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz",
@@ -5911,6 +5959,86 @@
5911
  "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==",
5912
  "license": "MIT"
5913
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
5914
  "node_modules/confusing-browser-globals": {
5915
  "version": "1.0.11",
5916
  "resolved": "https://registry.npmjs.org/confusing-browser-globals/-/confusing-browser-globals-1.0.11.tgz",
@@ -5927,15 +6055,16 @@
5927
  }
5928
  },
5929
  "node_modules/content-disposition": {
5930
- "version": "0.5.4",
5931
- "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz",
5932
- "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==",
5933
  "license": "MIT",
5934
- "dependencies": {
5935
- "safe-buffer": "5.2.1"
5936
- },
5937
  "engines": {
5938
- "node": ">= 0.6"
 
 
 
 
5939
  }
5940
  },
5941
  "node_modules/content-type": {
@@ -5963,10 +6092,13 @@
5963
  }
5964
  },
5965
  "node_modules/cookie-signature": {
5966
- "version": "1.0.7",
5967
- "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.7.tgz",
5968
- "integrity": "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==",
5969
- "license": "MIT"
 
 
 
5970
  },
5971
  "node_modules/core-js": {
5972
  "version": "3.48.0",
@@ -8036,65 +8168,94 @@
8036
  }
8037
  },
8038
  "node_modules/express": {
8039
- "version": "4.22.1",
8040
- "resolved": "https://registry.npmjs.org/express/-/express-4.22.1.tgz",
8041
- "integrity": "sha512-F2X8g9P1X7uCPZMA3MVf9wcTqlyNp7IhH5qPCI0izhaOIYXaW9L535tGA3qmjRzpH+bZczqq7hVKxTR4NWnu+g==",
8042
- "license": "MIT",
8043
- "dependencies": {
8044
- "accepts": "~1.3.8",
8045
- "array-flatten": "1.1.1",
8046
- "body-parser": "~1.20.3",
8047
- "content-disposition": "~0.5.4",
8048
- "content-type": "~1.0.4",
8049
- "cookie": "~0.7.1",
8050
- "cookie-signature": "~1.0.6",
8051
- "debug": "2.6.9",
8052
- "depd": "2.0.0",
8053
- "encodeurl": "~2.0.0",
8054
- "escape-html": "~1.0.3",
8055
- "etag": "~1.8.1",
8056
- "finalhandler": "~1.3.1",
8057
- "fresh": "~0.5.2",
8058
- "http-errors": "~2.0.0",
8059
- "merge-descriptors": "1.0.3",
8060
- "methods": "~1.1.2",
8061
- "on-finished": "~2.4.1",
8062
- "parseurl": "~1.3.3",
8063
- "path-to-regexp": "~0.1.12",
8064
- "proxy-addr": "~2.0.7",
8065
- "qs": "~6.14.0",
8066
- "range-parser": "~1.2.1",
8067
- "safe-buffer": "5.2.1",
8068
- "send": "~0.19.0",
8069
- "serve-static": "~1.16.2",
8070
- "setprototypeof": "1.2.0",
8071
- "statuses": "~2.0.1",
8072
- "type-is": "~1.6.18",
8073
- "utils-merge": "1.0.1",
8074
- "vary": "~1.1.2"
8075
  },
8076
  "engines": {
8077
- "node": ">= 0.10.0"
8078
  },
8079
  "funding": {
8080
  "type": "opencollective",
8081
  "url": "https://opencollective.com/express"
8082
  }
8083
  },
8084
- "node_modules/express/node_modules/debug": {
8085
- "version": "2.6.9",
8086
- "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
8087
- "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
8088
  "license": "MIT",
8089
  "dependencies": {
8090
- "ms": "2.0.0"
 
 
 
 
8091
  }
8092
  },
8093
- "node_modules/express/node_modules/ms": {
8094
- "version": "2.0.0",
8095
- "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
8096
- "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
8097
- "license": "MIT"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
8098
  },
8099
  "node_modules/fast-deep-equal": {
8100
  "version": "3.1.3",
@@ -8290,38 +8451,26 @@
8290
  }
8291
  },
8292
  "node_modules/finalhandler": {
8293
- "version": "1.3.2",
8294
- "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.2.tgz",
8295
- "integrity": "sha512-aA4RyPcd3badbdABGDuTXCMTtOneUCAYH/gxoYRTZlIJdF0YPWuGqiAsIrhNnnqdXGswYk6dGujem4w80UJFhg==",
8296
  "license": "MIT",
8297
  "dependencies": {
8298
- "debug": "2.6.9",
8299
- "encodeurl": "~2.0.0",
8300
- "escape-html": "~1.0.3",
8301
- "on-finished": "~2.4.1",
8302
- "parseurl": "~1.3.3",
8303
- "statuses": "~2.0.2",
8304
- "unpipe": "~1.0.0"
8305
  },
8306
  "engines": {
8307
- "node": ">= 0.8"
8308
- }
8309
- },
8310
- "node_modules/finalhandler/node_modules/debug": {
8311
- "version": "2.6.9",
8312
- "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
8313
- "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
8314
- "license": "MIT",
8315
- "dependencies": {
8316
- "ms": "2.0.0"
8317
  }
8318
  },
8319
- "node_modules/finalhandler/node_modules/ms": {
8320
- "version": "2.0.0",
8321
- "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
8322
- "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
8323
- "license": "MIT"
8324
- },
8325
  "node_modules/find-cache-dir": {
8326
  "version": "3.3.2",
8327
  "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-3.3.2.tgz",
@@ -8543,12 +8692,12 @@
8543
  }
8544
  },
8545
  "node_modules/fresh": {
8546
- "version": "0.5.2",
8547
- "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz",
8548
- "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==",
8549
  "license": "MIT",
8550
  "engines": {
8551
- "node": ">= 0.6"
8552
  }
8553
  },
8554
  "node_modules/fs-extra": {
@@ -9783,6 +9932,12 @@
9783
  "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==",
9784
  "license": "MIT"
9785
  },
 
 
 
 
 
 
9786
  "node_modules/is-regex": {
9787
  "version": "1.2.1",
9788
  "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz",
@@ -11457,12 +11612,12 @@
11457
  "license": "CC0-1.0"
11458
  },
11459
  "node_modules/media-typer": {
11460
- "version": "0.3.0",
11461
- "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz",
11462
- "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==",
11463
  "license": "MIT",
11464
  "engines": {
11465
- "node": ">= 0.6"
11466
  }
11467
  },
11468
  "node_modules/memfs": {
@@ -11478,10 +11633,13 @@
11478
  }
11479
  },
11480
  "node_modules/merge-descriptors": {
11481
- "version": "1.0.3",
11482
- "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz",
11483
- "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==",
11484
  "license": "MIT",
 
 
 
11485
  "funding": {
11486
  "url": "https://github.com/sponsors/sindresorhus"
11487
  }
@@ -12212,10 +12370,14 @@
12212
  "license": "MIT"
12213
  },
12214
  "node_modules/path-to-regexp": {
12215
- "version": "0.1.12",
12216
- "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz",
12217
- "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==",
12218
- "license": "MIT"
 
 
 
 
12219
  },
12220
  "node_modules/path-type": {
12221
  "version": "4.0.0",
@@ -12338,6 +12500,53 @@
12338
  "node": ">=4"
12339
  }
12340
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
12341
  "node_modules/possible-typed-array-names": {
12342
  "version": "1.1.0",
12343
  "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz",
@@ -13748,9 +13957,9 @@
13748
  }
13749
  },
13750
  "node_modules/qs": {
13751
- "version": "6.14.2",
13752
- "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.2.tgz",
13753
- "integrity": "sha512-V/yCWTTF7VJ9hIh18Ugr2zhJMP01MY7c5kh4J870L7imm6/DIzBsNLTXzMwUA3yZ5b/KBqLx8Kp3uRvd7xSe3Q==",
13754
  "license": "BSD-3-Clause",
13755
  "dependencies": {
13756
  "side-channel": "^1.1.0"
@@ -13816,30 +14025,34 @@
13816
  }
13817
  },
13818
  "node_modules/raw-body": {
13819
- "version": "2.5.3",
13820
- "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.3.tgz",
13821
- "integrity": "sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA==",
13822
  "license": "MIT",
13823
  "dependencies": {
13824
  "bytes": "~3.1.2",
13825
  "http-errors": "~2.0.1",
13826
- "iconv-lite": "~0.4.24",
13827
  "unpipe": "~1.0.0"
13828
  },
13829
  "engines": {
13830
- "node": ">= 0.8"
13831
  }
13832
  },
13833
  "node_modules/raw-body/node_modules/iconv-lite": {
13834
- "version": "0.4.24",
13835
- "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
13836
- "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==",
13837
  "license": "MIT",
13838
  "dependencies": {
13839
- "safer-buffer": ">= 2.1.2 < 3"
13840
  },
13841
  "engines": {
13842
  "node": ">=0.10.0"
 
 
 
 
13843
  }
13844
  },
13845
  "node_modules/react": {
@@ -14587,6 +14800,22 @@
14587
  "randombytes": "^2.1.0"
14588
  }
14589
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
14590
  "node_modules/run-parallel": {
14591
  "version": "1.2.0",
14592
  "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz",
@@ -14610,6 +14839,15 @@
14610
  "queue-microtask": "^1.2.2"
14611
  }
14612
  },
 
 
 
 
 
 
 
 
 
14613
  "node_modules/safe-array-concat": {
14614
  "version": "1.1.3",
14615
  "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.3.tgz",
@@ -14841,43 +15079,55 @@
14841
  }
14842
  },
14843
  "node_modules/send": {
14844
- "version": "0.19.2",
14845
- "resolved": "https://registry.npmjs.org/send/-/send-0.19.2.tgz",
14846
- "integrity": "sha512-VMbMxbDeehAxpOtWJXlcUS5E8iXh6QmN+BkRX1GARS3wRaXEEgzCcB10gTQazO42tpNIya8xIyNx8fll1OFPrg==",
14847
  "license": "MIT",
14848
  "dependencies": {
14849
- "debug": "2.6.9",
14850
- "depd": "2.0.0",
14851
- "destroy": "1.2.0",
14852
- "encodeurl": "~2.0.0",
14853
- "escape-html": "~1.0.3",
14854
- "etag": "~1.8.1",
14855
- "fresh": "~0.5.2",
14856
- "http-errors": "~2.0.1",
14857
- "mime": "1.6.0",
14858
- "ms": "2.1.3",
14859
- "on-finished": "~2.4.1",
14860
- "range-parser": "~1.2.1",
14861
- "statuses": "~2.0.2"
14862
  },
14863
  "engines": {
14864
- "node": ">= 0.8.0"
 
 
 
 
14865
  }
14866
  },
14867
- "node_modules/send/node_modules/debug": {
14868
- "version": "2.6.9",
14869
- "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
14870
- "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
14871
  "license": "MIT",
14872
- "dependencies": {
14873
- "ms": "2.0.0"
14874
  }
14875
  },
14876
- "node_modules/send/node_modules/debug/node_modules/ms": {
14877
- "version": "2.0.0",
14878
- "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
14879
- "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
14880
- "license": "MIT"
 
 
 
 
 
 
 
 
 
 
14881
  },
14882
  "node_modules/serialize-javascript": {
14883
  "version": "6.0.2",
@@ -14960,18 +15210,22 @@
14960
  }
14961
  },
14962
  "node_modules/serve-static": {
14963
- "version": "1.16.3",
14964
- "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.3.tgz",
14965
- "integrity": "sha512-x0RTqQel6g5SY7Lg6ZreMmsOzncHFU7nhnRWkKgWuMTu5NN0DR5oruckMqRvacAN9d5w6ARnRBXl9xhDCgfMeA==",
14966
  "license": "MIT",
14967
  "dependencies": {
14968
- "encodeurl": "~2.0.0",
14969
- "escape-html": "~1.0.3",
14970
- "parseurl": "~1.3.3",
14971
- "send": "~0.19.1"
14972
  },
14973
  "engines": {
14974
- "node": ">= 0.8.0"
 
 
 
 
14975
  }
14976
  },
14977
  "node_modules/set-function-length": {
@@ -16220,6 +16474,15 @@
16220
  "node": ">=8"
16221
  }
16222
  },
 
 
 
 
 
 
 
 
 
16223
  "node_modules/tryer": {
16224
  "version": "1.0.1",
16225
  "resolved": "https://registry.npmjs.org/tryer/-/tryer-1.0.1.tgz",
@@ -16326,18 +16589,44 @@
16326
  }
16327
  },
16328
  "node_modules/type-is": {
16329
- "version": "1.6.18",
16330
- "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz",
16331
- "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==",
16332
  "license": "MIT",
16333
  "dependencies": {
16334
- "media-typer": "0.3.0",
16335
- "mime-types": "~2.1.24"
 
16336
  },
16337
  "engines": {
16338
  "node": ">= 0.6"
16339
  }
16340
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
16341
  "node_modules/typed-array-buffer": {
16342
  "version": "1.0.3",
16343
  "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.3.tgz",
@@ -16903,6 +17192,254 @@
16903
  }
16904
  }
16905
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
16906
  "node_modules/webpack-dev-server/node_modules/ws": {
16907
  "version": "8.19.0",
16908
  "resolved": "https://registry.npmjs.org/ws/-/ws-8.19.0.tgz",
 
8
  "name": "react-template",
9
  "version": "0.1.0",
10
  "dependencies": {
11
+ "@huggingface/hub": "^2.6.4",
12
  "@testing-library/dom": "^10.4.0",
13
  "@testing-library/jest-dom": "^6.6.3",
14
  "@testing-library/react": "^16.3.0",
15
  "@testing-library/user-event": "^13.5.0",
16
+ "concurrently": "^9.2.1",
17
+ "express": "^5.1.0",
18
  "react": "^19.1.0",
19
  "react-dom": "^19.1.0",
20
  "react-scripts": "5.0.1",
21
  "recharts": "^3.7.0",
22
  "web-vitals": "^2.1.4"
23
+ },
24
+ "devDependencies": {
25
+ "@playwright/test": "^1.58.2"
26
  }
27
  },
28
  "node_modules/@adobe/css-tools": {
 
2463
  "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
2464
  }
2465
  },
2466
+ "node_modules/@huggingface/hub": {
2467
+ "version": "2.11.0",
2468
+ "resolved": "https://registry.npmjs.org/@huggingface/hub/-/hub-2.11.0.tgz",
2469
+ "integrity": "sha512-WS6QGaXYeBVFlaB4SOn6z4LGUpLB5kRZNL08uUni4izX353KxiwwZMK5+/AWX86MJh8SMZNa/JFcvFCcQsbszQ==",
2470
+ "license": "MIT",
2471
+ "dependencies": {
2472
+ "@huggingface/tasks": "^0.19.90"
2473
+ },
2474
+ "bin": {
2475
+ "hfjs": "dist/cli.js"
2476
+ },
2477
+ "engines": {
2478
+ "node": ">=18"
2479
+ },
2480
+ "optionalDependencies": {
2481
+ "cli-progress": "^3.12.0"
2482
+ }
2483
+ },
2484
+ "node_modules/@huggingface/tasks": {
2485
+ "version": "0.19.90",
2486
+ "resolved": "https://registry.npmjs.org/@huggingface/tasks/-/tasks-0.19.90.tgz",
2487
+ "integrity": "sha512-nfV9luJbvwGQ/5oKXkKhCV9h4X7mwh1YaGG3ORd6UMLDSwr1OFSSatcBX0O9OtBtmNK19aGSjbLFqqgcIR6+IA==",
2488
+ "license": "MIT"
2489
+ },
2490
  "node_modules/@humanwhocodes/config-array": {
2491
  "version": "0.13.0",
2492
  "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.13.0.tgz",
 
2968
  "node": ">= 8"
2969
  }
2970
  },
2971
+ "node_modules/@playwright/test": {
2972
+ "version": "1.58.2",
2973
+ "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.58.2.tgz",
2974
+ "integrity": "sha512-akea+6bHYBBfA9uQqSYmlJXn61cTa+jbO87xVLCWbTqbWadRVmhxlXATaOjOgcBaWU4ePo0wB41KMFv3o35IXA==",
2975
+ "dev": true,
2976
+ "license": "Apache-2.0",
2977
+ "dependencies": {
2978
+ "playwright": "1.58.2"
2979
+ },
2980
+ "bin": {
2981
+ "playwright": "cli.js"
2982
+ },
2983
+ "engines": {
2984
+ "node": ">=18"
2985
+ }
2986
+ },
2987
  "node_modules/@pmmmwh/react-refresh-webpack-plugin": {
2988
  "version": "0.5.17",
2989
  "resolved": "https://registry.npmjs.org/@pmmmwh/react-refresh-webpack-plugin/-/react-refresh-webpack-plugin-0.5.17.tgz",
 
5295
  "license": "MIT"
5296
  },
5297
  "node_modules/body-parser": {
5298
+ "version": "2.2.2",
5299
+ "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.2.tgz",
5300
+ "integrity": "sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA==",
5301
  "license": "MIT",
5302
  "dependencies": {
5303
+ "bytes": "^3.1.2",
5304
+ "content-type": "^1.0.5",
5305
+ "debug": "^4.4.3",
5306
+ "http-errors": "^2.0.0",
5307
+ "iconv-lite": "^0.7.0",
5308
+ "on-finished": "^2.4.1",
5309
+ "qs": "^6.14.1",
5310
+ "raw-body": "^3.0.1",
5311
+ "type-is": "^2.0.1"
 
 
 
5312
  },
5313
  "engines": {
5314
+ "node": ">=18"
5315
+ },
5316
+ "funding": {
5317
+ "type": "opencollective",
5318
+ "url": "https://opencollective.com/express"
 
 
 
 
 
 
5319
  }
5320
  },
5321
  "node_modules/body-parser/node_modules/iconv-lite": {
5322
+ "version": "0.7.2",
5323
+ "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz",
5324
+ "integrity": "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==",
5325
  "license": "MIT",
5326
  "dependencies": {
5327
+ "safer-buffer": ">= 2.1.2 < 3.0.0"
5328
  },
5329
  "engines": {
5330
  "node": ">=0.10.0"
5331
+ },
5332
+ "funding": {
5333
+ "type": "opencollective",
5334
+ "url": "https://opencollective.com/express"
5335
  }
5336
  },
 
 
 
 
 
 
5337
  "node_modules/bonjour-service": {
5338
  "version": "1.3.0",
5339
  "resolved": "https://registry.npmjs.org/bonjour-service/-/bonjour-service-1.3.0.tgz",
 
5708
  "node": ">=0.10.0"
5709
  }
5710
  },
5711
+ "node_modules/cli-progress": {
5712
+ "version": "3.12.0",
5713
+ "resolved": "https://registry.npmjs.org/cli-progress/-/cli-progress-3.12.0.tgz",
5714
+ "integrity": "sha512-tRkV3HJ1ASwm19THiiLIXLO7Im7wlTuKnvkYaTkyoAPefqjNg7W7DHKUlGRxy9vxDvbyCYQkQozvptuMkGCg8A==",
5715
+ "license": "MIT",
5716
+ "optional": true,
5717
+ "dependencies": {
5718
+ "string-width": "^4.2.3"
5719
+ },
5720
+ "engines": {
5721
+ "node": ">=4"
5722
+ }
5723
+ },
5724
  "node_modules/cliui": {
5725
  "version": "7.0.4",
5726
  "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz",
 
5959
  "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==",
5960
  "license": "MIT"
5961
  },
5962
+ "node_modules/concurrently": {
5963
+ "version": "9.2.1",
5964
+ "resolved": "https://registry.npmjs.org/concurrently/-/concurrently-9.2.1.tgz",
5965
+ "integrity": "sha512-fsfrO0MxV64Znoy8/l1vVIjjHa29SZyyqPgQBwhiDcaW8wJc2W3XWVOGx4M3oJBnv/zdUZIIp1gDeS98GzP8Ng==",
5966
+ "license": "MIT",
5967
+ "dependencies": {
5968
+ "chalk": "4.1.2",
5969
+ "rxjs": "7.8.2",
5970
+ "shell-quote": "1.8.3",
5971
+ "supports-color": "8.1.1",
5972
+ "tree-kill": "1.2.2",
5973
+ "yargs": "17.7.2"
5974
+ },
5975
+ "bin": {
5976
+ "conc": "dist/bin/concurrently.js",
5977
+ "concurrently": "dist/bin/concurrently.js"
5978
+ },
5979
+ "engines": {
5980
+ "node": ">=18"
5981
+ },
5982
+ "funding": {
5983
+ "url": "https://github.com/open-cli-tools/concurrently?sponsor=1"
5984
+ }
5985
+ },
5986
+ "node_modules/concurrently/node_modules/cliui": {
5987
+ "version": "8.0.1",
5988
+ "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz",
5989
+ "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==",
5990
+ "license": "ISC",
5991
+ "dependencies": {
5992
+ "string-width": "^4.2.0",
5993
+ "strip-ansi": "^6.0.1",
5994
+ "wrap-ansi": "^7.0.0"
5995
+ },
5996
+ "engines": {
5997
+ "node": ">=12"
5998
+ }
5999
+ },
6000
+ "node_modules/concurrently/node_modules/supports-color": {
6001
+ "version": "8.1.1",
6002
+ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz",
6003
+ "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==",
6004
+ "license": "MIT",
6005
+ "dependencies": {
6006
+ "has-flag": "^4.0.0"
6007
+ },
6008
+ "engines": {
6009
+ "node": ">=10"
6010
+ },
6011
+ "funding": {
6012
+ "url": "https://github.com/chalk/supports-color?sponsor=1"
6013
+ }
6014
+ },
6015
+ "node_modules/concurrently/node_modules/yargs": {
6016
+ "version": "17.7.2",
6017
+ "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz",
6018
+ "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==",
6019
+ "license": "MIT",
6020
+ "dependencies": {
6021
+ "cliui": "^8.0.1",
6022
+ "escalade": "^3.1.1",
6023
+ "get-caller-file": "^2.0.5",
6024
+ "require-directory": "^2.1.1",
6025
+ "string-width": "^4.2.3",
6026
+ "y18n": "^5.0.5",
6027
+ "yargs-parser": "^21.1.1"
6028
+ },
6029
+ "engines": {
6030
+ "node": ">=12"
6031
+ }
6032
+ },
6033
+ "node_modules/concurrently/node_modules/yargs-parser": {
6034
+ "version": "21.1.1",
6035
+ "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz",
6036
+ "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==",
6037
+ "license": "ISC",
6038
+ "engines": {
6039
+ "node": ">=12"
6040
+ }
6041
+ },
6042
  "node_modules/confusing-browser-globals": {
6043
  "version": "1.0.11",
6044
  "resolved": "https://registry.npmjs.org/confusing-browser-globals/-/confusing-browser-globals-1.0.11.tgz",
 
6055
  }
6056
  },
6057
  "node_modules/content-disposition": {
6058
+ "version": "1.0.1",
6059
+ "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.1.tgz",
6060
+ "integrity": "sha512-oIXISMynqSqm241k6kcQ5UwttDILMK4BiurCfGEREw6+X9jkkpEe5T9FZaApyLGGOnFuyMWZpdolTXMtvEJ08Q==",
6061
  "license": "MIT",
 
 
 
6062
  "engines": {
6063
+ "node": ">=18"
6064
+ },
6065
+ "funding": {
6066
+ "type": "opencollective",
6067
+ "url": "https://opencollective.com/express"
6068
  }
6069
  },
6070
  "node_modules/content-type": {
 
6092
  }
6093
  },
6094
  "node_modules/cookie-signature": {
6095
+ "version": "1.2.2",
6096
+ "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz",
6097
+ "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==",
6098
+ "license": "MIT",
6099
+ "engines": {
6100
+ "node": ">=6.6.0"
6101
+ }
6102
  },
6103
  "node_modules/core-js": {
6104
  "version": "3.48.0",
 
8168
  }
8169
  },
8170
  "node_modules/express": {
8171
+ "version": "5.2.1",
8172
+ "resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz",
8173
+ "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==",
8174
+ "license": "MIT",
8175
+ "dependencies": {
8176
+ "accepts": "^2.0.0",
8177
+ "body-parser": "^2.2.1",
8178
+ "content-disposition": "^1.0.0",
8179
+ "content-type": "^1.0.5",
8180
+ "cookie": "^0.7.1",
8181
+ "cookie-signature": "^1.2.1",
8182
+ "debug": "^4.4.0",
8183
+ "depd": "^2.0.0",
8184
+ "encodeurl": "^2.0.0",
8185
+ "escape-html": "^1.0.3",
8186
+ "etag": "^1.8.1",
8187
+ "finalhandler": "^2.1.0",
8188
+ "fresh": "^2.0.0",
8189
+ "http-errors": "^2.0.0",
8190
+ "merge-descriptors": "^2.0.0",
8191
+ "mime-types": "^3.0.0",
8192
+ "on-finished": "^2.4.1",
8193
+ "once": "^1.4.0",
8194
+ "parseurl": "^1.3.3",
8195
+ "proxy-addr": "^2.0.7",
8196
+ "qs": "^6.14.0",
8197
+ "range-parser": "^1.2.1",
8198
+ "router": "^2.2.0",
8199
+ "send": "^1.1.0",
8200
+ "serve-static": "^2.2.0",
8201
+ "statuses": "^2.0.1",
8202
+ "type-is": "^2.0.1",
8203
+ "vary": "^1.1.2"
 
 
 
8204
  },
8205
  "engines": {
8206
+ "node": ">= 18"
8207
  },
8208
  "funding": {
8209
  "type": "opencollective",
8210
  "url": "https://opencollective.com/express"
8211
  }
8212
  },
8213
+ "node_modules/express/node_modules/accepts": {
8214
+ "version": "2.0.0",
8215
+ "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz",
8216
+ "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==",
8217
  "license": "MIT",
8218
  "dependencies": {
8219
+ "mime-types": "^3.0.0",
8220
+ "negotiator": "^1.0.0"
8221
+ },
8222
+ "engines": {
8223
+ "node": ">= 0.6"
8224
  }
8225
  },
8226
+ "node_modules/express/node_modules/mime-db": {
8227
+ "version": "1.54.0",
8228
+ "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz",
8229
+ "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==",
8230
+ "license": "MIT",
8231
+ "engines": {
8232
+ "node": ">= 0.6"
8233
+ }
8234
+ },
8235
+ "node_modules/express/node_modules/mime-types": {
8236
+ "version": "3.0.2",
8237
+ "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz",
8238
+ "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==",
8239
+ "license": "MIT",
8240
+ "dependencies": {
8241
+ "mime-db": "^1.54.0"
8242
+ },
8243
+ "engines": {
8244
+ "node": ">=18"
8245
+ },
8246
+ "funding": {
8247
+ "type": "opencollective",
8248
+ "url": "https://opencollective.com/express"
8249
+ }
8250
+ },
8251
+ "node_modules/express/node_modules/negotiator": {
8252
+ "version": "1.0.0",
8253
+ "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz",
8254
+ "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==",
8255
+ "license": "MIT",
8256
+ "engines": {
8257
+ "node": ">= 0.6"
8258
+ }
8259
  },
8260
  "node_modules/fast-deep-equal": {
8261
  "version": "3.1.3",
 
8451
  }
8452
  },
8453
  "node_modules/finalhandler": {
8454
+ "version": "2.1.1",
8455
+ "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.1.tgz",
8456
+ "integrity": "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==",
8457
  "license": "MIT",
8458
  "dependencies": {
8459
+ "debug": "^4.4.0",
8460
+ "encodeurl": "^2.0.0",
8461
+ "escape-html": "^1.0.3",
8462
+ "on-finished": "^2.4.1",
8463
+ "parseurl": "^1.3.3",
8464
+ "statuses": "^2.0.1"
 
8465
  },
8466
  "engines": {
8467
+ "node": ">= 18.0.0"
8468
+ },
8469
+ "funding": {
8470
+ "type": "opencollective",
8471
+ "url": "https://opencollective.com/express"
 
 
 
 
 
8472
  }
8473
  },
 
 
 
 
 
 
8474
  "node_modules/find-cache-dir": {
8475
  "version": "3.3.2",
8476
  "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-3.3.2.tgz",
 
8692
  }
8693
  },
8694
  "node_modules/fresh": {
8695
+ "version": "2.0.0",
8696
+ "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz",
8697
+ "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==",
8698
  "license": "MIT",
8699
  "engines": {
8700
+ "node": ">= 0.8"
8701
  }
8702
  },
8703
  "node_modules/fs-extra": {
 
9932
  "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==",
9933
  "license": "MIT"
9934
  },
9935
+ "node_modules/is-promise": {
9936
+ "version": "4.0.0",
9937
+ "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz",
9938
+ "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==",
9939
+ "license": "MIT"
9940
+ },
9941
  "node_modules/is-regex": {
9942
  "version": "1.2.1",
9943
  "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz",
 
11612
  "license": "CC0-1.0"
11613
  },
11614
  "node_modules/media-typer": {
11615
+ "version": "1.1.0",
11616
+ "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz",
11617
+ "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==",
11618
  "license": "MIT",
11619
  "engines": {
11620
+ "node": ">= 0.8"
11621
  }
11622
  },
11623
  "node_modules/memfs": {
 
11633
  }
11634
  },
11635
  "node_modules/merge-descriptors": {
11636
+ "version": "2.0.0",
11637
+ "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz",
11638
+ "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==",
11639
  "license": "MIT",
11640
+ "engines": {
11641
+ "node": ">=18"
11642
+ },
11643
  "funding": {
11644
  "url": "https://github.com/sponsors/sindresorhus"
11645
  }
 
12370
  "license": "MIT"
12371
  },
12372
  "node_modules/path-to-regexp": {
12373
+ "version": "8.3.0",
12374
+ "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.3.0.tgz",
12375
+ "integrity": "sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA==",
12376
+ "license": "MIT",
12377
+ "funding": {
12378
+ "type": "opencollective",
12379
+ "url": "https://opencollective.com/express"
12380
+ }
12381
  },
12382
  "node_modules/path-type": {
12383
  "version": "4.0.0",
 
12500
  "node": ">=4"
12501
  }
12502
  },
12503
+ "node_modules/playwright": {
12504
+ "version": "1.58.2",
12505
+ "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.58.2.tgz",
12506
+ "integrity": "sha512-vA30H8Nvkq/cPBnNw4Q8TWz1EJyqgpuinBcHET0YVJVFldr8JDNiU9LaWAE1KqSkRYazuaBhTpB5ZzShOezQ6A==",
12507
+ "dev": true,
12508
+ "license": "Apache-2.0",
12509
+ "dependencies": {
12510
+ "playwright-core": "1.58.2"
12511
+ },
12512
+ "bin": {
12513
+ "playwright": "cli.js"
12514
+ },
12515
+ "engines": {
12516
+ "node": ">=18"
12517
+ },
12518
+ "optionalDependencies": {
12519
+ "fsevents": "2.3.2"
12520
+ }
12521
+ },
12522
+ "node_modules/playwright-core": {
12523
+ "version": "1.58.2",
12524
+ "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.58.2.tgz",
12525
+ "integrity": "sha512-yZkEtftgwS8CsfYo7nm0KE8jsvm6i/PTgVtB8DL726wNf6H2IMsDuxCpJj59KDaxCtSnrWan2AeDqM7JBaultg==",
12526
+ "dev": true,
12527
+ "license": "Apache-2.0",
12528
+ "bin": {
12529
+ "playwright-core": "cli.js"
12530
+ },
12531
+ "engines": {
12532
+ "node": ">=18"
12533
+ }
12534
+ },
12535
+ "node_modules/playwright/node_modules/fsevents": {
12536
+ "version": "2.3.2",
12537
+ "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
12538
+ "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
12539
+ "dev": true,
12540
+ "hasInstallScript": true,
12541
+ "license": "MIT",
12542
+ "optional": true,
12543
+ "os": [
12544
+ "darwin"
12545
+ ],
12546
+ "engines": {
12547
+ "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
12548
+ }
12549
+ },
12550
  "node_modules/possible-typed-array-names": {
12551
  "version": "1.1.0",
12552
  "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz",
 
13957
  }
13958
  },
13959
  "node_modules/qs": {
13960
+ "version": "6.15.0",
13961
+ "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.0.tgz",
13962
+ "integrity": "sha512-mAZTtNCeetKMH+pSjrb76NAM8V9a05I9aBZOHztWy/UqcJdQYNsf59vrRKWnojAT9Y+GbIvoTBC++CPHqpDBhQ==",
13963
  "license": "BSD-3-Clause",
13964
  "dependencies": {
13965
  "side-channel": "^1.1.0"
 
14025
  }
14026
  },
14027
  "node_modules/raw-body": {
14028
+ "version": "3.0.2",
14029
+ "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.2.tgz",
14030
+ "integrity": "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==",
14031
  "license": "MIT",
14032
  "dependencies": {
14033
  "bytes": "~3.1.2",
14034
  "http-errors": "~2.0.1",
14035
+ "iconv-lite": "~0.7.0",
14036
  "unpipe": "~1.0.0"
14037
  },
14038
  "engines": {
14039
+ "node": ">= 0.10"
14040
  }
14041
  },
14042
  "node_modules/raw-body/node_modules/iconv-lite": {
14043
+ "version": "0.7.2",
14044
+ "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz",
14045
+ "integrity": "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==",
14046
  "license": "MIT",
14047
  "dependencies": {
14048
+ "safer-buffer": ">= 2.1.2 < 3.0.0"
14049
  },
14050
  "engines": {
14051
  "node": ">=0.10.0"
14052
+ },
14053
+ "funding": {
14054
+ "type": "opencollective",
14055
+ "url": "https://opencollective.com/express"
14056
  }
14057
  },
14058
  "node_modules/react": {
 
14800
  "randombytes": "^2.1.0"
14801
  }
14802
  },
14803
+ "node_modules/router": {
14804
+ "version": "2.2.0",
14805
+ "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz",
14806
+ "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==",
14807
+ "license": "MIT",
14808
+ "dependencies": {
14809
+ "debug": "^4.4.0",
14810
+ "depd": "^2.0.0",
14811
+ "is-promise": "^4.0.0",
14812
+ "parseurl": "^1.3.3",
14813
+ "path-to-regexp": "^8.0.0"
14814
+ },
14815
+ "engines": {
14816
+ "node": ">= 18"
14817
+ }
14818
+ },
14819
  "node_modules/run-parallel": {
14820
  "version": "1.2.0",
14821
  "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz",
 
14839
  "queue-microtask": "^1.2.2"
14840
  }
14841
  },
14842
+ "node_modules/rxjs": {
14843
+ "version": "7.8.2",
14844
+ "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz",
14845
+ "integrity": "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==",
14846
+ "license": "Apache-2.0",
14847
+ "dependencies": {
14848
+ "tslib": "^2.1.0"
14849
+ }
14850
+ },
14851
  "node_modules/safe-array-concat": {
14852
  "version": "1.1.3",
14853
  "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.3.tgz",
 
15079
  }
15080
  },
15081
  "node_modules/send": {
15082
+ "version": "1.2.1",
15083
+ "resolved": "https://registry.npmjs.org/send/-/send-1.2.1.tgz",
15084
+ "integrity": "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==",
15085
  "license": "MIT",
15086
  "dependencies": {
15087
+ "debug": "^4.4.3",
15088
+ "encodeurl": "^2.0.0",
15089
+ "escape-html": "^1.0.3",
15090
+ "etag": "^1.8.1",
15091
+ "fresh": "^2.0.0",
15092
+ "http-errors": "^2.0.1",
15093
+ "mime-types": "^3.0.2",
15094
+ "ms": "^2.1.3",
15095
+ "on-finished": "^2.4.1",
15096
+ "range-parser": "^1.2.1",
15097
+ "statuses": "^2.0.2"
 
 
15098
  },
15099
  "engines": {
15100
+ "node": ">= 18"
15101
+ },
15102
+ "funding": {
15103
+ "type": "opencollective",
15104
+ "url": "https://opencollective.com/express"
15105
  }
15106
  },
15107
+ "node_modules/send/node_modules/mime-db": {
15108
+ "version": "1.54.0",
15109
+ "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz",
15110
+ "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==",
15111
  "license": "MIT",
15112
+ "engines": {
15113
+ "node": ">= 0.6"
15114
  }
15115
  },
15116
+ "node_modules/send/node_modules/mime-types": {
15117
+ "version": "3.0.2",
15118
+ "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz",
15119
+ "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==",
15120
+ "license": "MIT",
15121
+ "dependencies": {
15122
+ "mime-db": "^1.54.0"
15123
+ },
15124
+ "engines": {
15125
+ "node": ">=18"
15126
+ },
15127
+ "funding": {
15128
+ "type": "opencollective",
15129
+ "url": "https://opencollective.com/express"
15130
+ }
15131
  },
15132
  "node_modules/serialize-javascript": {
15133
  "version": "6.0.2",
 
15210
  }
15211
  },
15212
  "node_modules/serve-static": {
15213
+ "version": "2.2.1",
15214
+ "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.1.tgz",
15215
+ "integrity": "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==",
15216
  "license": "MIT",
15217
  "dependencies": {
15218
+ "encodeurl": "^2.0.0",
15219
+ "escape-html": "^1.0.3",
15220
+ "parseurl": "^1.3.3",
15221
+ "send": "^1.2.0"
15222
  },
15223
  "engines": {
15224
+ "node": ">= 18"
15225
+ },
15226
+ "funding": {
15227
+ "type": "opencollective",
15228
+ "url": "https://opencollective.com/express"
15229
  }
15230
  },
15231
  "node_modules/set-function-length": {
 
16474
  "node": ">=8"
16475
  }
16476
  },
16477
+ "node_modules/tree-kill": {
16478
+ "version": "1.2.2",
16479
+ "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz",
16480
+ "integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==",
16481
+ "license": "MIT",
16482
+ "bin": {
16483
+ "tree-kill": "cli.js"
16484
+ }
16485
+ },
16486
  "node_modules/tryer": {
16487
  "version": "1.0.1",
16488
  "resolved": "https://registry.npmjs.org/tryer/-/tryer-1.0.1.tgz",
 
16589
  }
16590
  },
16591
  "node_modules/type-is": {
16592
+ "version": "2.0.1",
16593
+ "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz",
16594
+ "integrity": "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==",
16595
  "license": "MIT",
16596
  "dependencies": {
16597
+ "content-type": "^1.0.5",
16598
+ "media-typer": "^1.1.0",
16599
+ "mime-types": "^3.0.0"
16600
  },
16601
  "engines": {
16602
  "node": ">= 0.6"
16603
  }
16604
  },
16605
+ "node_modules/type-is/node_modules/mime-db": {
16606
+ "version": "1.54.0",
16607
+ "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz",
16608
+ "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==",
16609
+ "license": "MIT",
16610
+ "engines": {
16611
+ "node": ">= 0.6"
16612
+ }
16613
+ },
16614
+ "node_modules/type-is/node_modules/mime-types": {
16615
+ "version": "3.0.2",
16616
+ "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz",
16617
+ "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==",
16618
+ "license": "MIT",
16619
+ "dependencies": {
16620
+ "mime-db": "^1.54.0"
16621
+ },
16622
+ "engines": {
16623
+ "node": ">=18"
16624
+ },
16625
+ "funding": {
16626
+ "type": "opencollective",
16627
+ "url": "https://opencollective.com/express"
16628
+ }
16629
+ },
16630
  "node_modules/typed-array-buffer": {
16631
  "version": "1.0.3",
16632
  "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.3.tgz",
 
17192
  }
17193
  }
17194
  },
17195
+ "node_modules/webpack-dev-server/node_modules/body-parser": {
17196
+ "version": "1.20.4",
17197
+ "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.4.tgz",
17198
+ "integrity": "sha512-ZTgYYLMOXY9qKU/57FAo8F+HA2dGX7bqGc71txDRC1rS4frdFI5R7NhluHxH6M0YItAP0sHB4uqAOcYKxO6uGA==",
17199
+ "license": "MIT",
17200
+ "dependencies": {
17201
+ "bytes": "~3.1.2",
17202
+ "content-type": "~1.0.5",
17203
+ "debug": "2.6.9",
17204
+ "depd": "2.0.0",
17205
+ "destroy": "~1.2.0",
17206
+ "http-errors": "~2.0.1",
17207
+ "iconv-lite": "~0.4.24",
17208
+ "on-finished": "~2.4.1",
17209
+ "qs": "~6.14.0",
17210
+ "raw-body": "~2.5.3",
17211
+ "type-is": "~1.6.18",
17212
+ "unpipe": "~1.0.0"
17213
+ },
17214
+ "engines": {
17215
+ "node": ">= 0.8",
17216
+ "npm": "1.2.8000 || >= 1.4.16"
17217
+ }
17218
+ },
17219
+ "node_modules/webpack-dev-server/node_modules/content-disposition": {
17220
+ "version": "0.5.4",
17221
+ "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz",
17222
+ "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==",
17223
+ "license": "MIT",
17224
+ "dependencies": {
17225
+ "safe-buffer": "5.2.1"
17226
+ },
17227
+ "engines": {
17228
+ "node": ">= 0.6"
17229
+ }
17230
+ },
17231
+ "node_modules/webpack-dev-server/node_modules/cookie-signature": {
17232
+ "version": "1.0.7",
17233
+ "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.7.tgz",
17234
+ "integrity": "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==",
17235
+ "license": "MIT"
17236
+ },
17237
+ "node_modules/webpack-dev-server/node_modules/debug": {
17238
+ "version": "2.6.9",
17239
+ "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
17240
+ "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
17241
+ "license": "MIT",
17242
+ "dependencies": {
17243
+ "ms": "2.0.0"
17244
+ }
17245
+ },
17246
+ "node_modules/webpack-dev-server/node_modules/debug/node_modules/ms": {
17247
+ "version": "2.0.0",
17248
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
17249
+ "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
17250
+ "license": "MIT"
17251
+ },
17252
+ "node_modules/webpack-dev-server/node_modules/express": {
17253
+ "version": "4.22.1",
17254
+ "resolved": "https://registry.npmjs.org/express/-/express-4.22.1.tgz",
17255
+ "integrity": "sha512-F2X8g9P1X7uCPZMA3MVf9wcTqlyNp7IhH5qPCI0izhaOIYXaW9L535tGA3qmjRzpH+bZczqq7hVKxTR4NWnu+g==",
17256
+ "license": "MIT",
17257
+ "dependencies": {
17258
+ "accepts": "~1.3.8",
17259
+ "array-flatten": "1.1.1",
17260
+ "body-parser": "~1.20.3",
17261
+ "content-disposition": "~0.5.4",
17262
+ "content-type": "~1.0.4",
17263
+ "cookie": "~0.7.1",
17264
+ "cookie-signature": "~1.0.6",
17265
+ "debug": "2.6.9",
17266
+ "depd": "2.0.0",
17267
+ "encodeurl": "~2.0.0",
17268
+ "escape-html": "~1.0.3",
17269
+ "etag": "~1.8.1",
17270
+ "finalhandler": "~1.3.1",
17271
+ "fresh": "~0.5.2",
17272
+ "http-errors": "~2.0.0",
17273
+ "merge-descriptors": "1.0.3",
17274
+ "methods": "~1.1.2",
17275
+ "on-finished": "~2.4.1",
17276
+ "parseurl": "~1.3.3",
17277
+ "path-to-regexp": "~0.1.12",
17278
+ "proxy-addr": "~2.0.7",
17279
+ "qs": "~6.14.0",
17280
+ "range-parser": "~1.2.1",
17281
+ "safe-buffer": "5.2.1",
17282
+ "send": "~0.19.0",
17283
+ "serve-static": "~1.16.2",
17284
+ "setprototypeof": "1.2.0",
17285
+ "statuses": "~2.0.1",
17286
+ "type-is": "~1.6.18",
17287
+ "utils-merge": "1.0.1",
17288
+ "vary": "~1.1.2"
17289
+ },
17290
+ "engines": {
17291
+ "node": ">= 0.10.0"
17292
+ },
17293
+ "funding": {
17294
+ "type": "opencollective",
17295
+ "url": "https://opencollective.com/express"
17296
+ }
17297
+ },
17298
+ "node_modules/webpack-dev-server/node_modules/finalhandler": {
17299
+ "version": "1.3.2",
17300
+ "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.2.tgz",
17301
+ "integrity": "sha512-aA4RyPcd3badbdABGDuTXCMTtOneUCAYH/gxoYRTZlIJdF0YPWuGqiAsIrhNnnqdXGswYk6dGujem4w80UJFhg==",
17302
+ "license": "MIT",
17303
+ "dependencies": {
17304
+ "debug": "2.6.9",
17305
+ "encodeurl": "~2.0.0",
17306
+ "escape-html": "~1.0.3",
17307
+ "on-finished": "~2.4.1",
17308
+ "parseurl": "~1.3.3",
17309
+ "statuses": "~2.0.2",
17310
+ "unpipe": "~1.0.0"
17311
+ },
17312
+ "engines": {
17313
+ "node": ">= 0.8"
17314
+ }
17315
+ },
17316
+ "node_modules/webpack-dev-server/node_modules/fresh": {
17317
+ "version": "0.5.2",
17318
+ "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz",
17319
+ "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==",
17320
+ "license": "MIT",
17321
+ "engines": {
17322
+ "node": ">= 0.6"
17323
+ }
17324
+ },
17325
+ "node_modules/webpack-dev-server/node_modules/iconv-lite": {
17326
+ "version": "0.4.24",
17327
+ "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
17328
+ "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==",
17329
+ "license": "MIT",
17330
+ "dependencies": {
17331
+ "safer-buffer": ">= 2.1.2 < 3"
17332
+ },
17333
+ "engines": {
17334
+ "node": ">=0.10.0"
17335
+ }
17336
+ },
17337
+ "node_modules/webpack-dev-server/node_modules/media-typer": {
17338
+ "version": "0.3.0",
17339
+ "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz",
17340
+ "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==",
17341
+ "license": "MIT",
17342
+ "engines": {
17343
+ "node": ">= 0.6"
17344
+ }
17345
+ },
17346
+ "node_modules/webpack-dev-server/node_modules/merge-descriptors": {
17347
+ "version": "1.0.3",
17348
+ "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz",
17349
+ "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==",
17350
+ "license": "MIT",
17351
+ "funding": {
17352
+ "url": "https://github.com/sponsors/sindresorhus"
17353
+ }
17354
+ },
17355
+ "node_modules/webpack-dev-server/node_modules/path-to-regexp": {
17356
+ "version": "0.1.12",
17357
+ "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz",
17358
+ "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==",
17359
+ "license": "MIT"
17360
+ },
17361
+ "node_modules/webpack-dev-server/node_modules/qs": {
17362
+ "version": "6.14.2",
17363
+ "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.2.tgz",
17364
+ "integrity": "sha512-V/yCWTTF7VJ9hIh18Ugr2zhJMP01MY7c5kh4J870L7imm6/DIzBsNLTXzMwUA3yZ5b/KBqLx8Kp3uRvd7xSe3Q==",
17365
+ "license": "BSD-3-Clause",
17366
+ "dependencies": {
17367
+ "side-channel": "^1.1.0"
17368
+ },
17369
+ "engines": {
17370
+ "node": ">=0.6"
17371
+ },
17372
+ "funding": {
17373
+ "url": "https://github.com/sponsors/ljharb"
17374
+ }
17375
+ },
17376
+ "node_modules/webpack-dev-server/node_modules/raw-body": {
17377
+ "version": "2.5.3",
17378
+ "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.3.tgz",
17379
+ "integrity": "sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA==",
17380
+ "license": "MIT",
17381
+ "dependencies": {
17382
+ "bytes": "~3.1.2",
17383
+ "http-errors": "~2.0.1",
17384
+ "iconv-lite": "~0.4.24",
17385
+ "unpipe": "~1.0.0"
17386
+ },
17387
+ "engines": {
17388
+ "node": ">= 0.8"
17389
+ }
17390
+ },
17391
+ "node_modules/webpack-dev-server/node_modules/send": {
17392
+ "version": "0.19.2",
17393
+ "resolved": "https://registry.npmjs.org/send/-/send-0.19.2.tgz",
17394
+ "integrity": "sha512-VMbMxbDeehAxpOtWJXlcUS5E8iXh6QmN+BkRX1GARS3wRaXEEgzCcB10gTQazO42tpNIya8xIyNx8fll1OFPrg==",
17395
+ "license": "MIT",
17396
+ "dependencies": {
17397
+ "debug": "2.6.9",
17398
+ "depd": "2.0.0",
17399
+ "destroy": "1.2.0",
17400
+ "encodeurl": "~2.0.0",
17401
+ "escape-html": "~1.0.3",
17402
+ "etag": "~1.8.1",
17403
+ "fresh": "~0.5.2",
17404
+ "http-errors": "~2.0.1",
17405
+ "mime": "1.6.0",
17406
+ "ms": "2.1.3",
17407
+ "on-finished": "~2.4.1",
17408
+ "range-parser": "~1.2.1",
17409
+ "statuses": "~2.0.2"
17410
+ },
17411
+ "engines": {
17412
+ "node": ">= 0.8.0"
17413
+ }
17414
+ },
17415
+ "node_modules/webpack-dev-server/node_modules/serve-static": {
17416
+ "version": "1.16.3",
17417
+ "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.3.tgz",
17418
+ "integrity": "sha512-x0RTqQel6g5SY7Lg6ZreMmsOzncHFU7nhnRWkKgWuMTu5NN0DR5oruckMqRvacAN9d5w6ARnRBXl9xhDCgfMeA==",
17419
+ "license": "MIT",
17420
+ "dependencies": {
17421
+ "encodeurl": "~2.0.0",
17422
+ "escape-html": "~1.0.3",
17423
+ "parseurl": "~1.3.3",
17424
+ "send": "~0.19.1"
17425
+ },
17426
+ "engines": {
17427
+ "node": ">= 0.8.0"
17428
+ }
17429
+ },
17430
+ "node_modules/webpack-dev-server/node_modules/type-is": {
17431
+ "version": "1.6.18",
17432
+ "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz",
17433
+ "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==",
17434
+ "license": "MIT",
17435
+ "dependencies": {
17436
+ "media-typer": "0.3.0",
17437
+ "mime-types": "~2.1.24"
17438
+ },
17439
+ "engines": {
17440
+ "node": ">= 0.6"
17441
+ }
17442
+ },
17443
  "node_modules/webpack-dev-server/node_modules/ws": {
17444
  "version": "8.19.0",
17445
  "resolved": "https://registry.npmjs.org/ws/-/ws-8.19.0.tgz",
package.json CHANGED
@@ -3,10 +3,13 @@
3
  "version": "0.1.0",
4
  "private": true,
5
  "dependencies": {
 
6
  "@testing-library/dom": "^10.4.0",
7
  "@testing-library/jest-dom": "^6.6.3",
8
  "@testing-library/react": "^16.3.0",
9
  "@testing-library/user-event": "^13.5.0",
 
 
10
  "react": "^19.1.0",
11
  "react-dom": "^19.1.0",
12
  "react-scripts": "5.0.1",
@@ -15,10 +18,13 @@
15
  },
16
  "scripts": {
17
  "start": "react-scripts start",
 
 
18
  "build": "react-scripts build",
19
  "test": "react-scripts test",
20
  "eject": "react-scripts eject"
21
  },
 
22
  "eslintConfig": {
23
  "extends": [
24
  "react-app",
@@ -36,5 +42,8 @@
36
  "last 1 firefox version",
37
  "last 1 safari version"
38
  ]
 
 
 
39
  }
40
  }
 
3
  "version": "0.1.0",
4
  "private": true,
5
  "dependencies": {
6
+ "@huggingface/hub": "^2.6.4",
7
  "@testing-library/dom": "^10.4.0",
8
  "@testing-library/jest-dom": "^6.6.3",
9
  "@testing-library/react": "^16.3.0",
10
  "@testing-library/user-event": "^13.5.0",
11
+ "concurrently": "^9.2.1",
12
+ "express": "^5.1.0",
13
  "react": "^19.1.0",
14
  "react-dom": "^19.1.0",
15
  "react-scripts": "5.0.1",
 
18
  },
19
  "scripts": {
20
  "start": "react-scripts start",
21
+ "server": "node server/index.js",
22
+ "dev": "concurrently \"npm run server\" \"npm start\"",
23
  "build": "react-scripts build",
24
  "test": "react-scripts test",
25
  "eject": "react-scripts eject"
26
  },
27
+ "proxy": "http://localhost:4000",
28
  "eslintConfig": {
29
  "extends": [
30
  "react-app",
 
42
  "last 1 firefox version",
43
  "last 1 safari version"
44
  ]
45
+ },
46
+ "devDependencies": {
47
+ "@playwright/test": "^1.58.2"
48
  }
49
  }
server/index.js ADDED
@@ -0,0 +1,204 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ const express = require('express');
2
+ const { randomUUID } = require('node:crypto');
3
+ const fs = require('node:fs/promises');
4
+ const path = require('node:path');
5
+
6
+ const app = express();
7
+
8
+ const PORT = Number(process.env.PORT || 4000);
9
+ const BUILD_DIR = path.join(__dirname, '..', 'build');
10
+ const LOCAL_DATA_DIR = path.join(__dirname, '..', 'data');
11
+ const LOCAL_DATA_FILE = process.env.SESSIONS_FILE || path.join(LOCAL_DATA_DIR, 'sessions.json');
12
+ const HUB_DATASET_REPO = process.env.HF_DATASET_REPO || '';
13
+ const HUB_TOKEN = process.env.HF_TOKEN || '';
14
+ const HUB_DATASET_FILE = process.env.HF_DATASET_FILE || 'sessions.json';
15
+
16
+ app.use(express.json({ limit: '1mb' }));
17
+
18
+ function sortSessionsByDateDesc(sessions) {
19
+ return [...sessions].sort((a, b) => {
20
+ const byDate = (b?.date || '').localeCompare(a?.date || '');
21
+ if (byDate !== 0) return byDate;
22
+ return (b?.id || '').localeCompare(a?.id || '');
23
+ });
24
+ }
25
+
26
+ function usingHubDataset() {
27
+ return Boolean(HUB_DATASET_REPO && HUB_TOKEN);
28
+ }
29
+
30
+ async function ensureLocalDataFile() {
31
+ await fs.mkdir(path.dirname(LOCAL_DATA_FILE), { recursive: true });
32
+ try {
33
+ await fs.access(LOCAL_DATA_FILE);
34
+ } catch {
35
+ await fs.writeFile(LOCAL_DATA_FILE, '[]\n', 'utf8');
36
+ }
37
+ }
38
+
39
+ async function importHub() {
40
+ return import('@huggingface/hub');
41
+ }
42
+
43
+ async function readLocalSessions() {
44
+ await ensureLocalDataFile();
45
+ const raw = await fs.readFile(LOCAL_DATA_FILE, 'utf8');
46
+ const parsed = JSON.parse(raw);
47
+ return Array.isArray(parsed) ? parsed : [];
48
+ }
49
+
50
+ async function writeLocalSessions(sessions) {
51
+ await ensureLocalDataFile();
52
+ await fs.writeFile(LOCAL_DATA_FILE, `${JSON.stringify(sessions, null, 2)}\n`, 'utf8');
53
+ }
54
+
55
+ async function readDatasetSessions() {
56
+ const hub = await importHub();
57
+ try {
58
+ const response = await hub.downloadFile({
59
+ repo: { type: 'dataset', name: HUB_DATASET_REPO },
60
+ path: HUB_DATASET_FILE,
61
+ accessToken: HUB_TOKEN,
62
+ });
63
+ const raw = await response.text();
64
+ const parsed = JSON.parse(raw);
65
+ return Array.isArray(parsed) ? parsed : [];
66
+ } catch (error) {
67
+ if (error && typeof error === 'object' && 'statusCode' in error && error.statusCode === 404) {
68
+ return [];
69
+ }
70
+ throw error;
71
+ }
72
+ }
73
+
74
+ async function writeDatasetSessions(sessions) {
75
+ const hub = await importHub();
76
+ const payload = `${JSON.stringify(sessions, null, 2)}\n`;
77
+ await hub.uploadFiles({
78
+ repo: { type: 'dataset', name: HUB_DATASET_REPO },
79
+ accessToken: HUB_TOKEN,
80
+ files: [
81
+ {
82
+ path: HUB_DATASET_FILE,
83
+ content: new Blob([payload], { type: 'application/json' }),
84
+ },
85
+ ],
86
+ commitTitle: 'Update climbing dashboard sessions',
87
+ });
88
+ }
89
+
90
+ async function readSessions() {
91
+ if (usingHubDataset()) {
92
+ return readDatasetSessions();
93
+ }
94
+ return readLocalSessions();
95
+ }
96
+
97
+ async function writeSessions(sessions) {
98
+ if (usingHubDataset()) {
99
+ return writeDatasetSessions(sessions);
100
+ }
101
+ return writeLocalSessions(sessions);
102
+ }
103
+
104
+ app.get('/api/health', (_req, res) => {
105
+ res.json({
106
+ ok: true,
107
+ storage: usingHubDataset() ? 'huggingface-dataset' : 'local-file',
108
+ datasetRepo: HUB_DATASET_REPO || null,
109
+ });
110
+ });
111
+
112
+ app.get('/api/sessions', async (_req, res, next) => {
113
+ try {
114
+ const sessions = await readSessions();
115
+ res.json(sortSessionsByDateDesc(sessions));
116
+ } catch (error) {
117
+ next(error);
118
+ }
119
+ });
120
+
121
+ app.post('/api/sessions', async (req, res, next) => {
122
+ try {
123
+ const incoming = req.body;
124
+ if (!incoming || typeof incoming !== 'object') {
125
+ return res.status(400).json({ error: 'Invalid session payload.' });
126
+ }
127
+
128
+ const sessions = await readSessions();
129
+ const session = {
130
+ ...incoming,
131
+ id: incoming.id || randomUUID(),
132
+ };
133
+
134
+ const withoutSameId = sessions.filter((entry) => entry.id !== session.id);
135
+ const updated = sortSessionsByDateDesc([session, ...withoutSameId]);
136
+
137
+ await writeSessions(updated);
138
+ return res.status(201).json(session);
139
+ } catch (error) {
140
+ next(error);
141
+ }
142
+ });
143
+
144
+ app.put('/api/sessions/:id', async (req, res, next) => {
145
+ try {
146
+ const { id } = req.params;
147
+ const changes = req.body;
148
+ if (!changes || typeof changes !== 'object') {
149
+ return res.status(400).json({ error: 'Invalid update payload.' });
150
+ }
151
+
152
+ const sessions = await readSessions();
153
+ const index = sessions.findIndex((entry) => entry.id === id);
154
+ if (index === -1) {
155
+ return res.status(404).json({ error: 'Session not found.' });
156
+ }
157
+
158
+ const updatedSession = { ...sessions[index], ...changes, id };
159
+ const nextSessions = [...sessions];
160
+ nextSessions[index] = updatedSession;
161
+
162
+ await writeSessions(sortSessionsByDateDesc(nextSessions));
163
+ return res.json(updatedSession);
164
+ } catch (error) {
165
+ next(error);
166
+ }
167
+ });
168
+
169
+ app.delete('/api/sessions/:id', async (req, res, next) => {
170
+ try {
171
+ const { id } = req.params;
172
+ const sessions = await readSessions();
173
+ const nextSessions = sessions.filter((entry) => entry.id !== id);
174
+
175
+ if (nextSessions.length === sessions.length) {
176
+ return res.status(404).json({ error: 'Session not found.' });
177
+ }
178
+
179
+ await writeSessions(nextSessions);
180
+ return res.status(204).send();
181
+ } catch (error) {
182
+ next(error);
183
+ }
184
+ });
185
+
186
+ if (process.env.NODE_ENV === 'production') {
187
+ app.use(express.static(BUILD_DIR));
188
+ app.get('*', (_req, res) => {
189
+ res.sendFile(path.join(BUILD_DIR, 'index.html'));
190
+ });
191
+ }
192
+
193
+ app.use((error, _req, res, _next) => {
194
+ // eslint-disable-next-line no-console
195
+ console.error(error);
196
+ res.status(500).json({ error: 'Internal server error' });
197
+ });
198
+
199
+ app.listen(PORT, () => {
200
+ // eslint-disable-next-line no-console
201
+ console.log(`Session API listening on http://localhost:${PORT}`);
202
+ // eslint-disable-next-line no-console
203
+ console.log(`Storage backend: ${usingHubDataset() ? `dataset:${HUB_DATASET_REPO}/${HUB_DATASET_FILE}` : `local:${LOCAL_DATA_FILE}`}`);
204
+ });
src/App.css CHANGED
@@ -19,6 +19,11 @@
19
  color: var(--color-text-muted);
20
  }
21
 
 
 
 
 
 
22
  .dashboard-main {
23
  display: flex;
24
  flex-direction: column;
@@ -38,6 +43,11 @@
38
  padding: 1.5rem;
39
  }
40
 
 
 
 
 
 
41
  @media (max-width: 768px) {
42
  .dashboard {
43
  padding: 1rem;
 
19
  color: var(--color-text-muted);
20
  }
21
 
22
+ .dashboard-header .sync-error {
23
+ color: var(--color-danger);
24
+ font-weight: 600;
25
+ }
26
+
27
  .dashboard-main {
28
  display: flex;
29
  flex-direction: column;
 
43
  padding: 1.5rem;
44
  }
45
 
46
+ .loading-message {
47
+ color: var(--color-text-muted);
48
+ font-weight: 600;
49
+ }
50
+
51
  @media (max-width: 768px) {
52
  .dashboard {
53
  padding: 1rem;
src/App.js CHANGED
@@ -1,5 +1,10 @@
1
  import { useEffect, useMemo, useState } from 'react';
2
- import { loadSessions, saveSessions } from './utils/storage';
 
 
 
 
 
3
  import {
4
  buildChartData,
5
  buildGradeProgressData,
@@ -17,11 +22,34 @@ import SessionLog from './components/SessionLog';
17
  import './App.css';
18
 
19
  function App() {
20
- const [sessions, setSessions] = useState(() => loadSessions());
 
 
21
 
22
  useEffect(() => {
23
- saveSessions(sessions);
24
- }, [sessions]);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
25
 
26
  const today = new Date().toISOString().split('T')[0];
27
  const currentWeekKey = getWeekKey(today);
@@ -56,16 +84,36 @@ function App() {
56
  const recommendation = computeRecommendation(recentSummaries);
57
  const maxRoutes = computeMaxRoutesThreshold(sessions, today);
58
 
59
- function handleAddSession(newSession) {
60
- setSessions((prev) => [{ ...newSession, id: crypto.randomUUID() }, ...prev]);
 
 
 
 
 
 
 
 
61
  }
62
 
63
- function handleEditSession(id, updatedFields) {
64
- setSessions((prev) => prev.map((session) => (session.id === id ? { ...session, ...updatedFields } : session)));
 
 
 
 
 
 
65
  }
66
 
67
- function handleDeleteSession(id) {
68
- setSessions((prev) => prev.filter((session) => session.id !== id));
 
 
 
 
 
 
69
  }
70
 
71
  return (
@@ -73,32 +121,39 @@ function App() {
73
  <header className="dashboard-header">
74
  <h1>Climbing Dashboard</h1>
75
  <p>Log each climbing session and track routes, load, and RPE over time.</p>
 
76
  </header>
77
 
78
  <main className="dashboard-main">
79
- <div className="dashboard-top">
80
- <ClimbForm onAddSession={handleAddSession} />
81
- <WeeklySummary
82
- summary={selectedWeekSummary}
83
- recommendation={recommendation}
84
- weekKey={selectedWeekKey}
85
- isCurrentWeek={selectedWeekKey === currentWeekKey}
86
- weeksOfData={weeksBeforeSelected.length}
87
- maxRoutes={maxRoutes}
88
- hasPrevWeek={hasPrevWeek}
89
- hasNextWeek={hasNextWeek}
90
- onPrevWeek={() => hasPrevWeek && setSelectedWeekKey(allWeekKeys[selectedIdx - 1])}
91
- onNextWeek={() => hasNextWeek && setSelectedWeekKey(allWeekKeys[selectedIdx + 1])}
92
- />
93
- </div>
94
-
95
- <Charts data={chartData} gradeData={gradeProgressData} painData={painData} />
96
-
97
- <SessionLog
98
- sessions={sessions}
99
- onEditSession={handleEditSession}
100
- onDeleteSession={handleDeleteSession}
101
- />
 
 
 
 
 
 
102
  </main>
103
  </div>
104
  );
 
1
  import { useEffect, useMemo, useState } from 'react';
2
+ import {
3
+ createSession,
4
+ deleteSession,
5
+ loadSessions,
6
+ updateSession,
7
+ } from './utils/storage';
8
  import {
9
  buildChartData,
10
  buildGradeProgressData,
 
22
  import './App.css';
23
 
24
  function App() {
25
+ const [sessions, setSessions] = useState([]);
26
+ const [isLoading, setIsLoading] = useState(true);
27
+ const [syncError, setSyncError] = useState('');
28
 
29
  useEffect(() => {
30
+ let active = true;
31
+
32
+ async function bootstrapSessions() {
33
+ try {
34
+ const loaded = await loadSessions();
35
+ if (active) {
36
+ setSessions(Array.isArray(loaded) ? loaded : []);
37
+ setSyncError('');
38
+ }
39
+ } catch {
40
+ if (active) {
41
+ setSyncError('Could not load shared session data.');
42
+ }
43
+ } finally {
44
+ if (active) setIsLoading(false);
45
+ }
46
+ }
47
+
48
+ bootstrapSessions();
49
+ return () => {
50
+ active = false;
51
+ };
52
+ }, []);
53
 
54
  const today = new Date().toISOString().split('T')[0];
55
  const currentWeekKey = getWeekKey(today);
 
84
  const recommendation = computeRecommendation(recentSummaries);
85
  const maxRoutes = computeMaxRoutesThreshold(sessions, today);
86
 
87
+ async function handleAddSession(newSession) {
88
+ const sessionWithId = { ...newSession, id: crypto.randomUUID() };
89
+
90
+ try {
91
+ const created = await createSession(sessionWithId);
92
+ setSessions((prev) => [created, ...prev.filter((session) => session.id !== created.id)]);
93
+ setSyncError('');
94
+ } catch {
95
+ setSyncError('Could not save session.');
96
+ }
97
  }
98
 
99
+ async function handleEditSession(id, updatedFields) {
100
+ try {
101
+ const updated = await updateSession(id, updatedFields);
102
+ setSessions((prev) => prev.map((session) => (session.id === id ? updated : session)));
103
+ setSyncError('');
104
+ } catch {
105
+ setSyncError('Could not update session.');
106
+ }
107
  }
108
 
109
+ async function handleDeleteSession(id) {
110
+ try {
111
+ await deleteSession(id);
112
+ setSessions((prev) => prev.filter((session) => session.id !== id));
113
+ setSyncError('');
114
+ } catch {
115
+ setSyncError('Could not delete session.');
116
+ }
117
  }
118
 
119
  return (
 
121
  <header className="dashboard-header">
122
  <h1>Climbing Dashboard</h1>
123
  <p>Log each climbing session and track routes, load, and RPE over time.</p>
124
+ {syncError && <p className="sync-error">{syncError}</p>}
125
  </header>
126
 
127
  <main className="dashboard-main">
128
+ {isLoading ? (
129
+ <p className="loading-message">Loading shared sessions...</p>
130
+ ) : (
131
+ <>
132
+ <div className="dashboard-top">
133
+ <ClimbForm onAddSession={handleAddSession} />
134
+ <WeeklySummary
135
+ summary={selectedWeekSummary}
136
+ recommendation={recommendation}
137
+ weekKey={selectedWeekKey}
138
+ isCurrentWeek={selectedWeekKey === currentWeekKey}
139
+ weeksOfData={weeksBeforeSelected.length}
140
+ maxRoutes={maxRoutes}
141
+ hasPrevWeek={hasPrevWeek}
142
+ hasNextWeek={hasNextWeek}
143
+ onPrevWeek={() => hasPrevWeek && setSelectedWeekKey(allWeekKeys[selectedIdx - 1])}
144
+ onNextWeek={() => hasNextWeek && setSelectedWeekKey(allWeekKeys[selectedIdx + 1])}
145
+ />
146
+ </div>
147
+
148
+ <Charts data={chartData} gradeData={gradeProgressData} painData={painData} />
149
+
150
+ <SessionLog
151
+ sessions={sessions}
152
+ onEditSession={handleEditSession}
153
+ onDeleteSession={handleDeleteSession}
154
+ />
155
+ </>
156
+ )}
157
  </main>
158
  </div>
159
  );
src/App.test.js CHANGED
@@ -1,8 +1,18 @@
1
- import { render, screen } from '@testing-library/react';
2
  import App from './App';
3
 
4
- test('renders climbing dashboard heading', () => {
 
 
 
 
 
 
 
5
  render(<App />);
6
  const heading = screen.getByRole('heading', { name: /climbing dashboard/i });
7
  expect(heading).toBeInTheDocument();
 
 
 
8
  });
 
1
+ import { render, screen, waitFor } from '@testing-library/react';
2
  import App from './App';
3
 
4
+ jest.mock('./utils/storage', () => ({
5
+ loadSessions: jest.fn().mockResolvedValue([]),
6
+ createSession: jest.fn(),
7
+ updateSession: jest.fn(),
8
+ deleteSession: jest.fn(),
9
+ }));
10
+
11
+ test('renders climbing dashboard heading', async () => {
12
  render(<App />);
13
  const heading = screen.getByRole('heading', { name: /climbing dashboard/i });
14
  expect(heading).toBeInTheDocument();
15
+ await waitFor(() =>
16
+ expect(screen.queryByText(/loading shared sessions/i)).not.toBeInTheDocument()
17
+ );
18
  });
src/utils/storage.js CHANGED
@@ -1,14 +1,40 @@
1
- const STORAGE_KEY = 'climbEntries';
2
-
3
- export function loadSessions() {
4
- try {
5
- const raw = localStorage.getItem(STORAGE_KEY);
6
- return raw ? JSON.parse(raw) : [];
7
- } catch {
8
- return [];
 
 
 
9
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
10
  }
11
 
12
- export function saveSessions(sessions) {
13
- localStorage.setItem(STORAGE_KEY, JSON.stringify(sessions));
 
 
14
  }
 
1
+ async function request(path, options = {}) {
2
+ const response = await fetch(path, {
3
+ headers: {
4
+ 'Content-Type': 'application/json',
5
+ ...(options.headers || {}),
6
+ },
7
+ ...options,
8
+ });
9
+
10
+ if (!response.ok) {
11
+ throw new Error(`API ${response.status}: ${response.statusText}`);
12
  }
13
+
14
+ if (response.status === 204) return null;
15
+ return response.json();
16
+ }
17
+
18
+ export async function loadSessions() {
19
+ return request('/api/sessions');
20
+ }
21
+
22
+ export async function createSession(session) {
23
+ return request('/api/sessions', {
24
+ method: 'POST',
25
+ body: JSON.stringify(session),
26
+ });
27
+ }
28
+
29
+ export async function updateSession(id, changes) {
30
+ return request(`/api/sessions/${encodeURIComponent(id)}`, {
31
+ method: 'PUT',
32
+ body: JSON.stringify(changes),
33
+ });
34
  }
35
 
36
+ export async function deleteSession(id) {
37
+ return request(`/api/sessions/${encodeURIComponent(id)}`, {
38
+ method: 'DELETE',
39
+ });
40
  }