+3
-1
README.md
+3
-1
README.md
+42
locales/en-US.json
+42
locales/en-US.json
···
197
197
"done_title": "Done",
198
198
"none": "—"
199
199
},
200
+
"cobalt": {
201
+
"name": "cobalt",
202
+
"description": "Download a video or audio from a given URL",
203
+
"options": {
204
+
"url": {
205
+
"name": "url",
206
+
"description": "The URL of the video to download"
207
+
},
208
+
"video_quality": {
209
+
"name": "video-quality",
210
+
"description": "The video quality to download"
211
+
},
212
+
"audio_only": {
213
+
"name": "audio-only",
214
+
"description": "Download audio only"
215
+
},
216
+
"mute_audio": {
217
+
"name": "mute-audio",
218
+
"description": "Mute audio"
219
+
},
220
+
"twitter_gif": {
221
+
"name": "twitter-gif",
222
+
"description": "Download as Twitter GIF"
223
+
},
224
+
"tiktok_original_audio": {
225
+
"name": "tiktok-original-audio",
226
+
"description": "Include TikTok original audio"
227
+
},
228
+
"audio_format": {
229
+
"name": "audio-format",
230
+
"description": "Format for audio (requires audio-only to be true)"
231
+
}
232
+
},
233
+
"button_label": "Download",
234
+
"success": "✅ **Download ready!**\n\n🔗 {url}\n📥 **Click the button below to download your file.**",
235
+
"error": "❌ **Error:** {error}",
236
+
"unknown_error": "An unknown error occurred.",
237
+
"rate_limit": "⏳ **Rate limit exceeded.** Please try again later.",
238
+
"unknown_response": "❓ **Unknown response from the API.** Please try again.",
239
+
"multiple_items": "📋 **Multiple items found for this URL.** Please provide a more specific URL.",
240
+
"local_processing_not_supported": "⚠️ **Local processing is not supported.** Please try a different URL or option."
241
+
},
200
242
"time": {
201
243
"name": "time",
202
244
"description": "Get the current time for a timezone",
+8
-2
package.json
+8
-2
package.json
···
19
19
},
20
20
"dependencies": {
21
21
"@discordjs/rest": "^2.5.1",
22
+
"axios": "^1.10.0",
22
23
"city-timezones": "^1.3.1",
23
24
"cors": "^2.8.5",
24
25
"discord.js": "^14.21.0",
25
26
"dotenv": "^16.6.1",
26
27
"eslint-plugin-prettier": "^5.5.1",
27
-
"express": "^5.1.0",
28
+
"express": "^4.21.2",
28
29
"express-rate-limit": "^7.5.1",
30
+
"express-validator": "^7.2.1",
29
31
"helmet": "^8.1.0",
32
+
"jsonwebtoken": "^9.0.2",
30
33
"moment-timezone": "^0.6.0",
31
34
"node-fetch": "^3.3.2",
32
35
"pg": "^8.16.3",
36
+
"uuid": "^11.1.0",
33
37
"validator": "^13.15.15",
34
38
"whois-json": "^2.0.4",
35
39
"winston": "^3.17.0"
···
37
41
"devDependencies": {
38
42
"@eslint/js": "^9.30.0",
39
43
"@types/cors": "^2.8.19",
40
-
"@types/express": "^5.0.3",
44
+
"@types/express": "^4.17.23",
45
+
"@types/jsonwebtoken": "^9.0.10",
41
46
"@types/node": "^24.0.7",
42
47
"@types/pg": "^8.15.4",
48
+
"@types/uuid": "^10.0.0",
43
49
"@types/validator": "^13.15.2",
44
50
"@types/whois-json": "^2.0.4",
45
51
"eslint": "^9.30.0",
+395
-157
pnpm-lock.yaml
+395
-157
pnpm-lock.yaml
···
11
11
'@discordjs/rest':
12
12
specifier: ^2.5.1
13
13
version: 2.5.1
14
+
axios:
15
+
specifier: ^1.10.0
16
+
version: 1.10.0
14
17
city-timezones:
15
18
specifier: ^1.3.1
16
19
version: 1.3.1
···
27
30
specifier: ^5.5.1
28
31
version: 5.5.1(eslint-config-prettier@10.1.5(eslint@9.30.1))(eslint@9.30.1)(prettier@3.6.2)
29
32
express:
30
-
specifier: ^5.1.0
31
-
version: 5.1.0
33
+
specifier: ^4.21.2
34
+
version: 4.21.2
32
35
express-rate-limit:
33
36
specifier: ^7.5.1
34
-
version: 7.5.1(express@5.1.0)
37
+
version: 7.5.1(express@4.21.2)
38
+
express-validator:
39
+
specifier: ^7.2.1
40
+
version: 7.2.1
35
41
helmet:
36
42
specifier: ^8.1.0
37
43
version: 8.1.0
44
+
jsonwebtoken:
45
+
specifier: ^9.0.2
46
+
version: 9.0.2
38
47
moment-timezone:
39
48
specifier: ^0.6.0
40
49
version: 0.6.0
···
44
53
pg:
45
54
specifier: ^8.16.3
46
55
version: 8.16.3
56
+
uuid:
57
+
specifier: ^11.1.0
58
+
version: 11.1.0
47
59
validator:
48
60
specifier: ^13.15.15
49
61
version: 13.15.15
···
61
73
specifier: ^2.8.19
62
74
version: 2.8.19
63
75
'@types/express':
64
-
specifier: ^5.0.3
65
-
version: 5.0.3
76
+
specifier: ^4.17.23
77
+
version: 4.17.23
78
+
'@types/jsonwebtoken':
79
+
specifier: ^9.0.10
80
+
version: 9.0.10
66
81
'@types/node':
67
82
specifier: ^24.0.7
68
83
version: 24.0.12
69
84
'@types/pg':
70
85
specifier: ^8.15.4
71
86
version: 8.15.4
87
+
'@types/uuid':
88
+
specifier: ^10.0.0
89
+
version: 10.0.0
72
90
'@types/validator':
73
91
specifier: ^13.15.2
74
92
version: 13.15.2
···
405
423
'@types/estree@1.0.8':
406
424
resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==}
407
425
408
-
'@types/express-serve-static-core@5.0.7':
409
-
resolution: {integrity: sha512-R+33OsgWw7rOhD1emjU7dzCDHucJrgJXMA5PYCzJxVil0dsyx5iBEPHqpPfiKNJQb7lZ1vxwoLR4Z87bBUpeGQ==}
426
+
'@types/express-serve-static-core@4.19.6':
427
+
resolution: {integrity: sha512-N4LZ2xG7DatVqhCZzOGb1Yi5lMbXSZcmdLDe9EzSndPV2HpWYWzRbaerl2n27irrm94EPpprqa8KpskPT085+A==}
410
428
411
-
'@types/express@5.0.3':
412
-
resolution: {integrity: sha512-wGA0NX93b19/dZC1J18tKWVIYWyyF2ZjT9vin/NRu0qzzvfVzWjs04iq2rQ3H65vCTQYlRqs3YHfY7zjdV+9Kw==}
429
+
'@types/express@4.17.23':
430
+
resolution: {integrity: sha512-Crp6WY9aTYP3qPi2wGDo9iUe/rceX01UMhnF1jmwDcKCFM6cx7YhGP/Mpr3y9AASpfHixIG0E6azCcL5OcDHsQ==}
413
431
414
432
'@types/http-errors@2.0.5':
415
433
resolution: {integrity: sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==}
···
417
435
'@types/json-schema@7.0.15':
418
436
resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==}
419
437
438
+
'@types/jsonwebtoken@9.0.10':
439
+
resolution: {integrity: sha512-asx5hIG9Qmf/1oStypjanR7iKTv0gXQ1Ov/jfrX6kS/EO0OFni8orbmGCn0672NHR3kXHwpAwR+B368ZGN/2rA==}
440
+
420
441
'@types/mime@1.3.5':
421
442
resolution: {integrity: sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==}
443
+
444
+
'@types/ms@2.1.0':
445
+
resolution: {integrity: sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==}
422
446
423
447
'@types/node@24.0.12':
424
448
resolution: {integrity: sha512-LtOrbvDf5ndC9Xi+4QZjVL0woFymF/xSTKZKPgrrl7H7XoeDvnD+E2IclKVDyaK9UM756W/3BXqSU+JEHopA9g==}
···
440
464
441
465
'@types/triple-beam@1.3.5':
442
466
resolution: {integrity: sha512-6WaYesThRMCl19iryMYP7/x2OVgCtbIVflDGFpWnb9irXI3UjYE4AzmYuiUKY1AJstGijoY+MgUszMgRxIYTYw==}
467
+
468
+
'@types/uuid@10.0.0':
469
+
resolution: {integrity: sha512-7gqG38EyHgyP1S+7+xomFtL+ZNHcKv6DwNaCZmJmo1vgMugyF3TCnXVg4t1uk89mLNwnLtnY3TpOpCOyp1/xHQ==}
443
470
444
471
'@types/validator@13.15.2':
445
472
resolution: {integrity: sha512-y7pa/oEJJ4iGYBxOpfAKn5b9+xuihvzDVnC/OSvlVnGxVg0pOqmjiMafiJ1KVNQEaPZf9HsEp5icEwGg8uIe5Q==}
···
513
540
resolution: {integrity: sha512-RaI5qZo6D2CVS6sTHFKg1v5Ohq/+Bo2LZ5gzUEwZ/WkHhwtGTCB/sVLw8ijOkAUxasZ+WshN/Rzj4ywsABJ5ZA==}
514
541
engines: {node: '>=v14.0.0', npm: '>=7.0.0'}
515
542
516
-
accepts@2.0.0:
517
-
resolution: {integrity: sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==}
543
+
accepts@1.3.8:
544
+
resolution: {integrity: sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==}
518
545
engines: {node: '>= 0.6'}
519
546
520
547
acorn-jsx@5.3.2:
···
545
572
argparse@2.0.1:
546
573
resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==}
547
574
575
+
array-flatten@1.1.1:
576
+
resolution: {integrity: sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==}
577
+
548
578
array-union@2.1.0:
549
579
resolution: {integrity: sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==}
550
580
engines: {node: '>=8'}
551
581
552
582
async@3.2.6:
553
583
resolution: {integrity: sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==}
584
+
585
+
asynckit@0.4.0:
586
+
resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==}
587
+
588
+
axios@1.10.0:
589
+
resolution: {integrity: sha512-/1xYAC4MP/HEG+3duIhFr4ZQXR4sQXOIe+o6sdqzeykGLx6Upp/1p8MHqhINOvGeP7xyNHe7tsiJByc4SSVUxw==}
554
590
555
591
balanced-match@1.0.2:
556
592
resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==}
···
559
595
resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==}
560
596
engines: {node: '>=8'}
561
597
562
-
body-parser@2.2.0:
563
-
resolution: {integrity: sha512-02qvAaxv8tp7fBa/mw1ga98OGm+eCbqzJOKoRt70sLmfEEi+jyBYVTDGfCL/k06/4EMk/z01gCe7HoCH/f2LTg==}
564
-
engines: {node: '>=18'}
598
+
body-parser@1.20.3:
599
+
resolution: {integrity: sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==}
600
+
engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16}
565
601
566
602
brace-expansion@1.1.12:
567
603
resolution: {integrity: sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==}
···
572
608
braces@3.0.3:
573
609
resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==}
574
610
engines: {node: '>=8'}
611
+
612
+
buffer-equal-constant-time@1.0.1:
613
+
resolution: {integrity: sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==}
575
614
576
615
bytes@3.1.2:
577
616
resolution: {integrity: sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==}
···
635
674
colorspace@1.1.4:
636
675
resolution: {integrity: sha512-BgvKJiuVu1igBUF2kEjRCZXol6wiiGbY5ipL/oVPwm0BL9sIpMIzM8IK7vwuxIIzOXMV3Ey5w+vxhm0rR/TN8w==}
637
676
677
+
combined-stream@1.0.8:
678
+
resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==}
679
+
engines: {node: '>= 0.8'}
680
+
638
681
commander@9.5.0:
639
682
resolution: {integrity: sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ==}
640
683
engines: {node: ^12.20.0 || >=14}
···
645
688
constant-case@2.0.0:
646
689
resolution: {integrity: sha512-eS0N9WwmjTqrOmR3o83F5vW8Z+9R1HnVz3xmzT2PMFug9ly+Au/fxRWlEBSb6LcZwspSsEn9Xs1uw9YgzAg1EQ==}
647
690
648
-
content-disposition@1.0.0:
649
-
resolution: {integrity: sha512-Au9nRL8VNUut/XSzbQA38+M78dzP4D+eqg3gfJHMIHHYa3bg067xj1KxMUWj+VULbiZMowKngFFbKczUrNJ1mg==}
691
+
content-disposition@0.5.4:
692
+
resolution: {integrity: sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==}
650
693
engines: {node: '>= 0.6'}
651
694
652
695
content-type@1.0.5:
653
696
resolution: {integrity: sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==}
654
697
engines: {node: '>= 0.6'}
655
698
656
-
cookie-signature@1.2.2:
657
-
resolution: {integrity: sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==}
658
-
engines: {node: '>=6.6.0'}
699
+
cookie-signature@1.0.6:
700
+
resolution: {integrity: sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==}
659
701
660
-
cookie@0.7.2:
661
-
resolution: {integrity: sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==}
702
+
cookie@0.7.1:
703
+
resolution: {integrity: sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==}
662
704
engines: {node: '>= 0.6'}
663
705
664
706
cors@2.8.5:
···
673
715
resolution: {integrity: sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==}
674
716
engines: {node: '>= 12'}
675
717
718
+
debug@2.6.9:
719
+
resolution: {integrity: sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==}
720
+
peerDependencies:
721
+
supports-color: '*'
722
+
peerDependenciesMeta:
723
+
supports-color:
724
+
optional: true
725
+
676
726
debug@4.4.1:
677
727
resolution: {integrity: sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==}
678
728
engines: {node: '>=6.0'}
···
691
741
692
742
deep-is@0.1.4:
693
743
resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==}
744
+
745
+
delayed-stream@1.0.0:
746
+
resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==}
747
+
engines: {node: '>=0.4.0'}
694
748
695
749
depd@2.0.0:
696
750
resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==}
697
751
engines: {node: '>= 0.8'}
698
752
753
+
destroy@1.2.0:
754
+
resolution: {integrity: sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==}
755
+
engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16}
756
+
699
757
dir-glob@3.0.1:
700
758
resolution: {integrity: sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==}
701
759
engines: {node: '>=8'}
···
718
776
resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==}
719
777
engines: {node: '>= 0.4'}
720
778
779
+
ecdsa-sig-formatter@1.0.11:
780
+
resolution: {integrity: sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==}
781
+
721
782
ee-first@1.1.1:
722
783
resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==}
723
784
···
727
788
enabled@2.0.0:
728
789
resolution: {integrity: sha512-AKrN98kuwOzMIdAizXGI86UFBoo26CL21UM763y1h/GMSJ4/OHU9k2YlsmBpyScFo/wbLzWQJBMCW4+IO3/+OQ==}
729
790
791
+
encodeurl@1.0.2:
792
+
resolution: {integrity: sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==}
793
+
engines: {node: '>= 0.8'}
794
+
730
795
encodeurl@2.0.0:
731
796
resolution: {integrity: sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==}
732
797
engines: {node: '>= 0.8'}
···
741
806
742
807
es-object-atoms@1.1.1:
743
808
resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==}
809
+
engines: {node: '>= 0.4'}
810
+
811
+
es-set-tostringtag@2.1.0:
812
+
resolution: {integrity: sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==}
744
813
engines: {node: '>= 0.4'}
745
814
746
815
esbuild@0.25.6:
···
827
896
peerDependencies:
828
897
express: '>= 4.11'
829
898
830
-
express@5.1.0:
831
-
resolution: {integrity: sha512-DT9ck5YIRU+8GYzzU5kT3eHGA5iL+1Zd0EutOmTE9Dtk+Tvuzd23VBU+ec7HPNSTxXYO55gPV/hq4pSBJDjFpA==}
832
-
engines: {node: '>= 18'}
899
+
express-validator@7.2.1:
900
+
resolution: {integrity: sha512-CjNE6aakfpuwGaHQZ3m8ltCG2Qvivd7RHtVMS/6nVxOM7xVGqr4bhflsm4+N5FP5zI7Zxp+Hae+9RE+o8e3ZOQ==}
901
+
engines: {node: '>= 8.0.0'}
902
+
903
+
express@4.21.2:
904
+
resolution: {integrity: sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==}
905
+
engines: {node: '>= 0.10.0'}
833
906
834
907
fast-deep-equal@3.1.3:
835
908
resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==}
···
865
938
resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==}
866
939
engines: {node: '>=8'}
867
940
868
-
finalhandler@2.1.0:
869
-
resolution: {integrity: sha512-/t88Ty3d5JWQbWYgaOGCCYfXRwV1+be02WqYYlL6h0lEiUAMPM8o8qKGO01YIkOHzka2up08wvgYD0mDiI+q3Q==}
941
+
finalhandler@1.3.1:
942
+
resolution: {integrity: sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==}
870
943
engines: {node: '>= 0.8'}
871
944
872
945
find-up@4.1.0:
···
886
959
887
960
fn.name@1.1.0:
888
961
resolution: {integrity: sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw==}
962
+
963
+
follow-redirects@1.15.9:
964
+
resolution: {integrity: sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==}
965
+
engines: {node: '>=4.0'}
966
+
peerDependencies:
967
+
debug: '*'
968
+
peerDependenciesMeta:
969
+
debug:
970
+
optional: true
971
+
972
+
form-data@4.0.4:
973
+
resolution: {integrity: sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==}
974
+
engines: {node: '>= 6'}
889
975
890
976
formdata-polyfill@4.0.10:
891
977
resolution: {integrity: sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==}
···
895
981
resolution: {integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==}
896
982
engines: {node: '>= 0.6'}
897
983
898
-
fresh@2.0.0:
899
-
resolution: {integrity: sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==}
900
-
engines: {node: '>= 0.8'}
984
+
fresh@0.5.2:
985
+
resolution: {integrity: sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==}
986
+
engines: {node: '>= 0.6'}
901
987
902
988
fsevents@2.3.3:
903
989
resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==}
···
961
1047
resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==}
962
1048
engines: {node: '>= 0.4'}
963
1049
1050
+
has-tostringtag@1.0.2:
1051
+
resolution: {integrity: sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==}
1052
+
engines: {node: '>= 0.4'}
1053
+
964
1054
hasown@2.0.2:
965
1055
resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==}
966
1056
engines: {node: '>= 0.4'}
···
979
1069
resolution: {integrity: sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==}
980
1070
engines: {node: '>= 0.8'}
981
1071
982
-
iconv-lite@0.6.3:
983
-
resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==}
1072
+
iconv-lite@0.4.24:
1073
+
resolution: {integrity: sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==}
984
1074
engines: {node: '>=0.10.0'}
985
1075
986
1076
ignore-by-default@1.0.1:
···
1039
1129
resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==}
1040
1130
engines: {node: '>=0.12.0'}
1041
1131
1042
-
is-promise@4.0.0:
1043
-
resolution: {integrity: sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==}
1044
-
1045
1132
is-stream@2.0.1:
1046
1133
resolution: {integrity: sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==}
1047
1134
engines: {node: '>=8'}
···
1073
1160
engines: {node: '>=6'}
1074
1161
hasBin: true
1075
1162
1163
+
jsonwebtoken@9.0.2:
1164
+
resolution: {integrity: sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ==}
1165
+
engines: {node: '>=12', npm: '>=6'}
1166
+
1167
+
jwa@1.4.2:
1168
+
resolution: {integrity: sha512-eeH5JO+21J78qMvTIDdBXidBd6nG2kZjg5Ohz/1fpa28Z4CcsWUzJ1ZZyFq/3z3N17aZy+ZuBoHljASbL1WfOw==}
1169
+
1170
+
jws@3.2.2:
1171
+
resolution: {integrity: sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==}
1172
+
1076
1173
keyv@4.5.4:
1077
1174
resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==}
1078
1175
···
1091
1188
resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==}
1092
1189
engines: {node: '>=10'}
1093
1190
1191
+
lodash.includes@4.3.0:
1192
+
resolution: {integrity: sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==}
1193
+
1194
+
lodash.isboolean@3.0.3:
1195
+
resolution: {integrity: sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==}
1196
+
1197
+
lodash.isinteger@4.0.4:
1198
+
resolution: {integrity: sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==}
1199
+
1200
+
lodash.isnumber@3.0.3:
1201
+
resolution: {integrity: sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==}
1202
+
1203
+
lodash.isplainobject@4.0.6:
1204
+
resolution: {integrity: sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==}
1205
+
1206
+
lodash.isstring@4.0.1:
1207
+
resolution: {integrity: sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==}
1208
+
1094
1209
lodash.merge@4.6.2:
1095
1210
resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==}
1211
+
1212
+
lodash.once@4.1.1:
1213
+
resolution: {integrity: sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==}
1096
1214
1097
1215
lodash.snakecase@4.1.1:
1098
1216
resolution: {integrity: sha512-QZ1d4xoBHYUeuouhEq3lk3Uq7ldgyFXGBhg04+oRLnIz8o9T65Eh+8YdroUwn846zchkA9yDsDl5CVVaV2nqYw==}
···
1117
1235
resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==}
1118
1236
engines: {node: '>= 0.4'}
1119
1237
1120
-
media-typer@1.1.0:
1121
-
resolution: {integrity: sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==}
1122
-
engines: {node: '>= 0.8'}
1238
+
media-typer@0.3.0:
1239
+
resolution: {integrity: sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==}
1240
+
engines: {node: '>= 0.6'}
1123
1241
1124
-
merge-descriptors@2.0.0:
1125
-
resolution: {integrity: sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==}
1126
-
engines: {node: '>=18'}
1242
+
merge-descriptors@1.0.3:
1243
+
resolution: {integrity: sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==}
1127
1244
1128
1245
merge2@1.4.1:
1129
1246
resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==}
1130
1247
engines: {node: '>= 8'}
1131
1248
1249
+
methods@1.1.2:
1250
+
resolution: {integrity: sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==}
1251
+
engines: {node: '>= 0.6'}
1252
+
1132
1253
micromatch@4.0.8:
1133
1254
resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==}
1134
1255
engines: {node: '>=8.6'}
1135
1256
1136
-
mime-db@1.54.0:
1137
-
resolution: {integrity: sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==}
1257
+
mime-db@1.52.0:
1258
+
resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==}
1138
1259
engines: {node: '>= 0.6'}
1139
1260
1140
-
mime-types@3.0.1:
1141
-
resolution: {integrity: sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==}
1261
+
mime-types@2.1.35:
1262
+
resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==}
1142
1263
engines: {node: '>= 0.6'}
1264
+
1265
+
mime@1.6.0:
1266
+
resolution: {integrity: sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==}
1267
+
engines: {node: '>=4'}
1268
+
hasBin: true
1143
1269
1144
1270
minimatch@3.1.2:
1145
1271
resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==}
···
1157
1283
moment@2.30.1:
1158
1284
resolution: {integrity: sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how==}
1159
1285
1286
+
ms@2.0.0:
1287
+
resolution: {integrity: sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==}
1288
+
1160
1289
ms@2.1.3:
1161
1290
resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==}
1162
1291
···
1167
1296
natural-compare@1.4.0:
1168
1297
resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==}
1169
1298
1170
-
negotiator@1.0.0:
1171
-
resolution: {integrity: sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==}
1299
+
negotiator@0.6.3:
1300
+
resolution: {integrity: sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==}
1172
1301
engines: {node: '>= 0.6'}
1173
1302
1174
1303
no-case@2.3.2:
···
1203
1332
on-finished@2.4.1:
1204
1333
resolution: {integrity: sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==}
1205
1334
engines: {node: '>= 0.8'}
1206
-
1207
-
once@1.4.0:
1208
-
resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==}
1209
1335
1210
1336
one-time@1.0.0:
1211
1337
resolution: {integrity: sha512-5DXOiRKwuSEcQ/l0kGCF6Q3jcADFv5tSmRaJck/OqkVFcOzutB134KRSfF0xDrL39MNnqxbHBbUUcjZIhTgb2g==}
···
1259
1385
resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==}
1260
1386
engines: {node: '>=8'}
1261
1387
1262
-
path-to-regexp@8.2.0:
1263
-
resolution: {integrity: sha512-TdrF7fW9Rphjq4RjrW0Kp2AW0Ahwu9sRGTkS6bvDi0SCwZlEZYmcfDbEsTz8RVk0EHIS/Vd1bv3JhG+1xZuAyQ==}
1264
-
engines: {node: '>=16'}
1388
+
path-to-regexp@0.1.12:
1389
+
resolution: {integrity: sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==}
1265
1390
1266
1391
path-type@4.0.0:
1267
1392
resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==}
···
1342
1467
resolution: {integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==}
1343
1468
engines: {node: '>= 0.10'}
1344
1469
1470
+
proxy-from-env@1.1.0:
1471
+
resolution: {integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==}
1472
+
1345
1473
pstree.remy@1.1.8:
1346
1474
resolution: {integrity: sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==}
1347
1475
···
1349
1477
resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==}
1350
1478
engines: {node: '>=6'}
1351
1479
1352
-
qs@6.14.0:
1353
-
resolution: {integrity: sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==}
1480
+
qs@6.13.0:
1481
+
resolution: {integrity: sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==}
1354
1482
engines: {node: '>=0.6'}
1355
1483
1356
1484
queue-lit@1.5.2:
···
1364
1492
resolution: {integrity: sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==}
1365
1493
engines: {node: '>= 0.6'}
1366
1494
1367
-
raw-body@3.0.0:
1368
-
resolution: {integrity: sha512-RmkhL8CAyCRPXCE28MMH0z2PNWQBNk2Q09ZdxM9IOOXwxwZbN+qbWaatPkdkWIKL2ZVDImrN/pK5HTRz2PcS4g==}
1495
+
raw-body@2.5.2:
1496
+
resolution: {integrity: sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==}
1369
1497
engines: {node: '>= 0.8'}
1370
1498
1371
1499
readable-stream@3.6.2:
···
1394
1522
resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==}
1395
1523
engines: {iojs: '>=1.0.0', node: '>=0.10.0'}
1396
1524
1397
-
router@2.2.0:
1398
-
resolution: {integrity: sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==}
1399
-
engines: {node: '>= 18'}
1400
-
1401
1525
run-parallel@1.2.0:
1402
1526
resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==}
1403
1527
···
1416
1540
engines: {node: '>=10'}
1417
1541
hasBin: true
1418
1542
1419
-
send@1.2.0:
1420
-
resolution: {integrity: sha512-uaW0WwXKpL9blXE2o0bRhoL2EGXIrZxQ2ZQ4mgcfoBxdFmQold+qWsD2jLrfZ0trjKL6vOw0j//eAwcALFjKSw==}
1421
-
engines: {node: '>= 18'}
1543
+
send@0.19.0:
1544
+
resolution: {integrity: sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==}
1545
+
engines: {node: '>= 0.8.0'}
1422
1546
1423
1547
sentence-case@2.1.1:
1424
1548
resolution: {integrity: sha512-ENl7cYHaK/Ktwk5OTD+aDbQ3uC8IByu/6Bkg+HDv8Mm+XnBnppVNalcfJTNsp1ibstKh030/JKQQWglDvtKwEQ==}
1425
1549
1426
-
serve-static@2.2.0:
1427
-
resolution: {integrity: sha512-61g9pCh0Vnh7IutZjtLGGpTA355+OPn2TyDv/6ivP2h/AdAVX9azsoxmg2/M6nZeQZNYBEwIcsne1mJd9oQItQ==}
1428
-
engines: {node: '>= 18'}
1550
+
serve-static@1.16.2:
1551
+
resolution: {integrity: sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==}
1552
+
engines: {node: '>= 0.8.0'}
1429
1553
1430
1554
set-blocking@2.0.0:
1431
1555
resolution: {integrity: sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==}
···
1493
1617
resolution: {integrity: sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==}
1494
1618
engines: {node: '>= 0.8'}
1495
1619
1496
-
statuses@2.0.2:
1497
-
resolution: {integrity: sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==}
1498
-
engines: {node: '>= 0.8'}
1499
-
1500
1620
string-width@4.2.3:
1501
1621
resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==}
1502
1622
engines: {node: '>=8'}
···
1583
1703
resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==}
1584
1704
engines: {node: '>= 0.8.0'}
1585
1705
1586
-
type-is@2.0.1:
1587
-
resolution: {integrity: sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==}
1706
+
type-is@1.6.18:
1707
+
resolution: {integrity: sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==}
1588
1708
engines: {node: '>= 0.6'}
1589
1709
1590
1710
typescript-eslint@8.36.0:
···
1627
1747
1628
1748
util-deprecate@1.0.2:
1629
1749
resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==}
1750
+
1751
+
utils-merge@1.0.1:
1752
+
resolution: {integrity: sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==}
1753
+
engines: {node: '>= 0.4.0'}
1754
+
1755
+
uuid@11.1.0:
1756
+
resolution: {integrity: sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==}
1757
+
hasBin: true
1758
+
1759
+
validator@13.12.0:
1760
+
resolution: {integrity: sha512-c1Q0mCiPlgdTVVVIJIrBuxNicYE+t/7oKeI9MWLj3fh/uq2Pxh/3eeWbVZ4OcGW1TUf53At0njHw5SMdA3tmMg==}
1761
+
engines: {node: '>= 0.10'}
1630
1762
1631
1763
validator@13.15.15:
1632
1764
resolution: {integrity: sha512-BgWVbCI72aIQy937xbawcs+hrVaN/CZ2UwutgaJ36hGqRrLNM+f5LUT/YPRbo8IV/ASeFzXszezV+y2+rq3l8A==}
···
1671
1803
resolution: {integrity: sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==}
1672
1804
engines: {node: '>=8'}
1673
1805
1674
-
wrappy@1.0.2:
1675
-
resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==}
1676
-
1677
1806
ws@8.18.3:
1678
1807
resolution: {integrity: sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==}
1679
1808
engines: {node: '>=10.0.0'}
···
1941
2070
1942
2071
'@types/estree@1.0.8': {}
1943
2072
1944
-
'@types/express-serve-static-core@5.0.7':
2073
+
'@types/express-serve-static-core@4.19.6':
1945
2074
dependencies:
1946
2075
'@types/node': 24.0.12
1947
2076
'@types/qs': 6.14.0
1948
2077
'@types/range-parser': 1.2.7
1949
2078
'@types/send': 0.17.5
1950
2079
1951
-
'@types/express@5.0.3':
2080
+
'@types/express@4.17.23':
1952
2081
dependencies:
1953
2082
'@types/body-parser': 1.19.6
1954
-
'@types/express-serve-static-core': 5.0.7
2083
+
'@types/express-serve-static-core': 4.19.6
2084
+
'@types/qs': 6.14.0
1955
2085
'@types/serve-static': 1.15.8
1956
2086
1957
2087
'@types/http-errors@2.0.5': {}
1958
2088
1959
2089
'@types/json-schema@7.0.15': {}
1960
2090
2091
+
'@types/jsonwebtoken@9.0.10':
2092
+
dependencies:
2093
+
'@types/ms': 2.1.0
2094
+
'@types/node': 24.0.12
2095
+
1961
2096
'@types/mime@1.3.5': {}
2097
+
2098
+
'@types/ms@2.1.0': {}
1962
2099
1963
2100
'@types/node@24.0.12':
1964
2101
dependencies:
···
1986
2123
'@types/send': 0.17.5
1987
2124
1988
2125
'@types/triple-beam@1.3.5': {}
2126
+
2127
+
'@types/uuid@10.0.0': {}
1989
2128
1990
2129
'@types/validator@13.15.2': {}
1991
2130
···
2089
2228
2090
2229
'@vladfrangu/async_event_emitter@2.4.6': {}
2091
2230
2092
-
accepts@2.0.0:
2231
+
accepts@1.3.8:
2093
2232
dependencies:
2094
-
mime-types: 3.0.1
2095
-
negotiator: 1.0.0
2233
+
mime-types: 2.1.35
2234
+
negotiator: 0.6.3
2096
2235
2097
2236
acorn-jsx@5.3.2(acorn@8.15.0):
2098
2237
dependencies:
···
2120
2259
2121
2260
argparse@2.0.1: {}
2122
2261
2262
+
array-flatten@1.1.1: {}
2263
+
2123
2264
array-union@2.1.0: {}
2124
2265
2125
2266
async@3.2.6: {}
2126
2267
2268
+
asynckit@0.4.0: {}
2269
+
2270
+
axios@1.10.0:
2271
+
dependencies:
2272
+
follow-redirects: 1.15.9
2273
+
form-data: 4.0.4
2274
+
proxy-from-env: 1.1.0
2275
+
transitivePeerDependencies:
2276
+
- debug
2277
+
2127
2278
balanced-match@1.0.2: {}
2128
2279
2129
2280
binary-extensions@2.3.0: {}
2130
2281
2131
-
body-parser@2.2.0:
2282
+
body-parser@1.20.3:
2132
2283
dependencies:
2133
2284
bytes: 3.1.2
2134
2285
content-type: 1.0.5
2135
-
debug: 4.4.1(supports-color@5.5.0)
2286
+
debug: 2.6.9
2287
+
depd: 2.0.0
2288
+
destroy: 1.2.0
2136
2289
http-errors: 2.0.0
2137
-
iconv-lite: 0.6.3
2290
+
iconv-lite: 0.4.24
2138
2291
on-finished: 2.4.1
2139
-
qs: 6.14.0
2140
-
raw-body: 3.0.0
2141
-
type-is: 2.0.1
2292
+
qs: 6.13.0
2293
+
raw-body: 2.5.2
2294
+
type-is: 1.6.18
2295
+
unpipe: 1.0.0
2142
2296
transitivePeerDependencies:
2143
2297
- supports-color
2144
2298
···
2154
2308
braces@3.0.3:
2155
2309
dependencies:
2156
2310
fill-range: 7.1.1
2311
+
2312
+
buffer-equal-constant-time@1.0.1: {}
2157
2313
2158
2314
bytes@3.1.2: {}
2159
2315
···
2251
2407
color: 3.2.1
2252
2408
text-hex: 1.0.0
2253
2409
2410
+
combined-stream@1.0.8:
2411
+
dependencies:
2412
+
delayed-stream: 1.0.0
2413
+
2254
2414
commander@9.5.0: {}
2255
2415
2256
2416
concat-map@0.0.1: {}
···
2260
2420
snake-case: 2.1.0
2261
2421
upper-case: 1.1.3
2262
2422
2263
-
content-disposition@1.0.0:
2423
+
content-disposition@0.5.4:
2264
2424
dependencies:
2265
2425
safe-buffer: 5.2.1
2266
2426
2267
2427
content-type@1.0.5: {}
2268
2428
2269
-
cookie-signature@1.2.2: {}
2429
+
cookie-signature@1.0.6: {}
2270
2430
2271
-
cookie@0.7.2: {}
2431
+
cookie@0.7.1: {}
2272
2432
2273
2433
cors@2.8.5:
2274
2434
dependencies:
···
2283
2443
2284
2444
data-uri-to-buffer@4.0.1: {}
2285
2445
2446
+
debug@2.6.9:
2447
+
dependencies:
2448
+
ms: 2.0.0
2449
+
2286
2450
debug@4.4.1(supports-color@5.5.0):
2287
2451
dependencies:
2288
2452
ms: 2.1.3
···
2295
2459
2296
2460
deep-is@0.1.4: {}
2297
2461
2462
+
delayed-stream@1.0.0: {}
2463
+
2298
2464
depd@2.0.0: {}
2465
+
2466
+
destroy@1.2.0: {}
2299
2467
2300
2468
dir-glob@3.0.1:
2301
2469
dependencies:
···
2334
2502
es-errors: 1.3.0
2335
2503
gopd: 1.2.0
2336
2504
2505
+
ecdsa-sig-formatter@1.0.11:
2506
+
dependencies:
2507
+
safe-buffer: 5.2.1
2508
+
2337
2509
ee-first@1.1.1: {}
2338
2510
2339
2511
emoji-regex@8.0.0: {}
2340
2512
2341
2513
enabled@2.0.0: {}
2342
2514
2515
+
encodeurl@1.0.2: {}
2516
+
2343
2517
encodeurl@2.0.0: {}
2344
2518
2345
2519
es-define-property@1.0.1: {}
···
2349
2523
es-object-atoms@1.1.1:
2350
2524
dependencies:
2351
2525
es-errors: 1.3.0
2526
+
2527
+
es-set-tostringtag@2.1.0:
2528
+
dependencies:
2529
+
es-errors: 1.3.0
2530
+
get-intrinsic: 1.3.0
2531
+
has-tostringtag: 1.0.2
2532
+
hasown: 2.0.2
2352
2533
2353
2534
esbuild@0.25.6:
2354
2535
optionalDependencies:
···
2465
2646
2466
2647
etag@1.8.1: {}
2467
2648
2468
-
express-rate-limit@7.5.1(express@5.1.0):
2649
+
express-rate-limit@7.5.1(express@4.21.2):
2469
2650
dependencies:
2470
-
express: 5.1.0
2651
+
express: 4.21.2
2471
2652
2472
-
express@5.1.0:
2653
+
express-validator@7.2.1:
2654
+
dependencies:
2655
+
lodash: 4.17.21
2656
+
validator: 13.12.0
2657
+
2658
+
express@4.21.2:
2473
2659
dependencies:
2474
-
accepts: 2.0.0
2475
-
body-parser: 2.2.0
2476
-
content-disposition: 1.0.0
2660
+
accepts: 1.3.8
2661
+
array-flatten: 1.1.1
2662
+
body-parser: 1.20.3
2663
+
content-disposition: 0.5.4
2477
2664
content-type: 1.0.5
2478
-
cookie: 0.7.2
2479
-
cookie-signature: 1.2.2
2480
-
debug: 4.4.1(supports-color@5.5.0)
2665
+
cookie: 0.7.1
2666
+
cookie-signature: 1.0.6
2667
+
debug: 2.6.9
2668
+
depd: 2.0.0
2481
2669
encodeurl: 2.0.0
2482
2670
escape-html: 1.0.3
2483
2671
etag: 1.8.1
2484
-
finalhandler: 2.1.0
2485
-
fresh: 2.0.0
2672
+
finalhandler: 1.3.1
2673
+
fresh: 0.5.2
2486
2674
http-errors: 2.0.0
2487
-
merge-descriptors: 2.0.0
2488
-
mime-types: 3.0.1
2675
+
merge-descriptors: 1.0.3
2676
+
methods: 1.1.2
2489
2677
on-finished: 2.4.1
2490
-
once: 1.4.0
2491
2678
parseurl: 1.3.3
2679
+
path-to-regexp: 0.1.12
2492
2680
proxy-addr: 2.0.7
2493
-
qs: 6.14.0
2681
+
qs: 6.13.0
2494
2682
range-parser: 1.2.1
2495
-
router: 2.2.0
2496
-
send: 1.2.0
2497
-
serve-static: 2.2.0
2498
-
statuses: 2.0.2
2499
-
type-is: 2.0.1
2683
+
safe-buffer: 5.2.1
2684
+
send: 0.19.0
2685
+
serve-static: 1.16.2
2686
+
setprototypeof: 1.2.0
2687
+
statuses: 2.0.1
2688
+
type-is: 1.6.18
2689
+
utils-merge: 1.0.1
2500
2690
vary: 1.1.2
2501
2691
transitivePeerDependencies:
2502
2692
- supports-color
···
2536
2726
dependencies:
2537
2727
to-regex-range: 5.0.1
2538
2728
2539
-
finalhandler@2.1.0:
2729
+
finalhandler@1.3.1:
2540
2730
dependencies:
2541
-
debug: 4.4.1(supports-color@5.5.0)
2731
+
debug: 2.6.9
2542
2732
encodeurl: 2.0.0
2543
2733
escape-html: 1.0.3
2544
2734
on-finished: 2.4.1
2545
2735
parseurl: 1.3.3
2546
-
statuses: 2.0.2
2736
+
statuses: 2.0.1
2737
+
unpipe: 1.0.0
2547
2738
transitivePeerDependencies:
2548
2739
- supports-color
2549
2740
···
2566
2757
2567
2758
fn.name@1.1.0: {}
2568
2759
2760
+
follow-redirects@1.15.9: {}
2761
+
2762
+
form-data@4.0.4:
2763
+
dependencies:
2764
+
asynckit: 0.4.0
2765
+
combined-stream: 1.0.8
2766
+
es-set-tostringtag: 2.1.0
2767
+
hasown: 2.0.2
2768
+
mime-types: 2.1.35
2769
+
2569
2770
formdata-polyfill@4.0.10:
2570
2771
dependencies:
2571
2772
fetch-blob: 3.2.0
2572
2773
2573
2774
forwarded@0.2.0: {}
2574
2775
2575
-
fresh@2.0.0: {}
2776
+
fresh@0.5.2: {}
2576
2777
2577
2778
fsevents@2.3.3:
2578
2779
optional: true
···
2634
2835
2635
2836
has-symbols@1.1.0: {}
2636
2837
2838
+
has-tostringtag@1.0.2:
2839
+
dependencies:
2840
+
has-symbols: 1.1.0
2841
+
2637
2842
hasown@2.0.2:
2638
2843
dependencies:
2639
2844
function-bind: 1.1.2
···
2655
2860
statuses: 2.0.1
2656
2861
toidentifier: 1.0.1
2657
2862
2658
-
iconv-lite@0.6.3:
2863
+
iconv-lite@0.4.24:
2659
2864
dependencies:
2660
2865
safer-buffer: 2.1.2
2661
2866
···
2700
2905
lower-case: 1.1.4
2701
2906
2702
2907
is-number@7.0.0: {}
2703
-
2704
-
is-promise@4.0.0: {}
2705
2908
2706
2909
is-stream@2.0.1: {}
2707
2910
···
2725
2928
2726
2929
json5@2.2.3: {}
2727
2930
2931
+
jsonwebtoken@9.0.2:
2932
+
dependencies:
2933
+
jws: 3.2.2
2934
+
lodash.includes: 4.3.0
2935
+
lodash.isboolean: 3.0.3
2936
+
lodash.isinteger: 4.0.4
2937
+
lodash.isnumber: 3.0.3
2938
+
lodash.isplainobject: 4.0.6
2939
+
lodash.isstring: 4.0.1
2940
+
lodash.once: 4.1.1
2941
+
ms: 2.1.3
2942
+
semver: 7.7.2
2943
+
2944
+
jwa@1.4.2:
2945
+
dependencies:
2946
+
buffer-equal-constant-time: 1.0.1
2947
+
ecdsa-sig-formatter: 1.0.11
2948
+
safe-buffer: 5.2.1
2949
+
2950
+
jws@3.2.2:
2951
+
dependencies:
2952
+
jwa: 1.4.2
2953
+
safe-buffer: 5.2.1
2954
+
2728
2955
keyv@4.5.4:
2729
2956
dependencies:
2730
2957
json-buffer: 3.0.1
···
2744
2971
dependencies:
2745
2972
p-locate: 5.0.0
2746
2973
2974
+
lodash.includes@4.3.0: {}
2975
+
2976
+
lodash.isboolean@3.0.3: {}
2977
+
2978
+
lodash.isinteger@4.0.4: {}
2979
+
2980
+
lodash.isnumber@3.0.3: {}
2981
+
2982
+
lodash.isplainobject@4.0.6: {}
2983
+
2984
+
lodash.isstring@4.0.1: {}
2985
+
2747
2986
lodash.merge@4.6.2: {}
2987
+
2988
+
lodash.once@4.1.1: {}
2748
2989
2749
2990
lodash.snakecase@4.1.1: {}
2750
2991
···
2769
3010
2770
3011
math-intrinsics@1.1.0: {}
2771
3012
2772
-
media-typer@1.1.0: {}
3013
+
media-typer@0.3.0: {}
2773
3014
2774
-
merge-descriptors@2.0.0: {}
3015
+
merge-descriptors@1.0.3: {}
2775
3016
2776
3017
merge2@1.4.1: {}
2777
3018
3019
+
methods@1.1.2: {}
3020
+
2778
3021
micromatch@4.0.8:
2779
3022
dependencies:
2780
3023
braces: 3.0.3
2781
3024
picomatch: 2.3.1
2782
3025
2783
-
mime-db@1.54.0: {}
3026
+
mime-db@1.52.0: {}
2784
3027
2785
-
mime-types@3.0.1:
3028
+
mime-types@2.1.35:
2786
3029
dependencies:
2787
-
mime-db: 1.54.0
3030
+
mime-db: 1.52.0
3031
+
3032
+
mime@1.6.0: {}
2788
3033
2789
3034
minimatch@3.1.2:
2790
3035
dependencies:
···
2802
3047
2803
3048
moment@2.30.1: {}
2804
3049
3050
+
ms@2.0.0: {}
3051
+
2805
3052
ms@2.1.3: {}
2806
3053
2807
3054
mylas@2.1.13: {}
2808
3055
2809
3056
natural-compare@1.4.0: {}
2810
3057
2811
-
negotiator@1.0.0: {}
3058
+
negotiator@0.6.3: {}
2812
3059
2813
3060
no-case@2.3.2:
2814
3061
dependencies:
···
2844
3091
on-finished@2.4.1:
2845
3092
dependencies:
2846
3093
ee-first: 1.1.1
2847
-
2848
-
once@1.4.0:
2849
-
dependencies:
2850
-
wrappy: 1.0.2
2851
3094
2852
3095
one-time@1.0.0:
2853
3096
dependencies:
···
2903
3146
2904
3147
path-key@3.1.1: {}
2905
3148
2906
-
path-to-regexp@8.2.0: {}
3149
+
path-to-regexp@0.1.12: {}
2907
3150
2908
3151
path-type@4.0.0: {}
2909
3152
···
2970
3213
dependencies:
2971
3214
forwarded: 0.2.0
2972
3215
ipaddr.js: 1.9.1
3216
+
3217
+
proxy-from-env@1.1.0: {}
2973
3218
2974
3219
pstree.remy@1.1.8: {}
2975
3220
2976
3221
punycode@2.3.1: {}
2977
3222
2978
-
qs@6.14.0:
3223
+
qs@6.13.0:
2979
3224
dependencies:
2980
3225
side-channel: 1.1.0
2981
3226
···
2985
3230
2986
3231
range-parser@1.2.1: {}
2987
3232
2988
-
raw-body@3.0.0:
3233
+
raw-body@2.5.2:
2989
3234
dependencies:
2990
3235
bytes: 3.1.2
2991
3236
http-errors: 2.0.0
2992
-
iconv-lite: 0.6.3
3237
+
iconv-lite: 0.4.24
2993
3238
unpipe: 1.0.0
2994
3239
2995
3240
readable-stream@3.6.2:
···
3012
3257
3013
3258
reusify@1.1.0: {}
3014
3259
3015
-
router@2.2.0:
3016
-
dependencies:
3017
-
debug: 4.4.1(supports-color@5.5.0)
3018
-
depd: 2.0.0
3019
-
is-promise: 4.0.0
3020
-
parseurl: 1.3.3
3021
-
path-to-regexp: 8.2.0
3022
-
transitivePeerDependencies:
3023
-
- supports-color
3024
-
3025
3260
run-parallel@1.2.0:
3026
3261
dependencies:
3027
3262
queue-microtask: 1.2.3
···
3034
3269
3035
3270
semver@7.7.2: {}
3036
3271
3037
-
send@1.2.0:
3272
+
send@0.19.0:
3038
3273
dependencies:
3039
-
debug: 4.4.1(supports-color@5.5.0)
3040
-
encodeurl: 2.0.0
3274
+
debug: 2.6.9
3275
+
depd: 2.0.0
3276
+
destroy: 1.2.0
3277
+
encodeurl: 1.0.2
3041
3278
escape-html: 1.0.3
3042
3279
etag: 1.8.1
3043
-
fresh: 2.0.0
3280
+
fresh: 0.5.2
3044
3281
http-errors: 2.0.0
3045
-
mime-types: 3.0.1
3282
+
mime: 1.6.0
3046
3283
ms: 2.1.3
3047
3284
on-finished: 2.4.1
3048
3285
range-parser: 1.2.1
3049
-
statuses: 2.0.2
3286
+
statuses: 2.0.1
3050
3287
transitivePeerDependencies:
3051
3288
- supports-color
3052
3289
···
3055
3292
no-case: 2.3.2
3056
3293
upper-case-first: 1.1.2
3057
3294
3058
-
serve-static@2.2.0:
3295
+
serve-static@1.16.2:
3059
3296
dependencies:
3060
3297
encodeurl: 2.0.0
3061
3298
escape-html: 1.0.3
3062
3299
parseurl: 1.3.3
3063
-
send: 1.2.0
3300
+
send: 0.19.0
3064
3301
transitivePeerDependencies:
3065
3302
- supports-color
3066
3303
···
3131
3368
3132
3369
statuses@2.0.1: {}
3133
3370
3134
-
statuses@2.0.2: {}
3135
-
3136
3371
string-width@4.2.3:
3137
3372
dependencies:
3138
3373
emoji-regex: 8.0.0
···
3220
3455
dependencies:
3221
3456
prelude-ls: 1.2.1
3222
3457
3223
-
type-is@2.0.1:
3458
+
type-is@1.6.18:
3224
3459
dependencies:
3225
-
content-type: 1.0.5
3226
-
media-typer: 1.1.0
3227
-
mime-types: 3.0.1
3460
+
media-typer: 0.3.0
3461
+
mime-types: 2.1.35
3228
3462
3229
3463
typescript-eslint@8.36.0(eslint@9.30.1)(typescript@5.8.3):
3230
3464
dependencies:
···
3260
3494
3261
3495
util-deprecate@1.0.2: {}
3262
3496
3497
+
utils-merge@1.0.1: {}
3498
+
3499
+
uuid@11.1.0: {}
3500
+
3501
+
validator@13.12.0: {}
3502
+
3263
3503
validator@13.15.15: {}
3264
3504
3265
3505
vary@1.1.2: {}
···
3313
3553
ansi-styles: 4.3.0
3314
3554
string-width: 4.2.3
3315
3555
strip-ansi: 6.0.1
3316
-
3317
-
wrappy@1.0.2: {}
3318
3556
3319
3557
ws@8.18.3: {}
3320
3558
+3
-1
src/commands/fun/joke.ts
+3
-1
src/commands/fun/joke.ts
···
140
140
);
141
141
embed.setFooter({ text: 'Ba dum tss! 🥁' });
142
142
await interaction.editReply({ embeds: [embed] });
143
-
} catch {}
143
+
} catch (error) {
144
+
console.error('Error showing punchline:', error);
145
+
}
144
146
}, 3000);
145
147
} catch (error) {
146
148
await errorHandler({
+1
-1
src/commands/utilities/ai.ts
+1
-1
src/commands/utilities/ai.ts
···
482
482
if (reset) {
483
483
userConversations.delete(userId);
484
484
await interaction.reply({
485
-
content: await client.getLocaleText('commands.reset', interaction.locale),
485
+
content: await client.getLocaleText('commands.ai.reset', interaction.locale),
486
486
ephemeral: true,
487
487
});
488
488
pendingRequests.delete(userId);
+298
src/commands/utilities/cobalt.ts
+298
src/commands/utilities/cobalt.ts
···
1
+
import {
2
+
SlashCommandBuilder,
3
+
ActionRowBuilder,
4
+
ButtonBuilder,
5
+
ButtonStyle,
6
+
ChatInputCommandInteraction,
7
+
ApplicationIntegrationType,
8
+
InteractionContextType,
9
+
} from 'discord.js';
10
+
import axios from 'axios';
11
+
import { SlashCommandProps } from '../../types/command';
12
+
import BotClient from '../../services/Client';
13
+
import { createCommandLogger } from '../../utils/commandLogger';
14
+
import { createErrorHandler } from '../../utils/errorHandler';
15
+
16
+
interface CobaltResponse {
17
+
status: string;
18
+
url?: string;
19
+
filename?: string;
20
+
text?: string;
21
+
error?: {
22
+
code: string;
23
+
message: string;
24
+
};
25
+
}
26
+
27
+
const commandLogger = createCommandLogger('cobalt');
28
+
const errorHandler = createErrorHandler('cobalt');
29
+
30
+
export default {
31
+
data: new SlashCommandBuilder()
32
+
.setName('cobalt')
33
+
.setNameLocalizations({
34
+
'es-ES': 'cobalt',
35
+
'es-419': 'cobalt',
36
+
'en-US': 'cobalt',
37
+
})
38
+
.setDescription('Download a video or audio from a given URL')
39
+
.setDescriptionLocalizations({
40
+
'es-ES': 'Descarga un video o audio desde una URL',
41
+
'es-419': 'Descarga un video o audio desde una URL',
42
+
'en-US': 'Download a video or audio from a given URL',
43
+
})
44
+
.addStringOption((option) =>
45
+
option
46
+
.setName('url')
47
+
.setNameLocalizations({
48
+
'es-ES': 'url',
49
+
'es-419': 'url',
50
+
'en-US': 'url',
51
+
})
52
+
.setDescription('The URL of the video to download')
53
+
.setDescriptionLocalizations({
54
+
'es-ES': 'La URL del video a descargar',
55
+
'es-419': 'La URL del video a descargar',
56
+
'en-US': 'The URL of the video to download',
57
+
})
58
+
.setRequired(true)
59
+
)
60
+
.addStringOption((option) =>
61
+
option
62
+
.setName('video-quality')
63
+
.setNameLocalizations({
64
+
'es-ES': 'calidad-video',
65
+
'es-419': 'calidad-video',
66
+
'en-US': 'video-quality',
67
+
})
68
+
.setDescription('The video quality to download')
69
+
.setDescriptionLocalizations({
70
+
'es-ES': 'La calidad de video a descargar',
71
+
'es-419': 'La calidad de video a descargar',
72
+
'en-US': 'The video quality to download',
73
+
})
74
+
.addChoices(
75
+
{ name: '144p', value: '144' },
76
+
{ name: '240p', value: '240' },
77
+
{ name: '360p', value: '360' },
78
+
{ name: '480p', value: '480' },
79
+
{ name: '720p', value: '720' },
80
+
{ name: '1440p', value: '1440' },
81
+
{ name: '2160p (4K)', value: '2160' },
82
+
{ name: '4320p (8K)', value: '4320' },
83
+
{ name: 'Max', value: 'max' }
84
+
)
85
+
)
86
+
.addBooleanOption((option) =>
87
+
option
88
+
.setName('audio-only')
89
+
.setNameLocalizations({
90
+
'es-ES': 'solo-audio',
91
+
'es-419': 'solo-audio',
92
+
'en-US': 'audio-only',
93
+
})
94
+
.setDescription('Download audio only')
95
+
.setDescriptionLocalizations({
96
+
'es-ES': 'Descargar solo audio',
97
+
'es-419': 'Descargar solo audio',
98
+
'en-US': 'Download audio only',
99
+
})
100
+
)
101
+
.addBooleanOption((option) =>
102
+
option
103
+
.setName('mute-audio')
104
+
.setNameLocalizations({
105
+
'es-ES': 'silenciar-audio',
106
+
'es-419': 'silenciar-audio',
107
+
'en-US': 'mute-audio',
108
+
})
109
+
.setDescription('Mute audio')
110
+
.setDescriptionLocalizations({
111
+
'es-ES': 'Silenciar audio',
112
+
'es-419': 'Silenciar audio',
113
+
'en-US': 'Mute audio',
114
+
})
115
+
)
116
+
.addBooleanOption((option) =>
117
+
option
118
+
.setName('twitter-gif')
119
+
.setNameLocalizations({
120
+
'es-ES': 'gif-twitter',
121
+
'es-419': 'gif-twitter',
122
+
'en-US': 'twitter-gif',
123
+
})
124
+
.setDescription('Download as Twitter GIF')
125
+
.setDescriptionLocalizations({
126
+
'es-ES': 'Descargar como GIF de Twitter',
127
+
'es-419': 'Descargar como GIF de Twitter',
128
+
'en-US': 'Download as Twitter GIF',
129
+
})
130
+
)
131
+
.addBooleanOption((option) =>
132
+
option
133
+
.setName('tiktok-original-audio')
134
+
.setNameLocalizations({
135
+
'es-ES': 'audio-original-tiktok',
136
+
'es-419': 'audio-original-tiktok',
137
+
'en-US': 'tiktok-original-audio',
138
+
})
139
+
.setDescription('Include TikTok original audio')
140
+
.setDescriptionLocalizations({
141
+
'es-ES': 'Incluir audio original de TikTok',
142
+
'es-419': 'Incluir audio original de TikTok',
143
+
'en-US': 'Include TikTok original audio',
144
+
})
145
+
)
146
+
.addStringOption((option) =>
147
+
option
148
+
.setName('audio-format')
149
+
.setNameLocalizations({
150
+
'es-ES': 'formato-audio',
151
+
'es-419': 'formato-audio',
152
+
'en-US': 'audio-format',
153
+
})
154
+
.setDescription('Format for audio (requires audio-only to be true)')
155
+
.setDescriptionLocalizations({
156
+
'es-ES': 'Formato para audio (requiere solo-audio activado)',
157
+
'es-419': 'Formato para audio (requiere solo-audio activado)',
158
+
'en-US': 'Format for audio (requires audio-only to be true)',
159
+
})
160
+
.addChoices(
161
+
{ name: 'MP3', value: 'mp3' },
162
+
{ name: 'OGG', value: 'ogg' },
163
+
{ name: 'WAV', value: 'wav' },
164
+
{ name: 'Best', value: 'best' }
165
+
)
166
+
)
167
+
.setContexts([
168
+
InteractionContextType.BotDM,
169
+
InteractionContextType.Guild,
170
+
InteractionContextType.PrivateChannel,
171
+
])
172
+
.setIntegrationTypes(ApplicationIntegrationType.UserInstall),
173
+
category: 'utilities',
174
+
async execute(client: BotClient, interaction: ChatInputCommandInteraction): Promise<void> {
175
+
try {
176
+
commandLogger.logFromInteraction(interaction);
177
+
178
+
const url = interaction.options.getString('url', true);
179
+
const videoQuality = interaction.options.getString('video-quality');
180
+
const audioOnly = interaction.options.getBoolean('audio-only');
181
+
const muteAudio = interaction.options.getBoolean('mute-audio');
182
+
const twitterGif = interaction.options.getBoolean('twitter-gif');
183
+
const tiktokOriginalAudio = interaction.options.getBoolean('tiktok-original-audio');
184
+
const audioFormat = interaction.options.getString('audio-format');
185
+
186
+
await interaction.deferReply();
187
+
188
+
const requestBody = {
189
+
url: url,
190
+
videoQuality: videoQuality || 'max',
191
+
audioFormat: audioFormat || 'mp3',
192
+
downloadMode: audioOnly ? 'audio' : muteAudio ? 'mute' : 'auto',
193
+
filenameStyle: 'basic',
194
+
tiktokFullAudio: tiktokOriginalAudio || false,
195
+
convertGif: twitterGif || false,
196
+
};
197
+
198
+
const response = await axios.post('https://cobalt.aethel.xyz/', requestBody, {
199
+
headers: {
200
+
Accept: 'application/json',
201
+
'Content-Type': 'application/json',
202
+
},
203
+
});
204
+
205
+
const data: CobaltResponse = response.data;
206
+
207
+
if (data.status === 'tunnel' || data.status === 'redirect') {
208
+
const downloadUrl = data.url;
209
+
if (!downloadUrl) {
210
+
throw new Error('No download URL received');
211
+
}
212
+
213
+
const buttonLabel = await client.getLocaleText(
214
+
'commands.cobalt.button_label',
215
+
interaction.locale
216
+
);
217
+
const successMessage = await client.getLocaleText(
218
+
'commands.cobalt.success',
219
+
interaction.locale,
220
+
{
221
+
url,
222
+
filename: data.filename || 'download',
223
+
}
224
+
);
225
+
226
+
const row = new ActionRowBuilder<ButtonBuilder>().addComponents(
227
+
new ButtonBuilder().setLabel(buttonLabel).setStyle(ButtonStyle.Link).setURL(downloadUrl)
228
+
);
229
+
230
+
await interaction.editReply({
231
+
content: successMessage,
232
+
components: [row],
233
+
});
234
+
} else if (data.status === 'error') {
235
+
const unknownError = await client.getLocaleText(
236
+
'commands.cobalt.unknown_error',
237
+
interaction.locale
238
+
);
239
+
let errorText = unknownError;
240
+
241
+
if (data.error) {
242
+
errorText = data.error.message || data.error.code || unknownError;
243
+
} else if (data.text) {
244
+
errorText = data.text;
245
+
}
246
+
247
+
const errorMessage = await client.getLocaleText(
248
+
'commands.cobalt.error',
249
+
interaction.locale,
250
+
{
251
+
error: errorText,
252
+
}
253
+
);
254
+
255
+
await interaction.editReply({
256
+
content: errorMessage,
257
+
});
258
+
} else if (data.status === 'picker') {
259
+
const multipleItemsMessage = await client.getLocaleText(
260
+
'commands.cobalt.multiple_items',
261
+
interaction.locale,
262
+
{ url }
263
+
);
264
+
265
+
await interaction.editReply({
266
+
content:
267
+
multipleItemsMessage || 'Multiple items found. Please provide a more specific URL.',
268
+
});
269
+
} else if (data.status === 'local-processing') {
270
+
const notSupportedMessage = await client.getLocaleText(
271
+
'commands.cobalt.local_processing_not_supported',
272
+
interaction.locale
273
+
);
274
+
275
+
await interaction.editReply({
276
+
content:
277
+
notSupportedMessage ||
278
+
'Local processing is not supported. Please try a different URL or option.',
279
+
});
280
+
} else {
281
+
const unknownResponseMessage = await client.getLocaleText(
282
+
'commands.cobalt.unknown_response',
283
+
interaction.locale
284
+
);
285
+
286
+
await interaction.editReply({
287
+
content: unknownResponseMessage,
288
+
});
289
+
}
290
+
} catch (error) {
291
+
await errorHandler({
292
+
interaction,
293
+
client,
294
+
error: error as Error,
295
+
});
296
+
}
297
+
},
298
+
} as SlashCommandProps;
+79
src/commands/utilities/remind.ts
+79
src/commands/utilities/remind.ts
···
24
24
completeReminder,
25
25
cleanupReminders,
26
26
ensureUserRegistered,
27
+
getActiveReminders,
27
28
DatabaseError,
28
29
} from '@/utils/reminderDb';
29
30
import { RemindCommandProps } from '@/types/command';
···
69
70
const activeReminders = new Map<string, ActiveReminder>();
70
71
const commandLogger = createCommandLogger('remind');
71
72
const errorHandler = createErrorHandler('remind');
73
+
74
+
export async function loadActiveReminders(client: BotClient) {
75
+
try {
76
+
const reminders = await getActiveReminders();
77
+
logger.info(`Loading ${reminders.length} active reminders from database`);
78
+
79
+
for (const reminder of reminders) {
80
+
const now = Date.now();
81
+
const expiresAt = new Date(reminder.expires_at).getTime();
82
+
const timeUntilExpiry = expiresAt - now;
83
+
84
+
if (timeUntilExpiry <= 0) {
85
+
logger.warn(`Skipping expired reminder ${reminder.reminder_id}`);
86
+
continue;
87
+
}
88
+
89
+
const timeoutId = setTimeout(
90
+
createReminderHandler(client, {
91
+
...reminder,
92
+
created_at: reminder.created_at || new Date(reminder.expires_at),
93
+
}),
94
+
timeUntilExpiry
95
+
);
96
+
97
+
activeReminders.set(reminder.reminder_id, {
98
+
timeoutId,
99
+
expiresAt,
100
+
});
101
+
102
+
logger.debug(
103
+
`Scheduled reminder ${reminder.reminder_id} for ${new Date(expiresAt).toISOString()}`
104
+
);
105
+
}
106
+
107
+
logger.info(`Successfully loaded ${activeReminders.size} active reminders`);
108
+
} catch (error) {
109
+
logger.error('Error loading active reminders:', error);
110
+
}
111
+
}
112
+
113
+
export function scheduleReminder(client: BotClient, reminder: Reminder) {
114
+
try {
115
+
const now = Date.now();
116
+
const expiresAt = new Date(reminder.expires_at).getTime();
117
+
const timeUntilExpiry = expiresAt - now;
118
+
119
+
if (timeUntilExpiry <= 0) {
120
+
logger.warn(`Cannot schedule expired reminder ${reminder.reminder_id}`);
121
+
return false;
122
+
}
123
+
124
+
const existingReminder = activeReminders.get(reminder.reminder_id);
125
+
if (existingReminder) {
126
+
clearTimeout(existingReminder.timeoutId);
127
+
}
128
+
129
+
const timeoutId = setTimeout(
130
+
createReminderHandler(client, {
131
+
...reminder,
132
+
created_at: reminder.created_at || new Date(),
133
+
}),
134
+
timeUntilExpiry
135
+
);
136
+
137
+
activeReminders.set(reminder.reminder_id, {
138
+
timeoutId,
139
+
expiresAt,
140
+
});
141
+
142
+
logger.info(
143
+
`Scheduled reminder ${reminder.reminder_id} for ${new Date(expiresAt).toISOString()}`
144
+
);
145
+
return true;
146
+
} catch (error) {
147
+
logger.error(`Error scheduling reminder ${reminder.reminder_id}:`, error);
148
+
return false;
149
+
}
150
+
}
72
151
73
152
declare global {
74
153
var _reminders: Map<string, MessageInfo>;
+3
src/events/ready.ts
+3
src/events/ready.ts
···
1
1
import BotClient from '@/services/Client';
2
2
import logger from '@/utils/logger';
3
+
import { loadActiveReminders } from '@/commands/utilities/remind';
3
4
4
5
export default class ReadyEvent {
5
6
constructor(c: BotClient) {
···
10
11
try {
11
12
logger.info(`Logged in as ${client.user?.username}`);
12
13
await client.application?.commands.fetch({ withLocalizations: true });
14
+
15
+
await loadActiveReminders(client);
13
16
} catch (error) {
14
17
logger.error('Error during ready event:', error);
15
18
}
+40
-10
src/index.ts
+40
-10
src/index.ts
···
8
8
import rateLimit from 'express-rate-limit';
9
9
import authenticateApiKey from './middlewares/verifyApiKey';
10
10
import status from './routes/status';
11
+
import authRoutes from './routes/auth';
12
+
import todosRoutes from './routes/todos';
13
+
import apiKeysRoutes from './routes/apiKeys';
14
+
import remindersRoutes from './routes/reminders';
11
15
import { resetOldStrikes } from './utils/userStrikes';
12
16
13
17
config();
···
26
30
app.use(helmet());
27
31
app.use(
28
32
cors({
29
-
origin: ALLOWED_ORIGINS ? ALLOWED_ORIGINS.split(',') : '*',
30
-
methods: ['GET'],
31
-
allowedHeaders: ['Content-Type', 'X-API-Key'],
33
+
origin: ALLOWED_ORIGINS
34
+
? ALLOWED_ORIGINS.split(',')
35
+
: ['http://localhost:3000', 'http://localhost:8080'],
36
+
methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'],
37
+
allowedHeaders: ['Content-Type', 'X-API-Key', 'Authorization'],
38
+
credentials: true,
32
39
maxAge: 86400,
33
40
})
34
41
);
···
43
50
})
44
51
);
45
52
53
+
app.use(e.json({ limit: '10mb' }));
54
+
app.use(e.urlencoded({ extended: true, limit: '10mb' }));
55
+
46
56
app.use((req, res, next) => {
47
57
res.setHeader('X-Content-Type-Options', 'nosniff');
48
58
res.setHeader('X-Frame-Options', 'DENY');
49
-
res.setHeader('Content-Security-Policy', "default-src 'none'");
50
-
res.setHeader('Cache-Control', 'no-store, no-cache, must-revalidate, proxy-revalidate');
51
-
res.setHeader('Pragma', 'no-cache');
52
-
res.setHeader('Expires', '0');
53
-
res.setHeader('Surrogate-Control', 'no-store');
59
+
if (req.path.startsWith('/api/') || req.path.startsWith('/status')) {
60
+
res.setHeader('Content-Security-Policy', "default-src 'none'");
61
+
res.setHeader('Cache-Control', 'no-store, no-cache, must-revalidate, proxy-revalidate');
62
+
res.setHeader('Pragma', 'no-cache');
63
+
res.setHeader('Expires', '0');
64
+
res.setHeader('Surrogate-Control', 'no-store');
65
+
} else {
66
+
res.setHeader(
67
+
'Content-Security-Policy',
68
+
"default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; img-src 'self' data: https:; font-src 'self' data: https://fonts.gstatic.com; connect-src 'self' https:"
69
+
);
70
+
}
54
71
next();
55
72
});
56
73
57
74
const bot = new BotClient();
58
75
bot.init();
59
76
60
-
app.use(authenticateApiKey);
61
-
app.use('/status', status(bot));
77
+
app.use('/api/auth', authRoutes);
78
+
app.use('/api/todos', todosRoutes);
79
+
app.use('/api/user/api-keys', apiKeysRoutes);
80
+
app.use('/api/reminders', remindersRoutes);
81
+
82
+
app.use('/status', authenticateApiKey, status(bot));
83
+
84
+
app.use(e.static('web/dist'));
85
+
86
+
app.get('*', (req, res) => {
87
+
if (req.path.startsWith('/api/') || req.path.startsWith('/status')) {
88
+
return res.status(404).json({ error: 'Not found' });
89
+
}
90
+
res.sendFile('index.html', { root: 'web/dist' });
91
+
});
62
92
63
93
setInterval(
64
94
() => {
+58
src/middlewares/auth.ts
+58
src/middlewares/auth.ts
···
1
+
import { Request, Response, NextFunction } from 'express';
2
+
import jwt from 'jsonwebtoken';
3
+
import logger from '../utils/logger';
4
+
5
+
const JWT_SECRET = process.env.JWT_SECRET || 'your-jwt-secret';
6
+
7
+
interface JwtPayload {
8
+
userId: string;
9
+
username: string;
10
+
discriminator: string;
11
+
avatar?: string;
12
+
iat?: number;
13
+
exp?: number;
14
+
}
15
+
16
+
export const authenticateToken = (req: Request, res: Response, next: NextFunction) => {
17
+
const authHeader = req.headers['authorization'];
18
+
const token = authHeader && authHeader.split(' ')[1]; // Bearer TOKEN
19
+
20
+
if (!token) {
21
+
return res.status(401).json({ error: 'Access token required' });
22
+
}
23
+
24
+
try {
25
+
const decoded = jwt.verify(token, JWT_SECRET) as JwtPayload;
26
+
req.user = decoded;
27
+
next();
28
+
} catch (error) {
29
+
if (error instanceof jwt.TokenExpiredError) {
30
+
logger.debug('Expired JWT token used');
31
+
return res.status(401).json({ error: 'Token expired' });
32
+
} else if (error instanceof jwt.JsonWebTokenError) {
33
+
logger.debug('Invalid JWT token used');
34
+
return res.status(401).json({ error: 'Invalid token' });
35
+
} else {
36
+
logger.error('JWT verification error:', error);
37
+
return res.status(500).json({ error: 'Token verification failed' });
38
+
}
39
+
}
40
+
};
41
+
42
+
export const optionalAuth = (req: Request, res: Response, next: NextFunction) => {
43
+
const authHeader = req.headers['authorization'];
44
+
const token = authHeader && authHeader.split(' ')[1];
45
+
46
+
if (!token) {
47
+
return next();
48
+
}
49
+
50
+
try {
51
+
const decoded = jwt.verify(token, JWT_SECRET) as JwtPayload;
52
+
req.user = decoded;
53
+
} catch (error) {
54
+
logger.debug('Optional auth token verification failed:', error);
55
+
}
56
+
57
+
next();
58
+
};
+268
src/routes/apiKeys.ts
+268
src/routes/apiKeys.ts
···
1
+
import { Router } from 'express';
2
+
import pool from '../utils/pgClient';
3
+
import logger from '../utils/logger';
4
+
import { authenticateToken } from '../middlewares/auth';
5
+
import { body, validationResult } from 'express-validator';
6
+
import { encrypt as encryptApiKey } from '../utils/encrypt';
7
+
8
+
const router = Router();
9
+
10
+
router.use(authenticateToken);
11
+
12
+
router.get('/', async (req, res) => {
13
+
try {
14
+
const userId = req.user?.userId;
15
+
if (!userId) {
16
+
return res.status(401).json({ error: 'User not authenticated' });
17
+
}
18
+
19
+
const query = `
20
+
SELECT custom_model, custom_api_url,
21
+
CASE WHEN api_key_encrypted IS NOT NULL THEN TRUE ELSE FALSE END as has_api_key
22
+
FROM users
23
+
WHERE user_id = $1
24
+
`;
25
+
const result = await pool.query(query, [userId]);
26
+
27
+
if (result.rows.length === 0) {
28
+
return res.status(404).json({ error: 'User not found' });
29
+
}
30
+
31
+
const user = result.rows[0];
32
+
res.json({
33
+
hasApiKey: user.has_api_key,
34
+
model: user.custom_model,
35
+
apiUrl: user.custom_api_url,
36
+
});
37
+
} catch (error) {
38
+
logger.error('Error fetching API key info:', error);
39
+
res.status(500).json({ error: 'Internal server error' });
40
+
}
41
+
});
42
+
43
+
router.post(
44
+
'/',
45
+
body('apiKey')
46
+
.trim()
47
+
.isLength({ min: 1, max: 1000 })
48
+
.withMessage('API key is required and must be less than 1000 characters'),
49
+
body('model')
50
+
.optional()
51
+
.trim()
52
+
.isLength({ max: 100 })
53
+
.withMessage('Model name must be less than 100 characters'),
54
+
body('apiUrl')
55
+
.optional()
56
+
.trim()
57
+
.isURL({ require_protocol: true })
58
+
.withMessage('API URL must be a valid URL with protocol'),
59
+
async (req, res) => {
60
+
try {
61
+
const errors = validationResult(req);
62
+
if (!errors.isEmpty()) {
63
+
return res.status(400).json({ error: errors.array()[0].msg });
64
+
}
65
+
66
+
const userId = req.user?.userId;
67
+
if (!userId) {
68
+
return res.status(401).json({ error: 'User not authenticated' });
69
+
}
70
+
71
+
const { apiKey, model, apiUrl } = req.body;
72
+
73
+
const encryptedApiKey = encryptApiKey(apiKey);
74
+
75
+
const query = `
76
+
UPDATE users
77
+
SET api_key_encrypted = $1,
78
+
custom_model = $2,
79
+
custom_api_url = $3
80
+
WHERE user_id = $4
81
+
RETURNING user_id
82
+
`;
83
+
const result = await pool.query(query, [
84
+
encryptedApiKey,
85
+
model || null,
86
+
apiUrl || null,
87
+
userId,
88
+
]);
89
+
90
+
if (result.rows.length === 0) {
91
+
return res.status(404).json({ error: 'User not found' });
92
+
}
93
+
94
+
logger.info(`API key updated for user ${userId}`);
95
+
res.json({ message: 'API key updated successfully' });
96
+
} catch (error) {
97
+
logger.error('Error updating API key:', error);
98
+
res.status(500).json({ error: 'Internal server error' });
99
+
}
100
+
}
101
+
);
102
+
103
+
router.put(
104
+
'/',
105
+
body('apiKey')
106
+
.trim()
107
+
.isLength({ min: 1, max: 1000 })
108
+
.withMessage('API key is required and must be less than 1000 characters'),
109
+
body('model')
110
+
.optional()
111
+
.trim()
112
+
.isLength({ max: 100 })
113
+
.withMessage('Model name must be less than 100 characters'),
114
+
body('apiUrl')
115
+
.optional()
116
+
.trim()
117
+
.isURL({ require_protocol: true })
118
+
.withMessage('API URL must be a valid URL with protocol'),
119
+
async (req, res) => {
120
+
try {
121
+
const errors = validationResult(req);
122
+
if (!errors.isEmpty()) {
123
+
return res.status(400).json({ error: errors.array()[0].msg });
124
+
}
125
+
126
+
const userId = req.user?.userId;
127
+
if (!userId) {
128
+
return res.status(401).json({ error: 'User not authenticated' });
129
+
}
130
+
131
+
const { apiKey, model, apiUrl } = req.body;
132
+
133
+
const encryptedApiKey = encryptApiKey(apiKey);
134
+
135
+
const query = `
136
+
UPDATE users
137
+
SET api_key_encrypted = $1,
138
+
custom_model = $2,
139
+
custom_api_url = $3
140
+
WHERE user_id = $4
141
+
RETURNING user_id
142
+
`;
143
+
const result = await pool.query(query, [
144
+
encryptedApiKey,
145
+
model || null,
146
+
apiUrl || null,
147
+
userId,
148
+
]);
149
+
150
+
if (result.rows.length === 0) {
151
+
return res.status(404).json({ error: 'User not found' });
152
+
}
153
+
154
+
logger.info(`API key updated for user ${userId}`);
155
+
res.json({ message: 'API key updated successfully' });
156
+
} catch (error) {
157
+
logger.error('Error updating API key:', error);
158
+
res.status(500).json({ error: 'Internal server error' });
159
+
}
160
+
}
161
+
);
162
+
163
+
router.delete('/', async (req, res) => {
164
+
try {
165
+
const userId = req.user?.userId;
166
+
if (!userId) {
167
+
return res.status(401).json({ error: 'User not authenticated' });
168
+
}
169
+
170
+
const query = `
171
+
UPDATE users
172
+
SET api_key_encrypted = NULL,
173
+
custom_model = NULL,
174
+
custom_api_url = NULL
175
+
WHERE user_id = $1
176
+
RETURNING user_id
177
+
`;
178
+
const result = await pool.query(query, [userId]);
179
+
180
+
if (result.rows.length === 0) {
181
+
return res.status(404).json({ error: 'User not found' });
182
+
}
183
+
184
+
logger.info(`API key deleted for user ${userId}`);
185
+
res.json({ message: 'API key deleted successfully' });
186
+
} catch (error) {
187
+
logger.error('Error deleting API key:', error);
188
+
res.status(500).json({ error: 'Internal server error' });
189
+
}
190
+
});
191
+
192
+
router.post(
193
+
'/test',
194
+
body('apiKey').trim().isLength({ min: 1 }).withMessage('API key is required'),
195
+
body('model').optional().trim(),
196
+
body('apiUrl')
197
+
.optional()
198
+
.trim()
199
+
.isURL({ require_protocol: true })
200
+
.withMessage('API URL must be a valid URL with protocol'),
201
+
async (req, res) => {
202
+
try {
203
+
const errors = validationResult(req);
204
+
if (!errors.isEmpty()) {
205
+
return res.status(400).json({ error: errors.array()[0].msg });
206
+
}
207
+
208
+
const { apiKey, model, apiUrl } = req.body;
209
+
const userId = req.user?.userId;
210
+
211
+
const fullApiUrl = apiUrl || 'https://api.openai.com/v1/chat/completions';
212
+
const testModel = model || 'gpt-3.5-turbo';
213
+
214
+
const testResponse = await fetch(fullApiUrl, {
215
+
method: 'POST',
216
+
headers: {
217
+
Authorization: `Bearer ${apiKey}`,
218
+
'Content-Type': 'application/json',
219
+
},
220
+
body: JSON.stringify({
221
+
model: testModel,
222
+
messages: [
223
+
{
224
+
role: 'user',
225
+
content:
226
+
'Hello! This is a test message. Please respond with "API key test successful!"',
227
+
},
228
+
],
229
+
max_tokens: 50,
230
+
temperature: 0.1,
231
+
}),
232
+
});
233
+
234
+
if (!testResponse.ok) {
235
+
const errorData = await testResponse.json().catch(() => ({}));
236
+
const errorMessage =
237
+
errorData.error?.message || `HTTP ${testResponse.status}: ${testResponse.statusText}`;
238
+
239
+
logger.warn(`API key test failed for user ${userId}: ${errorMessage}`);
240
+
return res.status(400).json({
241
+
error: `API key test failed: ${errorMessage}`,
242
+
});
243
+
}
244
+
245
+
const responseData = await testResponse.json();
246
+
const testMessage = responseData.choices?.[0]?.message?.content || 'Test completed';
247
+
248
+
logger.info(`API key test successful for user ${userId}`);
249
+
res.json({
250
+
success: true,
251
+
message: 'API key test successful!',
252
+
testResponse: testMessage.substring(0, 100),
253
+
});
254
+
} catch (error) {
255
+
logger.error('Error testing API key:', error);
256
+
257
+
if (error instanceof TypeError && error.message.includes('fetch')) {
258
+
return res
259
+
.status(400)
260
+
.json({ error: 'Failed to connect to API endpoint. Please check the URL.' });
261
+
}
262
+
263
+
res.status(500).json({ error: 'API key test failed due to server error' });
264
+
}
265
+
}
266
+
);
267
+
268
+
export default router;
+151
src/routes/auth.ts
+151
src/routes/auth.ts
···
1
+
import { Router } from 'express';
2
+
import jwt from 'jsonwebtoken';
3
+
import pool from '../utils/pgClient';
4
+
import logger from '../utils/logger';
5
+
import { authenticateToken } from '../middlewares/auth';
6
+
7
+
const router = Router();
8
+
9
+
const DISCORD_CLIENT_ID = process.env.DISCORD_CLIENT_ID;
10
+
const DISCORD_CLIENT_SECRET = process.env.DISCORD_CLIENT_SECRET;
11
+
const DISCORD_REDIRECT_URI =
12
+
process.env.DISCORD_REDIRECT_URI || 'http://localhost:8080/api/auth/discord/callback';
13
+
const JWT_SECRET = process.env.JWT_SECRET || 'your-jwt-secret';
14
+
const FRONTEND_URL = process.env.FRONTEND_URL || 'http://localhost:3000';
15
+
16
+
interface DiscordUser {
17
+
id: string;
18
+
username: string;
19
+
discriminator: string;
20
+
avatar?: string;
21
+
email?: string;
22
+
}
23
+
24
+
router.get('/discord', (req, res) => {
25
+
const discordAuthUrl = `https://discord.com/api/oauth2/authorize?client_id=${DISCORD_CLIENT_ID}&redirect_uri=${encodeURIComponent(DISCORD_REDIRECT_URI)}&response_type=code&scope=identify`;
26
+
res.redirect(discordAuthUrl);
27
+
});
28
+
29
+
router.get('/discord/callback', async (req, res) => {
30
+
const { code, error } = req.query;
31
+
32
+
if (error) {
33
+
logger.error('Discord OAuth error:', error);
34
+
return res.redirect(`${FRONTEND_URL}/login?error=oauth_error`);
35
+
}
36
+
37
+
if (!code) {
38
+
logger.error('No authorization code received from Discord');
39
+
return res.redirect(`${FRONTEND_URL}/login?error=no_code`);
40
+
}
41
+
42
+
try {
43
+
const tokenResponse = await fetch('https://discord.com/api/oauth2/token', {
44
+
method: 'POST',
45
+
headers: {
46
+
'Content-Type': 'application/x-www-form-urlencoded',
47
+
},
48
+
body: new URLSearchParams({
49
+
client_id: DISCORD_CLIENT_ID!,
50
+
client_secret: DISCORD_CLIENT_SECRET!,
51
+
grant_type: 'authorization_code',
52
+
code: code as string,
53
+
redirect_uri: DISCORD_REDIRECT_URI,
54
+
}),
55
+
});
56
+
57
+
if (!tokenResponse.ok) {
58
+
throw new Error('Failed to exchange code for token');
59
+
}
60
+
61
+
const tokenData = await tokenResponse.json();
62
+
63
+
const userResponse = await fetch('https://discord.com/api/users/@me', {
64
+
headers: {
65
+
Authorization: `Bearer ${tokenData.access_token}`,
66
+
},
67
+
});
68
+
69
+
if (!userResponse.ok) {
70
+
throw new Error('Failed to fetch user information');
71
+
}
72
+
73
+
const discordUser: DiscordUser = await userResponse.json();
74
+
75
+
const userQuery = 'SELECT user_id, created_at FROM users WHERE user_id = $1';
76
+
const userResult = await pool.query(userQuery, [discordUser.id]);
77
+
78
+
if (userResult.rows.length === 0) {
79
+
const insertQuery =
80
+
'INSERT INTO users (user_id, language, created_at) VALUES ($1, $2, NOW())';
81
+
await pool.query(insertQuery, [discordUser.id, 'en']);
82
+
logger.info(
83
+
`New user created: ${discordUser.username}#${discordUser.discriminator} (${discordUser.id})`
84
+
);
85
+
}
86
+
87
+
const jwtToken = jwt.sign(
88
+
{
89
+
userId: discordUser.id,
90
+
username: discordUser.username,
91
+
discriminator: discordUser.discriminator === '0' ? null : discordUser.discriminator,
92
+
avatar: discordUser.avatar,
93
+
},
94
+
JWT_SECRET,
95
+
{ expiresIn: '7d' }
96
+
);
97
+
98
+
const redirectUrl = new URL(`${FRONTEND_URL}/login`);
99
+
redirectUrl.searchParams.set('token', jwtToken);
100
+
redirectUrl.searchParams.set('user_id', discordUser.id);
101
+
redirectUrl.searchParams.set('username', discordUser.username);
102
+
if (discordUser.discriminator && discordUser.discriminator !== '0') {
103
+
redirectUrl.searchParams.set('discriminator', discordUser.discriminator);
104
+
}
105
+
if (discordUser.avatar) {
106
+
redirectUrl.searchParams.set('avatar', discordUser.avatar);
107
+
}
108
+
109
+
res.redirect(redirectUrl.toString());
110
+
} catch (error) {
111
+
logger.error('Discord OAuth callback error:', error);
112
+
res.redirect(`${FRONTEND_URL}/login?error=auth_failed`);
113
+
}
114
+
});
115
+
116
+
router.get('/me', authenticateToken, async (req, res) => {
117
+
try {
118
+
const userId = req.user?.userId;
119
+
if (!userId) {
120
+
return res.status(401).json({ error: 'User not authenticated' });
121
+
}
122
+
123
+
const userQuery = 'SELECT user_id, language, created_at FROM users WHERE user_id = $1';
124
+
const userResult = await pool.query(userQuery, [userId]);
125
+
126
+
if (userResult.rows.length === 0) {
127
+
return res.status(404).json({ error: 'User not found' });
128
+
}
129
+
130
+
const user = userResult.rows[0];
131
+
res.json({
132
+
user: {
133
+
id: user.user_id,
134
+
username: req.user?.username,
135
+
discriminator: req.user?.discriminator,
136
+
avatar: req.user?.avatar,
137
+
language: user.language,
138
+
createdAt: user.created_at,
139
+
},
140
+
});
141
+
} catch (error) {
142
+
logger.error('Error fetching user info:', error);
143
+
res.status(500).json({ error: 'Internal server error' });
144
+
}
145
+
});
146
+
147
+
router.post('/logout', authenticateToken, (req, res) => {
148
+
res.json({ message: 'Logged out successfully' });
149
+
});
150
+
151
+
export default router;
+273
src/routes/reminders.ts
+273
src/routes/reminders.ts
···
1
+
import { Router, Request, Response } from 'express';
2
+
import { v4 as uuidv4 } from 'uuid';
3
+
import {
4
+
saveReminder,
5
+
getUserReminders,
6
+
getReminder,
7
+
completeReminder,
8
+
getActiveReminders,
9
+
clearCompletedReminders,
10
+
} from '../utils/reminderDb';
11
+
import { authenticateToken } from '../middlewares/auth';
12
+
import logger from '../utils/logger';
13
+
import BotClient from '../services/Client';
14
+
import { EmbedBuilder } from 'discord.js';
15
+
import { formatTimeString } from '../utils/validation';
16
+
import { scheduleReminder } from '../commands/utilities/remind';
17
+
18
+
const router = Router();
19
+
20
+
router.get('/', authenticateToken, async (req: Request, res: Response) => {
21
+
try {
22
+
const userId = req.user?.userId;
23
+
if (!userId) {
24
+
return res.status(401).json({ error: 'User not authenticated' });
25
+
}
26
+
27
+
const reminders = await getUserReminders(userId);
28
+
res.json({ reminders });
29
+
} catch (error) {
30
+
logger.error('Error fetching user reminders:', error);
31
+
res.status(500).json({ error: 'Failed to fetch reminders' });
32
+
}
33
+
});
34
+
35
+
router.post('/', authenticateToken, async (req: Request, res: Response) => {
36
+
try {
37
+
const userId = req.user?.userId;
38
+
const userTag = req.user?.username || 'Unknown';
39
+
40
+
if (!userId) {
41
+
return res.status(401).json({ error: 'User not authenticated' });
42
+
}
43
+
44
+
const { message, expires_at } = req.body;
45
+
46
+
if (!message || !expires_at) {
47
+
return res.status(400).json({ error: 'Message and expiration date are required' });
48
+
}
49
+
50
+
const reminderData = {
51
+
reminder_id: uuidv4(),
52
+
user_id: userId,
53
+
user_tag: userTag,
54
+
channel_id: 'web',
55
+
guild_id: null,
56
+
message,
57
+
expires_at: new Date(expires_at),
58
+
locale: 'en',
59
+
metadata: {
60
+
source: 'web',
61
+
created_via: 'dashboard',
62
+
},
63
+
};
64
+
65
+
const savedReminder = await saveReminder(reminderData);
66
+
67
+
try {
68
+
const client = BotClient.getInstance();
69
+
if (client) {
70
+
const scheduled = scheduleReminder(client, {
71
+
...savedReminder,
72
+
created_at: savedReminder.created_at || new Date(),
73
+
});
74
+
75
+
if (scheduled) {
76
+
logger.info(
77
+
`Successfully scheduled reminder ${savedReminder.reminder_id} from dashboard`
78
+
);
79
+
} else {
80
+
logger.warn(`Failed to schedule reminder ${savedReminder.reminder_id} from dashboard`);
81
+
}
82
+
} else {
83
+
logger.warn('Bot client not available, reminder saved but not scheduled');
84
+
}
85
+
} catch (schedulingError) {
86
+
logger.error(`Error scheduling reminder ${savedReminder.reminder_id}:`, schedulingError);
87
+
}
88
+
89
+
res.status(201).json({ reminder: savedReminder });
90
+
} catch (error) {
91
+
logger.error('Error creating reminder:', error);
92
+
res.status(500).json({ error: 'Failed to create reminder' });
93
+
}
94
+
});
95
+
96
+
router.get('/:id', authenticateToken, async (req: Request, res: Response) => {
97
+
try {
98
+
const userId = req.user?.userId;
99
+
const reminderId = req.params.id;
100
+
101
+
if (!userId) {
102
+
return res.status(401).json({ error: 'User not authenticated' });
103
+
}
104
+
105
+
const reminder = await getReminder(reminderId);
106
+
107
+
if (!reminder) {
108
+
return res.status(404).json({ error: 'Reminder not found' });
109
+
}
110
+
111
+
if (reminder.user_id !== userId) {
112
+
return res.status(403).json({ error: 'Access denied' });
113
+
}
114
+
115
+
res.json({ reminder });
116
+
} catch (error) {
117
+
logger.error('Error fetching reminder:', error);
118
+
res.status(500).json({ error: 'Failed to fetch reminder' });
119
+
}
120
+
});
121
+
122
+
router.patch('/:id/complete', authenticateToken, async (req: Request, res: Response) => {
123
+
try {
124
+
const userId = req.user?.userId;
125
+
const reminderId = req.params.id;
126
+
127
+
if (!userId) {
128
+
return res.status(401).json({ error: 'User not authenticated' });
129
+
}
130
+
131
+
const reminder = await getReminder(reminderId);
132
+
133
+
if (!reminder) {
134
+
return res.status(404).json({ error: 'Reminder not found' });
135
+
}
136
+
137
+
if (reminder.user_id !== userId) {
138
+
return res.status(403).json({ error: 'Access denied' });
139
+
}
140
+
141
+
try {
142
+
const client = BotClient.getInstance();
143
+
if (client) {
144
+
const user = await client.users.fetch(reminder.user_id);
145
+
if (user) {
146
+
const minutes = Math.floor(
147
+
(new Date(reminder.expires_at).getTime() - new Date(reminder.created_at!).getTime()) /
148
+
(60 * 1000)
149
+
);
150
+
151
+
const reminderTitle =
152
+
'⏰ ' + (await client.getLocaleText('commands.remind.reminder', reminder.locale));
153
+
const reminderDesc = await client.getLocaleText(
154
+
'commands.remind.remindyou',
155
+
reminder.locale,
156
+
{ message: reminder.message }
157
+
);
158
+
159
+
const timeElapsedText =
160
+
'⏱️ ' + (await client.getLocaleText('commands.remind.timeelapsed', reminder.locale));
161
+
const originalTimeText =
162
+
'📅 ' + (await client.getLocaleText('commands.remind.originaltime', reminder.locale));
163
+
164
+
const reminderEmbed = new EmbedBuilder()
165
+
.setColor(0xfaa0a0)
166
+
.setTitle(reminderTitle)
167
+
.setDescription(reminderDesc)
168
+
.addFields(
169
+
{ name: timeElapsedText, value: formatTimeString(minutes), inline: true },
170
+
{
171
+
name: originalTimeText,
172
+
value: `<t:${Math.floor(new Date(reminder.created_at!).getTime() / 1000)}:f>`,
173
+
inline: true,
174
+
}
175
+
)
176
+
.setFooter({ text: `ID: ${reminder.reminder_id.slice(-6)}` })
177
+
.setTimestamp();
178
+
179
+
if (reminder.metadata?.message_url) {
180
+
const originalMessageText = await client.getLocaleText(
181
+
'common.ogmessage',
182
+
reminder.locale
183
+
);
184
+
const jumpToMessageText = await client.getLocaleText(
185
+
'common.jumptomessage',
186
+
reminder.locale
187
+
);
188
+
189
+
reminderEmbed.addFields({
190
+
name: originalMessageText,
191
+
value: `[${jumpToMessageText}](${reminder.metadata.message_url})`,
192
+
inline: false,
193
+
});
194
+
}
195
+
196
+
if (reminder.message.includes('http') && !reminder.metadata?.message_url) {
197
+
const messageLinkText = await client.getLocaleText(
198
+
'common.messagelink',
199
+
reminder.locale
200
+
);
201
+
reminderEmbed.addFields({
202
+
name: messageLinkText,
203
+
value: reminder.message,
204
+
inline: false,
205
+
});
206
+
}
207
+
208
+
await user.send({
209
+
embeds: [reminderEmbed],
210
+
});
211
+
212
+
logger.info(`Successfully sent reminder to ${reminder.user_tag} (${reminder.user_id})`, {
213
+
reminderId: reminder.reminder_id,
214
+
});
215
+
}
216
+
}
217
+
} catch (notificationError) {
218
+
logger.error(
219
+
`Failed to send reminder notification to ${reminder.user_tag} (${reminder.user_id}): ${(notificationError as Error).message}`,
220
+
{
221
+
error: notificationError,
222
+
reminderId: reminder.reminder_id,
223
+
}
224
+
);
225
+
}
226
+
227
+
const completedReminder = await completeReminder(reminderId);
228
+
res.json({ reminder: completedReminder });
229
+
} catch (error) {
230
+
logger.error('Error completing reminder:', error);
231
+
res.status(500).json({ error: 'Failed to complete reminder' });
232
+
}
233
+
});
234
+
235
+
router.get('/active/all', authenticateToken, async (req: Request, res: Response) => {
236
+
try {
237
+
const userId = req.user?.userId;
238
+
239
+
if (!userId) {
240
+
return res.status(401).json({ error: 'User not authenticated' });
241
+
}
242
+
243
+
const activeReminders = await getActiveReminders();
244
+
const userActiveReminders = activeReminders.filter((reminder) => reminder.user_id === userId);
245
+
246
+
res.json({ reminders: userActiveReminders });
247
+
} catch (error) {
248
+
logger.error('Error fetching active reminders:', error);
249
+
res.status(500).json({ error: 'Failed to fetch active reminders' });
250
+
}
251
+
});
252
+
253
+
router.delete('/completed', authenticateToken, async (req: Request, res: Response) => {
254
+
try {
255
+
const userId = req.user?.userId;
256
+
257
+
if (!userId) {
258
+
return res.status(401).json({ error: 'User not authenticated' });
259
+
}
260
+
261
+
const deletedCount = await clearCompletedReminders(userId);
262
+
263
+
res.json({
264
+
message: `Successfully cleared ${deletedCount} completed reminders`,
265
+
deletedCount,
266
+
});
267
+
} catch (error) {
268
+
logger.error('Error clearing completed reminders:', error);
269
+
res.status(500).json({ error: 'Failed to clear completed reminders' });
270
+
}
271
+
});
272
+
273
+
export default router;
+190
src/routes/todos.ts
+190
src/routes/todos.ts
···
1
+
import { Router } from 'express';
2
+
import pool from '../utils/pgClient';
3
+
import logger from '../utils/logger';
4
+
import { authenticateToken } from '../middlewares/auth';
5
+
import { body, validationResult } from 'express-validator';
6
+
7
+
const router = Router();
8
+
9
+
router.use(authenticateToken);
10
+
11
+
router.get('/', async (req, res) => {
12
+
try {
13
+
const userId = req.user?.userId;
14
+
if (!userId) {
15
+
return res.status(401).json({ error: 'User not authenticated' });
16
+
}
17
+
18
+
const query = `
19
+
SELECT id, item, done, created_at, completed_at
20
+
FROM todos
21
+
WHERE user_id = $1
22
+
ORDER BY created_at DESC
23
+
`;
24
+
const result = await pool.query(query, [userId]);
25
+
26
+
res.json(result.rows);
27
+
} catch (error) {
28
+
logger.error('Error fetching todos:', error);
29
+
res.status(500).json({ error: 'Internal server error' });
30
+
}
31
+
});
32
+
33
+
router.post(
34
+
'/',
35
+
body('item')
36
+
.trim()
37
+
.isLength({ min: 1, max: 500 })
38
+
.withMessage('Todo item must be between 1 and 500 characters'),
39
+
async (req, res) => {
40
+
try {
41
+
const errors = validationResult(req);
42
+
if (!errors.isEmpty()) {
43
+
return res.status(400).json({ error: errors.array()[0].msg });
44
+
}
45
+
46
+
const userId = req.user?.userId;
47
+
if (!userId) {
48
+
return res.status(401).json({ error: 'User not authenticated' });
49
+
}
50
+
51
+
const { item } = req.body;
52
+
53
+
const query = `
54
+
INSERT INTO todos (user_id, item, done, created_at)
55
+
VALUES ($1, $2, FALSE, NOW())
56
+
RETURNING id, item, done, created_at, completed_at
57
+
`;
58
+
const result = await pool.query(query, [userId, item]);
59
+
60
+
logger.info(`Todo created for user ${userId}: ${item}`);
61
+
res.status(201).json(result.rows[0]);
62
+
} catch (error) {
63
+
logger.error('Error creating todo:', error);
64
+
res.status(500).json({ error: 'Internal server error' });
65
+
}
66
+
}
67
+
);
68
+
69
+
router.put(
70
+
'/:id',
71
+
body('done').isBoolean().withMessage('Done must be a boolean value'),
72
+
async (req, res) => {
73
+
try {
74
+
const errors = validationResult(req);
75
+
if (!errors.isEmpty()) {
76
+
return res.status(400).json({ error: errors.array()[0].msg });
77
+
}
78
+
79
+
const userId = req.user?.userId;
80
+
if (!userId) {
81
+
return res.status(401).json({ error: 'User not authenticated' });
82
+
}
83
+
84
+
const { id } = req.params as { id: string };
85
+
const { done } = req.body;
86
+
87
+
const checkQuery = 'SELECT id FROM todos WHERE id = $1 AND user_id = $2';
88
+
const checkResult = await pool.query(checkQuery, [id, userId]);
89
+
90
+
if (checkResult.rows.length === 0) {
91
+
return res.status(404).json({ error: 'Todo not found' });
92
+
}
93
+
94
+
const query = `
95
+
UPDATE todos
96
+
SET done = $1, completed_at = CASE WHEN $1 = TRUE THEN NOW() ELSE NULL END
97
+
WHERE id = $2 AND user_id = $3
98
+
RETURNING id, item, done, created_at, completed_at
99
+
`;
100
+
const result = await pool.query(query, [done, id, userId]);
101
+
102
+
logger.info(`Todo ${done ? 'completed' : 'uncompleted'} for user ${userId}: ${id}`);
103
+
res.json(result.rows[0]);
104
+
} catch (error) {
105
+
logger.error('Error updating todo:', error);
106
+
res.status(500).json({ error: 'Internal server error' });
107
+
}
108
+
}
109
+
);
110
+
111
+
router.delete('/:id', async (req, res) => {
112
+
try {
113
+
const userId = req.user?.userId;
114
+
if (!userId) {
115
+
return res.status(401).json({ error: 'User not authenticated' });
116
+
}
117
+
118
+
const { id } = req.params as { id: string };
119
+
120
+
const checkQuery = 'SELECT id, item FROM todos WHERE id = $1 AND user_id = $2';
121
+
const checkResult = await pool.query(checkQuery, [id, userId]);
122
+
123
+
if (checkResult.rows.length === 0) {
124
+
return res.status(404).json({ error: 'Todo not found' });
125
+
}
126
+
127
+
const deleteQuery = 'DELETE FROM todos WHERE id = $1 AND user_id = $2';
128
+
await pool.query(deleteQuery, [id, userId]);
129
+
130
+
logger.info(`Todo deleted for user ${userId}: ${checkResult.rows[0].item}`);
131
+
res.json({ message: 'Todo deleted successfully' });
132
+
} catch (error) {
133
+
logger.error('Error deleting todo:', error);
134
+
res.status(500).json({ error: 'Internal server error' });
135
+
}
136
+
});
137
+
138
+
router.delete('/', async (req, res) => {
139
+
try {
140
+
const userId = req.user?.userId;
141
+
if (!userId) {
142
+
return res.status(401).json({ error: 'User not authenticated' });
143
+
}
144
+
145
+
const countQuery = 'SELECT COUNT(*) as count FROM todos WHERE user_id = $1';
146
+
const countResult = await pool.query(countQuery, [userId]);
147
+
const todoCount = parseInt(countResult.rows[0].count);
148
+
149
+
const deleteQuery = 'DELETE FROM todos WHERE user_id = $1';
150
+
await pool.query(deleteQuery, [userId]);
151
+
152
+
logger.info(`All todos cleared for user ${userId}: ${todoCount} todos deleted`);
153
+
res.json({ message: `${todoCount} todos cleared successfully` });
154
+
} catch (error) {
155
+
logger.error('Error clearing todos:', error);
156
+
res.status(500).json({ error: 'Internal server error' });
157
+
}
158
+
});
159
+
160
+
router.get('/stats', async (req, res) => {
161
+
try {
162
+
const userId = req.user?.userId;
163
+
if (!userId) {
164
+
return res.status(401).json({ error: 'User not authenticated' });
165
+
}
166
+
167
+
const query = `
168
+
SELECT
169
+
COUNT(*) as total,
170
+
COUNT(CASE WHEN done = TRUE THEN 1 END) as completed,
171
+
COUNT(CASE WHEN done = FALSE THEN 1 END) as pending
172
+
FROM todos
173
+
WHERE user_id = $1
174
+
`;
175
+
const result = await pool.query(query, [userId]);
176
+
177
+
const stats = {
178
+
total: parseInt(result.rows[0].total),
179
+
completed: parseInt(result.rows[0].completed),
180
+
pending: parseInt(result.rows[0].pending),
181
+
};
182
+
183
+
res.json(stats);
184
+
} catch (error) {
185
+
logger.error('Error fetching todo stats:', error);
186
+
res.status(500).json({ error: 'Internal server error' });
187
+
}
188
+
});
189
+
190
+
export default router;
+12
src/services/Client.ts
+12
src/services/Client.ts
···
13
13
export const srcDir = path.join(__dirname, '..');
14
14
15
15
export default class BotClient extends Client {
16
+
private static instance: BotClient | null = null;
16
17
public commands = new Collection<string, SlashCommandProps>();
17
18
// eslint-disable-next-line @typescript-eslint/no-explicit-any
18
19
public t = new Collection<string, any>();
20
+
19
21
constructor() {
20
22
super({
21
23
intents: [GatewayIntentBits.MessageContent],
···
28
30
],
29
31
},
30
32
});
33
+
BotClient.instance = this;
34
+
}
35
+
36
+
public static getInstance(): BotClient | null {
37
+
return BotClient.instance;
31
38
}
32
39
33
40
public async init() {
···
76
83
}
77
84
public async getLocaleText(key: string, locale: string, replaces = {}): Promise<string> {
78
85
const fallbackLocale = 'en-US';
86
+
87
+
if (!locale) {
88
+
locale = fallbackLocale;
89
+
}
90
+
79
91
let langMap = this.t.get(locale);
80
92
if (!langMap) {
81
93
const langOnly = locale.split('-')[0];
+14
src/types/express.d.ts
+14
src/types/express.d.ts
+7
-2
src/utils/commandLogger.ts
+7
-2
src/utils/commandLogger.ts
···
11
11
}
12
12
13
13
export function logUserAction(options: CommandLogOptions): void {
14
-
const { commandName, userId, username, guildId, channelId } = options;
14
+
const { commandName, userId, username, guildId, channelId, additionalInfo } = options;
15
15
16
16
let logMessage = `User ${username} (${userId}) used ${commandName} command`;
17
17
···
23
23
logMessage += ` in channel ${channelId}`;
24
24
}
25
25
26
+
if (additionalInfo) {
27
+
logMessage += ` with ${additionalInfo}`;
28
+
}
29
+
26
30
logger.info(logMessage);
27
31
}
28
32
···
37
41
username: interaction.user.tag,
38
42
guildId: interaction.guildId || undefined,
39
43
channelId: interaction.channelId,
44
+
additionalInfo,
40
45
});
41
46
}
42
47
···
46
51
logUserAction({ ...options, commandName });
47
52
},
48
53
logFromInteraction: (interaction: CommandInteraction, additionalInfo?: string) => {
49
-
logUserActionFromInteraction(interaction, commandName);
54
+
logUserActionFromInteraction(interaction, commandName, additionalInfo);
50
55
},
51
56
};
52
57
}
+82
src/utils/encryption.ts
+82
src/utils/encryption.ts
···
1
+
import crypto from 'crypto';
2
+
import { API_KEY_ENCRYPTION_SECRET } from '../config';
3
+
4
+
const ENCRYPTION_KEY = API_KEY_ENCRYPTION_SECRET;
5
+
const ALGORITHM = 'aes-256-gcm';
6
+
7
+
const getEncryptionKey = (): Buffer => {
8
+
if (ENCRYPTION_KEY.length !== 32) {
9
+
throw new Error('ENCRYPTION_KEY must be exactly 32 characters long');
10
+
}
11
+
return Buffer.from(ENCRYPTION_KEY, 'utf8');
12
+
};
13
+
14
+
/**
15
+
* Encrypts a string using AES-256-GCM
16
+
* @param text The text to encrypt
17
+
* @returns Base64 encoded encrypted data with IV and auth tag
18
+
*/
19
+
export const encryptApiKey = (text: string): string => {
20
+
try {
21
+
const key = getEncryptionKey();
22
+
const iv = crypto.randomBytes(16);
23
+
24
+
const cipher = crypto.createCipheriv(ALGORITHM, key, iv);
25
+
cipher.setAAD(Buffer.from('aethel-api-key', 'utf8'));
26
+
27
+
let encrypted = cipher.update(text, 'utf8', 'hex');
28
+
encrypted += cipher.final('hex');
29
+
30
+
const authTag = cipher.getAuthTag();
31
+
32
+
const combined = Buffer.concat([iv, authTag, Buffer.from(encrypted, 'hex')]);
33
+
34
+
return combined.toString('base64');
35
+
} catch {
36
+
throw new Error('Failed to encrypt API key');
37
+
}
38
+
};
39
+
40
+
/**
41
+
* Decrypts a string that was encrypted with encryptApiKey
42
+
* @param encryptedData Base64 encoded encrypted data
43
+
* @returns The decrypted text
44
+
*/
45
+
export const decryptApiKey = (encryptedData: string): string => {
46
+
try {
47
+
const key = getEncryptionKey();
48
+
const combined = Buffer.from(encryptedData, 'base64');
49
+
50
+
const extractedIv = combined.subarray(0, 16);
51
+
const authTag = combined.subarray(16, 32);
52
+
const encrypted = combined.subarray(32);
53
+
54
+
const decipher = crypto.createDecipheriv(ALGORITHM, key, extractedIv);
55
+
decipher.setAAD(Buffer.from('aethel-api-key', 'utf8'));
56
+
decipher.setAuthTag(authTag);
57
+
58
+
let decrypted = decipher.update(encrypted, undefined, 'utf8');
59
+
decrypted += decipher.final('utf8');
60
+
61
+
return decrypted;
62
+
} catch {
63
+
throw new Error('Failed to decrypt API key');
64
+
}
65
+
};
66
+
67
+
/**
68
+
* Generates a secure random encryption key
69
+
* @returns A 32-character random string suitable for use as ENCRYPTION_KEY
70
+
*/
71
+
export const generateEncryptionKey = (): string => {
72
+
return crypto.randomBytes(32).toString('base64').substring(0, 32);
73
+
};
74
+
75
+
/**
76
+
* Validates that an encryption key is properly formatted
77
+
* @param key The key to validate
78
+
* @returns True if the key is valid
79
+
*/
80
+
export const validateEncryptionKey = (key: string): boolean => {
81
+
return typeof key === 'string' && key.length === 32;
82
+
};
+17
src/utils/reminderDb.ts
+17
src/utils/reminderDb.ts
···
197
197
}
198
198
}
199
199
200
+
async function clearCompletedReminders(userId: string) {
201
+
const query = `
202
+
DELETE FROM reminders
203
+
WHERE user_id = $1
204
+
AND is_completed = TRUE
205
+
RETURNING *
206
+
`;
207
+
208
+
try {
209
+
const result = await pool.query(query, [userId]);
210
+
return result.rowCount;
211
+
} catch (error) {
212
+
throw createDatabaseError(error, 'clearing completed reminders');
213
+
}
214
+
}
215
+
200
216
export {
201
217
saveReminder,
202
218
completeReminder,
···
204
220
getReminder,
205
221
getUserReminders,
206
222
cleanupReminders,
223
+
clearCompletedReminders,
207
224
ensureUserRegistered,
208
225
DatabaseError,
209
226
};
+1
-1
tsconfig.json
+1
-1
tsconfig.json
···
105
105
// "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */
106
106
"skipLibCheck": true /* Skip type checking all .d.ts files. */
107
107
},
108
-
"include": ["src/**/*"],
108
+
"include": ["src/**/*", "environment.d.ts"],
109
109
"exclude": ["express-server/*", "node_modules", "dist"],
110
110
"ts-node": {
111
111
"pretty": true,
+16
web/index.html
+16
web/index.html
···
1
+
<!doctype html>
2
+
<html lang="en">
3
+
<head>
4
+
<meta charset="UTF-8" />
5
+
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
6
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
7
+
<title>Aethel Dashboard</title>
8
+
<link rel="preconnect" href="https://fonts.googleapis.com">
9
+
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
10
+
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet">
11
+
</head>
12
+
<body>
13
+
<div id="root"></div>
14
+
<script type="module" src="/src/main.tsx"></script>
15
+
</body>
16
+
</html>
+36
web/package.json
+36
web/package.json
···
1
+
{
2
+
"name": "aethel-dashboard",
3
+
"version": "1.0.0",
4
+
"type": "module",
5
+
"scripts": {
6
+
"dev": "vite",
7
+
"build": "tsc && vite build",
8
+
"preview": "vite preview",
9
+
"lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0"
10
+
},
11
+
"dependencies": {
12
+
"@tanstack/react-query": "^5.83.0",
13
+
"axios": "^1.10.0",
14
+
"lucide-react": "^0.294.0",
15
+
"react": "^18.3.1",
16
+
"react-dom": "^18.3.1",
17
+
"react-router-dom": "^6.30.1",
18
+
"sonner": "^1.7.4",
19
+
"zustand": "^4.5.7"
20
+
},
21
+
"devDependencies": {
22
+
"@types/react": "^18.3.23",
23
+
"@types/react-dom": "^18.3.7",
24
+
"@typescript-eslint/eslint-plugin": "^6.21.0",
25
+
"@typescript-eslint/parser": "^6.21.0",
26
+
"@vitejs/plugin-react": "^4.7.0",
27
+
"autoprefixer": "^10.4.21",
28
+
"eslint": "^8.57.1",
29
+
"eslint-plugin-react-hooks": "^4.6.2",
30
+
"eslint-plugin-react-refresh": "^0.4.20",
31
+
"postcss": "^8.5.6",
32
+
"tailwindcss": "^3.4.17",
33
+
"typescript": "^5.8.3",
34
+
"vite": "^4.5.14"
35
+
}
36
+
}
+2724
web/pnpm-lock.yaml
+2724
web/pnpm-lock.yaml
···
1
+
lockfileVersion: '9.0'
2
+
3
+
settings:
4
+
autoInstallPeers: true
5
+
excludeLinksFromLockfile: false
6
+
7
+
importers:
8
+
9
+
.:
10
+
dependencies:
11
+
'@tanstack/react-query':
12
+
specifier: ^5.83.0
13
+
version: 5.83.0(react@18.3.1)
14
+
axios:
15
+
specifier: ^1.10.0
16
+
version: 1.10.0
17
+
lucide-react:
18
+
specifier: ^0.294.0
19
+
version: 0.294.0(react@18.3.1)
20
+
react:
21
+
specifier: ^18.3.1
22
+
version: 18.3.1
23
+
react-dom:
24
+
specifier: ^18.3.1
25
+
version: 18.3.1(react@18.3.1)
26
+
react-router-dom:
27
+
specifier: ^6.30.1
28
+
version: 6.30.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
29
+
sonner:
30
+
specifier: ^1.7.4
31
+
version: 1.7.4(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
32
+
zustand:
33
+
specifier: ^4.5.7
34
+
version: 4.5.7(@types/react@18.3.23)(react@18.3.1)
35
+
devDependencies:
36
+
'@types/react':
37
+
specifier: ^18.3.23
38
+
version: 18.3.23
39
+
'@types/react-dom':
40
+
specifier: ^18.3.7
41
+
version: 18.3.7(@types/react@18.3.23)
42
+
'@typescript-eslint/eslint-plugin':
43
+
specifier: ^6.21.0
44
+
version: 6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.8.3))(eslint@8.57.1)(typescript@5.8.3)
45
+
'@typescript-eslint/parser':
46
+
specifier: ^6.21.0
47
+
version: 6.21.0(eslint@8.57.1)(typescript@5.8.3)
48
+
'@vitejs/plugin-react':
49
+
specifier: ^4.7.0
50
+
version: 4.7.0(vite@4.5.14)
51
+
autoprefixer:
52
+
specifier: ^10.4.21
53
+
version: 10.4.21(postcss@8.5.6)
54
+
eslint:
55
+
specifier: ^8.57.1
56
+
version: 8.57.1
57
+
eslint-plugin-react-hooks:
58
+
specifier: ^4.6.2
59
+
version: 4.6.2(eslint@8.57.1)
60
+
eslint-plugin-react-refresh:
61
+
specifier: ^0.4.20
62
+
version: 0.4.20(eslint@8.57.1)
63
+
postcss:
64
+
specifier: ^8.5.6
65
+
version: 8.5.6
66
+
tailwindcss:
67
+
specifier: ^3.4.17
68
+
version: 3.4.17
69
+
typescript:
70
+
specifier: ^5.8.3
71
+
version: 5.8.3
72
+
vite:
73
+
specifier: ^4.5.14
74
+
version: 4.5.14
75
+
76
+
packages:
77
+
78
+
'@alloc/quick-lru@5.2.0':
79
+
resolution: {integrity: sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==}
80
+
engines: {node: '>=10'}
81
+
82
+
'@ampproject/remapping@2.3.0':
83
+
resolution: {integrity: sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==}
84
+
engines: {node: '>=6.0.0'}
85
+
86
+
'@babel/code-frame@7.27.1':
87
+
resolution: {integrity: sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==}
88
+
engines: {node: '>=6.9.0'}
89
+
90
+
'@babel/compat-data@7.28.0':
91
+
resolution: {integrity: sha512-60X7qkglvrap8mn1lh2ebxXdZYtUcpd7gsmy9kLaBJ4i/WdY8PqTSdxyA8qraikqKQK5C1KRBKXqznrVapyNaw==}
92
+
engines: {node: '>=6.9.0'}
93
+
94
+
'@babel/core@7.28.0':
95
+
resolution: {integrity: sha512-UlLAnTPrFdNGoFtbSXwcGFQBtQZJCNjaN6hQNP3UPvuNXT1i82N26KL3dZeIpNalWywr9IuQuncaAfUaS1g6sQ==}
96
+
engines: {node: '>=6.9.0'}
97
+
98
+
'@babel/generator@7.28.0':
99
+
resolution: {integrity: sha512-lJjzvrbEeWrhB4P3QBsH7tey117PjLZnDbLiQEKjQ/fNJTjuq4HSqgFA+UNSwZT8D7dxxbnuSBMsa1lrWzKlQg==}
100
+
engines: {node: '>=6.9.0'}
101
+
102
+
'@babel/helper-compilation-targets@7.27.2':
103
+
resolution: {integrity: sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==}
104
+
engines: {node: '>=6.9.0'}
105
+
106
+
'@babel/helper-globals@7.28.0':
107
+
resolution: {integrity: sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==}
108
+
engines: {node: '>=6.9.0'}
109
+
110
+
'@babel/helper-module-imports@7.27.1':
111
+
resolution: {integrity: sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==}
112
+
engines: {node: '>=6.9.0'}
113
+
114
+
'@babel/helper-module-transforms@7.27.3':
115
+
resolution: {integrity: sha512-dSOvYwvyLsWBeIRyOeHXp5vPj5l1I011r52FM1+r1jCERv+aFXYk4whgQccYEGYxK2H3ZAIA8nuPkQ0HaUo3qg==}
116
+
engines: {node: '>=6.9.0'}
117
+
peerDependencies:
118
+
'@babel/core': ^7.0.0
119
+
120
+
'@babel/helper-plugin-utils@7.27.1':
121
+
resolution: {integrity: sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw==}
122
+
engines: {node: '>=6.9.0'}
123
+
124
+
'@babel/helper-string-parser@7.27.1':
125
+
resolution: {integrity: sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==}
126
+
engines: {node: '>=6.9.0'}
127
+
128
+
'@babel/helper-validator-identifier@7.27.1':
129
+
resolution: {integrity: sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==}
130
+
engines: {node: '>=6.9.0'}
131
+
132
+
'@babel/helper-validator-option@7.27.1':
133
+
resolution: {integrity: sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==}
134
+
engines: {node: '>=6.9.0'}
135
+
136
+
'@babel/helpers@7.27.6':
137
+
resolution: {integrity: sha512-muE8Tt8M22638HU31A3CgfSUciwz1fhATfoVai05aPXGor//CdWDCbnlY1yvBPo07njuVOCNGCSp/GTt12lIug==}
138
+
engines: {node: '>=6.9.0'}
139
+
140
+
'@babel/parser@7.28.0':
141
+
resolution: {integrity: sha512-jVZGvOxOuNSsuQuLRTh13nU0AogFlw32w/MT+LV6D3sP5WdbW61E77RnkbaO2dUvmPAYrBDJXGn5gGS6tH4j8g==}
142
+
engines: {node: '>=6.0.0'}
143
+
hasBin: true
144
+
145
+
'@babel/plugin-transform-react-jsx-self@7.27.1':
146
+
resolution: {integrity: sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==}
147
+
engines: {node: '>=6.9.0'}
148
+
peerDependencies:
149
+
'@babel/core': ^7.0.0-0
150
+
151
+
'@babel/plugin-transform-react-jsx-source@7.27.1':
152
+
resolution: {integrity: sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==}
153
+
engines: {node: '>=6.9.0'}
154
+
peerDependencies:
155
+
'@babel/core': ^7.0.0-0
156
+
157
+
'@babel/template@7.27.2':
158
+
resolution: {integrity: sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==}
159
+
engines: {node: '>=6.9.0'}
160
+
161
+
'@babel/traverse@7.28.0':
162
+
resolution: {integrity: sha512-mGe7UK5wWyh0bKRfupsUchrQGqvDbZDbKJw+kcRGSmdHVYrv+ltd0pnpDTVpiTqnaBru9iEvA8pz8W46v0Amwg==}
163
+
engines: {node: '>=6.9.0'}
164
+
165
+
'@babel/types@7.28.1':
166
+
resolution: {integrity: sha512-x0LvFTekgSX+83TI28Y9wYPUfzrnl2aT5+5QLnO6v7mSJYtEEevuDRN0F0uSHRk1G1IWZC43o00Y0xDDrpBGPQ==}
167
+
engines: {node: '>=6.9.0'}
168
+
169
+
'@esbuild/android-arm64@0.18.20':
170
+
resolution: {integrity: sha512-Nz4rJcchGDtENV0eMKUNa6L12zz2zBDXuhj/Vjh18zGqB44Bi7MBMSXjgunJgjRhCmKOjnPuZp4Mb6OKqtMHLQ==}
171
+
engines: {node: '>=12'}
172
+
cpu: [arm64]
173
+
os: [android]
174
+
175
+
'@esbuild/android-arm@0.18.20':
176
+
resolution: {integrity: sha512-fyi7TDI/ijKKNZTUJAQqiG5T7YjJXgnzkURqmGj13C6dCqckZBLdl4h7bkhHt/t0WP+zO9/zwroDvANaOqO5Sw==}
177
+
engines: {node: '>=12'}
178
+
cpu: [arm]
179
+
os: [android]
180
+
181
+
'@esbuild/android-x64@0.18.20':
182
+
resolution: {integrity: sha512-8GDdlePJA8D6zlZYJV/jnrRAi6rOiNaCC/JclcXpB+KIuvfBN4owLtgzY2bsxnx666XjJx2kDPUmnTtR8qKQUg==}
183
+
engines: {node: '>=12'}
184
+
cpu: [x64]
185
+
os: [android]
186
+
187
+
'@esbuild/darwin-arm64@0.18.20':
188
+
resolution: {integrity: sha512-bxRHW5kHU38zS2lPTPOyuyTm+S+eobPUnTNkdJEfAddYgEcll4xkT8DB9d2008DtTbl7uJag2HuE5NZAZgnNEA==}
189
+
engines: {node: '>=12'}
190
+
cpu: [arm64]
191
+
os: [darwin]
192
+
193
+
'@esbuild/darwin-x64@0.18.20':
194
+
resolution: {integrity: sha512-pc5gxlMDxzm513qPGbCbDukOdsGtKhfxD1zJKXjCCcU7ju50O7MeAZ8c4krSJcOIJGFR+qx21yMMVYwiQvyTyQ==}
195
+
engines: {node: '>=12'}
196
+
cpu: [x64]
197
+
os: [darwin]
198
+
199
+
'@esbuild/freebsd-arm64@0.18.20':
200
+
resolution: {integrity: sha512-yqDQHy4QHevpMAaxhhIwYPMv1NECwOvIpGCZkECn8w2WFHXjEwrBn3CeNIYsibZ/iZEUemj++M26W3cNR5h+Tw==}
201
+
engines: {node: '>=12'}
202
+
cpu: [arm64]
203
+
os: [freebsd]
204
+
205
+
'@esbuild/freebsd-x64@0.18.20':
206
+
resolution: {integrity: sha512-tgWRPPuQsd3RmBZwarGVHZQvtzfEBOreNuxEMKFcd5DaDn2PbBxfwLcj4+aenoh7ctXcbXmOQIn8HI6mCSw5MQ==}
207
+
engines: {node: '>=12'}
208
+
cpu: [x64]
209
+
os: [freebsd]
210
+
211
+
'@esbuild/linux-arm64@0.18.20':
212
+
resolution: {integrity: sha512-2YbscF+UL7SQAVIpnWvYwM+3LskyDmPhe31pE7/aoTMFKKzIc9lLbyGUpmmb8a8AixOL61sQ/mFh3jEjHYFvdA==}
213
+
engines: {node: '>=12'}
214
+
cpu: [arm64]
215
+
os: [linux]
216
+
217
+
'@esbuild/linux-arm@0.18.20':
218
+
resolution: {integrity: sha512-/5bHkMWnq1EgKr1V+Ybz3s1hWXok7mDFUMQ4cG10AfW3wL02PSZi5kFpYKrptDsgb2WAJIvRcDm+qIvXf/apvg==}
219
+
engines: {node: '>=12'}
220
+
cpu: [arm]
221
+
os: [linux]
222
+
223
+
'@esbuild/linux-ia32@0.18.20':
224
+
resolution: {integrity: sha512-P4etWwq6IsReT0E1KHU40bOnzMHoH73aXp96Fs8TIT6z9Hu8G6+0SHSw9i2isWrD2nbx2qo5yUqACgdfVGx7TA==}
225
+
engines: {node: '>=12'}
226
+
cpu: [ia32]
227
+
os: [linux]
228
+
229
+
'@esbuild/linux-loong64@0.18.20':
230
+
resolution: {integrity: sha512-nXW8nqBTrOpDLPgPY9uV+/1DjxoQ7DoB2N8eocyq8I9XuqJ7BiAMDMf9n1xZM9TgW0J8zrquIb/A7s3BJv7rjg==}
231
+
engines: {node: '>=12'}
232
+
cpu: [loong64]
233
+
os: [linux]
234
+
235
+
'@esbuild/linux-mips64el@0.18.20':
236
+
resolution: {integrity: sha512-d5NeaXZcHp8PzYy5VnXV3VSd2D328Zb+9dEq5HE6bw6+N86JVPExrA6O68OPwobntbNJ0pzCpUFZTo3w0GyetQ==}
237
+
engines: {node: '>=12'}
238
+
cpu: [mips64el]
239
+
os: [linux]
240
+
241
+
'@esbuild/linux-ppc64@0.18.20':
242
+
resolution: {integrity: sha512-WHPyeScRNcmANnLQkq6AfyXRFr5D6N2sKgkFo2FqguP44Nw2eyDlbTdZwd9GYk98DZG9QItIiTlFLHJHjxP3FA==}
243
+
engines: {node: '>=12'}
244
+
cpu: [ppc64]
245
+
os: [linux]
246
+
247
+
'@esbuild/linux-riscv64@0.18.20':
248
+
resolution: {integrity: sha512-WSxo6h5ecI5XH34KC7w5veNnKkju3zBRLEQNY7mv5mtBmrP/MjNBCAlsM2u5hDBlS3NGcTQpoBvRzqBcRtpq1A==}
249
+
engines: {node: '>=12'}
250
+
cpu: [riscv64]
251
+
os: [linux]
252
+
253
+
'@esbuild/linux-s390x@0.18.20':
254
+
resolution: {integrity: sha512-+8231GMs3mAEth6Ja1iK0a1sQ3ohfcpzpRLH8uuc5/KVDFneH6jtAJLFGafpzpMRO6DzJ6AvXKze9LfFMrIHVQ==}
255
+
engines: {node: '>=12'}
256
+
cpu: [s390x]
257
+
os: [linux]
258
+
259
+
'@esbuild/linux-x64@0.18.20':
260
+
resolution: {integrity: sha512-UYqiqemphJcNsFEskc73jQ7B9jgwjWrSayxawS6UVFZGWrAAtkzjxSqnoclCXxWtfwLdzU+vTpcNYhpn43uP1w==}
261
+
engines: {node: '>=12'}
262
+
cpu: [x64]
263
+
os: [linux]
264
+
265
+
'@esbuild/netbsd-x64@0.18.20':
266
+
resolution: {integrity: sha512-iO1c++VP6xUBUmltHZoMtCUdPlnPGdBom6IrO4gyKPFFVBKioIImVooR5I83nTew5UOYrk3gIJhbZh8X44y06A==}
267
+
engines: {node: '>=12'}
268
+
cpu: [x64]
269
+
os: [netbsd]
270
+
271
+
'@esbuild/openbsd-x64@0.18.20':
272
+
resolution: {integrity: sha512-e5e4YSsuQfX4cxcygw/UCPIEP6wbIL+se3sxPdCiMbFLBWu0eiZOJ7WoD+ptCLrmjZBK1Wk7I6D/I3NglUGOxg==}
273
+
engines: {node: '>=12'}
274
+
cpu: [x64]
275
+
os: [openbsd]
276
+
277
+
'@esbuild/sunos-x64@0.18.20':
278
+
resolution: {integrity: sha512-kDbFRFp0YpTQVVrqUd5FTYmWo45zGaXe0X8E1G/LKFC0v8x0vWrhOWSLITcCn63lmZIxfOMXtCfti/RxN/0wnQ==}
279
+
engines: {node: '>=12'}
280
+
cpu: [x64]
281
+
os: [sunos]
282
+
283
+
'@esbuild/win32-arm64@0.18.20':
284
+
resolution: {integrity: sha512-ddYFR6ItYgoaq4v4JmQQaAI5s7npztfV4Ag6NrhiaW0RrnOXqBkgwZLofVTlq1daVTQNhtI5oieTvkRPfZrePg==}
285
+
engines: {node: '>=12'}
286
+
cpu: [arm64]
287
+
os: [win32]
288
+
289
+
'@esbuild/win32-ia32@0.18.20':
290
+
resolution: {integrity: sha512-Wv7QBi3ID/rROT08SABTS7eV4hX26sVduqDOTe1MvGMjNd3EjOz4b7zeexIR62GTIEKrfJXKL9LFxTYgkyeu7g==}
291
+
engines: {node: '>=12'}
292
+
cpu: [ia32]
293
+
os: [win32]
294
+
295
+
'@esbuild/win32-x64@0.18.20':
296
+
resolution: {integrity: sha512-kTdfRcSiDfQca/y9QIkng02avJ+NCaQvrMejlsB3RRv5sE9rRoeBPISaZpKxHELzRxZyLvNts1P27W3wV+8geQ==}
297
+
engines: {node: '>=12'}
298
+
cpu: [x64]
299
+
os: [win32]
300
+
301
+
'@eslint-community/eslint-utils@4.7.0':
302
+
resolution: {integrity: sha512-dyybb3AcajC7uha6CvhdVRJqaKyn7w2YKqKyAN37NKYgZT36w+iRb0Dymmc5qEJ549c/S31cMMSFd75bteCpCw==}
303
+
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
304
+
peerDependencies:
305
+
eslint: ^6.0.0 || ^7.0.0 || >=8.0.0
306
+
307
+
'@eslint-community/regexpp@4.12.1':
308
+
resolution: {integrity: sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==}
309
+
engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0}
310
+
311
+
'@eslint/eslintrc@2.1.4':
312
+
resolution: {integrity: sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==}
313
+
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
314
+
315
+
'@eslint/js@8.57.1':
316
+
resolution: {integrity: sha512-d9zaMRSTIKDLhctzH12MtXvJKSSUhaHcjV+2Z+GK+EEY7XKpP5yR4x+N3TAcHTcu963nIr+TMcCb4DBCYX1z6Q==}
317
+
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
318
+
319
+
'@humanwhocodes/config-array@0.13.0':
320
+
resolution: {integrity: sha512-DZLEEqFWQFiyK6h5YIeynKx7JlvCYWL0cImfSRXZ9l4Sg2efkFGTuFf6vzXjK1cq6IYkU+Eg/JizXw+TD2vRNw==}
321
+
engines: {node: '>=10.10.0'}
322
+
deprecated: Use @eslint/config-array instead
323
+
324
+
'@humanwhocodes/module-importer@1.0.1':
325
+
resolution: {integrity: sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==}
326
+
engines: {node: '>=12.22'}
327
+
328
+
'@humanwhocodes/object-schema@2.0.3':
329
+
resolution: {integrity: sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==}
330
+
deprecated: Use @eslint/object-schema instead
331
+
332
+
'@isaacs/cliui@8.0.2':
333
+
resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==}
334
+
engines: {node: '>=12'}
335
+
336
+
'@jridgewell/gen-mapping@0.3.12':
337
+
resolution: {integrity: sha512-OuLGC46TjB5BbN1dH8JULVVZY4WTdkF7tV9Ys6wLL1rubZnCMstOhNHueU5bLCrnRuDhKPDM4g6sw4Bel5Gzqg==}
338
+
339
+
'@jridgewell/resolve-uri@3.1.2':
340
+
resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==}
341
+
engines: {node: '>=6.0.0'}
342
+
343
+
'@jridgewell/sourcemap-codec@1.5.4':
344
+
resolution: {integrity: sha512-VT2+G1VQs/9oz078bLrYbecdZKs912zQlkelYpuf+SXF+QvZDYJlbx/LSx+meSAwdDFnF8FVXW92AVjjkVmgFw==}
345
+
346
+
'@jridgewell/trace-mapping@0.3.29':
347
+
resolution: {integrity: sha512-uw6guiW/gcAGPDhLmd77/6lW8QLeiV5RUTsAX46Db6oLhGaVj4lhnPwb184s1bkc8kdVg/+h988dro8GRDpmYQ==}
348
+
349
+
'@nodelib/fs.scandir@2.1.5':
350
+
resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==}
351
+
engines: {node: '>= 8'}
352
+
353
+
'@nodelib/fs.stat@2.0.5':
354
+
resolution: {integrity: sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==}
355
+
engines: {node: '>= 8'}
356
+
357
+
'@nodelib/fs.walk@1.2.8':
358
+
resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==}
359
+
engines: {node: '>= 8'}
360
+
361
+
'@pkgjs/parseargs@0.11.0':
362
+
resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==}
363
+
engines: {node: '>=14'}
364
+
365
+
'@remix-run/router@1.23.0':
366
+
resolution: {integrity: sha512-O3rHJzAQKamUz1fvE0Qaw0xSFqsA/yafi2iqeE0pvdFtCO1viYx8QL6f3Ln/aCCTLxs68SLf0KPM9eSeM8yBnA==}
367
+
engines: {node: '>=14.0.0'}
368
+
369
+
'@rolldown/pluginutils@1.0.0-beta.27':
370
+
resolution: {integrity: sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==}
371
+
372
+
'@tanstack/query-core@5.83.0':
373
+
resolution: {integrity: sha512-0M8dA+amXUkyz5cVUm/B+zSk3xkQAcuXuz5/Q/LveT4ots2rBpPTZOzd7yJa2Utsf8D2Upl5KyjhHRY+9lB/XA==}
374
+
375
+
'@tanstack/react-query@5.83.0':
376
+
resolution: {integrity: sha512-/XGYhZ3foc5H0VM2jLSD/NyBRIOK4q9kfeml4+0x2DlL6xVuAcVEW+hTlTapAmejObg0i3eNqhkr2dT+eciwoQ==}
377
+
peerDependencies:
378
+
react: ^18 || ^19
379
+
380
+
'@types/babel__core@7.20.5':
381
+
resolution: {integrity: sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==}
382
+
383
+
'@types/babel__generator@7.27.0':
384
+
resolution: {integrity: sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==}
385
+
386
+
'@types/babel__template@7.4.4':
387
+
resolution: {integrity: sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==}
388
+
389
+
'@types/babel__traverse@7.20.7':
390
+
resolution: {integrity: sha512-dkO5fhS7+/oos4ciWxyEyjWe48zmG6wbCheo/G2ZnHx4fs3EU6YC6UM8rk56gAjNJ9P3MTH2jo5jb92/K6wbng==}
391
+
392
+
'@types/json-schema@7.0.15':
393
+
resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==}
394
+
395
+
'@types/prop-types@15.7.15':
396
+
resolution: {integrity: sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==}
397
+
398
+
'@types/react-dom@18.3.7':
399
+
resolution: {integrity: sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==}
400
+
peerDependencies:
401
+
'@types/react': ^18.0.0
402
+
403
+
'@types/react@18.3.23':
404
+
resolution: {integrity: sha512-/LDXMQh55EzZQ0uVAZmKKhfENivEvWz6E+EYzh+/MCjMhNsotd+ZHhBGIjFDTi6+fz0OhQQQLbTgdQIxxCsC0w==}
405
+
406
+
'@types/semver@7.7.0':
407
+
resolution: {integrity: sha512-k107IF4+Xr7UHjwDc7Cfd6PRQfbdkiRabXGRjo07b4WyPahFBZCZ1sE+BNxYIJPPg73UkfOsVOLwqVc/6ETrIA==}
408
+
409
+
'@typescript-eslint/eslint-plugin@6.21.0':
410
+
resolution: {integrity: sha512-oy9+hTPCUFpngkEZUSzbf9MxI65wbKFoQYsgPdILTfbUldp5ovUuphZVe4i30emU9M/kP+T64Di0mxl7dSw3MA==}
411
+
engines: {node: ^16.0.0 || >=18.0.0}
412
+
peerDependencies:
413
+
'@typescript-eslint/parser': ^6.0.0 || ^6.0.0-alpha
414
+
eslint: ^7.0.0 || ^8.0.0
415
+
typescript: '*'
416
+
peerDependenciesMeta:
417
+
typescript:
418
+
optional: true
419
+
420
+
'@typescript-eslint/parser@6.21.0':
421
+
resolution: {integrity: sha512-tbsV1jPne5CkFQCgPBcDOt30ItF7aJoZL997JSF7MhGQqOeT3svWRYxiqlfA5RUdlHN6Fi+EI9bxqbdyAUZjYQ==}
422
+
engines: {node: ^16.0.0 || >=18.0.0}
423
+
peerDependencies:
424
+
eslint: ^7.0.0 || ^8.0.0
425
+
typescript: '*'
426
+
peerDependenciesMeta:
427
+
typescript:
428
+
optional: true
429
+
430
+
'@typescript-eslint/scope-manager@6.21.0':
431
+
resolution: {integrity: sha512-OwLUIWZJry80O99zvqXVEioyniJMa+d2GrqpUTqi5/v5D5rOrppJVBPa0yKCblcigC0/aYAzxxqQ1B+DS2RYsg==}
432
+
engines: {node: ^16.0.0 || >=18.0.0}
433
+
434
+
'@typescript-eslint/type-utils@6.21.0':
435
+
resolution: {integrity: sha512-rZQI7wHfao8qMX3Rd3xqeYSMCL3SoiSQLBATSiVKARdFGCYSRvmViieZjqc58jKgs8Y8i9YvVVhRbHSTA4VBag==}
436
+
engines: {node: ^16.0.0 || >=18.0.0}
437
+
peerDependencies:
438
+
eslint: ^7.0.0 || ^8.0.0
439
+
typescript: '*'
440
+
peerDependenciesMeta:
441
+
typescript:
442
+
optional: true
443
+
444
+
'@typescript-eslint/types@6.21.0':
445
+
resolution: {integrity: sha512-1kFmZ1rOm5epu9NZEZm1kckCDGj5UJEf7P1kliH4LKu/RkwpsfqqGmY2OOcUs18lSlQBKLDYBOGxRVtrMN5lpg==}
446
+
engines: {node: ^16.0.0 || >=18.0.0}
447
+
448
+
'@typescript-eslint/typescript-estree@6.21.0':
449
+
resolution: {integrity: sha512-6npJTkZcO+y2/kr+z0hc4HwNfrrP4kNYh57ek7yCNlrBjWQ1Y0OS7jiZTkgumrvkX5HkEKXFZkkdFNkaW2wmUQ==}
450
+
engines: {node: ^16.0.0 || >=18.0.0}
451
+
peerDependencies:
452
+
typescript: '*'
453
+
peerDependenciesMeta:
454
+
typescript:
455
+
optional: true
456
+
457
+
'@typescript-eslint/utils@6.21.0':
458
+
resolution: {integrity: sha512-NfWVaC8HP9T8cbKQxHcsJBY5YE1O33+jpMwN45qzWWaPDZgLIbo12toGMWnmhvCpd3sIxkpDw3Wv1B3dYrbDQQ==}
459
+
engines: {node: ^16.0.0 || >=18.0.0}
460
+
peerDependencies:
461
+
eslint: ^7.0.0 || ^8.0.0
462
+
463
+
'@typescript-eslint/visitor-keys@6.21.0':
464
+
resolution: {integrity: sha512-JJtkDduxLi9bivAB+cYOVMtbkqdPOhZ+ZI5LC47MIRrDV4Yn2o+ZnW10Nkmr28xRpSpdJ6Sm42Hjf2+REYXm0A==}
465
+
engines: {node: ^16.0.0 || >=18.0.0}
466
+
467
+
'@ungap/structured-clone@1.3.0':
468
+
resolution: {integrity: sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==}
469
+
470
+
'@vitejs/plugin-react@4.7.0':
471
+
resolution: {integrity: sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA==}
472
+
engines: {node: ^14.18.0 || >=16.0.0}
473
+
peerDependencies:
474
+
vite: ^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0
475
+
476
+
acorn-jsx@5.3.2:
477
+
resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==}
478
+
peerDependencies:
479
+
acorn: ^6.0.0 || ^7.0.0 || ^8.0.0
480
+
481
+
acorn@8.15.0:
482
+
resolution: {integrity: sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==}
483
+
engines: {node: '>=0.4.0'}
484
+
hasBin: true
485
+
486
+
ajv@6.12.6:
487
+
resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==}
488
+
489
+
ansi-regex@5.0.1:
490
+
resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==}
491
+
engines: {node: '>=8'}
492
+
493
+
ansi-regex@6.1.0:
494
+
resolution: {integrity: sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==}
495
+
engines: {node: '>=12'}
496
+
497
+
ansi-styles@4.3.0:
498
+
resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==}
499
+
engines: {node: '>=8'}
500
+
501
+
ansi-styles@6.2.1:
502
+
resolution: {integrity: sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==}
503
+
engines: {node: '>=12'}
504
+
505
+
any-promise@1.3.0:
506
+
resolution: {integrity: sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==}
507
+
508
+
anymatch@3.1.3:
509
+
resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==}
510
+
engines: {node: '>= 8'}
511
+
512
+
arg@5.0.2:
513
+
resolution: {integrity: sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==}
514
+
515
+
argparse@2.0.1:
516
+
resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==}
517
+
518
+
array-union@2.1.0:
519
+
resolution: {integrity: sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==}
520
+
engines: {node: '>=8'}
521
+
522
+
asynckit@0.4.0:
523
+
resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==}
524
+
525
+
autoprefixer@10.4.21:
526
+
resolution: {integrity: sha512-O+A6LWV5LDHSJD3LjHYoNi4VLsj/Whi7k6zG12xTYaU4cQ8oxQGckXNX8cRHK5yOZ/ppVHe0ZBXGzSV9jXdVbQ==}
527
+
engines: {node: ^10 || ^12 || >=14}
528
+
hasBin: true
529
+
peerDependencies:
530
+
postcss: ^8.1.0
531
+
532
+
axios@1.10.0:
533
+
resolution: {integrity: sha512-/1xYAC4MP/HEG+3duIhFr4ZQXR4sQXOIe+o6sdqzeykGLx6Upp/1p8MHqhINOvGeP7xyNHe7tsiJByc4SSVUxw==}
534
+
535
+
balanced-match@1.0.2:
536
+
resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==}
537
+
538
+
binary-extensions@2.3.0:
539
+
resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==}
540
+
engines: {node: '>=8'}
541
+
542
+
brace-expansion@1.1.12:
543
+
resolution: {integrity: sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==}
544
+
545
+
brace-expansion@2.0.2:
546
+
resolution: {integrity: sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==}
547
+
548
+
braces@3.0.3:
549
+
resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==}
550
+
engines: {node: '>=8'}
551
+
552
+
browserslist@4.25.1:
553
+
resolution: {integrity: sha512-KGj0KoOMXLpSNkkEI6Z6mShmQy0bc1I+T7K9N81k4WWMrfz+6fQ6es80B/YLAeRoKvjYE1YSHHOW1qe9xIVzHw==}
554
+
engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7}
555
+
hasBin: true
556
+
557
+
call-bind-apply-helpers@1.0.2:
558
+
resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==}
559
+
engines: {node: '>= 0.4'}
560
+
561
+
callsites@3.1.0:
562
+
resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==}
563
+
engines: {node: '>=6'}
564
+
565
+
camelcase-css@2.0.1:
566
+
resolution: {integrity: sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==}
567
+
engines: {node: '>= 6'}
568
+
569
+
caniuse-lite@1.0.30001727:
570
+
resolution: {integrity: sha512-pB68nIHmbN6L/4C6MH1DokyR3bYqFwjaSs/sWDHGj4CTcFtQUQMuJftVwWkXq7mNWOybD3KhUv3oWHoGxgP14Q==}
571
+
572
+
chalk@4.1.2:
573
+
resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==}
574
+
engines: {node: '>=10'}
575
+
576
+
chokidar@3.6.0:
577
+
resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==}
578
+
engines: {node: '>= 8.10.0'}
579
+
580
+
color-convert@2.0.1:
581
+
resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==}
582
+
engines: {node: '>=7.0.0'}
583
+
584
+
color-name@1.1.4:
585
+
resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==}
586
+
587
+
combined-stream@1.0.8:
588
+
resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==}
589
+
engines: {node: '>= 0.8'}
590
+
591
+
commander@4.1.1:
592
+
resolution: {integrity: sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==}
593
+
engines: {node: '>= 6'}
594
+
595
+
concat-map@0.0.1:
596
+
resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==}
597
+
598
+
convert-source-map@2.0.0:
599
+
resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==}
600
+
601
+
cross-spawn@7.0.6:
602
+
resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==}
603
+
engines: {node: '>= 8'}
604
+
605
+
cssesc@3.0.0:
606
+
resolution: {integrity: sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==}
607
+
engines: {node: '>=4'}
608
+
hasBin: true
609
+
610
+
csstype@3.1.3:
611
+
resolution: {integrity: sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==}
612
+
613
+
debug@4.4.1:
614
+
resolution: {integrity: sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==}
615
+
engines: {node: '>=6.0'}
616
+
peerDependencies:
617
+
supports-color: '*'
618
+
peerDependenciesMeta:
619
+
supports-color:
620
+
optional: true
621
+
622
+
deep-is@0.1.4:
623
+
resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==}
624
+
625
+
delayed-stream@1.0.0:
626
+
resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==}
627
+
engines: {node: '>=0.4.0'}
628
+
629
+
didyoumean@1.2.2:
630
+
resolution: {integrity: sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==}
631
+
632
+
dir-glob@3.0.1:
633
+
resolution: {integrity: sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==}
634
+
engines: {node: '>=8'}
635
+
636
+
dlv@1.1.3:
637
+
resolution: {integrity: sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==}
638
+
639
+
doctrine@3.0.0:
640
+
resolution: {integrity: sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==}
641
+
engines: {node: '>=6.0.0'}
642
+
643
+
dunder-proto@1.0.1:
644
+
resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==}
645
+
engines: {node: '>= 0.4'}
646
+
647
+
eastasianwidth@0.2.0:
648
+
resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==}
649
+
650
+
electron-to-chromium@1.5.189:
651
+
resolution: {integrity: sha512-y9D1ntS1ruO/pZ/V2FtLE+JXLQe28XoRpZ7QCCo0T8LdQladzdcOVQZH/IWLVJvCw12OGMb6hYOeOAjntCmJRQ==}
652
+
653
+
emoji-regex@8.0.0:
654
+
resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==}
655
+
656
+
emoji-regex@9.2.2:
657
+
resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==}
658
+
659
+
es-define-property@1.0.1:
660
+
resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==}
661
+
engines: {node: '>= 0.4'}
662
+
663
+
es-errors@1.3.0:
664
+
resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==}
665
+
engines: {node: '>= 0.4'}
666
+
667
+
es-object-atoms@1.1.1:
668
+
resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==}
669
+
engines: {node: '>= 0.4'}
670
+
671
+
es-set-tostringtag@2.1.0:
672
+
resolution: {integrity: sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==}
673
+
engines: {node: '>= 0.4'}
674
+
675
+
esbuild@0.18.20:
676
+
resolution: {integrity: sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA==}
677
+
engines: {node: '>=12'}
678
+
hasBin: true
679
+
680
+
escalade@3.2.0:
681
+
resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==}
682
+
engines: {node: '>=6'}
683
+
684
+
escape-string-regexp@4.0.0:
685
+
resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==}
686
+
engines: {node: '>=10'}
687
+
688
+
eslint-plugin-react-hooks@4.6.2:
689
+
resolution: {integrity: sha512-QzliNJq4GinDBcD8gPB5v0wh6g8q3SUi6EFF0x8N/BL9PoVs0atuGc47ozMRyOWAKdwaZ5OnbOEa3WR+dSGKuQ==}
690
+
engines: {node: '>=10'}
691
+
peerDependencies:
692
+
eslint: ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0
693
+
694
+
eslint-plugin-react-refresh@0.4.20:
695
+
resolution: {integrity: sha512-XpbHQ2q5gUF8BGOX4dHe+71qoirYMhApEPZ7sfhF/dNnOF1UXnCMGZf79SFTBO7Bz5YEIT4TMieSlJBWhP9WBA==}
696
+
peerDependencies:
697
+
eslint: '>=8.40'
698
+
699
+
eslint-scope@7.2.2:
700
+
resolution: {integrity: sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==}
701
+
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
702
+
703
+
eslint-visitor-keys@3.4.3:
704
+
resolution: {integrity: sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==}
705
+
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
706
+
707
+
eslint@8.57.1:
708
+
resolution: {integrity: sha512-ypowyDxpVSYpkXr9WPv2PAZCtNip1Mv5KTW0SCurXv/9iOpcrH9PaqUElksqEB6pChqHGDRCFTyrZlGhnLNGiA==}
709
+
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
710
+
deprecated: This version is no longer supported. Please see https://eslint.org/version-support for other options.
711
+
hasBin: true
712
+
713
+
espree@9.6.1:
714
+
resolution: {integrity: sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==}
715
+
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
716
+
717
+
esquery@1.6.0:
718
+
resolution: {integrity: sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==}
719
+
engines: {node: '>=0.10'}
720
+
721
+
esrecurse@4.3.0:
722
+
resolution: {integrity: sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==}
723
+
engines: {node: '>=4.0'}
724
+
725
+
estraverse@5.3.0:
726
+
resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==}
727
+
engines: {node: '>=4.0'}
728
+
729
+
esutils@2.0.3:
730
+
resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==}
731
+
engines: {node: '>=0.10.0'}
732
+
733
+
fast-deep-equal@3.1.3:
734
+
resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==}
735
+
736
+
fast-glob@3.3.3:
737
+
resolution: {integrity: sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==}
738
+
engines: {node: '>=8.6.0'}
739
+
740
+
fast-json-stable-stringify@2.1.0:
741
+
resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==}
742
+
743
+
fast-levenshtein@2.0.6:
744
+
resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==}
745
+
746
+
fastq@1.19.1:
747
+
resolution: {integrity: sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==}
748
+
749
+
file-entry-cache@6.0.1:
750
+
resolution: {integrity: sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==}
751
+
engines: {node: ^10.12.0 || >=12.0.0}
752
+
753
+
fill-range@7.1.1:
754
+
resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==}
755
+
engines: {node: '>=8'}
756
+
757
+
find-up@5.0.0:
758
+
resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==}
759
+
engines: {node: '>=10'}
760
+
761
+
flat-cache@3.2.0:
762
+
resolution: {integrity: sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==}
763
+
engines: {node: ^10.12.0 || >=12.0.0}
764
+
765
+
flatted@3.3.3:
766
+
resolution: {integrity: sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==}
767
+
768
+
follow-redirects@1.15.9:
769
+
resolution: {integrity: sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==}
770
+
engines: {node: '>=4.0'}
771
+
peerDependencies:
772
+
debug: '*'
773
+
peerDependenciesMeta:
774
+
debug:
775
+
optional: true
776
+
777
+
foreground-child@3.3.1:
778
+
resolution: {integrity: sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==}
779
+
engines: {node: '>=14'}
780
+
781
+
form-data@4.0.4:
782
+
resolution: {integrity: sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==}
783
+
engines: {node: '>= 6'}
784
+
785
+
fraction.js@4.3.7:
786
+
resolution: {integrity: sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==}
787
+
788
+
fs.realpath@1.0.0:
789
+
resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==}
790
+
791
+
fsevents@2.3.3:
792
+
resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==}
793
+
engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
794
+
os: [darwin]
795
+
796
+
function-bind@1.1.2:
797
+
resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==}
798
+
799
+
gensync@1.0.0-beta.2:
800
+
resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==}
801
+
engines: {node: '>=6.9.0'}
802
+
803
+
get-intrinsic@1.3.0:
804
+
resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==}
805
+
engines: {node: '>= 0.4'}
806
+
807
+
get-proto@1.0.1:
808
+
resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==}
809
+
engines: {node: '>= 0.4'}
810
+
811
+
glob-parent@5.1.2:
812
+
resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==}
813
+
engines: {node: '>= 6'}
814
+
815
+
glob-parent@6.0.2:
816
+
resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==}
817
+
engines: {node: '>=10.13.0'}
818
+
819
+
glob@10.4.5:
820
+
resolution: {integrity: sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==}
821
+
hasBin: true
822
+
823
+
glob@7.2.3:
824
+
resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==}
825
+
deprecated: Glob versions prior to v9 are no longer supported
826
+
827
+
globals@13.24.0:
828
+
resolution: {integrity: sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==}
829
+
engines: {node: '>=8'}
830
+
831
+
globby@11.1.0:
832
+
resolution: {integrity: sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==}
833
+
engines: {node: '>=10'}
834
+
835
+
gopd@1.2.0:
836
+
resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==}
837
+
engines: {node: '>= 0.4'}
838
+
839
+
graphemer@1.4.0:
840
+
resolution: {integrity: sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==}
841
+
842
+
has-flag@4.0.0:
843
+
resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==}
844
+
engines: {node: '>=8'}
845
+
846
+
has-symbols@1.1.0:
847
+
resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==}
848
+
engines: {node: '>= 0.4'}
849
+
850
+
has-tostringtag@1.0.2:
851
+
resolution: {integrity: sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==}
852
+
engines: {node: '>= 0.4'}
853
+
854
+
hasown@2.0.2:
855
+
resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==}
856
+
engines: {node: '>= 0.4'}
857
+
858
+
ignore@5.3.2:
859
+
resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==}
860
+
engines: {node: '>= 4'}
861
+
862
+
import-fresh@3.3.1:
863
+
resolution: {integrity: sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==}
864
+
engines: {node: '>=6'}
865
+
866
+
imurmurhash@0.1.4:
867
+
resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==}
868
+
engines: {node: '>=0.8.19'}
869
+
870
+
inflight@1.0.6:
871
+
resolution: {integrity: sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==}
872
+
deprecated: This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.
873
+
874
+
inherits@2.0.4:
875
+
resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==}
876
+
877
+
is-binary-path@2.1.0:
878
+
resolution: {integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==}
879
+
engines: {node: '>=8'}
880
+
881
+
is-core-module@2.16.1:
882
+
resolution: {integrity: sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==}
883
+
engines: {node: '>= 0.4'}
884
+
885
+
is-extglob@2.1.1:
886
+
resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==}
887
+
engines: {node: '>=0.10.0'}
888
+
889
+
is-fullwidth-code-point@3.0.0:
890
+
resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==}
891
+
engines: {node: '>=8'}
892
+
893
+
is-glob@4.0.3:
894
+
resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==}
895
+
engines: {node: '>=0.10.0'}
896
+
897
+
is-number@7.0.0:
898
+
resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==}
899
+
engines: {node: '>=0.12.0'}
900
+
901
+
is-path-inside@3.0.3:
902
+
resolution: {integrity: sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==}
903
+
engines: {node: '>=8'}
904
+
905
+
isexe@2.0.0:
906
+
resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==}
907
+
908
+
jackspeak@3.4.3:
909
+
resolution: {integrity: sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==}
910
+
911
+
jiti@1.21.7:
912
+
resolution: {integrity: sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==}
913
+
hasBin: true
914
+
915
+
js-tokens@4.0.0:
916
+
resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==}
917
+
918
+
js-yaml@4.1.0:
919
+
resolution: {integrity: sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==}
920
+
hasBin: true
921
+
922
+
jsesc@3.1.0:
923
+
resolution: {integrity: sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==}
924
+
engines: {node: '>=6'}
925
+
hasBin: true
926
+
927
+
json-buffer@3.0.1:
928
+
resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==}
929
+
930
+
json-schema-traverse@0.4.1:
931
+
resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==}
932
+
933
+
json-stable-stringify-without-jsonify@1.0.1:
934
+
resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==}
935
+
936
+
json5@2.2.3:
937
+
resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==}
938
+
engines: {node: '>=6'}
939
+
hasBin: true
940
+
941
+
keyv@4.5.4:
942
+
resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==}
943
+
944
+
levn@0.4.1:
945
+
resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==}
946
+
engines: {node: '>= 0.8.0'}
947
+
948
+
lilconfig@3.1.3:
949
+
resolution: {integrity: sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==}
950
+
engines: {node: '>=14'}
951
+
952
+
lines-and-columns@1.2.4:
953
+
resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==}
954
+
955
+
locate-path@6.0.0:
956
+
resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==}
957
+
engines: {node: '>=10'}
958
+
959
+
lodash.merge@4.6.2:
960
+
resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==}
961
+
962
+
loose-envify@1.4.0:
963
+
resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==}
964
+
hasBin: true
965
+
966
+
lru-cache@10.4.3:
967
+
resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==}
968
+
969
+
lru-cache@5.1.1:
970
+
resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==}
971
+
972
+
lucide-react@0.294.0:
973
+
resolution: {integrity: sha512-V7o0/VECSGbLHn3/1O67FUgBwWB+hmzshrgDVRJQhMh8uj5D3HBuIvhuAmQTtlupILSplwIZg5FTc4tTKMA2SA==}
974
+
peerDependencies:
975
+
react: ^16.5.1 || ^17.0.0 || ^18.0.0
976
+
977
+
math-intrinsics@1.1.0:
978
+
resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==}
979
+
engines: {node: '>= 0.4'}
980
+
981
+
merge2@1.4.1:
982
+
resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==}
983
+
engines: {node: '>= 8'}
984
+
985
+
micromatch@4.0.8:
986
+
resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==}
987
+
engines: {node: '>=8.6'}
988
+
989
+
mime-db@1.52.0:
990
+
resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==}
991
+
engines: {node: '>= 0.6'}
992
+
993
+
mime-types@2.1.35:
994
+
resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==}
995
+
engines: {node: '>= 0.6'}
996
+
997
+
minimatch@3.1.2:
998
+
resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==}
999
+
1000
+
minimatch@9.0.3:
1001
+
resolution: {integrity: sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==}
1002
+
engines: {node: '>=16 || 14 >=14.17'}
1003
+
1004
+
minimatch@9.0.5:
1005
+
resolution: {integrity: sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==}
1006
+
engines: {node: '>=16 || 14 >=14.17'}
1007
+
1008
+
minipass@7.1.2:
1009
+
resolution: {integrity: sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==}
1010
+
engines: {node: '>=16 || 14 >=14.17'}
1011
+
1012
+
ms@2.1.3:
1013
+
resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==}
1014
+
1015
+
mz@2.7.0:
1016
+
resolution: {integrity: sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==}
1017
+
1018
+
nanoid@3.3.11:
1019
+
resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==}
1020
+
engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
1021
+
hasBin: true
1022
+
1023
+
natural-compare@1.4.0:
1024
+
resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==}
1025
+
1026
+
node-releases@2.0.19:
1027
+
resolution: {integrity: sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==}
1028
+
1029
+
normalize-path@3.0.0:
1030
+
resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==}
1031
+
engines: {node: '>=0.10.0'}
1032
+
1033
+
normalize-range@0.1.2:
1034
+
resolution: {integrity: sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==}
1035
+
engines: {node: '>=0.10.0'}
1036
+
1037
+
object-assign@4.1.1:
1038
+
resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==}
1039
+
engines: {node: '>=0.10.0'}
1040
+
1041
+
object-hash@3.0.0:
1042
+
resolution: {integrity: sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==}
1043
+
engines: {node: '>= 6'}
1044
+
1045
+
once@1.4.0:
1046
+
resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==}
1047
+
1048
+
optionator@0.9.4:
1049
+
resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==}
1050
+
engines: {node: '>= 0.8.0'}
1051
+
1052
+
p-limit@3.1.0:
1053
+
resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==}
1054
+
engines: {node: '>=10'}
1055
+
1056
+
p-locate@5.0.0:
1057
+
resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==}
1058
+
engines: {node: '>=10'}
1059
+
1060
+
package-json-from-dist@1.0.1:
1061
+
resolution: {integrity: sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==}
1062
+
1063
+
parent-module@1.0.1:
1064
+
resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==}
1065
+
engines: {node: '>=6'}
1066
+
1067
+
path-exists@4.0.0:
1068
+
resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==}
1069
+
engines: {node: '>=8'}
1070
+
1071
+
path-is-absolute@1.0.1:
1072
+
resolution: {integrity: sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==}
1073
+
engines: {node: '>=0.10.0'}
1074
+
1075
+
path-key@3.1.1:
1076
+
resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==}
1077
+
engines: {node: '>=8'}
1078
+
1079
+
path-parse@1.0.7:
1080
+
resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==}
1081
+
1082
+
path-scurry@1.11.1:
1083
+
resolution: {integrity: sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==}
1084
+
engines: {node: '>=16 || 14 >=14.18'}
1085
+
1086
+
path-type@4.0.0:
1087
+
resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==}
1088
+
engines: {node: '>=8'}
1089
+
1090
+
picocolors@1.1.1:
1091
+
resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==}
1092
+
1093
+
picomatch@2.3.1:
1094
+
resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==}
1095
+
engines: {node: '>=8.6'}
1096
+
1097
+
pify@2.3.0:
1098
+
resolution: {integrity: sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==}
1099
+
engines: {node: '>=0.10.0'}
1100
+
1101
+
pirates@4.0.7:
1102
+
resolution: {integrity: sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==}
1103
+
engines: {node: '>= 6'}
1104
+
1105
+
postcss-import@15.1.0:
1106
+
resolution: {integrity: sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==}
1107
+
engines: {node: '>=14.0.0'}
1108
+
peerDependencies:
1109
+
postcss: ^8.0.0
1110
+
1111
+
postcss-js@4.0.1:
1112
+
resolution: {integrity: sha512-dDLF8pEO191hJMtlHFPRa8xsizHaM82MLfNkUHdUtVEV3tgTp5oj+8qbEqYM57SLfc74KSbw//4SeJma2LRVIw==}
1113
+
engines: {node: ^12 || ^14 || >= 16}
1114
+
peerDependencies:
1115
+
postcss: ^8.4.21
1116
+
1117
+
postcss-load-config@4.0.2:
1118
+
resolution: {integrity: sha512-bSVhyJGL00wMVoPUzAVAnbEoWyqRxkjv64tUl427SKnPrENtq6hJwUojroMz2VB+Q1edmi4IfrAPpami5VVgMQ==}
1119
+
engines: {node: '>= 14'}
1120
+
peerDependencies:
1121
+
postcss: '>=8.0.9'
1122
+
ts-node: '>=9.0.0'
1123
+
peerDependenciesMeta:
1124
+
postcss:
1125
+
optional: true
1126
+
ts-node:
1127
+
optional: true
1128
+
1129
+
postcss-nested@6.2.0:
1130
+
resolution: {integrity: sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ==}
1131
+
engines: {node: '>=12.0'}
1132
+
peerDependencies:
1133
+
postcss: ^8.2.14
1134
+
1135
+
postcss-selector-parser@6.1.2:
1136
+
resolution: {integrity: sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==}
1137
+
engines: {node: '>=4'}
1138
+
1139
+
postcss-value-parser@4.2.0:
1140
+
resolution: {integrity: sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==}
1141
+
1142
+
postcss@8.5.6:
1143
+
resolution: {integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==}
1144
+
engines: {node: ^10 || ^12 || >=14}
1145
+
1146
+
prelude-ls@1.2.1:
1147
+
resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==}
1148
+
engines: {node: '>= 0.8.0'}
1149
+
1150
+
proxy-from-env@1.1.0:
1151
+
resolution: {integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==}
1152
+
1153
+
punycode@2.3.1:
1154
+
resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==}
1155
+
engines: {node: '>=6'}
1156
+
1157
+
queue-microtask@1.2.3:
1158
+
resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==}
1159
+
1160
+
react-dom@18.3.1:
1161
+
resolution: {integrity: sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==}
1162
+
peerDependencies:
1163
+
react: ^18.3.1
1164
+
1165
+
react-refresh@0.17.0:
1166
+
resolution: {integrity: sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==}
1167
+
engines: {node: '>=0.10.0'}
1168
+
1169
+
react-router-dom@6.30.1:
1170
+
resolution: {integrity: sha512-llKsgOkZdbPU1Eg3zK8lCn+sjD9wMRZZPuzmdWWX5SUs8OFkN5HnFVC0u5KMeMaC9aoancFI/KoLuKPqN+hxHw==}
1171
+
engines: {node: '>=14.0.0'}
1172
+
peerDependencies:
1173
+
react: '>=16.8'
1174
+
react-dom: '>=16.8'
1175
+
1176
+
react-router@6.30.1:
1177
+
resolution: {integrity: sha512-X1m21aEmxGXqENEPG3T6u0Th7g0aS4ZmoNynhbs+Cn+q+QGTLt+d5IQ2bHAXKzKcxGJjxACpVbnYQSCRcfxHlQ==}
1178
+
engines: {node: '>=14.0.0'}
1179
+
peerDependencies:
1180
+
react: '>=16.8'
1181
+
1182
+
react@18.3.1:
1183
+
resolution: {integrity: sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==}
1184
+
engines: {node: '>=0.10.0'}
1185
+
1186
+
read-cache@1.0.0:
1187
+
resolution: {integrity: sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==}
1188
+
1189
+
readdirp@3.6.0:
1190
+
resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==}
1191
+
engines: {node: '>=8.10.0'}
1192
+
1193
+
resolve-from@4.0.0:
1194
+
resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==}
1195
+
engines: {node: '>=4'}
1196
+
1197
+
resolve@1.22.10:
1198
+
resolution: {integrity: sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==}
1199
+
engines: {node: '>= 0.4'}
1200
+
hasBin: true
1201
+
1202
+
reusify@1.1.0:
1203
+
resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==}
1204
+
engines: {iojs: '>=1.0.0', node: '>=0.10.0'}
1205
+
1206
+
rimraf@3.0.2:
1207
+
resolution: {integrity: sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==}
1208
+
deprecated: Rimraf versions prior to v4 are no longer supported
1209
+
hasBin: true
1210
+
1211
+
rollup@3.29.5:
1212
+
resolution: {integrity: sha512-GVsDdsbJzzy4S/v3dqWPJ7EfvZJfCHiDqe80IyrF59LYuP+e6U1LJoUqeuqRbwAWoMNoXivMNeNAOf5E22VA1w==}
1213
+
engines: {node: '>=14.18.0', npm: '>=8.0.0'}
1214
+
hasBin: true
1215
+
1216
+
run-parallel@1.2.0:
1217
+
resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==}
1218
+
1219
+
scheduler@0.23.2:
1220
+
resolution: {integrity: sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==}
1221
+
1222
+
semver@6.3.1:
1223
+
resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==}
1224
+
hasBin: true
1225
+
1226
+
semver@7.7.2:
1227
+
resolution: {integrity: sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==}
1228
+
engines: {node: '>=10'}
1229
+
hasBin: true
1230
+
1231
+
shebang-command@2.0.0:
1232
+
resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==}
1233
+
engines: {node: '>=8'}
1234
+
1235
+
shebang-regex@3.0.0:
1236
+
resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==}
1237
+
engines: {node: '>=8'}
1238
+
1239
+
signal-exit@4.1.0:
1240
+
resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==}
1241
+
engines: {node: '>=14'}
1242
+
1243
+
slash@3.0.0:
1244
+
resolution: {integrity: sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==}
1245
+
engines: {node: '>=8'}
1246
+
1247
+
sonner@1.7.4:
1248
+
resolution: {integrity: sha512-DIS8z4PfJRbIyfVFDVnK9rO3eYDtse4Omcm6bt0oEr5/jtLgysmjuBl1frJ9E/EQZrFmKx2A8m/s5s9CRXIzhw==}
1249
+
peerDependencies:
1250
+
react: ^18.0.0 || ^19.0.0 || ^19.0.0-rc
1251
+
react-dom: ^18.0.0 || ^19.0.0 || ^19.0.0-rc
1252
+
1253
+
source-map-js@1.2.1:
1254
+
resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==}
1255
+
engines: {node: '>=0.10.0'}
1256
+
1257
+
string-width@4.2.3:
1258
+
resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==}
1259
+
engines: {node: '>=8'}
1260
+
1261
+
string-width@5.1.2:
1262
+
resolution: {integrity: sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==}
1263
+
engines: {node: '>=12'}
1264
+
1265
+
strip-ansi@6.0.1:
1266
+
resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==}
1267
+
engines: {node: '>=8'}
1268
+
1269
+
strip-ansi@7.1.0:
1270
+
resolution: {integrity: sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==}
1271
+
engines: {node: '>=12'}
1272
+
1273
+
strip-json-comments@3.1.1:
1274
+
resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==}
1275
+
engines: {node: '>=8'}
1276
+
1277
+
sucrase@3.35.0:
1278
+
resolution: {integrity: sha512-8EbVDiu9iN/nESwxeSxDKe0dunta1GOlHufmSSXxMD2z2/tMZpDMpvXQGsc+ajGo8y2uYUmixaSRUc/QPoQ0GA==}
1279
+
engines: {node: '>=16 || 14 >=14.17'}
1280
+
hasBin: true
1281
+
1282
+
supports-color@7.2.0:
1283
+
resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==}
1284
+
engines: {node: '>=8'}
1285
+
1286
+
supports-preserve-symlinks-flag@1.0.0:
1287
+
resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==}
1288
+
engines: {node: '>= 0.4'}
1289
+
1290
+
tailwindcss@3.4.17:
1291
+
resolution: {integrity: sha512-w33E2aCvSDP0tW9RZuNXadXlkHXqFzSkQew/aIa2i/Sj8fThxwovwlXHSPXTbAHwEIhBFXAedUhP2tueAKP8Og==}
1292
+
engines: {node: '>=14.0.0'}
1293
+
hasBin: true
1294
+
1295
+
text-table@0.2.0:
1296
+
resolution: {integrity: sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==}
1297
+
1298
+
thenify-all@1.6.0:
1299
+
resolution: {integrity: sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==}
1300
+
engines: {node: '>=0.8'}
1301
+
1302
+
thenify@3.3.1:
1303
+
resolution: {integrity: sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==}
1304
+
1305
+
to-regex-range@5.0.1:
1306
+
resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==}
1307
+
engines: {node: '>=8.0'}
1308
+
1309
+
ts-api-utils@1.4.3:
1310
+
resolution: {integrity: sha512-i3eMG77UTMD0hZhgRS562pv83RC6ukSAC2GMNWc+9dieh/+jDM5u5YG+NHX6VNDRHQcHwmsTHctP9LhbC3WxVw==}
1311
+
engines: {node: '>=16'}
1312
+
peerDependencies:
1313
+
typescript: '>=4.2.0'
1314
+
1315
+
ts-interface-checker@0.1.13:
1316
+
resolution: {integrity: sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==}
1317
+
1318
+
type-check@0.4.0:
1319
+
resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==}
1320
+
engines: {node: '>= 0.8.0'}
1321
+
1322
+
type-fest@0.20.2:
1323
+
resolution: {integrity: sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==}
1324
+
engines: {node: '>=10'}
1325
+
1326
+
typescript@5.8.3:
1327
+
resolution: {integrity: sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==}
1328
+
engines: {node: '>=14.17'}
1329
+
hasBin: true
1330
+
1331
+
update-browserslist-db@1.1.3:
1332
+
resolution: {integrity: sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==}
1333
+
hasBin: true
1334
+
peerDependencies:
1335
+
browserslist: '>= 4.21.0'
1336
+
1337
+
uri-js@4.4.1:
1338
+
resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==}
1339
+
1340
+
use-sync-external-store@1.5.0:
1341
+
resolution: {integrity: sha512-Rb46I4cGGVBmjamjphe8L/UnvJD+uPPtTkNvX5mZgqdbavhI4EbgIWJiIHXJ8bc/i9EQGPRh4DwEURJ552Do0A==}
1342
+
peerDependencies:
1343
+
react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
1344
+
1345
+
util-deprecate@1.0.2:
1346
+
resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==}
1347
+
1348
+
vite@4.5.14:
1349
+
resolution: {integrity: sha512-+v57oAaoYNnO3hIu5Z/tJRZjq5aHM2zDve9YZ8HngVHbhk66RStobhb1sqPMIPEleV6cNKYK4eGrAbE9Ulbl2g==}
1350
+
engines: {node: ^14.18.0 || >=16.0.0}
1351
+
hasBin: true
1352
+
peerDependencies:
1353
+
'@types/node': '>= 14'
1354
+
less: '*'
1355
+
lightningcss: ^1.21.0
1356
+
sass: '*'
1357
+
stylus: '*'
1358
+
sugarss: '*'
1359
+
terser: ^5.4.0
1360
+
peerDependenciesMeta:
1361
+
'@types/node':
1362
+
optional: true
1363
+
less:
1364
+
optional: true
1365
+
lightningcss:
1366
+
optional: true
1367
+
sass:
1368
+
optional: true
1369
+
stylus:
1370
+
optional: true
1371
+
sugarss:
1372
+
optional: true
1373
+
terser:
1374
+
optional: true
1375
+
1376
+
which@2.0.2:
1377
+
resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==}
1378
+
engines: {node: '>= 8'}
1379
+
hasBin: true
1380
+
1381
+
word-wrap@1.2.5:
1382
+
resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==}
1383
+
engines: {node: '>=0.10.0'}
1384
+
1385
+
wrap-ansi@7.0.0:
1386
+
resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==}
1387
+
engines: {node: '>=10'}
1388
+
1389
+
wrap-ansi@8.1.0:
1390
+
resolution: {integrity: sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==}
1391
+
engines: {node: '>=12'}
1392
+
1393
+
wrappy@1.0.2:
1394
+
resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==}
1395
+
1396
+
yallist@3.1.1:
1397
+
resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==}
1398
+
1399
+
yaml@2.8.0:
1400
+
resolution: {integrity: sha512-4lLa/EcQCB0cJkyts+FpIRx5G/llPxfP6VQU5KByHEhLxY3IJCH0f0Hy1MHI8sClTvsIb8qwRJ6R/ZdlDJ/leQ==}
1401
+
engines: {node: '>= 14.6'}
1402
+
hasBin: true
1403
+
1404
+
yocto-queue@0.1.0:
1405
+
resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==}
1406
+
engines: {node: '>=10'}
1407
+
1408
+
zustand@4.5.7:
1409
+
resolution: {integrity: sha512-CHOUy7mu3lbD6o6LJLfllpjkzhHXSBlX8B9+qPddUsIfeF5S/UZ5q0kmCsnRqT1UHFQZchNFDDzMbQsuesHWlw==}
1410
+
engines: {node: '>=12.7.0'}
1411
+
peerDependencies:
1412
+
'@types/react': '>=16.8'
1413
+
immer: '>=9.0.6'
1414
+
react: '>=16.8'
1415
+
peerDependenciesMeta:
1416
+
'@types/react':
1417
+
optional: true
1418
+
immer:
1419
+
optional: true
1420
+
react:
1421
+
optional: true
1422
+
1423
+
snapshots:
1424
+
1425
+
'@alloc/quick-lru@5.2.0': {}
1426
+
1427
+
'@ampproject/remapping@2.3.0':
1428
+
dependencies:
1429
+
'@jridgewell/gen-mapping': 0.3.12
1430
+
'@jridgewell/trace-mapping': 0.3.29
1431
+
1432
+
'@babel/code-frame@7.27.1':
1433
+
dependencies:
1434
+
'@babel/helper-validator-identifier': 7.27.1
1435
+
js-tokens: 4.0.0
1436
+
picocolors: 1.1.1
1437
+
1438
+
'@babel/compat-data@7.28.0': {}
1439
+
1440
+
'@babel/core@7.28.0':
1441
+
dependencies:
1442
+
'@ampproject/remapping': 2.3.0
1443
+
'@babel/code-frame': 7.27.1
1444
+
'@babel/generator': 7.28.0
1445
+
'@babel/helper-compilation-targets': 7.27.2
1446
+
'@babel/helper-module-transforms': 7.27.3(@babel/core@7.28.0)
1447
+
'@babel/helpers': 7.27.6
1448
+
'@babel/parser': 7.28.0
1449
+
'@babel/template': 7.27.2
1450
+
'@babel/traverse': 7.28.0
1451
+
'@babel/types': 7.28.1
1452
+
convert-source-map: 2.0.0
1453
+
debug: 4.4.1
1454
+
gensync: 1.0.0-beta.2
1455
+
json5: 2.2.3
1456
+
semver: 6.3.1
1457
+
transitivePeerDependencies:
1458
+
- supports-color
1459
+
1460
+
'@babel/generator@7.28.0':
1461
+
dependencies:
1462
+
'@babel/parser': 7.28.0
1463
+
'@babel/types': 7.28.1
1464
+
'@jridgewell/gen-mapping': 0.3.12
1465
+
'@jridgewell/trace-mapping': 0.3.29
1466
+
jsesc: 3.1.0
1467
+
1468
+
'@babel/helper-compilation-targets@7.27.2':
1469
+
dependencies:
1470
+
'@babel/compat-data': 7.28.0
1471
+
'@babel/helper-validator-option': 7.27.1
1472
+
browserslist: 4.25.1
1473
+
lru-cache: 5.1.1
1474
+
semver: 6.3.1
1475
+
1476
+
'@babel/helper-globals@7.28.0': {}
1477
+
1478
+
'@babel/helper-module-imports@7.27.1':
1479
+
dependencies:
1480
+
'@babel/traverse': 7.28.0
1481
+
'@babel/types': 7.28.1
1482
+
transitivePeerDependencies:
1483
+
- supports-color
1484
+
1485
+
'@babel/helper-module-transforms@7.27.3(@babel/core@7.28.0)':
1486
+
dependencies:
1487
+
'@babel/core': 7.28.0
1488
+
'@babel/helper-module-imports': 7.27.1
1489
+
'@babel/helper-validator-identifier': 7.27.1
1490
+
'@babel/traverse': 7.28.0
1491
+
transitivePeerDependencies:
1492
+
- supports-color
1493
+
1494
+
'@babel/helper-plugin-utils@7.27.1': {}
1495
+
1496
+
'@babel/helper-string-parser@7.27.1': {}
1497
+
1498
+
'@babel/helper-validator-identifier@7.27.1': {}
1499
+
1500
+
'@babel/helper-validator-option@7.27.1': {}
1501
+
1502
+
'@babel/helpers@7.27.6':
1503
+
dependencies:
1504
+
'@babel/template': 7.27.2
1505
+
'@babel/types': 7.28.1
1506
+
1507
+
'@babel/parser@7.28.0':
1508
+
dependencies:
1509
+
'@babel/types': 7.28.1
1510
+
1511
+
'@babel/plugin-transform-react-jsx-self@7.27.1(@babel/core@7.28.0)':
1512
+
dependencies:
1513
+
'@babel/core': 7.28.0
1514
+
'@babel/helper-plugin-utils': 7.27.1
1515
+
1516
+
'@babel/plugin-transform-react-jsx-source@7.27.1(@babel/core@7.28.0)':
1517
+
dependencies:
1518
+
'@babel/core': 7.28.0
1519
+
'@babel/helper-plugin-utils': 7.27.1
1520
+
1521
+
'@babel/template@7.27.2':
1522
+
dependencies:
1523
+
'@babel/code-frame': 7.27.1
1524
+
'@babel/parser': 7.28.0
1525
+
'@babel/types': 7.28.1
1526
+
1527
+
'@babel/traverse@7.28.0':
1528
+
dependencies:
1529
+
'@babel/code-frame': 7.27.1
1530
+
'@babel/generator': 7.28.0
1531
+
'@babel/helper-globals': 7.28.0
1532
+
'@babel/parser': 7.28.0
1533
+
'@babel/template': 7.27.2
1534
+
'@babel/types': 7.28.1
1535
+
debug: 4.4.1
1536
+
transitivePeerDependencies:
1537
+
- supports-color
1538
+
1539
+
'@babel/types@7.28.1':
1540
+
dependencies:
1541
+
'@babel/helper-string-parser': 7.27.1
1542
+
'@babel/helper-validator-identifier': 7.27.1
1543
+
1544
+
'@esbuild/android-arm64@0.18.20':
1545
+
optional: true
1546
+
1547
+
'@esbuild/android-arm@0.18.20':
1548
+
optional: true
1549
+
1550
+
'@esbuild/android-x64@0.18.20':
1551
+
optional: true
1552
+
1553
+
'@esbuild/darwin-arm64@0.18.20':
1554
+
optional: true
1555
+
1556
+
'@esbuild/darwin-x64@0.18.20':
1557
+
optional: true
1558
+
1559
+
'@esbuild/freebsd-arm64@0.18.20':
1560
+
optional: true
1561
+
1562
+
'@esbuild/freebsd-x64@0.18.20':
1563
+
optional: true
1564
+
1565
+
'@esbuild/linux-arm64@0.18.20':
1566
+
optional: true
1567
+
1568
+
'@esbuild/linux-arm@0.18.20':
1569
+
optional: true
1570
+
1571
+
'@esbuild/linux-ia32@0.18.20':
1572
+
optional: true
1573
+
1574
+
'@esbuild/linux-loong64@0.18.20':
1575
+
optional: true
1576
+
1577
+
'@esbuild/linux-mips64el@0.18.20':
1578
+
optional: true
1579
+
1580
+
'@esbuild/linux-ppc64@0.18.20':
1581
+
optional: true
1582
+
1583
+
'@esbuild/linux-riscv64@0.18.20':
1584
+
optional: true
1585
+
1586
+
'@esbuild/linux-s390x@0.18.20':
1587
+
optional: true
1588
+
1589
+
'@esbuild/linux-x64@0.18.20':
1590
+
optional: true
1591
+
1592
+
'@esbuild/netbsd-x64@0.18.20':
1593
+
optional: true
1594
+
1595
+
'@esbuild/openbsd-x64@0.18.20':
1596
+
optional: true
1597
+
1598
+
'@esbuild/sunos-x64@0.18.20':
1599
+
optional: true
1600
+
1601
+
'@esbuild/win32-arm64@0.18.20':
1602
+
optional: true
1603
+
1604
+
'@esbuild/win32-ia32@0.18.20':
1605
+
optional: true
1606
+
1607
+
'@esbuild/win32-x64@0.18.20':
1608
+
optional: true
1609
+
1610
+
'@eslint-community/eslint-utils@4.7.0(eslint@8.57.1)':
1611
+
dependencies:
1612
+
eslint: 8.57.1
1613
+
eslint-visitor-keys: 3.4.3
1614
+
1615
+
'@eslint-community/regexpp@4.12.1': {}
1616
+
1617
+
'@eslint/eslintrc@2.1.4':
1618
+
dependencies:
1619
+
ajv: 6.12.6
1620
+
debug: 4.4.1
1621
+
espree: 9.6.1
1622
+
globals: 13.24.0
1623
+
ignore: 5.3.2
1624
+
import-fresh: 3.3.1
1625
+
js-yaml: 4.1.0
1626
+
minimatch: 3.1.2
1627
+
strip-json-comments: 3.1.1
1628
+
transitivePeerDependencies:
1629
+
- supports-color
1630
+
1631
+
'@eslint/js@8.57.1': {}
1632
+
1633
+
'@humanwhocodes/config-array@0.13.0':
1634
+
dependencies:
1635
+
'@humanwhocodes/object-schema': 2.0.3
1636
+
debug: 4.4.1
1637
+
minimatch: 3.1.2
1638
+
transitivePeerDependencies:
1639
+
- supports-color
1640
+
1641
+
'@humanwhocodes/module-importer@1.0.1': {}
1642
+
1643
+
'@humanwhocodes/object-schema@2.0.3': {}
1644
+
1645
+
'@isaacs/cliui@8.0.2':
1646
+
dependencies:
1647
+
string-width: 5.1.2
1648
+
string-width-cjs: string-width@4.2.3
1649
+
strip-ansi: 7.1.0
1650
+
strip-ansi-cjs: strip-ansi@6.0.1
1651
+
wrap-ansi: 8.1.0
1652
+
wrap-ansi-cjs: wrap-ansi@7.0.0
1653
+
1654
+
'@jridgewell/gen-mapping@0.3.12':
1655
+
dependencies:
1656
+
'@jridgewell/sourcemap-codec': 1.5.4
1657
+
'@jridgewell/trace-mapping': 0.3.29
1658
+
1659
+
'@jridgewell/resolve-uri@3.1.2': {}
1660
+
1661
+
'@jridgewell/sourcemap-codec@1.5.4': {}
1662
+
1663
+
'@jridgewell/trace-mapping@0.3.29':
1664
+
dependencies:
1665
+
'@jridgewell/resolve-uri': 3.1.2
1666
+
'@jridgewell/sourcemap-codec': 1.5.4
1667
+
1668
+
'@nodelib/fs.scandir@2.1.5':
1669
+
dependencies:
1670
+
'@nodelib/fs.stat': 2.0.5
1671
+
run-parallel: 1.2.0
1672
+
1673
+
'@nodelib/fs.stat@2.0.5': {}
1674
+
1675
+
'@nodelib/fs.walk@1.2.8':
1676
+
dependencies:
1677
+
'@nodelib/fs.scandir': 2.1.5
1678
+
fastq: 1.19.1
1679
+
1680
+
'@pkgjs/parseargs@0.11.0':
1681
+
optional: true
1682
+
1683
+
'@remix-run/router@1.23.0': {}
1684
+
1685
+
'@rolldown/pluginutils@1.0.0-beta.27': {}
1686
+
1687
+
'@tanstack/query-core@5.83.0': {}
1688
+
1689
+
'@tanstack/react-query@5.83.0(react@18.3.1)':
1690
+
dependencies:
1691
+
'@tanstack/query-core': 5.83.0
1692
+
react: 18.3.1
1693
+
1694
+
'@types/babel__core@7.20.5':
1695
+
dependencies:
1696
+
'@babel/parser': 7.28.0
1697
+
'@babel/types': 7.28.1
1698
+
'@types/babel__generator': 7.27.0
1699
+
'@types/babel__template': 7.4.4
1700
+
'@types/babel__traverse': 7.20.7
1701
+
1702
+
'@types/babel__generator@7.27.0':
1703
+
dependencies:
1704
+
'@babel/types': 7.28.1
1705
+
1706
+
'@types/babel__template@7.4.4':
1707
+
dependencies:
1708
+
'@babel/parser': 7.28.0
1709
+
'@babel/types': 7.28.1
1710
+
1711
+
'@types/babel__traverse@7.20.7':
1712
+
dependencies:
1713
+
'@babel/types': 7.28.1
1714
+
1715
+
'@types/json-schema@7.0.15': {}
1716
+
1717
+
'@types/prop-types@15.7.15': {}
1718
+
1719
+
'@types/react-dom@18.3.7(@types/react@18.3.23)':
1720
+
dependencies:
1721
+
'@types/react': 18.3.23
1722
+
1723
+
'@types/react@18.3.23':
1724
+
dependencies:
1725
+
'@types/prop-types': 15.7.15
1726
+
csstype: 3.1.3
1727
+
1728
+
'@types/semver@7.7.0': {}
1729
+
1730
+
'@typescript-eslint/eslint-plugin@6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.8.3))(eslint@8.57.1)(typescript@5.8.3)':
1731
+
dependencies:
1732
+
'@eslint-community/regexpp': 4.12.1
1733
+
'@typescript-eslint/parser': 6.21.0(eslint@8.57.1)(typescript@5.8.3)
1734
+
'@typescript-eslint/scope-manager': 6.21.0
1735
+
'@typescript-eslint/type-utils': 6.21.0(eslint@8.57.1)(typescript@5.8.3)
1736
+
'@typescript-eslint/utils': 6.21.0(eslint@8.57.1)(typescript@5.8.3)
1737
+
'@typescript-eslint/visitor-keys': 6.21.0
1738
+
debug: 4.4.1
1739
+
eslint: 8.57.1
1740
+
graphemer: 1.4.0
1741
+
ignore: 5.3.2
1742
+
natural-compare: 1.4.0
1743
+
semver: 7.7.2
1744
+
ts-api-utils: 1.4.3(typescript@5.8.3)
1745
+
optionalDependencies:
1746
+
typescript: 5.8.3
1747
+
transitivePeerDependencies:
1748
+
- supports-color
1749
+
1750
+
'@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.8.3)':
1751
+
dependencies:
1752
+
'@typescript-eslint/scope-manager': 6.21.0
1753
+
'@typescript-eslint/types': 6.21.0
1754
+
'@typescript-eslint/typescript-estree': 6.21.0(typescript@5.8.3)
1755
+
'@typescript-eslint/visitor-keys': 6.21.0
1756
+
debug: 4.4.1
1757
+
eslint: 8.57.1
1758
+
optionalDependencies:
1759
+
typescript: 5.8.3
1760
+
transitivePeerDependencies:
1761
+
- supports-color
1762
+
1763
+
'@typescript-eslint/scope-manager@6.21.0':
1764
+
dependencies:
1765
+
'@typescript-eslint/types': 6.21.0
1766
+
'@typescript-eslint/visitor-keys': 6.21.0
1767
+
1768
+
'@typescript-eslint/type-utils@6.21.0(eslint@8.57.1)(typescript@5.8.3)':
1769
+
dependencies:
1770
+
'@typescript-eslint/typescript-estree': 6.21.0(typescript@5.8.3)
1771
+
'@typescript-eslint/utils': 6.21.0(eslint@8.57.1)(typescript@5.8.3)
1772
+
debug: 4.4.1
1773
+
eslint: 8.57.1
1774
+
ts-api-utils: 1.4.3(typescript@5.8.3)
1775
+
optionalDependencies:
1776
+
typescript: 5.8.3
1777
+
transitivePeerDependencies:
1778
+
- supports-color
1779
+
1780
+
'@typescript-eslint/types@6.21.0': {}
1781
+
1782
+
'@typescript-eslint/typescript-estree@6.21.0(typescript@5.8.3)':
1783
+
dependencies:
1784
+
'@typescript-eslint/types': 6.21.0
1785
+
'@typescript-eslint/visitor-keys': 6.21.0
1786
+
debug: 4.4.1
1787
+
globby: 11.1.0
1788
+
is-glob: 4.0.3
1789
+
minimatch: 9.0.3
1790
+
semver: 7.7.2
1791
+
ts-api-utils: 1.4.3(typescript@5.8.3)
1792
+
optionalDependencies:
1793
+
typescript: 5.8.3
1794
+
transitivePeerDependencies:
1795
+
- supports-color
1796
+
1797
+
'@typescript-eslint/utils@6.21.0(eslint@8.57.1)(typescript@5.8.3)':
1798
+
dependencies:
1799
+
'@eslint-community/eslint-utils': 4.7.0(eslint@8.57.1)
1800
+
'@types/json-schema': 7.0.15
1801
+
'@types/semver': 7.7.0
1802
+
'@typescript-eslint/scope-manager': 6.21.0
1803
+
'@typescript-eslint/types': 6.21.0
1804
+
'@typescript-eslint/typescript-estree': 6.21.0(typescript@5.8.3)
1805
+
eslint: 8.57.1
1806
+
semver: 7.7.2
1807
+
transitivePeerDependencies:
1808
+
- supports-color
1809
+
- typescript
1810
+
1811
+
'@typescript-eslint/visitor-keys@6.21.0':
1812
+
dependencies:
1813
+
'@typescript-eslint/types': 6.21.0
1814
+
eslint-visitor-keys: 3.4.3
1815
+
1816
+
'@ungap/structured-clone@1.3.0': {}
1817
+
1818
+
'@vitejs/plugin-react@4.7.0(vite@4.5.14)':
1819
+
dependencies:
1820
+
'@babel/core': 7.28.0
1821
+
'@babel/plugin-transform-react-jsx-self': 7.27.1(@babel/core@7.28.0)
1822
+
'@babel/plugin-transform-react-jsx-source': 7.27.1(@babel/core@7.28.0)
1823
+
'@rolldown/pluginutils': 1.0.0-beta.27
1824
+
'@types/babel__core': 7.20.5
1825
+
react-refresh: 0.17.0
1826
+
vite: 4.5.14
1827
+
transitivePeerDependencies:
1828
+
- supports-color
1829
+
1830
+
acorn-jsx@5.3.2(acorn@8.15.0):
1831
+
dependencies:
1832
+
acorn: 8.15.0
1833
+
1834
+
acorn@8.15.0: {}
1835
+
1836
+
ajv@6.12.6:
1837
+
dependencies:
1838
+
fast-deep-equal: 3.1.3
1839
+
fast-json-stable-stringify: 2.1.0
1840
+
json-schema-traverse: 0.4.1
1841
+
uri-js: 4.4.1
1842
+
1843
+
ansi-regex@5.0.1: {}
1844
+
1845
+
ansi-regex@6.1.0: {}
1846
+
1847
+
ansi-styles@4.3.0:
1848
+
dependencies:
1849
+
color-convert: 2.0.1
1850
+
1851
+
ansi-styles@6.2.1: {}
1852
+
1853
+
any-promise@1.3.0: {}
1854
+
1855
+
anymatch@3.1.3:
1856
+
dependencies:
1857
+
normalize-path: 3.0.0
1858
+
picomatch: 2.3.1
1859
+
1860
+
arg@5.0.2: {}
1861
+
1862
+
argparse@2.0.1: {}
1863
+
1864
+
array-union@2.1.0: {}
1865
+
1866
+
asynckit@0.4.0: {}
1867
+
1868
+
autoprefixer@10.4.21(postcss@8.5.6):
1869
+
dependencies:
1870
+
browserslist: 4.25.1
1871
+
caniuse-lite: 1.0.30001727
1872
+
fraction.js: 4.3.7
1873
+
normalize-range: 0.1.2
1874
+
picocolors: 1.1.1
1875
+
postcss: 8.5.6
1876
+
postcss-value-parser: 4.2.0
1877
+
1878
+
axios@1.10.0:
1879
+
dependencies:
1880
+
follow-redirects: 1.15.9
1881
+
form-data: 4.0.4
1882
+
proxy-from-env: 1.1.0
1883
+
transitivePeerDependencies:
1884
+
- debug
1885
+
1886
+
balanced-match@1.0.2: {}
1887
+
1888
+
binary-extensions@2.3.0: {}
1889
+
1890
+
brace-expansion@1.1.12:
1891
+
dependencies:
1892
+
balanced-match: 1.0.2
1893
+
concat-map: 0.0.1
1894
+
1895
+
brace-expansion@2.0.2:
1896
+
dependencies:
1897
+
balanced-match: 1.0.2
1898
+
1899
+
braces@3.0.3:
1900
+
dependencies:
1901
+
fill-range: 7.1.1
1902
+
1903
+
browserslist@4.25.1:
1904
+
dependencies:
1905
+
caniuse-lite: 1.0.30001727
1906
+
electron-to-chromium: 1.5.189
1907
+
node-releases: 2.0.19
1908
+
update-browserslist-db: 1.1.3(browserslist@4.25.1)
1909
+
1910
+
call-bind-apply-helpers@1.0.2:
1911
+
dependencies:
1912
+
es-errors: 1.3.0
1913
+
function-bind: 1.1.2
1914
+
1915
+
callsites@3.1.0: {}
1916
+
1917
+
camelcase-css@2.0.1: {}
1918
+
1919
+
caniuse-lite@1.0.30001727: {}
1920
+
1921
+
chalk@4.1.2:
1922
+
dependencies:
1923
+
ansi-styles: 4.3.0
1924
+
supports-color: 7.2.0
1925
+
1926
+
chokidar@3.6.0:
1927
+
dependencies:
1928
+
anymatch: 3.1.3
1929
+
braces: 3.0.3
1930
+
glob-parent: 5.1.2
1931
+
is-binary-path: 2.1.0
1932
+
is-glob: 4.0.3
1933
+
normalize-path: 3.0.0
1934
+
readdirp: 3.6.0
1935
+
optionalDependencies:
1936
+
fsevents: 2.3.3
1937
+
1938
+
color-convert@2.0.1:
1939
+
dependencies:
1940
+
color-name: 1.1.4
1941
+
1942
+
color-name@1.1.4: {}
1943
+
1944
+
combined-stream@1.0.8:
1945
+
dependencies:
1946
+
delayed-stream: 1.0.0
1947
+
1948
+
commander@4.1.1: {}
1949
+
1950
+
concat-map@0.0.1: {}
1951
+
1952
+
convert-source-map@2.0.0: {}
1953
+
1954
+
cross-spawn@7.0.6:
1955
+
dependencies:
1956
+
path-key: 3.1.1
1957
+
shebang-command: 2.0.0
1958
+
which: 2.0.2
1959
+
1960
+
cssesc@3.0.0: {}
1961
+
1962
+
csstype@3.1.3: {}
1963
+
1964
+
debug@4.4.1:
1965
+
dependencies:
1966
+
ms: 2.1.3
1967
+
1968
+
deep-is@0.1.4: {}
1969
+
1970
+
delayed-stream@1.0.0: {}
1971
+
1972
+
didyoumean@1.2.2: {}
1973
+
1974
+
dir-glob@3.0.1:
1975
+
dependencies:
1976
+
path-type: 4.0.0
1977
+
1978
+
dlv@1.1.3: {}
1979
+
1980
+
doctrine@3.0.0:
1981
+
dependencies:
1982
+
esutils: 2.0.3
1983
+
1984
+
dunder-proto@1.0.1:
1985
+
dependencies:
1986
+
call-bind-apply-helpers: 1.0.2
1987
+
es-errors: 1.3.0
1988
+
gopd: 1.2.0
1989
+
1990
+
eastasianwidth@0.2.0: {}
1991
+
1992
+
electron-to-chromium@1.5.189: {}
1993
+
1994
+
emoji-regex@8.0.0: {}
1995
+
1996
+
emoji-regex@9.2.2: {}
1997
+
1998
+
es-define-property@1.0.1: {}
1999
+
2000
+
es-errors@1.3.0: {}
2001
+
2002
+
es-object-atoms@1.1.1:
2003
+
dependencies:
2004
+
es-errors: 1.3.0
2005
+
2006
+
es-set-tostringtag@2.1.0:
2007
+
dependencies:
2008
+
es-errors: 1.3.0
2009
+
get-intrinsic: 1.3.0
2010
+
has-tostringtag: 1.0.2
2011
+
hasown: 2.0.2
2012
+
2013
+
esbuild@0.18.20:
2014
+
optionalDependencies:
2015
+
'@esbuild/android-arm': 0.18.20
2016
+
'@esbuild/android-arm64': 0.18.20
2017
+
'@esbuild/android-x64': 0.18.20
2018
+
'@esbuild/darwin-arm64': 0.18.20
2019
+
'@esbuild/darwin-x64': 0.18.20
2020
+
'@esbuild/freebsd-arm64': 0.18.20
2021
+
'@esbuild/freebsd-x64': 0.18.20
2022
+
'@esbuild/linux-arm': 0.18.20
2023
+
'@esbuild/linux-arm64': 0.18.20
2024
+
'@esbuild/linux-ia32': 0.18.20
2025
+
'@esbuild/linux-loong64': 0.18.20
2026
+
'@esbuild/linux-mips64el': 0.18.20
2027
+
'@esbuild/linux-ppc64': 0.18.20
2028
+
'@esbuild/linux-riscv64': 0.18.20
2029
+
'@esbuild/linux-s390x': 0.18.20
2030
+
'@esbuild/linux-x64': 0.18.20
2031
+
'@esbuild/netbsd-x64': 0.18.20
2032
+
'@esbuild/openbsd-x64': 0.18.20
2033
+
'@esbuild/sunos-x64': 0.18.20
2034
+
'@esbuild/win32-arm64': 0.18.20
2035
+
'@esbuild/win32-ia32': 0.18.20
2036
+
'@esbuild/win32-x64': 0.18.20
2037
+
2038
+
escalade@3.2.0: {}
2039
+
2040
+
escape-string-regexp@4.0.0: {}
2041
+
2042
+
eslint-plugin-react-hooks@4.6.2(eslint@8.57.1):
2043
+
dependencies:
2044
+
eslint: 8.57.1
2045
+
2046
+
eslint-plugin-react-refresh@0.4.20(eslint@8.57.1):
2047
+
dependencies:
2048
+
eslint: 8.57.1
2049
+
2050
+
eslint-scope@7.2.2:
2051
+
dependencies:
2052
+
esrecurse: 4.3.0
2053
+
estraverse: 5.3.0
2054
+
2055
+
eslint-visitor-keys@3.4.3: {}
2056
+
2057
+
eslint@8.57.1:
2058
+
dependencies:
2059
+
'@eslint-community/eslint-utils': 4.7.0(eslint@8.57.1)
2060
+
'@eslint-community/regexpp': 4.12.1
2061
+
'@eslint/eslintrc': 2.1.4
2062
+
'@eslint/js': 8.57.1
2063
+
'@humanwhocodes/config-array': 0.13.0
2064
+
'@humanwhocodes/module-importer': 1.0.1
2065
+
'@nodelib/fs.walk': 1.2.8
2066
+
'@ungap/structured-clone': 1.3.0
2067
+
ajv: 6.12.6
2068
+
chalk: 4.1.2
2069
+
cross-spawn: 7.0.6
2070
+
debug: 4.4.1
2071
+
doctrine: 3.0.0
2072
+
escape-string-regexp: 4.0.0
2073
+
eslint-scope: 7.2.2
2074
+
eslint-visitor-keys: 3.4.3
2075
+
espree: 9.6.1
2076
+
esquery: 1.6.0
2077
+
esutils: 2.0.3
2078
+
fast-deep-equal: 3.1.3
2079
+
file-entry-cache: 6.0.1
2080
+
find-up: 5.0.0
2081
+
glob-parent: 6.0.2
2082
+
globals: 13.24.0
2083
+
graphemer: 1.4.0
2084
+
ignore: 5.3.2
2085
+
imurmurhash: 0.1.4
2086
+
is-glob: 4.0.3
2087
+
is-path-inside: 3.0.3
2088
+
js-yaml: 4.1.0
2089
+
json-stable-stringify-without-jsonify: 1.0.1
2090
+
levn: 0.4.1
2091
+
lodash.merge: 4.6.2
2092
+
minimatch: 3.1.2
2093
+
natural-compare: 1.4.0
2094
+
optionator: 0.9.4
2095
+
strip-ansi: 6.0.1
2096
+
text-table: 0.2.0
2097
+
transitivePeerDependencies:
2098
+
- supports-color
2099
+
2100
+
espree@9.6.1:
2101
+
dependencies:
2102
+
acorn: 8.15.0
2103
+
acorn-jsx: 5.3.2(acorn@8.15.0)
2104
+
eslint-visitor-keys: 3.4.3
2105
+
2106
+
esquery@1.6.0:
2107
+
dependencies:
2108
+
estraverse: 5.3.0
2109
+
2110
+
esrecurse@4.3.0:
2111
+
dependencies:
2112
+
estraverse: 5.3.0
2113
+
2114
+
estraverse@5.3.0: {}
2115
+
2116
+
esutils@2.0.3: {}
2117
+
2118
+
fast-deep-equal@3.1.3: {}
2119
+
2120
+
fast-glob@3.3.3:
2121
+
dependencies:
2122
+
'@nodelib/fs.stat': 2.0.5
2123
+
'@nodelib/fs.walk': 1.2.8
2124
+
glob-parent: 5.1.2
2125
+
merge2: 1.4.1
2126
+
micromatch: 4.0.8
2127
+
2128
+
fast-json-stable-stringify@2.1.0: {}
2129
+
2130
+
fast-levenshtein@2.0.6: {}
2131
+
2132
+
fastq@1.19.1:
2133
+
dependencies:
2134
+
reusify: 1.1.0
2135
+
2136
+
file-entry-cache@6.0.1:
2137
+
dependencies:
2138
+
flat-cache: 3.2.0
2139
+
2140
+
fill-range@7.1.1:
2141
+
dependencies:
2142
+
to-regex-range: 5.0.1
2143
+
2144
+
find-up@5.0.0:
2145
+
dependencies:
2146
+
locate-path: 6.0.0
2147
+
path-exists: 4.0.0
2148
+
2149
+
flat-cache@3.2.0:
2150
+
dependencies:
2151
+
flatted: 3.3.3
2152
+
keyv: 4.5.4
2153
+
rimraf: 3.0.2
2154
+
2155
+
flatted@3.3.3: {}
2156
+
2157
+
follow-redirects@1.15.9: {}
2158
+
2159
+
foreground-child@3.3.1:
2160
+
dependencies:
2161
+
cross-spawn: 7.0.6
2162
+
signal-exit: 4.1.0
2163
+
2164
+
form-data@4.0.4:
2165
+
dependencies:
2166
+
asynckit: 0.4.0
2167
+
combined-stream: 1.0.8
2168
+
es-set-tostringtag: 2.1.0
2169
+
hasown: 2.0.2
2170
+
mime-types: 2.1.35
2171
+
2172
+
fraction.js@4.3.7: {}
2173
+
2174
+
fs.realpath@1.0.0: {}
2175
+
2176
+
fsevents@2.3.3:
2177
+
optional: true
2178
+
2179
+
function-bind@1.1.2: {}
2180
+
2181
+
gensync@1.0.0-beta.2: {}
2182
+
2183
+
get-intrinsic@1.3.0:
2184
+
dependencies:
2185
+
call-bind-apply-helpers: 1.0.2
2186
+
es-define-property: 1.0.1
2187
+
es-errors: 1.3.0
2188
+
es-object-atoms: 1.1.1
2189
+
function-bind: 1.1.2
2190
+
get-proto: 1.0.1
2191
+
gopd: 1.2.0
2192
+
has-symbols: 1.1.0
2193
+
hasown: 2.0.2
2194
+
math-intrinsics: 1.1.0
2195
+
2196
+
get-proto@1.0.1:
2197
+
dependencies:
2198
+
dunder-proto: 1.0.1
2199
+
es-object-atoms: 1.1.1
2200
+
2201
+
glob-parent@5.1.2:
2202
+
dependencies:
2203
+
is-glob: 4.0.3
2204
+
2205
+
glob-parent@6.0.2:
2206
+
dependencies:
2207
+
is-glob: 4.0.3
2208
+
2209
+
glob@10.4.5:
2210
+
dependencies:
2211
+
foreground-child: 3.3.1
2212
+
jackspeak: 3.4.3
2213
+
minimatch: 9.0.5
2214
+
minipass: 7.1.2
2215
+
package-json-from-dist: 1.0.1
2216
+
path-scurry: 1.11.1
2217
+
2218
+
glob@7.2.3:
2219
+
dependencies:
2220
+
fs.realpath: 1.0.0
2221
+
inflight: 1.0.6
2222
+
inherits: 2.0.4
2223
+
minimatch: 3.1.2
2224
+
once: 1.4.0
2225
+
path-is-absolute: 1.0.1
2226
+
2227
+
globals@13.24.0:
2228
+
dependencies:
2229
+
type-fest: 0.20.2
2230
+
2231
+
globby@11.1.0:
2232
+
dependencies:
2233
+
array-union: 2.1.0
2234
+
dir-glob: 3.0.1
2235
+
fast-glob: 3.3.3
2236
+
ignore: 5.3.2
2237
+
merge2: 1.4.1
2238
+
slash: 3.0.0
2239
+
2240
+
gopd@1.2.0: {}
2241
+
2242
+
graphemer@1.4.0: {}
2243
+
2244
+
has-flag@4.0.0: {}
2245
+
2246
+
has-symbols@1.1.0: {}
2247
+
2248
+
has-tostringtag@1.0.2:
2249
+
dependencies:
2250
+
has-symbols: 1.1.0
2251
+
2252
+
hasown@2.0.2:
2253
+
dependencies:
2254
+
function-bind: 1.1.2
2255
+
2256
+
ignore@5.3.2: {}
2257
+
2258
+
import-fresh@3.3.1:
2259
+
dependencies:
2260
+
parent-module: 1.0.1
2261
+
resolve-from: 4.0.0
2262
+
2263
+
imurmurhash@0.1.4: {}
2264
+
2265
+
inflight@1.0.6:
2266
+
dependencies:
2267
+
once: 1.4.0
2268
+
wrappy: 1.0.2
2269
+
2270
+
inherits@2.0.4: {}
2271
+
2272
+
is-binary-path@2.1.0:
2273
+
dependencies:
2274
+
binary-extensions: 2.3.0
2275
+
2276
+
is-core-module@2.16.1:
2277
+
dependencies:
2278
+
hasown: 2.0.2
2279
+
2280
+
is-extglob@2.1.1: {}
2281
+
2282
+
is-fullwidth-code-point@3.0.0: {}
2283
+
2284
+
is-glob@4.0.3:
2285
+
dependencies:
2286
+
is-extglob: 2.1.1
2287
+
2288
+
is-number@7.0.0: {}
2289
+
2290
+
is-path-inside@3.0.3: {}
2291
+
2292
+
isexe@2.0.0: {}
2293
+
2294
+
jackspeak@3.4.3:
2295
+
dependencies:
2296
+
'@isaacs/cliui': 8.0.2
2297
+
optionalDependencies:
2298
+
'@pkgjs/parseargs': 0.11.0
2299
+
2300
+
jiti@1.21.7: {}
2301
+
2302
+
js-tokens@4.0.0: {}
2303
+
2304
+
js-yaml@4.1.0:
2305
+
dependencies:
2306
+
argparse: 2.0.1
2307
+
2308
+
jsesc@3.1.0: {}
2309
+
2310
+
json-buffer@3.0.1: {}
2311
+
2312
+
json-schema-traverse@0.4.1: {}
2313
+
2314
+
json-stable-stringify-without-jsonify@1.0.1: {}
2315
+
2316
+
json5@2.2.3: {}
2317
+
2318
+
keyv@4.5.4:
2319
+
dependencies:
2320
+
json-buffer: 3.0.1
2321
+
2322
+
levn@0.4.1:
2323
+
dependencies:
2324
+
prelude-ls: 1.2.1
2325
+
type-check: 0.4.0
2326
+
2327
+
lilconfig@3.1.3: {}
2328
+
2329
+
lines-and-columns@1.2.4: {}
2330
+
2331
+
locate-path@6.0.0:
2332
+
dependencies:
2333
+
p-locate: 5.0.0
2334
+
2335
+
lodash.merge@4.6.2: {}
2336
+
2337
+
loose-envify@1.4.0:
2338
+
dependencies:
2339
+
js-tokens: 4.0.0
2340
+
2341
+
lru-cache@10.4.3: {}
2342
+
2343
+
lru-cache@5.1.1:
2344
+
dependencies:
2345
+
yallist: 3.1.1
2346
+
2347
+
lucide-react@0.294.0(react@18.3.1):
2348
+
dependencies:
2349
+
react: 18.3.1
2350
+
2351
+
math-intrinsics@1.1.0: {}
2352
+
2353
+
merge2@1.4.1: {}
2354
+
2355
+
micromatch@4.0.8:
2356
+
dependencies:
2357
+
braces: 3.0.3
2358
+
picomatch: 2.3.1
2359
+
2360
+
mime-db@1.52.0: {}
2361
+
2362
+
mime-types@2.1.35:
2363
+
dependencies:
2364
+
mime-db: 1.52.0
2365
+
2366
+
minimatch@3.1.2:
2367
+
dependencies:
2368
+
brace-expansion: 1.1.12
2369
+
2370
+
minimatch@9.0.3:
2371
+
dependencies:
2372
+
brace-expansion: 2.0.2
2373
+
2374
+
minimatch@9.0.5:
2375
+
dependencies:
2376
+
brace-expansion: 2.0.2
2377
+
2378
+
minipass@7.1.2: {}
2379
+
2380
+
ms@2.1.3: {}
2381
+
2382
+
mz@2.7.0:
2383
+
dependencies:
2384
+
any-promise: 1.3.0
2385
+
object-assign: 4.1.1
2386
+
thenify-all: 1.6.0
2387
+
2388
+
nanoid@3.3.11: {}
2389
+
2390
+
natural-compare@1.4.0: {}
2391
+
2392
+
node-releases@2.0.19: {}
2393
+
2394
+
normalize-path@3.0.0: {}
2395
+
2396
+
normalize-range@0.1.2: {}
2397
+
2398
+
object-assign@4.1.1: {}
2399
+
2400
+
object-hash@3.0.0: {}
2401
+
2402
+
once@1.4.0:
2403
+
dependencies:
2404
+
wrappy: 1.0.2
2405
+
2406
+
optionator@0.9.4:
2407
+
dependencies:
2408
+
deep-is: 0.1.4
2409
+
fast-levenshtein: 2.0.6
2410
+
levn: 0.4.1
2411
+
prelude-ls: 1.2.1
2412
+
type-check: 0.4.0
2413
+
word-wrap: 1.2.5
2414
+
2415
+
p-limit@3.1.0:
2416
+
dependencies:
2417
+
yocto-queue: 0.1.0
2418
+
2419
+
p-locate@5.0.0:
2420
+
dependencies:
2421
+
p-limit: 3.1.0
2422
+
2423
+
package-json-from-dist@1.0.1: {}
2424
+
2425
+
parent-module@1.0.1:
2426
+
dependencies:
2427
+
callsites: 3.1.0
2428
+
2429
+
path-exists@4.0.0: {}
2430
+
2431
+
path-is-absolute@1.0.1: {}
2432
+
2433
+
path-key@3.1.1: {}
2434
+
2435
+
path-parse@1.0.7: {}
2436
+
2437
+
path-scurry@1.11.1:
2438
+
dependencies:
2439
+
lru-cache: 10.4.3
2440
+
minipass: 7.1.2
2441
+
2442
+
path-type@4.0.0: {}
2443
+
2444
+
picocolors@1.1.1: {}
2445
+
2446
+
picomatch@2.3.1: {}
2447
+
2448
+
pify@2.3.0: {}
2449
+
2450
+
pirates@4.0.7: {}
2451
+
2452
+
postcss-import@15.1.0(postcss@8.5.6):
2453
+
dependencies:
2454
+
postcss: 8.5.6
2455
+
postcss-value-parser: 4.2.0
2456
+
read-cache: 1.0.0
2457
+
resolve: 1.22.10
2458
+
2459
+
postcss-js@4.0.1(postcss@8.5.6):
2460
+
dependencies:
2461
+
camelcase-css: 2.0.1
2462
+
postcss: 8.5.6
2463
+
2464
+
postcss-load-config@4.0.2(postcss@8.5.6):
2465
+
dependencies:
2466
+
lilconfig: 3.1.3
2467
+
yaml: 2.8.0
2468
+
optionalDependencies:
2469
+
postcss: 8.5.6
2470
+
2471
+
postcss-nested@6.2.0(postcss@8.5.6):
2472
+
dependencies:
2473
+
postcss: 8.5.6
2474
+
postcss-selector-parser: 6.1.2
2475
+
2476
+
postcss-selector-parser@6.1.2:
2477
+
dependencies:
2478
+
cssesc: 3.0.0
2479
+
util-deprecate: 1.0.2
2480
+
2481
+
postcss-value-parser@4.2.0: {}
2482
+
2483
+
postcss@8.5.6:
2484
+
dependencies:
2485
+
nanoid: 3.3.11
2486
+
picocolors: 1.1.1
2487
+
source-map-js: 1.2.1
2488
+
2489
+
prelude-ls@1.2.1: {}
2490
+
2491
+
proxy-from-env@1.1.0: {}
2492
+
2493
+
punycode@2.3.1: {}
2494
+
2495
+
queue-microtask@1.2.3: {}
2496
+
2497
+
react-dom@18.3.1(react@18.3.1):
2498
+
dependencies:
2499
+
loose-envify: 1.4.0
2500
+
react: 18.3.1
2501
+
scheduler: 0.23.2
2502
+
2503
+
react-refresh@0.17.0: {}
2504
+
2505
+
react-router-dom@6.30.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1):
2506
+
dependencies:
2507
+
'@remix-run/router': 1.23.0
2508
+
react: 18.3.1
2509
+
react-dom: 18.3.1(react@18.3.1)
2510
+
react-router: 6.30.1(react@18.3.1)
2511
+
2512
+
react-router@6.30.1(react@18.3.1):
2513
+
dependencies:
2514
+
'@remix-run/router': 1.23.0
2515
+
react: 18.3.1
2516
+
2517
+
react@18.3.1:
2518
+
dependencies:
2519
+
loose-envify: 1.4.0
2520
+
2521
+
read-cache@1.0.0:
2522
+
dependencies:
2523
+
pify: 2.3.0
2524
+
2525
+
readdirp@3.6.0:
2526
+
dependencies:
2527
+
picomatch: 2.3.1
2528
+
2529
+
resolve-from@4.0.0: {}
2530
+
2531
+
resolve@1.22.10:
2532
+
dependencies:
2533
+
is-core-module: 2.16.1
2534
+
path-parse: 1.0.7
2535
+
supports-preserve-symlinks-flag: 1.0.0
2536
+
2537
+
reusify@1.1.0: {}
2538
+
2539
+
rimraf@3.0.2:
2540
+
dependencies:
2541
+
glob: 7.2.3
2542
+
2543
+
rollup@3.29.5:
2544
+
optionalDependencies:
2545
+
fsevents: 2.3.3
2546
+
2547
+
run-parallel@1.2.0:
2548
+
dependencies:
2549
+
queue-microtask: 1.2.3
2550
+
2551
+
scheduler@0.23.2:
2552
+
dependencies:
2553
+
loose-envify: 1.4.0
2554
+
2555
+
semver@6.3.1: {}
2556
+
2557
+
semver@7.7.2: {}
2558
+
2559
+
shebang-command@2.0.0:
2560
+
dependencies:
2561
+
shebang-regex: 3.0.0
2562
+
2563
+
shebang-regex@3.0.0: {}
2564
+
2565
+
signal-exit@4.1.0: {}
2566
+
2567
+
slash@3.0.0: {}
2568
+
2569
+
sonner@1.7.4(react-dom@18.3.1(react@18.3.1))(react@18.3.1):
2570
+
dependencies:
2571
+
react: 18.3.1
2572
+
react-dom: 18.3.1(react@18.3.1)
2573
+
2574
+
source-map-js@1.2.1: {}
2575
+
2576
+
string-width@4.2.3:
2577
+
dependencies:
2578
+
emoji-regex: 8.0.0
2579
+
is-fullwidth-code-point: 3.0.0
2580
+
strip-ansi: 6.0.1
2581
+
2582
+
string-width@5.1.2:
2583
+
dependencies:
2584
+
eastasianwidth: 0.2.0
2585
+
emoji-regex: 9.2.2
2586
+
strip-ansi: 7.1.0
2587
+
2588
+
strip-ansi@6.0.1:
2589
+
dependencies:
2590
+
ansi-regex: 5.0.1
2591
+
2592
+
strip-ansi@7.1.0:
2593
+
dependencies:
2594
+
ansi-regex: 6.1.0
2595
+
2596
+
strip-json-comments@3.1.1: {}
2597
+
2598
+
sucrase@3.35.0:
2599
+
dependencies:
2600
+
'@jridgewell/gen-mapping': 0.3.12
2601
+
commander: 4.1.1
2602
+
glob: 10.4.5
2603
+
lines-and-columns: 1.2.4
2604
+
mz: 2.7.0
2605
+
pirates: 4.0.7
2606
+
ts-interface-checker: 0.1.13
2607
+
2608
+
supports-color@7.2.0:
2609
+
dependencies:
2610
+
has-flag: 4.0.0
2611
+
2612
+
supports-preserve-symlinks-flag@1.0.0: {}
2613
+
2614
+
tailwindcss@3.4.17:
2615
+
dependencies:
2616
+
'@alloc/quick-lru': 5.2.0
2617
+
arg: 5.0.2
2618
+
chokidar: 3.6.0
2619
+
didyoumean: 1.2.2
2620
+
dlv: 1.1.3
2621
+
fast-glob: 3.3.3
2622
+
glob-parent: 6.0.2
2623
+
is-glob: 4.0.3
2624
+
jiti: 1.21.7
2625
+
lilconfig: 3.1.3
2626
+
micromatch: 4.0.8
2627
+
normalize-path: 3.0.0
2628
+
object-hash: 3.0.0
2629
+
picocolors: 1.1.1
2630
+
postcss: 8.5.6
2631
+
postcss-import: 15.1.0(postcss@8.5.6)
2632
+
postcss-js: 4.0.1(postcss@8.5.6)
2633
+
postcss-load-config: 4.0.2(postcss@8.5.6)
2634
+
postcss-nested: 6.2.0(postcss@8.5.6)
2635
+
postcss-selector-parser: 6.1.2
2636
+
resolve: 1.22.10
2637
+
sucrase: 3.35.0
2638
+
transitivePeerDependencies:
2639
+
- ts-node
2640
+
2641
+
text-table@0.2.0: {}
2642
+
2643
+
thenify-all@1.6.0:
2644
+
dependencies:
2645
+
thenify: 3.3.1
2646
+
2647
+
thenify@3.3.1:
2648
+
dependencies:
2649
+
any-promise: 1.3.0
2650
+
2651
+
to-regex-range@5.0.1:
2652
+
dependencies:
2653
+
is-number: 7.0.0
2654
+
2655
+
ts-api-utils@1.4.3(typescript@5.8.3):
2656
+
dependencies:
2657
+
typescript: 5.8.3
2658
+
2659
+
ts-interface-checker@0.1.13: {}
2660
+
2661
+
type-check@0.4.0:
2662
+
dependencies:
2663
+
prelude-ls: 1.2.1
2664
+
2665
+
type-fest@0.20.2: {}
2666
+
2667
+
typescript@5.8.3: {}
2668
+
2669
+
update-browserslist-db@1.1.3(browserslist@4.25.1):
2670
+
dependencies:
2671
+
browserslist: 4.25.1
2672
+
escalade: 3.2.0
2673
+
picocolors: 1.1.1
2674
+
2675
+
uri-js@4.4.1:
2676
+
dependencies:
2677
+
punycode: 2.3.1
2678
+
2679
+
use-sync-external-store@1.5.0(react@18.3.1):
2680
+
dependencies:
2681
+
react: 18.3.1
2682
+
2683
+
util-deprecate@1.0.2: {}
2684
+
2685
+
vite@4.5.14:
2686
+
dependencies:
2687
+
esbuild: 0.18.20
2688
+
postcss: 8.5.6
2689
+
rollup: 3.29.5
2690
+
optionalDependencies:
2691
+
fsevents: 2.3.3
2692
+
2693
+
which@2.0.2:
2694
+
dependencies:
2695
+
isexe: 2.0.0
2696
+
2697
+
word-wrap@1.2.5: {}
2698
+
2699
+
wrap-ansi@7.0.0:
2700
+
dependencies:
2701
+
ansi-styles: 4.3.0
2702
+
string-width: 4.2.3
2703
+
strip-ansi: 6.0.1
2704
+
2705
+
wrap-ansi@8.1.0:
2706
+
dependencies:
2707
+
ansi-styles: 6.2.1
2708
+
string-width: 5.1.2
2709
+
strip-ansi: 7.1.0
2710
+
2711
+
wrappy@1.0.2: {}
2712
+
2713
+
yallist@3.1.1: {}
2714
+
2715
+
yaml@2.8.0: {}
2716
+
2717
+
yocto-queue@0.1.0: {}
2718
+
2719
+
zustand@4.5.7(@types/react@18.3.23)(react@18.3.1):
2720
+
dependencies:
2721
+
use-sync-external-store: 1.5.0(react@18.3.1)
2722
+
optionalDependencies:
2723
+
'@types/react': 18.3.23
2724
+
react: 18.3.1
+6
web/postcss.config.js
+6
web/postcss.config.js
+51
web/src/App.tsx
+51
web/src/App.tsx
···
1
+
import { Routes, Route, Navigate } from 'react-router-dom'
2
+
import { useAuthStore } from './stores/authStore'
3
+
import LandingPage from './pages/LandingPage'
4
+
import LoginPage from './pages/LoginPage'
5
+
import StatusPage from './pages/StatusPage'
6
+
import PrivacyPage from './pages/PrivacyPage'
7
+
import TermsPage from './pages/TermsPage'
8
+
import DashboardPage from './pages/DashboardPage'
9
+
import TodosPage from './pages/TodosPage'
10
+
import ApiKeysPage from './pages/ApiKeysPage'
11
+
import RemindersPage from './pages/RemindersPage'
12
+
import Layout from './components/Layout'
13
+
import { useEffect } from 'react'
14
+
15
+
function App() {
16
+
const { isAuthenticated, checkAuth } = useAuthStore()
17
+
18
+
useEffect(() => {
19
+
checkAuth()
20
+
}, [])
21
+
22
+
return (
23
+
<Routes>
24
+
{/* Public routes */}
25
+
<Route path="/" element={<LandingPage />} />
26
+
<Route path="/login" element={isAuthenticated ? <Navigate to="/dashboard" replace /> : <LoginPage />} />
27
+
<Route path="/status" element={<StatusPage />} />
28
+
<Route path="/legal/privacy" element={<PrivacyPage />} />
29
+
<Route path="/legal/terms" element={<TermsPage />} />
30
+
31
+
{/* Protected routes */}
32
+
{isAuthenticated ? (
33
+
<Route path="/*" element={
34
+
<Layout>
35
+
<Routes>
36
+
<Route path="/dashboard" element={<DashboardPage />} />
37
+
<Route path="/todos" element={<TodosPage />} />
38
+
<Route path="/reminders" element={<RemindersPage />} />
39
+
<Route path="/api-keys" element={<ApiKeysPage />} />
40
+
<Route path="*" element={<Navigate to="/dashboard" replace />} />
41
+
</Routes>
42
+
</Layout>
43
+
} />
44
+
) : (
45
+
<Route path="*" element={<Navigate to="/" replace />} />
46
+
)}
47
+
</Routes>
48
+
)
49
+
}
50
+
51
+
export default App
+167
web/src/components/Layout.tsx
+167
web/src/components/Layout.tsx
···
1
+
import { ReactNode, useState } from 'react'
2
+
import { Link, useLocation } from 'react-router-dom'
3
+
import {
4
+
LayoutDashboard,
5
+
CheckSquare,
6
+
Key,
7
+
Bell,
8
+
Menu,
9
+
X,
10
+
LogOut,
11
+
User
12
+
} from 'lucide-react'
13
+
import { useAuthStore } from '../stores/authStore'
14
+
import { toast } from 'sonner'
15
+
16
+
interface LayoutProps {
17
+
children: ReactNode
18
+
}
19
+
20
+
const Layout = ({ children }: LayoutProps) => {
21
+
const [sidebarOpen, setSidebarOpen] = useState(false)
22
+
const location = useLocation()
23
+
const { user, logout } = useAuthStore()
24
+
25
+
const navigation = [
26
+
{ name: 'Dashboard', href: '/dashboard', icon: LayoutDashboard },
27
+
{ name: 'Todos', href: '/todos', icon: CheckSquare },
28
+
{ name: 'Reminders', href: '/reminders', icon: Bell },
29
+
{ name: 'API Keys', href: '/api-keys', icon: Key },
30
+
]
31
+
32
+
const handleLogout = () => {
33
+
logout()
34
+
toast.success('Logged out successfully')
35
+
}
36
+
37
+
return (
38
+
<div className="min-h-screen bg-[#0A0A0A]">
39
+
{/* Mobile sidebar */}
40
+
<div className={`fixed inset-0 z-50 lg:hidden ${
41
+
sidebarOpen ? 'block' : 'hidden'
42
+
}`}>
43
+
<div className="fixed inset-0 bg-gray-600 bg-opacity-75" onClick={() => setSidebarOpen(false)} />
44
+
<div className="fixed inset-y-0 left-0 flex w-64 flex-col bg-gray-900/50 border-r border-gray-700">
45
+
<div className="flex h-16 items-center justify-between px-4">
46
+
<h1 className="text-xl font-bold text-white">Aethel</h1>
47
+
<button
48
+
onClick={() => setSidebarOpen(false)}
49
+
className="text-gray-400 hover:text-gray-600"
50
+
>
51
+
<X className="h-6 w-6" />
52
+
</button>
53
+
</div>
54
+
<nav className="flex-1 space-y-1 px-2 py-4">
55
+
{navigation.map((item) => {
56
+
const Icon = item.icon
57
+
const isActive = location.pathname === item.href
58
+
return (
59
+
<Link
60
+
key={item.name}
61
+
to={item.href}
62
+
onClick={() => setSidebarOpen(false)}
63
+
className={`group flex items-center px-2 py-2 text-sm font-medium rounded-lg ${
64
+
isActive
65
+
? 'bg-gray-800 text-white'
66
+
: 'text-gray-300 hover:bg-gray-700 hover:text-white'
67
+
}`}
68
+
>
69
+
<Icon className="mr-3 h-5 w-5" />
70
+
{item.name}
71
+
</Link>
72
+
)
73
+
})}
74
+
</nav>
75
+
</div>
76
+
</div>
77
+
78
+
{/* Desktop sidebar */}
79
+
<div className="hidden lg:fixed lg:inset-y-0 lg:flex lg:w-64 lg:flex-col">
80
+
<div className="flex flex-col flex-grow bg-gray-900/50 border-r border-gray-700">
81
+
<div className="flex h-16 items-center px-4">
82
+
<h1 className="text-xl font-bold text-white">Aethel Dashboard</h1>
83
+
</div>
84
+
<nav className="flex-1 space-y-1 px-2 py-4">
85
+
{navigation.map((item) => {
86
+
const Icon = item.icon
87
+
const isActive = location.pathname === item.href
88
+
return (
89
+
<Link
90
+
key={item.name}
91
+
to={item.href}
92
+
className={`group flex items-center px-2 py-2 text-sm font-medium rounded-lg ${
93
+
isActive
94
+
? 'bg-gray-800 text-white'
95
+
: 'text-gray-300 hover:bg-gray-700 hover:text-white'
96
+
}`}
97
+
>
98
+
<Icon className="mr-3 h-5 w-5" />
99
+
{item.name}
100
+
</Link>
101
+
)
102
+
})}
103
+
</nav>
104
+
105
+
{/* User info and logout */}
106
+
<div className="border-t border-gray-700 p-4">
107
+
<div className="flex items-center mb-3">
108
+
<div className="flex-shrink-0">
109
+
{user?.avatar ? (
110
+
<img
111
+
className="h-8 w-8 rounded-full"
112
+
src={`https://cdn.discordapp.com/avatars/${user.id}/${user.avatar}.png`}
113
+
alt={user.username}
114
+
/>
115
+
) : (
116
+
<div className="h-8 w-8 rounded-full bg-gray-800 flex items-center justify-center">
117
+
<User className="h-4 w-4 text-white" />
118
+
</div>
119
+
)}
120
+
</div>
121
+
<div className="ml-3">
122
+
<p className="text-sm font-medium text-gray-300">
123
+
{user?.discriminator && user.discriminator !== '0'
124
+
? `${user.username}#${user.discriminator}`
125
+
: user?.username}
126
+
</p>
127
+
</div>
128
+
</div>
129
+
<button
130
+
onClick={handleLogout}
131
+
className="flex w-full items-center px-2 py-2 text-sm font-medium text-gray-300 rounded-lg hover:bg-gray-700 hover:text-white"
132
+
>
133
+
<LogOut className="mr-3 h-5 w-5" />
134
+
Logout
135
+
</button>
136
+
</div>
137
+
</div>
138
+
</div>
139
+
140
+
{/* Main content */}
141
+
<div className="lg:pl-64">
142
+
{/* Mobile header */}
143
+
<div className="sticky top-0 z-40 flex h-16 shrink-0 items-center gap-x-4 border-b border-gray-700 bg-gray-900/50 px-4 shadow-sm lg:hidden">
144
+
<button
145
+
type="button"
146
+
className="-m-2.5 p-2.5 text-gray-300 lg:hidden"
147
+
onClick={() => setSidebarOpen(true)}
148
+
>
149
+
<Menu className="h-6 w-6" />
150
+
</button>
151
+
<div className="flex-1 text-sm font-semibold leading-6 text-white">
152
+
Aethel Dashboard
153
+
</div>
154
+
</div>
155
+
156
+
{/* Page content */}
157
+
<main className="pt-20 pb-12">
158
+
<div className="mx-auto max-w-6xl px-12 sm:px-16 lg:px-20">
159
+
{children}
160
+
</div>
161
+
</main>
162
+
</div>
163
+
</div>
164
+
)
165
+
}
166
+
167
+
export default Layout
+35
web/src/index.css
+35
web/src/index.css
···
1
+
@tailwind base;
2
+
@tailwind components;
3
+
@tailwind utilities;
4
+
5
+
body {
6
+
font-family: Inter, system-ui, sans-serif;
7
+
}
8
+
9
+
.btn {
10
+
@apply inline-flex items-center justify-center rounded-lg px-4 py-2 text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50;
11
+
}
12
+
13
+
.btn-primary {
14
+
@apply bg-blue-600 text-white hover:bg-blue-700;
15
+
}
16
+
17
+
.btn-secondary {
18
+
@apply bg-gray-700 text-white hover:bg-gray-600 border border-gray-600;
19
+
}
20
+
21
+
.btn-danger {
22
+
@apply bg-red-600 text-white hover:bg-red-700;
23
+
}
24
+
25
+
.btn-success {
26
+
@apply bg-green-600 text-white hover:bg-green-700;
27
+
}
28
+
29
+
.card {
30
+
@apply bg-gray-900/50 rounded-lg border border-gray-700 shadow-sm;
31
+
}
32
+
33
+
.input {
34
+
@apply flex h-10 w-full rounded-lg border border-gray-600 bg-gray-800 text-white px-3 py-2 text-sm placeholder:text-gray-400 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-gray-500 focus-visible:ring-offset-0 disabled:cursor-not-allowed disabled:opacity-50;
35
+
}
+73
web/src/lib/api.ts
+73
web/src/lib/api.ts
···
1
+
import axios from 'axios';
2
+
import { toast } from 'sonner';
3
+
4
+
const api = axios.create({
5
+
baseURL: '/api',
6
+
timeout: 10000,
7
+
});
8
+
9
+
api.interceptors.request.use(
10
+
(config) => {
11
+
const token = localStorage.getItem('token');
12
+
if (token) {
13
+
config.headers.Authorization = `Bearer ${token}`;
14
+
}
15
+
return config;
16
+
},
17
+
(error) => {
18
+
return Promise.reject(error);
19
+
}
20
+
);
21
+
22
+
api.interceptors.response.use(
23
+
(response) => response,
24
+
(error) => {
25
+
if (error.response?.status === 401) {
26
+
localStorage.removeItem('token');
27
+
window.location.href = '/';
28
+
toast.error('Session expired. Please login again.');
29
+
} else if (error.response?.status >= 500) {
30
+
toast.error('Server error. Please try again later.');
31
+
} else if (error.response?.data?.message) {
32
+
toast.error(error.response.data.message);
33
+
} else {
34
+
toast.error('An unexpected error occurred.');
35
+
}
36
+
return Promise.reject(error);
37
+
}
38
+
);
39
+
40
+
export default api;
41
+
42
+
export const authAPI = {
43
+
getDiscordAuthUrl: () => api.get('/auth/discord'),
44
+
getMe: () => api.get('/auth/me'),
45
+
logout: () => api.post('/auth/logout'),
46
+
};
47
+
48
+
export const todosAPI = {
49
+
getTodos: () => api.get('/todos'),
50
+
createTodo: (data: { item: string }) => api.post('/todos', data),
51
+
updateTodo: (id: number, data: { item?: string; done?: boolean }) =>
52
+
api.put(`/todos/${id}`, data),
53
+
deleteTodo: (id: number) => api.delete(`/todos/${id}`),
54
+
clearTodos: () => api.delete('/todos'),
55
+
};
56
+
57
+
export const apiKeysAPI = {
58
+
getApiKeys: () => api.get('/user/api-keys'),
59
+
updateApiKey: (data: { apiKey?: string; model?: string; apiUrl?: string }) =>
60
+
api.post('/user/api-keys', data),
61
+
deleteApiKey: () => api.delete('/user/api-keys'),
62
+
testApiKey: (data: { apiKey: string; model?: string; apiUrl?: string }) =>
63
+
api.post('/user/api-keys/test', data),
64
+
};
65
+
66
+
export const remindersAPI = {
67
+
getReminders: () => api.get('/reminders'),
68
+
createReminder: (data: { message: string; expires_at: string }) => api.post('/reminders', data),
69
+
getReminder: (id: string) => api.get(`/reminders/${id}`),
70
+
completeReminder: (id: string) => api.patch(`/reminders/${id}/complete`),
71
+
getActiveReminders: () => api.get('/reminders/active/all'),
72
+
clearCompletedReminders: () => api.delete('/reminders/completed'),
73
+
};
+27
web/src/main.tsx
+27
web/src/main.tsx
···
1
+
import React from 'react'
2
+
import ReactDOM from 'react-dom/client'
3
+
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
4
+
import { BrowserRouter } from 'react-router-dom'
5
+
import { Toaster } from 'sonner'
6
+
import App from './App.tsx'
7
+
import './index.css'
8
+
9
+
const queryClient = new QueryClient({
10
+
defaultOptions: {
11
+
queries: {
12
+
retry: 1,
13
+
refetchOnWindowFocus: false,
14
+
},
15
+
},
16
+
})
17
+
18
+
ReactDOM.createRoot(document.getElementById('root')!).render(
19
+
<React.StrictMode>
20
+
<QueryClientProvider client={queryClient}>
21
+
<BrowserRouter>
22
+
<App />
23
+
<Toaster position="top-right" />
24
+
</BrowserRouter>
25
+
</QueryClientProvider>
26
+
</React.StrictMode>
27
+
)
+340
web/src/pages/ApiKeysPage.tsx
+340
web/src/pages/ApiKeysPage.tsx
···
1
+
import { useState } from 'react'
2
+
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
3
+
import { Key, Eye, EyeOff, TestTube, Save, Trash2, AlertCircle, CheckCircle } from 'lucide-react'
4
+
import { toast } from 'sonner'
5
+
import { apiKeysAPI } from '../lib/api'
6
+
7
+
const ApiKeysPage = () => {
8
+
const [showApiKey, setShowApiKey] = useState(false)
9
+
const [formData, setFormData] = useState({
10
+
apiKey: '',
11
+
model: '',
12
+
apiUrl: ''
13
+
})
14
+
const [isEditing, setIsEditing] = useState(false)
15
+
const [testResult, setTestResult] = useState<{ success: boolean; message: string } | null>(null)
16
+
const queryClient = useQueryClient()
17
+
18
+
const { data: apiKeyInfo, isLoading } = useQuery({
19
+
queryKey: ['api-keys'],
20
+
queryFn: () => apiKeysAPI.getApiKeys().then(res => res.data),
21
+
})
22
+
23
+
const updateApiKeyMutation = useMutation({
24
+
mutationFn: (data: { apiKey: string; model?: string; apiUrl?: string }) =>
25
+
apiKeysAPI.updateApiKey(data),
26
+
onSuccess: () => {
27
+
queryClient.invalidateQueries({ queryKey: ['api-keys'] })
28
+
setIsEditing(false)
29
+
setFormData({ apiKey: '', model: '', apiUrl: '' })
30
+
toast.success('API key updated successfully!')
31
+
},
32
+
onError: () => {
33
+
toast.error('Failed to update API key')
34
+
},
35
+
})
36
+
37
+
const deleteApiKeyMutation = useMutation({
38
+
mutationFn: () => apiKeysAPI.deleteApiKey(),
39
+
onSuccess: () => {
40
+
queryClient.invalidateQueries({ queryKey: ['api-keys'] })
41
+
setFormData({ apiKey: '', model: '', apiUrl: '' })
42
+
setIsEditing(false)
43
+
toast.success('API key deleted successfully!')
44
+
},
45
+
onError: () => {
46
+
toast.error('Failed to delete API key')
47
+
},
48
+
})
49
+
50
+
const testApiKeyMutation = useMutation({
51
+
mutationFn: (data: { apiKey: string; model?: string; apiUrl?: string }) =>
52
+
apiKeysAPI.testApiKey(data),
53
+
onSuccess: () => {
54
+
setTestResult({ success: true, message: 'API key is valid and working!' })
55
+
toast.success('API key test successful!')
56
+
},
57
+
onError: (error: any) => {
58
+
const message = error.response?.data?.error || 'API key test failed'
59
+
setTestResult({ success: false, message })
60
+
toast.error(message)
61
+
},
62
+
})
63
+
64
+
const handleSubmit = (e: React.FormEvent) => {
65
+
e.preventDefault()
66
+
if (!formData.apiKey.trim()) {
67
+
toast.error('API key is required')
68
+
return
69
+
}
70
+
updateApiKeyMutation.mutate({
71
+
apiKey: formData.apiKey,
72
+
model: formData.model || undefined,
73
+
apiUrl: formData.apiUrl || undefined,
74
+
})
75
+
}
76
+
77
+
const handleTest = () => {
78
+
if (!formData.apiKey.trim()) {
79
+
toast.error('API key is required for testing')
80
+
return
81
+
}
82
+
setTestResult(null)
83
+
testApiKeyMutation.mutate({
84
+
apiKey: formData.apiKey,
85
+
model: formData.model || undefined,
86
+
apiUrl: formData.apiUrl || undefined,
87
+
})
88
+
}
89
+
90
+
const handleEdit = () => {
91
+
setIsEditing(true)
92
+
setFormData({
93
+
apiKey: '',
94
+
model: apiKeyInfo?.model || '',
95
+
apiUrl: apiKeyInfo?.apiUrl || ''
96
+
})
97
+
setTestResult(null)
98
+
}
99
+
100
+
const handleCancel = () => {
101
+
setIsEditing(false)
102
+
setFormData({ apiKey: '', model: '', apiUrl: '' })
103
+
setTestResult(null)
104
+
}
105
+
106
+
if (isLoading) {
107
+
return (
108
+
<div className="flex items-center justify-center h-64">
109
+
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-discord-blurple"></div>
110
+
</div>
111
+
)
112
+
}
113
+
114
+
return (
115
+
<div className="space-y-6">
116
+
{/* Header */}
117
+
<div>
118
+
<h1 className="text-2xl font-bold text-white">AI API Keys</h1>
119
+
<p className="text-gray-400">
120
+
Configure your custom AI API keys and endpoints for personalized AI interactions.
121
+
</p>
122
+
</div>
123
+
124
+
{/* Current Status */}
125
+
<div className="bg-gray-900/50 p-6 rounded-lg border border-gray-700">
126
+
<div className="flex items-center justify-between mb-4">
127
+
<h2 className="text-lg font-medium text-white">Current Configuration</h2>
128
+
{apiKeyInfo?.hasApiKey && !isEditing && (
129
+
<div className="flex space-x-2">
130
+
<button
131
+
onClick={handleEdit}
132
+
className="btn btn-secondary"
133
+
>
134
+
<Key className="h-4 w-4 mr-2" />
135
+
Edit
136
+
</button>
137
+
<button
138
+
onClick={() => deleteApiKeyMutation.mutate()}
139
+
className="btn btn-danger"
140
+
disabled={deleteApiKeyMutation.isPending}
141
+
>
142
+
<Trash2 className="h-4 w-4 mr-2" />
143
+
Remove
144
+
</button>
145
+
</div>
146
+
)}
147
+
</div>
148
+
149
+
{apiKeyInfo?.hasApiKey && !isEditing ? (
150
+
<div className="space-y-3">
151
+
<div className="flex items-center justify-between">
152
+
<span className="text-sm font-medium text-gray-400">Status</span>
153
+
<span className="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-green-900/30 text-green-400 border border-green-700">
154
+
<CheckCircle className="h-3 w-3 mr-1" />
155
+
Configured
156
+
</span>
157
+
</div>
158
+
{apiKeyInfo.model && (
159
+
<div className="flex items-center justify-between">
160
+
<span className="text-sm font-medium text-gray-400">Model</span>
161
+
<span className="text-sm text-white">{apiKeyInfo.model}</span>
162
+
</div>
163
+
)}
164
+
{apiKeyInfo.apiUrl && (
165
+
<div className="flex items-center justify-between">
166
+
<span className="text-sm font-medium text-gray-400">API Endpoint</span>
167
+
<span className="text-sm text-white truncate max-w-64">
168
+
{apiKeyInfo.apiUrl}
169
+
</span>
170
+
</div>
171
+
)}
172
+
</div>
173
+
) : (
174
+
<div className="text-center py-8">
175
+
<Key className="h-12 w-12 text-gray-400 mx-auto mb-4" />
176
+
<h3 className="text-lg font-medium text-white mb-2">
177
+
No API Key Configured
178
+
</h3>
179
+
<p className="text-gray-400 mb-4">
180
+
Set up your custom AI API key to use personalized models and endpoints.
181
+
</p>
182
+
<button
183
+
onClick={() => setIsEditing(true)}
184
+
className="btn btn-primary"
185
+
>
186
+
<Key className="h-4 w-4 mr-2" />
187
+
Configure API Key
188
+
</button>
189
+
</div>
190
+
)}
191
+
</div>
192
+
193
+
{/* Configuration Form */}
194
+
{isEditing && (
195
+
<div className="bg-gray-900/50 p-6 rounded-lg border border-gray-700">
196
+
<h2 className="text-lg font-medium text-white mb-4">
197
+
{apiKeyInfo?.hasApiKey ? 'Update' : 'Configure'} API Key
198
+
</h2>
199
+
200
+
<form onSubmit={handleSubmit} className="space-y-4">
201
+
{/* API Key */}
202
+
<div>
203
+
<label className="block text-sm font-medium text-gray-400 mb-2">
204
+
API Key *
205
+
</label>
206
+
<div className="relative">
207
+
<input
208
+
type={showApiKey ? 'text' : 'password'}
209
+
value={formData.apiKey}
210
+
onChange={(e) => setFormData({ ...formData, apiKey: e.target.value })}
211
+
placeholder="Enter your API key"
212
+
className="input pr-10"
213
+
required
214
+
/>
215
+
<button
216
+
type="button"
217
+
onClick={() => setShowApiKey(!showApiKey)}
218
+
className="absolute inset-y-0 right-0 pr-3 flex items-center"
219
+
>
220
+
{showApiKey ? (
221
+
<EyeOff className="h-4 w-4 text-gray-400" />
222
+
) : (
223
+
<Eye className="h-4 w-4 text-gray-400" />
224
+
)}
225
+
</button>
226
+
</div>
227
+
</div>
228
+
229
+
{/* Model */}
230
+
<div>
231
+
<label className="block text-sm font-medium text-gray-400 mb-2">
232
+
Model (Optional)
233
+
</label>
234
+
<input
235
+
type="text"
236
+
value={formData.model}
237
+
onChange={(e) => setFormData({ ...formData, model: e.target.value })}
238
+
placeholder="e.g., gpt-4, claude-3-opus"
239
+
className="input"
240
+
/>
241
+
<p className="text-xs text-gray-400 mt-1">
242
+
Leave empty to use the default model
243
+
</p>
244
+
</div>
245
+
246
+
{/* API URL */}
247
+
<div>
248
+
<label className="block text-sm font-medium text-gray-400 mb-2">
249
+
API Endpoint URL (Optional)
250
+
</label>
251
+
<input
252
+
type="url"
253
+
value={formData.apiUrl}
254
+
onChange={(e) => setFormData({ ...formData, apiUrl: e.target.value })}
255
+
placeholder="https://api.openai.com/v1/chat/completions"
256
+
className="input"
257
+
/>
258
+
<p className="text-xs text-gray-400 mt-1">
259
+
Enter the full API endpoint URL including the path (e.g., /chat/completions)
260
+
</p>
261
+
</div>
262
+
263
+
{/* Test Result */}
264
+
{testResult && (
265
+
<div className={`p-3 rounded-lg flex items-center space-x-2 ${
266
+
testResult.success ? 'bg-green-50 text-green-800' : 'bg-red-50 text-red-800'
267
+
}`}>
268
+
{testResult.success ? (
269
+
<CheckCircle className="h-4 w-4" />
270
+
) : (
271
+
<AlertCircle className="h-4 w-4" />
272
+
)}
273
+
<span className="text-sm">{testResult.message}</span>
274
+
</div>
275
+
)}
276
+
277
+
{/* Actions */}
278
+
<div className="flex justify-between pt-4">
279
+
<button
280
+
type="button"
281
+
onClick={handleTest}
282
+
disabled={!formData.apiKey.trim() || testApiKeyMutation.isPending}
283
+
className="btn btn-secondary"
284
+
>
285
+
<TestTube className="h-4 w-4 mr-2" />
286
+
{testApiKeyMutation.isPending ? 'Testing...' : 'Test API Key'}
287
+
</button>
288
+
289
+
<div className="flex space-x-3">
290
+
<button
291
+
type="button"
292
+
onClick={handleCancel}
293
+
className="btn btn-secondary"
294
+
disabled={updateApiKeyMutation.isPending}
295
+
>
296
+
Cancel
297
+
</button>
298
+
<button
299
+
type="submit"
300
+
disabled={!formData.apiKey.trim() || updateApiKeyMutation.isPending}
301
+
className="btn btn-primary"
302
+
>
303
+
<Save className="h-4 w-4 mr-2" />
304
+
{updateApiKeyMutation.isPending ? 'Saving...' : 'Save'}
305
+
</button>
306
+
</div>
307
+
</div>
308
+
</form>
309
+
</div>
310
+
)}
311
+
312
+
{/* Information */}
313
+
<div className="bg-gray-800/50 p-6 rounded-lg border border-gray-700">
314
+
<h2 className="text-lg font-medium text-white mb-4">Information</h2>
315
+
<div className="space-y-3 text-sm text-gray-400">
316
+
<div className="flex items-start space-x-2">
317
+
<AlertCircle className="h-4 w-4 text-blue-500 mt-0.5 flex-shrink-0" />
318
+
<p>
319
+
Your API key is encrypted and stored securely. It will only be used for AI interactions within the Discord bot.
320
+
</p>
321
+
</div>
322
+
<div className="flex items-start space-x-2">
323
+
<Key className="h-4 w-4 text-green-500 mt-0.5 flex-shrink-0" />
324
+
<p>
325
+
Supported providers include OpenAI, Anthropic, and any OpenAI-compatible API endpoints.
326
+
</p>
327
+
</div>
328
+
<div className="flex items-start space-x-2">
329
+
<TestTube className="h-4 w-4 text-purple-500 mt-0.5 flex-shrink-0" />
330
+
<p>
331
+
Use the test function to verify your API key works before saving.
332
+
</p>
333
+
</div>
334
+
</div>
335
+
</div>
336
+
</div>
337
+
)
338
+
}
339
+
340
+
export default ApiKeysPage
+338
web/src/pages/DashboardPage.tsx
+338
web/src/pages/DashboardPage.tsx
···
1
+
import { useQuery } from '@tanstack/react-query'
2
+
import { CheckSquare, Key, Clock, TrendingUp, Bell, AlertCircle } from 'lucide-react'
3
+
import { todosAPI, apiKeysAPI, remindersAPI } from '../lib/api'
4
+
import { useAuthStore } from '../stores/authStore'
5
+
import { useEffect, useState } from 'react'
6
+
import { toast } from 'sonner'
7
+
8
+
const DashboardPage = () => {
9
+
const { user } = useAuthStore()
10
+
const [notifications, setNotifications] = useState<any[]>([])
11
+
12
+
const { data: todos } = useQuery({
13
+
queryKey: ['todos'],
14
+
queryFn: () => todosAPI.getTodos().then(res => res.data),
15
+
})
16
+
17
+
const { data: apiKeyInfo } = useQuery({
18
+
queryKey: ['api-keys'],
19
+
queryFn: () => apiKeysAPI.getApiKeys().then(res => res.data),
20
+
})
21
+
22
+
const { data: reminders } = useQuery({
23
+
queryKey: ['reminders'],
24
+
queryFn: () => remindersAPI.getReminders().then(res => res.data.reminders),
25
+
})
26
+
27
+
const { data: activeReminders } = useQuery({
28
+
queryKey: ['active-reminders'],
29
+
queryFn: () => remindersAPI.getActiveReminders().then(res => res.data.reminders),
30
+
refetchInterval: 30000, // Check every 30 seconds
31
+
})
32
+
33
+
const completedTodos = todos?.filter((todo: any) => todo.done).length || 0
34
+
const pendingTodos = todos?.filter((todo: any) => !todo.done).length || 0
35
+
const totalTodos = todos?.length || 0
36
+
const hasApiKey = !!apiKeyInfo?.hasApiKey
37
+
38
+
const activeRemindersCount = activeReminders?.length || 0
39
+
const overdueReminders = activeReminders?.filter((reminder: any) =>
40
+
new Date(reminder.expires_at) < new Date()
41
+
) || []
42
+
43
+
const stats = [
44
+
{
45
+
name: 'Total Todos',
46
+
value: totalTodos,
47
+
icon: CheckSquare,
48
+
color: 'text-blue-600',
49
+
bgColor: 'bg-blue-100',
50
+
},
51
+
{
52
+
name: 'Completed',
53
+
value: completedTodos,
54
+
icon: TrendingUp,
55
+
color: 'text-green-600',
56
+
bgColor: 'bg-green-100',
57
+
},
58
+
{
59
+
name: 'Pending',
60
+
value: pendingTodos,
61
+
icon: Clock,
62
+
color: 'text-yellow-600',
63
+
bgColor: 'bg-yellow-100',
64
+
},
65
+
{
66
+
name: 'Active Reminders',
67
+
value: activeRemindersCount,
68
+
icon: Bell,
69
+
color: overdueReminders.length > 0 ? 'text-red-600' : 'text-blue-600',
70
+
bgColor: overdueReminders.length > 0 ? 'bg-red-100' : 'bg-blue-100',
71
+
},
72
+
{
73
+
name: 'API Key',
74
+
value: hasApiKey ? 'Configured' : 'Not Set',
75
+
icon: Key,
76
+
color: hasApiKey ? 'text-green-600' : 'text-red-600',
77
+
bgColor: hasApiKey ? 'bg-green-100' : 'bg-red-100',
78
+
},
79
+
]
80
+
81
+
const recentTodos = todos?.slice(0, 5) || []
82
+
const recentReminders = reminders?.slice(0, 5) || []
83
+
84
+
useEffect(() => {
85
+
if (overdueReminders.length > 0) {
86
+
overdueReminders.forEach((reminder: any) => {
87
+
const notificationId = `reminder-${reminder.reminder_id}`
88
+
if (!notifications.includes(notificationId)) {
89
+
toast.error(`Reminder: ${reminder.message}`, {
90
+
duration: 10000,
91
+
action: {
92
+
label: 'Mark Complete',
93
+
onClick: () => handleCompleteReminder(reminder.reminder_id)
94
+
}
95
+
})
96
+
setNotifications(prev => [...prev, notificationId])
97
+
}
98
+
})
99
+
}
100
+
}, [overdueReminders])
101
+
102
+
const handleCompleteReminder = async (id: string) => {
103
+
try {
104
+
await remindersAPI.completeReminder(id)
105
+
toast.success('Reminder completed!')
106
+
setNotifications(prev => prev.filter(notif => notif !== `reminder-${id}`))
107
+
} catch (error) {
108
+
toast.error('Failed to complete reminder')
109
+
}
110
+
}
111
+
112
+
const formatDate = (dateString: string) => {
113
+
const date = new Date(dateString)
114
+
return date.toLocaleString()
115
+
}
116
+
117
+
const isExpired = (dateString: string) => {
118
+
return new Date(dateString) < new Date()
119
+
}
120
+
121
+
return (
122
+
<div className="space-y-6">
123
+
{/* Header */}
124
+
<div>
125
+
<h1 className="text-2xl font-bold text-white">
126
+
Welcome back, {user?.username}!
127
+
</h1>
128
+
<p className="text-gray-400">
129
+
Here's an overview of your todos and settings.
130
+
</p>
131
+
</div>
132
+
133
+
{/* Stats Grid */}
134
+
<div className="grid grid-cols-1 gap-5 sm:grid-cols-2 lg:grid-cols-4">
135
+
{stats.map((stat) => {
136
+
const Icon = stat.icon
137
+
return (
138
+
<div key={stat.name} className="bg-gray-900/50 border border-gray-700 rounded-lg p-5">
139
+
<div className="flex items-center">
140
+
<div className="flex-shrink-0">
141
+
<div className={`p-3 rounded-lg ${stat.bgColor}`}>
142
+
<Icon className={`h-6 w-6 ${stat.color}`} />
143
+
</div>
144
+
</div>
145
+
<div className="ml-5 w-0 flex-1">
146
+
<dl>
147
+
<dt className="text-sm font-medium text-gray-400 truncate">
148
+
{stat.name}
149
+
</dt>
150
+
<dd className="text-lg font-medium text-white">
151
+
{stat.value}
152
+
</dd>
153
+
</dl>
154
+
</div>
155
+
</div>
156
+
</div>
157
+
)
158
+
})}
159
+
</div>
160
+
161
+
{/* Overdue Reminders Alert */}
162
+
{overdueReminders.length > 0 && (
163
+
<div className="bg-red-900/20 border border-red-700 rounded-lg p-4">
164
+
<div className="flex items-center">
165
+
<AlertCircle className="h-5 w-5 text-red-600 mr-2" />
166
+
<h3 className="text-sm font-medium text-red-300">
167
+
You have {overdueReminders.length} overdue reminder{overdueReminders.length > 1 ? 's' : ''}
168
+
</h3>
169
+
</div>
170
+
<div className="mt-2 space-y-1">
171
+
{overdueReminders.slice(0, 3).map((reminder: any) => (
172
+
<div key={reminder.reminder_id} className="flex items-center justify-between">
173
+
<p className="text-sm text-red-200 truncate">{reminder.message}</p>
174
+
<button
175
+
onClick={() => handleCompleteReminder(reminder.reminder_id)}
176
+
className="text-xs bg-red-600 hover:bg-red-700 text-white px-2 py-1 rounded transition-colors"
177
+
>
178
+
Complete
179
+
</button>
180
+
</div>
181
+
))}
182
+
{overdueReminders.length > 3 && (
183
+
<p className="text-xs text-red-400">And {overdueReminders.length - 3} more...</p>
184
+
)}
185
+
</div>
186
+
</div>
187
+
)}
188
+
189
+
{/* Recent Activity */}
190
+
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
191
+
{/* Recent Todos */}
192
+
<div className="bg-gray-900/50 border border-gray-700 rounded-lg p-6">
193
+
<div className="flex items-center justify-between mb-4">
194
+
<h2 className="text-lg font-medium text-white">Recent Todos</h2>
195
+
<a
196
+
href="/todos"
197
+
className="text-sm text-white hover:text-gray-300"
198
+
>
199
+
View all
200
+
</a>
201
+
</div>
202
+
{recentTodos.length > 0 ? (
203
+
<div className="space-y-3">
204
+
{recentTodos.map((todo: any) => (
205
+
<div key={todo.id} className="flex items-center space-x-3">
206
+
<div className={`flex-shrink-0 w-2 h-2 rounded-full ${
207
+
todo.done ? 'bg-green-400' : 'bg-yellow-400'
208
+
}`} />
209
+
<span className={`text-sm ${
210
+
todo.done ? 'text-gray-500 line-through' : 'text-white'
211
+
}`}>
212
+
{todo.item}
213
+
</span>
214
+
</div>
215
+
))}
216
+
</div>
217
+
) : (
218
+
<p className="text-gray-400 text-sm">No todos yet. Create your first one!</p>
219
+
)}
220
+
</div>
221
+
222
+
{/* Recent Reminders */}
223
+
<div className="bg-gray-900/50 border border-gray-700 rounded-lg p-6">
224
+
<div className="flex items-center justify-between mb-4">
225
+
<h2 className="text-lg font-medium text-white">Recent Reminders</h2>
226
+
<a
227
+
href="/reminders"
228
+
className="text-sm text-white hover:text-gray-300"
229
+
>
230
+
View all
231
+
</a>
232
+
</div>
233
+
{recentReminders.length > 0 ? (
234
+
<div className="space-y-3">
235
+
{recentReminders.map((reminder: any) => (
236
+
<div key={reminder.reminder_id} className="flex items-start space-x-3">
237
+
<div className={`flex-shrink-0 w-2 h-2 rounded-full mt-2 ${
238
+
reminder.is_completed
239
+
? 'bg-green-400'
240
+
: isExpired(reminder.expires_at)
241
+
? 'bg-red-400'
242
+
: 'bg-blue-400'
243
+
}`} />
244
+
<div className="flex-1 min-w-0">
245
+
<p className={`text-sm ${
246
+
reminder.is_completed ? 'text-gray-500 line-through' : 'text-white'
247
+
}`}>
248
+
{reminder.message}
249
+
</p>
250
+
<p className="text-xs text-gray-400 mt-1">
251
+
{formatDate(reminder.expires_at)}
252
+
</p>
253
+
</div>
254
+
</div>
255
+
))}
256
+
</div>
257
+
) : (
258
+
<p className="text-gray-400 text-sm">No reminders yet. Create your first one!</p>
259
+
)}
260
+
</div>
261
+
262
+
{/* API Key Status */}
263
+
<div className="bg-gray-900/50 border border-gray-700 rounded-lg p-6">
264
+
<div className="flex items-center justify-between mb-4">
265
+
<h2 className="text-lg font-medium text-white">AI Configuration</h2>
266
+
<a
267
+
href="/api-keys"
268
+
className="text-sm text-white hover:text-gray-300"
269
+
>
270
+
Manage
271
+
</a>
272
+
</div>
273
+
<div className="space-y-3">
274
+
<div className="flex items-center justify-between">
275
+
<span className="text-sm text-gray-400">API Key</span>
276
+
<span className={`text-sm font-medium ${
277
+
hasApiKey ? 'text-green-600' : 'text-red-600'
278
+
}`}>
279
+
{hasApiKey ? 'Configured' : 'Not Set'}
280
+
</span>
281
+
</div>
282
+
{apiKeyInfo?.model && (
283
+
<div className="flex items-center justify-between">
284
+
<span className="text-sm text-gray-400">Model</span>
285
+
<span className="text-sm font-medium text-white">
286
+
{apiKeyInfo.model}
287
+
</span>
288
+
</div>
289
+
)}
290
+
{apiKeyInfo?.apiUrl && (
291
+
<div className="flex items-center justify-between">
292
+
<span className="text-sm text-gray-400">Endpoint</span>
293
+
<span className="text-sm font-medium text-white truncate max-w-32">
294
+
{new URL(apiKeyInfo.apiUrl).hostname}
295
+
</span>
296
+
</div>
297
+
)}
298
+
{!hasApiKey && (
299
+
<p className="text-sm text-gray-400">
300
+
Configure your AI API key to use custom models and endpoints.
301
+
</p>
302
+
)}
303
+
</div>
304
+
</div>
305
+
</div>
306
+
307
+
{/* Quick Actions */}
308
+
<div className="bg-gray-900/50 border border-gray-700 rounded-lg p-6">
309
+
<h2 className="text-lg font-medium text-white mb-4">Quick Actions</h2>
310
+
<div className="flex flex-wrap gap-3">
311
+
<a
312
+
href="/todos"
313
+
className="bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded-lg flex items-center transition-colors"
314
+
>
315
+
<CheckSquare className="h-4 w-4 mr-2" />
316
+
Manage Todos
317
+
</a>
318
+
<a
319
+
href="/reminders"
320
+
className="bg-gray-700 hover:bg-gray-600 text-white px-4 py-2 rounded-lg flex items-center transition-colors"
321
+
>
322
+
<Bell className="h-4 w-4 mr-2" />
323
+
Manage Reminders
324
+
</a>
325
+
<a
326
+
href="/api-keys"
327
+
className="bg-gray-700 hover:bg-gray-600 text-white px-4 py-2 rounded-lg flex items-center transition-colors"
328
+
>
329
+
<Key className="h-4 w-4 mr-2" />
330
+
Configure AI
331
+
</a>
332
+
</div>
333
+
</div>
334
+
</div>
335
+
)
336
+
}
337
+
338
+
export default DashboardPage
+156
web/src/pages/LandingPage.tsx
+156
web/src/pages/LandingPage.tsx
···
1
+
import { Bot, MessageSquare, Cloud, Bell, Shield, Zap } from 'lucide-react';
2
+
import { Link } from 'react-router-dom';
3
+
4
+
const LandingPage = () => {
5
+
return (
6
+
<div className="min-h-screen bg-[#0A0A0A] text-white">
7
+
{/* Header */}
8
+
<header>
9
+
<div className="max-w-6xl mx-auto px-6 py-4">
10
+
<div className="flex justify-between items-center">
11
+
<div className="flex items-center space-x-3">
12
+
<span className="text-xl font-semibold text-white">Aethel</span>
13
+
</div>
14
+
15
+
<nav className="hidden md:flex items-center space-x-8">
16
+
<a href="#features" className="text-gray-400 hover:text-white transition-colors">
17
+
Features
18
+
</a>
19
+
<Link
20
+
to="/status"
21
+
className="text-gray-400 hover:text-white transition-colors"
22
+
>
23
+
Status
24
+
</Link>
25
+
</nav>
26
+
27
+
<Link
28
+
to="/login"
29
+
className="bg-white text-black px-4 py-2 rounded-lg font-medium hover:bg-gray-100 transition-colors"
30
+
>
31
+
Dashboard
32
+
</Link>
33
+
</div>
34
+
</div>
35
+
</header>
36
+
37
+
{/* Hero Section */}
38
+
<section className="py-32 px-6">
39
+
<div className="max-w-4xl mx-auto text-center">
40
+
<h1 className="text-5xl md:text-6xl font-bold mb-6 text-white">
41
+
A useful Discord user bot
42
+
<span className="block text-gray-400 mt-2">for your account</span>
43
+
</h1>
44
+
45
+
<p className="text-xl text-gray-400 mb-12 max-w-2xl mx-auto">
46
+
Enhance your Discord experience with AI chat, weather updates, reminders, and more useful features.
47
+
</p>
48
+
49
+
<a
50
+
href="https://discord.com/api/oauth2/authorize?client_id=YOUR_BOT_CLIENT_ID&permissions=8&scope=bot%20applications.commands"
51
+
target="_blank"
52
+
rel="noopener noreferrer"
53
+
className="inline-flex items-center space-x-3 bg-[#5865F2] text-white px-8 py-4 rounded-lg hover:bg-[#4752C4] transition-colors font-medium"
54
+
>
55
+
<svg className="h-5 w-5" viewBox="0 0 24 24" fill="currentColor">
56
+
<path d="M20.317 4.37a19.791 19.791 0 0 0-4.885-1.515.074.074 0 0 0-.079.037c-.21.375-.444.864-.608 1.25a18.27 18.27 0 0 0-5.487 0 12.64 12.64 0 0 0-.617-1.25.077.077 0 0 0-.079-.037A19.736 19.736 0 0 0 3.677 4.37a.07.07 0 0 0-.032.027C.533 9.046-.32 13.58.099 18.057a.082.082 0 0 0 .031.057 19.9 19.9 0 0 0 5.993 3.03.078.078 0 0 0 .084-.028 14.09 14.09 0 0 0 1.226-1.994.076.076 0 0 0-.041-.106 13.107 13.107 0 0 1-1.872-.892.077.077 0 0 1-.008-.128 10.2 10.2 0 0 0 .372-.292.074.074 0 0 1 .077-.01c3.928 1.793 8.18 1.793 12.062 0a.074.074 0 0 1 .078.01c.12.098.246.198.373.292a.077.077 0 0 1-.006.127 12.299 12.299 0 0 1-1.873.892.077.077 0 0 0-.041.107c.36.698.772 1.362 1.225 1.993a.076.076 0 0 0 .084.028 19.839 19.839 0 0 0 6.002-3.03.077.077 0 0 0 .032-.054c.5-5.177-.838-9.674-3.549-13.66a.061.061 0 0 0-.031-.03zM8.02 15.33c-1.183 0-2.157-1.085-2.157-2.419 0-1.333.956-2.419 2.157-2.419 1.21 0 2.176 1.096 2.157 2.42 0 1.333-.956 2.418-2.157 2.418zm7.975 0c-1.183 0-2.157-1.085-2.157-2.419 0-1.333.955-2.419 2.157-2.419 1.21 0 2.176 1.096 2.157 2.42 0 1.333-.946 2.418-2.157 2.418z"/>
57
+
</svg>
58
+
<span>Add to Discord</span>
59
+
</a>
60
+
</div>
61
+
</section>
62
+
63
+
{/* Features Section */}
64
+
<section id="features" className="py-24 px-6">
65
+
<div className="max-w-6xl mx-auto">
66
+
<div className="text-center mb-16">
67
+
<h2 className="text-4xl font-bold text-white mb-4">Features</h2>
68
+
<p className="text-xl text-gray-400 max-w-2xl mx-auto">
69
+
Everything you need to enhance your Discord experience
70
+
</p>
71
+
</div>
72
+
73
+
<div className="grid md:grid-cols-2 lg:grid-cols-3 gap-8">
74
+
<div className="bg-gray-900/50 backdrop-blur-sm p-8 rounded-xl border border-gray-700/50 hover:border-gray-600/50 transition-all duration-300">
75
+
<div className="bg-blue-500/10 w-12 h-12 rounded-lg flex items-center justify-center mb-6">
76
+
<MessageSquare className="h-6 w-6 text-blue-400" />
77
+
</div>
78
+
<h3 className="text-xl font-semibold text-white mb-4">AI Chat Assistant</h3>
79
+
<p className="text-gray-400 leading-relaxed">
80
+
Get intelligent responses and assistance with our advanced AI chat system.
81
+
</p>
82
+
</div>
83
+
84
+
<div className="bg-gray-900/50 backdrop-blur-sm p-8 rounded-xl border border-gray-700/50 hover:border-gray-600/50 transition-all duration-300">
85
+
<div className="bg-green-500/10 w-12 h-12 rounded-lg flex items-center justify-center mb-6">
86
+
<Cloud className="h-6 w-6 text-green-400" />
87
+
</div>
88
+
<h3 className="text-xl font-semibold text-white mb-4">Weather Updates</h3>
89
+
<p className="text-gray-400 leading-relaxed">
90
+
Stay informed with real-time weather information for any location.
91
+
</p>
92
+
</div>
93
+
94
+
<div className="bg-gray-900/50 backdrop-blur-sm p-8 rounded-xl border border-gray-700/50 hover:border-gray-600/50 transition-all duration-300">
95
+
<div className="bg-yellow-500/10 w-12 h-12 rounded-lg flex items-center justify-center mb-6">
96
+
<Bell className="h-6 w-6 text-yellow-400" />
97
+
</div>
98
+
<h3 className="text-xl font-semibold text-white mb-4">Smart Reminders</h3>
99
+
<p className="text-gray-400 leading-relaxed">
100
+
Never miss important events with our intelligent reminder system.
101
+
</p>
102
+
</div>
103
+
104
+
<div className="bg-gray-900/50 backdrop-blur-sm p-8 rounded-xl border border-gray-700/50 hover:border-gray-600/50 transition-all duration-300">
105
+
<div className="bg-purple-500/10 w-12 h-12 rounded-lg flex items-center justify-center mb-6">
106
+
<Shield className="h-6 w-6 text-purple-400" />
107
+
</div>
108
+
<h3 className="text-xl font-semibold text-white mb-4">Secure & Private</h3>
109
+
<p className="text-gray-400 leading-relaxed">
110
+
Your data is protected with enterprise-grade security measures.
111
+
</p>
112
+
</div>
113
+
114
+
<div className="bg-gray-900/50 backdrop-blur-sm p-8 rounded-xl border border-gray-700/50 hover:border-gray-600/50 transition-all duration-300">
115
+
<div className="bg-orange-500/10 w-12 h-12 rounded-lg flex items-center justify-center mb-6">
116
+
<Zap className="h-6 w-6 text-orange-400" />
117
+
</div>
118
+
<h3 className="text-xl font-semibold text-white mb-4">Lightning Fast</h3>
119
+
<p className="text-gray-400 leading-relaxed">
120
+
Experience blazing fast response times and seamless performance.
121
+
</p>
122
+
</div>
123
+
124
+
<div className="bg-gray-900/50 backdrop-blur-sm p-8 rounded-xl border border-gray-700/50 hover:border-gray-600/50 transition-all duration-300">
125
+
<div className="bg-indigo-500/10 w-12 h-12 rounded-lg flex items-center justify-center mb-6">
126
+
<Bot className="h-6 w-6 text-indigo-400" />
127
+
</div>
128
+
<h3 className="text-xl font-semibold text-white mb-4">Discord Native</h3>
129
+
<p className="text-gray-400 leading-relaxed">
130
+
Built specifically for Discord with seamless integration.
131
+
</p>
132
+
</div>
133
+
</div>
134
+
</div>
135
+
</section>
136
+
137
+
{/* Footer */}
138
+
<footer className="py-12 px-6">
139
+
<div className="max-w-6xl mx-auto text-center">
140
+
<div className="flex items-center justify-center space-x-3 mb-4">
141
+
<span className="text-lg font-semibold text-white">Aethel</span>
142
+
</div>
143
+
<p className="text-gray-400 mb-6">
144
+
A useful Discord user bot for your account
145
+
</p>
146
+
<div className="flex justify-center space-x-8 text-sm text-gray-400">
147
+
<a href="#features" className="hover:text-white transition-colors">Features</a>
148
+
<a href="/status" className="hover:text-white transition-colors">Status</a>
149
+
</div>
150
+
</div>
151
+
</footer>
152
+
</div>
153
+
)
154
+
}
155
+
156
+
export default LandingPage
+84
web/src/pages/LoginPage.tsx
+84
web/src/pages/LoginPage.tsx
···
1
+
import { useEffect } from 'react'
2
+
import { useSearchParams } from 'react-router-dom'
3
+
import { toast } from 'sonner'
4
+
import { useAuthStore } from '../stores/authStore'
5
+
6
+
const LoginPage = () => {
7
+
const [searchParams] = useSearchParams()
8
+
const { login } = useAuthStore()
9
+
10
+
useEffect(() => {
11
+
const token = searchParams.get('token')
12
+
const error = searchParams.get('error')
13
+
14
+
if (error) {
15
+
toast.error('Authentication failed. Please try again.')
16
+
} else if (token) {
17
+
const userData = {
18
+
id: searchParams.get('user_id') || '',
19
+
username: searchParams.get('username') || '',
20
+
discriminator: searchParams.get('discriminator') || '',
21
+
avatar: searchParams.get('avatar'),
22
+
}
23
+
24
+
login(token, userData)
25
+
toast.success('Successfully logged in!')
26
+
}
27
+
}, [searchParams, login])
28
+
29
+
const handleDiscordLogin = async () => {
30
+
try {
31
+
window.location.href = '/api/auth/discord'
32
+
} catch (error) {
33
+
toast.error('Failed to initiate Discord login')
34
+
}
35
+
}
36
+
37
+
return (
38
+
<div className="min-h-screen flex items-center justify-center bg-[#0A0A0A]">
39
+
<div className="max-w-md w-full space-y-8">
40
+
<div className="text-center">
41
+
<h1 className="text-4xl font-bold text-white mb-2">Aethel Dashboard</h1>
42
+
<p className="text-gray-400 text-lg">
43
+
Manage your todos and AI API keys
44
+
</p>
45
+
</div>
46
+
47
+
<div className="card p-8">
48
+
<div className="text-center space-y-6">
49
+
<div>
50
+
<h2 className="text-2xl font-bold text-white mb-2">
51
+
Welcome back
52
+
</h2>
53
+
<p className="text-gray-400">
54
+
Sign in with your Discord account to continue
55
+
</p>
56
+
</div>
57
+
58
+
<button
59
+
onClick={handleDiscordLogin}
60
+
className="w-full flex items-center justify-center px-4 py-3 border border-transparent rounded-lg shadow-sm text-white bg-discord-blurple hover:bg-primary-600 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-discord-blurple transition-colors font-medium"
61
+
>
62
+
<svg className="w-5 h-5 mr-3" viewBox="0 0 24 24" fill="currentColor">
63
+
<path d="M20.317 4.37a19.791 19.791 0 0 0-4.885-1.515a.074.074 0 0 0-.079.037c-.21.375-.444.864-.608 1.25a18.27 18.27 0 0 0-5.487 0a12.64 12.64 0 0 0-.617-1.25a.077.077 0 0 0-.079-.037A19.736 19.736 0 0 0 3.677 4.37a.07.07 0 0 0-.032.027C.533 9.046-.32 13.58.099 18.057a.082.082 0 0 0 .031.057a19.9 19.9 0 0 0 5.993 3.03a.078.078 0 0 0 .084-.028a14.09 14.09 0 0 0 1.226-1.994a.076.076 0 0 0-.041-.106a13.107 13.107 0 0 1-1.872-.892a.077.077 0 0 1-.008-.128a10.2 10.2 0 0 0 .372-.292a.074.074 0 0 1 .077-.01c3.928 1.793 8.18 1.793 12.062 0a.074.074 0 0 1 .078.01c.12.098.246.198.373.292a.077.077 0 0 1-.006.127a12.299 12.299 0 0 1-1.873.892a.077.077 0 0 0-.041.107c.36.698.772 1.362 1.225 1.993a.076.076 0 0 0 .084.028a19.839 19.839 0 0 0 6.002-3.03a.077.077 0 0 0 .032-.054c.5-5.177-.838-9.674-3.549-13.66a.061.061 0 0 0-.031-.03zM8.02 15.33c-1.183 0-2.157-1.085-2.157-2.419c0-1.333.956-2.419 2.157-2.419c1.21 0 2.176 1.096 2.157 2.42c0 1.333-.956 2.418-2.157 2.418zm7.975 0c-1.183 0-2.157-1.085-2.157-2.419c0-1.333.955-2.419 2.157-2.419c1.21 0 2.176 1.096 2.157 2.42c0 1.333-.946 2.418-2.157 2.418z"/>
64
+
</svg>
65
+
Continue with Discord
66
+
</button>
67
+
68
+
<div className="text-xs text-gray-400 text-center">
69
+
By signing in, you agree to our terms of service and privacy policy.
70
+
</div>
71
+
</div>
72
+
</div>
73
+
74
+
<div className="text-center">
75
+
<p className="text-gray-400 text-sm">
76
+
Need help? Contact us on our Discord server
77
+
</p>
78
+
</div>
79
+
</div>
80
+
</div>
81
+
)
82
+
}
83
+
84
+
export default LoginPage
+114
web/src/pages/PrivacyPage.tsx
+114
web/src/pages/PrivacyPage.tsx
···
1
+
import { Link } from 'react-router-dom';
2
+
import { ArrowLeft } from 'lucide-react';
3
+
4
+
export default function PrivacyPolicy() {
5
+
return (
6
+
<div className="min-h-screen bg-[#0A0A0A] text-white">
7
+
{/* Header */}
8
+
<header className="border-b border-gray-800">
9
+
<div className="max-w-4xl mx-auto px-6 py-4">
10
+
<Link
11
+
to="/"
12
+
className="inline-flex items-center gap-2 text-gray-400 hover:text-white transition-colors"
13
+
>
14
+
<ArrowLeft className="w-4 h-4" />
15
+
Back to Home
16
+
</Link>
17
+
</div>
18
+
</header>
19
+
20
+
{/* Content */}
21
+
<main className="max-w-4xl mx-auto px-6 py-12">
22
+
<div className="mb-8">
23
+
<h1 className="text-4xl font-bold text-white mb-2">Privacy Policy</h1>
24
+
<p className="text-gray-400">Last Updated: July 21, 2025</p>
25
+
</div>
26
+
<div className="space-y-8">
27
+
<section>
28
+
<h2 className="text-2xl font-bold text-white mb-4 pt-8 border-t border-gray-800 first:border-t-0 first:pt-0">1. Information We Collect</h2>
29
+
<p className="text-gray-400 leading-relaxed">
30
+
The bot ("the Bot") collects the following information:
31
+
</p>
32
+
<ul className="list-disc pl-6 space-y-3 text-gray-400 mt-2">
33
+
<li>Discord user IDs for command processing and functionality</li>
34
+
<li>Server IDs where the Bot is used</li>
35
+
<li>Channel IDs where commands are used</li>
36
+
<li>Message content for commands that require it (e.g., reminders, AI chat)</li>
37
+
<li>API keys provided by users (encrypted and stored safely on our database until the user says otherwise)</li>
38
+
<li>Commands ran and users who ran them</li>
39
+
40
+
</ul>
41
+
</section>
42
+
43
+
<section>
44
+
<h2 className="text-2xl font-bold text-white mb-4 pt-8 border-t border-gray-800 first:border-t-0 first:pt-0">2. How We Use Your Information</h2>
45
+
<p className="text-gray-400 leading-relaxed">
46
+
We use the collected information to provide, maintain, and improve our Bot's services, including:
47
+
</p>
48
+
<ul className="list-disc pl-6 space-y-3 text-gray-400 mt-2">
49
+
<li>Provide and maintain the Bot's functionality</li>
50
+
<li>Process commands and provide responses</li>
51
+
<li>Improve the Bot's performance and features</li>
52
+
<li>Monitor for abuse and prevent violations of our Terms of Service</li>
53
+
</ul>
54
+
</section>
55
+
56
+
<section>
57
+
<h2 className="text-2xl font-bold text-white mb-4 pt-8 border-t border-gray-800 first:border-t-0 first:pt-0">3. Data Storage</h2>
58
+
<p className="text-gray-400 leading-relaxed">
59
+
We take your privacy seriously:
60
+
</p>
61
+
<ul className="list-disc pl-6 space-y-3 text-gray-400 mt-2">
62
+
<li>API keys are securely hashed using industry-standard encryption before being stored in our database</li>
63
+
<li>Your custom API keys and model preferences are stored until you choose to remove them using the <code className="bg-gray-800 px-2 py-0.5 rounded text-sm font-mono text-gray-200">/ai use_custom_api:false</code> command</li>
64
+
<li>We log all message content (like Wiki searches, reminders, and 8-ball queries) for monitoring purposes.</li>
65
+
<li>We do not sell or share your personal information with third parties</li>
66
+
<li>You can delete your stored API key and preferences at any time by running <code className="bg-gray-800 px-1 rounded">/ai use_custom_api:false</code></li>
67
+
</ul>
68
+
</section>
69
+
70
+
<section>
71
+
<h2 className="text-2xl font-bold text-white mb-4 pt-8 border-t border-gray-800 first:border-t-0 first:pt-0">4. Third-Party Services</h2>
72
+
<p className="text-gray-400 leading-relaxed">
73
+
Our Bot may contain links to third-party websites or services that are not operated by us. We have no control over and assume no responsibility for the content, privacy policies, or practices of any third-party sites or services.
74
+
</p>
75
+
<ul className="list-disc pl-6 space-y-3 text-gray-400 mt-2">
76
+
<li>Discord's Privacy Policy for user and server data</li>
77
+
<li>OpenRouter's Privacy Policy for AI chat functionality</li>
78
+
<li>Weather API providers for weather information</li>
79
+
<li>Wikipedia's Terms of Service for wiki lookups</li>
80
+
</ul>
81
+
</section>
82
+
83
+
<section>
84
+
<h2 className="text-2xl font-bold text-white mb-4 pt-8 border-t border-gray-800 first:border-t-0 first:pt-0">5. Data Security</h2>
85
+
<p className="text-gray-400 leading-relaxed">
86
+
We implement reasonable security measures to protect your information, but no method of transmission over the internet is 100% secure.
87
+
</p>
88
+
</section>
89
+
90
+
<section>
91
+
<h2 className="text-2xl font-bold text-white mb-4 pt-8 border-t border-gray-800 first:border-t-0 first:pt-0">6. Children's Privacy</h2>
92
+
<p className="text-gray-400 leading-relaxed">
93
+
Our Bot is not intended for use by children under the age of 13. We do not knowingly collect personally identifiable information from children under 13. If you are a parent or guardian and you are aware that your child has provided us with personal information, please contact us.
94
+
</p>
95
+
</section>
96
+
97
+
<section>
98
+
<h2 className="text-2xl font-bold text-white mb-4 pt-8 border-t border-gray-800 first:border-t-0 first:pt-0">7. Changes to This Policy</h2>
99
+
<p className="text-gray-400 leading-relaxed">
100
+
We may update our Privacy Policy from time to time. We will notify you of any changes by posting the new Privacy Policy on this page.
101
+
</p>
102
+
</section>
103
+
104
+
<section>
105
+
<h2 className="text-2xl font-bold text-white mb-4 pt-8 border-t border-gray-800 first:border-t-0 first:pt-0">8. Contact Us</h2>
106
+
<p className="text-gray-400 leading-relaxed">
107
+
If you have any questions about this Privacy Policy, please contact us at <a href="mailto:scan@scanash.com" className="text-blue-400 hover:text-blue-300 hover:underline font-medium">scan@scanash.com</a>.
108
+
</p>
109
+
</section>
110
+
</div>
111
+
</main>
112
+
</div>
113
+
);
114
+
}
+262
web/src/pages/RemindersPage.tsx
+262
web/src/pages/RemindersPage.tsx
···
1
+
import React, { useState, useEffect } from 'react'
2
+
import { Plus, Clock, Check, Trash2, Bell } from 'lucide-react'
3
+
import { remindersAPI } from '../lib/api'
4
+
import { toast } from 'sonner'
5
+
6
+
interface Reminder {
7
+
reminder_id: string
8
+
message: string
9
+
expires_at: string
10
+
is_completed: boolean
11
+
created_at: string
12
+
}
13
+
14
+
const RemindersPage: React.FC = () => {
15
+
const [reminders, setReminders] = useState<Reminder[]>([])
16
+
const [loading, setLoading] = useState(true)
17
+
const [showCreateForm, setShowCreateForm] = useState(false)
18
+
const [newReminder, setNewReminder] = useState({
19
+
message: '',
20
+
expires_at: ''
21
+
})
22
+
23
+
useEffect(() => {
24
+
fetchReminders()
25
+
}, [])
26
+
27
+
const fetchReminders = async () => {
28
+
try {
29
+
const response = await remindersAPI.getReminders()
30
+
setReminders(response.data.reminders || [])
31
+
} catch (error) {
32
+
toast.error('Failed to fetch reminders')
33
+
} finally {
34
+
setLoading(false)
35
+
}
36
+
}
37
+
38
+
const handleCreateReminder = async (e: React.FormEvent) => {
39
+
e.preventDefault()
40
+
41
+
if (!newReminder.message.trim() || !newReminder.expires_at) {
42
+
toast.error('Please fill in all fields')
43
+
return
44
+
}
45
+
46
+
try {
47
+
await remindersAPI.createReminder({
48
+
message: newReminder.message.trim(),
49
+
expires_at: newReminder.expires_at
50
+
})
51
+
52
+
toast.success('Reminder created successfully!')
53
+
setNewReminder({ message: '', expires_at: '' })
54
+
setShowCreateForm(false)
55
+
fetchReminders()
56
+
} catch (error) {
57
+
toast.error('Failed to create reminder')
58
+
}
59
+
}
60
+
61
+
const handleCompleteReminder = async (id: string) => {
62
+
try {
63
+
await remindersAPI.completeReminder(id)
64
+
toast.success('Reminder completed!')
65
+
fetchReminders()
66
+
} catch (error) {
67
+
toast.error('Failed to complete reminder')
68
+
}
69
+
}
70
+
71
+
const handleClearCompleted = async () => {
72
+
try {
73
+
const completedReminders = reminders.filter(r => r.is_completed)
74
+
if (completedReminders.length === 0) {
75
+
toast.info('No completed reminders to clear')
76
+
return
77
+
}
78
+
79
+
await remindersAPI.clearCompletedReminders()
80
+
toast.success('Completed reminders cleared!')
81
+
fetchReminders()
82
+
} catch (error) {
83
+
toast.error('Failed to clear completed reminders')
84
+
}
85
+
}
86
+
87
+
const formatDate = (dateString: string) => {
88
+
const date = new Date(dateString)
89
+
return date.toLocaleString()
90
+
}
91
+
92
+
const isExpired = (dateString: string) => {
93
+
return new Date(dateString) < new Date()
94
+
}
95
+
96
+
const getMinDateTime = () => {
97
+
const now = new Date()
98
+
const minTime = new Date(now.getTime() + 60000)
99
+
const year = minTime.getFullYear()
100
+
const month = String(minTime.getMonth() + 1).padStart(2, '0')
101
+
const day = String(minTime.getDate()).padStart(2, '0')
102
+
const hours = String(minTime.getHours()).padStart(2, '0')
103
+
const minutes = String(minTime.getMinutes()).padStart(2, '0')
104
+
105
+
return `${year}-${month}-${day}T${hours}:${minutes}`
106
+
}
107
+
108
+
if (loading) {
109
+
return (
110
+
<div className="flex items-center justify-center h-64">
111
+
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-discord-blurple"></div>
112
+
</div>
113
+
)
114
+
}
115
+
116
+
return (
117
+
<div className="space-y-8">
118
+
<div className="flex items-center justify-between">
119
+
<div>
120
+
<h1 className="text-3xl font-bold text-white">Reminders</h1>
121
+
<p className="text-gray-400 mt-2">Manage your personal reminders and notifications</p>
122
+
</div>
123
+
<div className="flex items-center gap-3">
124
+
{reminders.some(r => r.is_completed) && (
125
+
<button
126
+
onClick={handleClearCompleted}
127
+
className="btn btn-danger"
128
+
>
129
+
<Trash2 className="h-4 w-4 mr-2" />
130
+
Clear Completed
131
+
</button>
132
+
)}
133
+
<button
134
+
onClick={() => setShowCreateForm(true)}
135
+
className="btn btn-primary"
136
+
>
137
+
<Plus className="h-4 w-4 mr-2" />
138
+
New Reminder
139
+
</button>
140
+
</div>
141
+
</div>
142
+
143
+
{/* Create Reminder Form */}
144
+
{showCreateForm && (
145
+
<div className="bg-gray-900/50 p-8 rounded-lg border border-gray-700">
146
+
<h2 className="text-xl font-semibold mb-6 text-white">Create New Reminder</h2>
147
+
<form onSubmit={handleCreateReminder} className="space-y-6">
148
+
<div>
149
+
<label htmlFor="message" className="block text-sm font-medium text-gray-400 mb-2">
150
+
Reminder Message
151
+
</label>
152
+
<textarea
153
+
id="message"
154
+
value={newReminder.message}
155
+
onChange={(e) => setNewReminder({ ...newReminder, message: e.target.value })}
156
+
placeholder="What would you like to be reminded about?"
157
+
className="input"
158
+
rows={3}
159
+
required
160
+
/>
161
+
</div>
162
+
<div>
163
+
<label htmlFor="expires_at" className="block text-sm font-medium text-gray-400 mb-2">
164
+
Remind me at
165
+
</label>
166
+
<input
167
+
type="datetime-local"
168
+
id="expires_at"
169
+
value={newReminder.expires_at}
170
+
onChange={(e) => setNewReminder({ ...newReminder, expires_at: e.target.value })}
171
+
min={getMinDateTime()}
172
+
className="input"
173
+
required
174
+
/>
175
+
</div>
176
+
<div className="flex gap-3">
177
+
<button
178
+
type="submit"
179
+
className="btn btn-primary"
180
+
>
181
+
Create Reminder
182
+
</button>
183
+
<button
184
+
type="button"
185
+
onClick={() => {
186
+
setShowCreateForm(false)
187
+
setNewReminder({ message: '', expires_at: '' })
188
+
}}
189
+
className="btn btn-secondary"
190
+
>
191
+
Cancel
192
+
</button>
193
+
</div>
194
+
</form>
195
+
</div>
196
+
)}
197
+
198
+
{/* Reminders List */}
199
+
<div className="space-y-4">
200
+
{reminders.length === 0 ? (
201
+
<div className="text-center py-12">
202
+
<Bell className="h-12 w-12 text-gray-400 mx-auto mb-4" />
203
+
<h3 className="text-lg font-medium text-white mb-2">No reminders yet</h3>
204
+
<p className="text-gray-400 mb-4">Create your first reminder to get started!</p>
205
+
<button
206
+
onClick={() => setShowCreateForm(true)}
207
+
className="btn btn-primary"
208
+
>
209
+
Create Reminder
210
+
</button>
211
+
</div>
212
+
) : (
213
+
reminders.map((reminder) => (
214
+
<div
215
+
key={reminder.reminder_id}
216
+
className={`card p-6 border-l-4 ${
217
+
reminder.is_completed
218
+
? 'border-green-500 bg-green-900/20'
219
+
: isExpired(reminder.expires_at)
220
+
? 'border-red-500 bg-red-900/20'
221
+
: 'border-blue-500'
222
+
}`}
223
+
>
224
+
<div className="flex items-start justify-between">
225
+
<div className="flex-1">
226
+
<p className={`text-lg ${reminder.is_completed ? 'line-through text-gray-500' : 'text-white'}`}>
227
+
{reminder.message}
228
+
</p>
229
+
<div className="flex items-center gap-4 mt-3 text-sm text-gray-400">
230
+
<div className="flex items-center gap-1">
231
+
<Clock className="h-4 w-4" />
232
+
<span>Remind at: {formatDate(reminder.expires_at)}</span>
233
+
</div>
234
+
{isExpired(reminder.expires_at) && !reminder.is_completed && (
235
+
<span className="text-red-600 font-medium">Overdue</span>
236
+
)}
237
+
{reminder.is_completed && (
238
+
<span className="text-green-600 font-medium">Completed</span>
239
+
)}
240
+
</div>
241
+
</div>
242
+
<div className="flex items-center gap-2 ml-4">
243
+
{!reminder.is_completed && (
244
+
<button
245
+
onClick={() => handleCompleteReminder(reminder.reminder_id)}
246
+
className="btn btn-success p-2"
247
+
title="Mark as completed"
248
+
>
249
+
<Check className="h-4 w-4" />
250
+
</button>
251
+
)}
252
+
</div>
253
+
</div>
254
+
</div>
255
+
))
256
+
)}
257
+
</div>
258
+
</div>
259
+
)
260
+
}
261
+
262
+
export default RemindersPage
+322
web/src/pages/StatusPage.tsx
+322
web/src/pages/StatusPage.tsx
···
1
+
import { useEffect, useState } from 'react'
2
+
import { Link } from 'react-router-dom'
3
+
import {
4
+
Activity,
5
+
Clock,
6
+
GitBranch,
7
+
Home,
8
+
RefreshCw,
9
+
Server,
10
+
Wifi,
11
+
WifiOff,
12
+
AlertCircle
13
+
} from 'lucide-react'
14
+
15
+
interface StatusData {
16
+
status: string
17
+
uptime: {
18
+
days: number
19
+
hours: number
20
+
minutes: number
21
+
seconds: number
22
+
}
23
+
botStatus: string
24
+
ping: number
25
+
lastReady: string | null
26
+
commitHash: string
27
+
}
28
+
29
+
const StatusPage = () => {
30
+
const [statusData, setStatusData] = useState<StatusData | null>(null)
31
+
const [loading, setLoading] = useState(true)
32
+
const [error, setError] = useState<string | null>(null)
33
+
const [lastUpdated, setLastUpdated] = useState<Date>(new Date())
34
+
35
+
const fetchStatus = async () => {
36
+
try {
37
+
setError(null)
38
+
const response = await fetch('http://localhost:2020/status', {
39
+
headers: {
40
+
'X-API-Key': 'XIoypvTfaDxWLTFFcHu9ta0aJpvRPVIGADxSMNCNJ50QYtIpSIUsi1WKLglQ7TTRYX6mWgYq15i4NqPl92l0Lzepsrju2fXV1aZpNdDTtIu5mFMvcLhouhmxwb7R93'
41
+
}
42
+
})
43
+
44
+
if (!response.ok) {
45
+
throw new Error(`HTTP ${response.status}: ${response.statusText}`)
46
+
}
47
+
48
+
const data = await response.json()
49
+
setStatusData(data)
50
+
setLastUpdated(new Date())
51
+
} catch (err) {
52
+
setError(err instanceof Error ? err.message : 'Failed to fetch status')
53
+
} finally {
54
+
setLoading(false)
55
+
}
56
+
}
57
+
58
+
useEffect(() => {
59
+
fetchStatus()
60
+
61
+
const interval = setInterval(fetchStatus, 30000)
62
+
return () => clearInterval(interval)
63
+
}, [])
64
+
65
+
const formatUptime = (uptime: StatusData['uptime']) => {
66
+
const parts = []
67
+
if (uptime.days > 0) parts.push(`${uptime.days}d`)
68
+
if (uptime.hours > 0) parts.push(`${uptime.hours}h`)
69
+
if (uptime.minutes > 0) parts.push(`${uptime.minutes}m`)
70
+
if (uptime.seconds > 0 || parts.length === 0) parts.push(`${uptime.seconds}s`)
71
+
return parts.join(' ')
72
+
}
73
+
74
+
const getStatusColor = (status: string) => {
75
+
switch (status.toLowerCase()) {
76
+
case 'online':
77
+
case 'connected':
78
+
return 'text-green-400 bg-green-500/20'
79
+
case 'offline':
80
+
case 'disconnected':
81
+
return 'text-red-400 bg-red-500/20'
82
+
default:
83
+
return 'text-yellow-400 bg-yellow-500/20'
84
+
}
85
+
}
86
+
87
+
const getPingColor = (ping: number) => {
88
+
if (ping < 100) return 'text-green-400'
89
+
if (ping < 300) return 'text-yellow-400'
90
+
return 'text-red-400'
91
+
}
92
+
93
+
return (
94
+
<div className="min-h-screen bg-[#0A0A0A] text-white">
95
+
{/* Header */}
96
+
<header className="border-b border-gray-800">
97
+
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
98
+
<div className="flex justify-between items-center py-6">
99
+
<div className="flex items-center space-x-4">
100
+
<Link to="/" className="flex items-center space-x-2 text-gray-400 hover:text-white transition-colors">
101
+
<Home className="h-5 w-5" />
102
+
<span>Back to Home</span>
103
+
</Link>
104
+
</div>
105
+
<div className="flex items-center space-x-3">
106
+
<span className="text-2xl font-bold text-white">Aethel Status</span>
107
+
</div>
108
+
<button
109
+
onClick={fetchStatus}
110
+
disabled={loading}
111
+
className="flex items-center space-x-2 px-4 py-2 bg-white text-black rounded-lg hover:bg-gray-100 disabled:opacity-50 disabled:cursor-not-allowed transition-colors font-medium"
112
+
>
113
+
<RefreshCw className={`h-4 w-4 ${loading ? 'animate-spin' : ''}`} />
114
+
<span>Refresh</span>
115
+
</button>
116
+
</div>
117
+
</div>
118
+
</header>
119
+
120
+
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
121
+
{error && (
122
+
<div className="mb-8 bg-red-900/20 border border-red-800 rounded-lg p-4">
123
+
<div className="flex items-center space-x-2">
124
+
<AlertCircle className="h-5 w-5 text-red-400" />
125
+
<span className="text-red-300 font-medium">Error loading status</span>
126
+
</div>
127
+
<p className="text-red-400 mt-2">{error}</p>
128
+
</div>
129
+
)}
130
+
131
+
{loading && !statusData ? (
132
+
<div className="flex items-center justify-center py-12">
133
+
<div className="flex items-center space-x-3">
134
+
<RefreshCw className="h-6 w-6 animate-spin text-white" />
135
+
<span className="text-lg text-gray-400">Loading status...</span>
136
+
</div>
137
+
</div>
138
+
) : statusData ? (
139
+
<div className="space-y-8">
140
+
{/* Overall Status */}
141
+
<div className="bg-gray-900/50 backdrop-blur-sm rounded-xl border border-gray-700/50 p-8">
142
+
<div className="flex items-center justify-between mb-6">
143
+
<h2 className="text-3xl font-bold text-white">System Status</h2>
144
+
<div className="text-sm text-gray-400">
145
+
Last updated: {lastUpdated.toLocaleTimeString()}
146
+
</div>
147
+
</div>
148
+
149
+
<div className="grid md:grid-cols-2 lg:grid-cols-4 gap-6">
150
+
<div className="bg-gray-800/50 rounded-xl p-6 border border-gray-700/30">
151
+
<div className="flex items-center space-x-4">
152
+
<div className={`p-3 rounded-lg ${getStatusColor(statusData.status)}`}>
153
+
<Server className="h-6 w-6" />
154
+
</div>
155
+
<div>
156
+
<p className="text-sm text-gray-400 mb-1">Server Status</p>
157
+
<p className="font-semibold text-white text-lg capitalize">{statusData.status}</p>
158
+
</div>
159
+
</div>
160
+
</div>
161
+
162
+
<div className="bg-gray-800/50 rounded-xl p-6 border border-gray-700/30">
163
+
<div className="flex items-center space-x-4">
164
+
<div className={`p-3 rounded-lg ${getStatusColor(statusData.botStatus)}`}>
165
+
{statusData.botStatus === 'connected' ? (
166
+
<Wifi className="h-6 w-6" />
167
+
) : (
168
+
<WifiOff className="h-6 w-6" />
169
+
)}
170
+
</div>
171
+
<div>
172
+
<p className="text-sm text-gray-400 mb-1">Bot Status</p>
173
+
<p className="font-semibold text-white text-lg capitalize">{statusData.botStatus}</p>
174
+
</div>
175
+
</div>
176
+
</div>
177
+
178
+
<div className="bg-gray-800/50 rounded-xl p-6 border border-gray-700/30">
179
+
<div className="flex items-center space-x-4">
180
+
<div className="p-3 rounded-lg bg-blue-500/20 text-blue-400">
181
+
<Activity className="h-6 w-6" />
182
+
</div>
183
+
<div>
184
+
<p className="text-sm text-gray-400 mb-1">Ping</p>
185
+
<p className={`font-semibold text-lg ${getPingColor(statusData.ping)}`}>
186
+
{statusData.ping}ms
187
+
</p>
188
+
</div>
189
+
</div>
190
+
</div>
191
+
192
+
<div className="bg-gray-800/50 rounded-xl p-6 border border-gray-700/30">
193
+
<div className="flex items-center space-x-4">
194
+
<div className="p-3 rounded-lg bg-purple-500/20 text-purple-400">
195
+
<Clock className="h-6 w-6" />
196
+
</div>
197
+
<div>
198
+
<p className="text-sm text-gray-400 mb-1">Uptime</p>
199
+
<p className="font-semibold text-white text-lg">
200
+
{formatUptime(statusData.uptime)}
201
+
</p>
202
+
</div>
203
+
</div>
204
+
</div>
205
+
</div>
206
+
</div>
207
+
208
+
{/* Detailed Information */}
209
+
<div className="grid md:grid-cols-2 gap-6">
210
+
<div className="bg-gray-900/50 backdrop-blur-sm rounded-xl border border-gray-700/50 p-6">
211
+
<h3 className="text-xl font-semibold text-white mb-6 flex items-center space-x-2">
212
+
<Clock className="h-6 w-6 text-purple-400" />
213
+
<span>Uptime Details</span>
214
+
</h3>
215
+
<div className="space-y-4">
216
+
<div className="flex justify-between items-center">
217
+
<span className="text-gray-400">Days:</span>
218
+
<span className="font-semibold text-white text-lg">{statusData.uptime.days}</span>
219
+
</div>
220
+
<div className="flex justify-between items-center">
221
+
<span className="text-gray-400">Hours:</span>
222
+
<span className="font-semibold text-white text-lg">{statusData.uptime.hours}</span>
223
+
</div>
224
+
<div className="flex justify-between items-center">
225
+
<span className="text-gray-400">Minutes:</span>
226
+
<span className="font-semibold text-white text-lg">{statusData.uptime.minutes}</span>
227
+
</div>
228
+
<div className="flex justify-between items-center">
229
+
<span className="text-gray-400">Seconds:</span>
230
+
<span className="font-semibold text-white text-lg">{statusData.uptime.seconds}</span>
231
+
</div>
232
+
</div>
233
+
</div>
234
+
235
+
<div className="bg-gray-900/50 backdrop-blur-sm rounded-xl border border-gray-700/50 p-6">
236
+
<h3 className="text-xl font-semibold text-white mb-6 flex items-center space-x-2">
237
+
<GitBranch className="h-6 w-6 text-blue-400" />
238
+
<span>System Information</span>
239
+
</h3>
240
+
<div className="space-y-4">
241
+
<div className="flex justify-between items-center">
242
+
<span className="text-gray-400">Commit Hash:</span>
243
+
<span className="font-mono text-sm bg-gray-800 text-gray-300 px-3 py-1 rounded-lg">
244
+
{statusData.commitHash || 'Unknown'}
245
+
</span>
246
+
</div>
247
+
<div className="flex justify-between items-center">
248
+
<span className="text-gray-400">Last Ready:</span>
249
+
<span className="font-medium text-white">
250
+
{statusData.lastReady
251
+
? new Date(statusData.lastReady).toLocaleString()
252
+
: 'Never'
253
+
}
254
+
</span>
255
+
</div>
256
+
<div className="flex justify-between items-center">
257
+
<span className="text-gray-400">Response Time:</span>
258
+
<span className={`font-semibold text-lg ${getPingColor(statusData.ping)}`}>
259
+
{statusData.ping}ms
260
+
</span>
261
+
</div>
262
+
</div>
263
+
</div>
264
+
</div>
265
+
266
+
{/* Status Indicators */}
267
+
<div className="bg-gray-900/50 backdrop-blur-sm rounded-xl border border-gray-700/50 p-6">
268
+
<h3 className="text-xl font-semibold text-white mb-6">Service Health</h3>
269
+
<div className="grid md:grid-cols-3 gap-4">
270
+
<div className="flex items-center space-x-3 p-4 bg-gray-800/50 rounded-lg border border-gray-700/30">
271
+
<div className={`w-4 h-4 rounded-full ${
272
+
statusData.status === 'online' ? 'bg-green-500' : 'bg-red-500'
273
+
}`}></div>
274
+
<span className="text-gray-300 font-medium">API Server</span>
275
+
<span className={`ml-auto px-3 py-1 text-xs rounded-full font-medium ${
276
+
statusData.status === 'online'
277
+
? 'bg-green-500/20 text-green-400'
278
+
: 'bg-red-500/20 text-red-400'
279
+
}`}>
280
+
{statusData.status}
281
+
</span>
282
+
</div>
283
+
284
+
<div className="flex items-center space-x-3 p-4 bg-gray-800/50 rounded-lg border border-gray-700/30">
285
+
<div className={`w-4 h-4 rounded-full ${
286
+
statusData.botStatus === 'connected' ? 'bg-green-500' : 'bg-red-500'
287
+
}`}></div>
288
+
<span className="text-gray-300 font-medium">Discord Bot</span>
289
+
<span className={`ml-auto px-3 py-1 text-xs rounded-full font-medium ${
290
+
statusData.botStatus === 'connected'
291
+
? 'bg-green-500/20 text-green-400'
292
+
: 'bg-red-500/20 text-red-400'
293
+
}`}>
294
+
{statusData.botStatus}
295
+
</span>
296
+
</div>
297
+
298
+
<div className="flex items-center space-x-3 p-4 bg-gray-800/50 rounded-lg border border-gray-700/30">
299
+
<div className={`w-4 h-4 rounded-full ${
300
+
statusData.ping < 300 ? 'bg-green-500' : 'bg-yellow-500'
301
+
}`}></div>
302
+
<span className="text-gray-300 font-medium">Network</span>
303
+
<span className={`ml-auto px-3 py-1 text-xs rounded-full font-medium ${
304
+
statusData.ping < 100
305
+
? 'bg-green-500/20 text-green-400'
306
+
: statusData.ping < 300
307
+
? 'bg-yellow-500/20 text-yellow-400'
308
+
: 'bg-red-500/20 text-red-400'
309
+
}`}>
310
+
{statusData.ping}ms
311
+
</span>
312
+
</div>
313
+
</div>
314
+
</div>
315
+
</div>
316
+
) : null}
317
+
</div>
318
+
</div>
319
+
)
320
+
}
321
+
322
+
export default StatusPage
+99
web/src/pages/TermsPage.tsx
+99
web/src/pages/TermsPage.tsx
···
1
+
import { Link } from 'react-router-dom';
2
+
import { ArrowLeft } from 'lucide-react';
3
+
4
+
export default function TermsOfService() {
5
+
return (
6
+
<div className="min-h-screen bg-[#0A0A0A] text-white">
7
+
{/* Header */}
8
+
<header className="border-b border-gray-800">
9
+
<div className="max-w-4xl mx-auto px-6 py-4">
10
+
<Link
11
+
to="/"
12
+
className="inline-flex items-center gap-2 text-gray-400 hover:text-white transition-colors"
13
+
>
14
+
<ArrowLeft className="w-4 h-4" />
15
+
Back to Home
16
+
</Link>
17
+
</div>
18
+
</header>
19
+
20
+
{/* Content */}
21
+
<main className="max-w-4xl mx-auto px-6 py-12">
22
+
<div className="mb-8">
23
+
<h1 className="text-4xl font-bold text-white mb-2">Terms of Service</h1>
24
+
<p className="text-gray-400">Last Updated: June 16, 2025</p>
25
+
</div>
26
+
<div className="space-y-8">
27
+
<section>
28
+
<h2 className="text-2xl font-bold text-white mb-4 pt-8 border-t border-gray-800 first:border-t-0 first:pt-0">1. Acceptance of Terms</h2>
29
+
<p className="text-gray-400 leading-relaxed">
30
+
By using the Bot, you agree to be bound by these Terms of Service. If you do not agree to these terms, please do not use the Bot.
31
+
</p>
32
+
</section>
33
+
34
+
<section>
35
+
<h2 className="text-2xl font-bold text-white mb-4 pt-8 border-t border-gray-800 first:border-t-0 first:pt-0">2. Description of Service</h2>
36
+
<p className="text-gray-400 leading-relaxed mb-4">
37
+
The Bot provides various Discord utilities including but not limited to: reminders, random cat and dog images, weather information, wiki lookups, and fun commands. You agree to use the Bot in accordance with Discord's Terms of Service and Community Guidelines.
38
+
</p>
39
+
<ul className="list-disc pl-6 space-y-3 text-gray-400">
40
+
<li>Reminder system</li>
41
+
<li>Random cat and dog images</li>
42
+
<li>Weather information</li>
43
+
<li>Wiki lookups</li>
44
+
<li>And other Discord utilities</li>
45
+
</ul>
46
+
</section>
47
+
48
+
<section>
49
+
<h2 className="text-2xl font-bold text-white mb-4 pt-8 border-t border-gray-800 first:border-t-0 first:pt-0">3. User Responsibilities</h2>
50
+
<p className="text-gray-400 leading-relaxed mb-4">
51
+
When using the Bot, you agree not to:
52
+
</p>
53
+
<ul className="list-disc pl-6 space-y-3 text-gray-400">
54
+
<li>Use the Bot for any illegal or unauthorized purpose</li>
55
+
<li>Violate any laws in your jurisdiction</li>
56
+
<li>Attempt to disrupt or interfere with the Bot's operation</li>
57
+
<li>Spam or harass others</li>
58
+
<li>Attempt to reverse engineer or modify the Bot</li>
59
+
</ul>
60
+
</section>
61
+
62
+
<section>
63
+
<h2 className="text-2xl font-bold text-white mb-4 pt-8 border-t border-gray-800 first:border-t-0 first:pt-0">4. API Usage</h2>
64
+
<p className="text-gray-400 leading-relaxed">
65
+
The Bot may use third-party APIs and services ("Third-Party Services"). Your use of these services is subject to their respective terms and privacy policies.
66
+
</p>
67
+
<ul className="list-disc pl-6 space-y-3 text-gray-400 mt-2">
68
+
<li>You are responsible for the security of your API keys</li>
69
+
<li>We do not store your API keys permanently - they are only kept in memory during your active session</li>
70
+
<li>You must comply with the terms of service of any third-party APIs you use with the Bot</li>
71
+
<li>We are not responsible for any charges or fees you may incur from third-party API usage</li>
72
+
</ul>
73
+
</section>
74
+
75
+
<section>
76
+
<h2 className="text-2xl font-bold text-white mb-4 pt-8 border-t border-gray-800 first:border-t-0 first:pt-0">5. Limitation of Liability</h2>
77
+
<p className="text-gray-400 leading-relaxed">
78
+
The Bot is provided "as is" without any warranties. We are not responsible for any direct, indirect, incidental, or consequential damages resulting from the use of the Bot.
79
+
</p>
80
+
</section>
81
+
82
+
<section>
83
+
<h2 className="text-2xl font-bold text-white mb-4 pt-8 border-t border-gray-800 first:border-t-0 first:pt-0">6. Changes to Terms</h2>
84
+
<p className="text-gray-400 leading-relaxed">
85
+
We reserve the right to modify these terms at any time. Continued use of the Bot after changes constitutes acceptance of the new terms.
86
+
</p>
87
+
</section>
88
+
89
+
<section>
90
+
<h2 className="text-2xl font-bold text-white mb-4 pt-8 border-t border-gray-800 first:border-t-0 first:pt-0">7. Contact</h2>
91
+
<p className="text-gray-400 leading-relaxed">
92
+
If you have any questions about these Terms of Service, please contact us at <a href="mailto:scan@scanash.com" className="text-blue-400 hover:text-blue-300 hover:underline font-medium">scan@scanash.com</a>.
93
+
</p>
94
+
</section>
95
+
</div>
96
+
</main>
97
+
</div>
98
+
);
99
+
}
+268
web/src/pages/TodosPage.tsx
+268
web/src/pages/TodosPage.tsx
···
1
+
import { useState } from 'react'
2
+
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
3
+
import { Plus, Check, Trash2, AlertCircle } from 'lucide-react'
4
+
import { toast } from 'sonner'
5
+
import { todosAPI } from '../lib/api'
6
+
7
+
interface Todo {
8
+
id: number
9
+
item: string
10
+
done: boolean
11
+
created_at: string
12
+
completed_at?: string
13
+
}
14
+
15
+
const TodosPage = () => {
16
+
const [newTodo, setNewTodo] = useState('')
17
+
const [showClearConfirm, setShowClearConfirm] = useState(false)
18
+
const queryClient = useQueryClient()
19
+
20
+
const { data: todos, isLoading } = useQuery({
21
+
queryKey: ['todos'],
22
+
queryFn: () => todosAPI.getTodos().then(res => res.data),
23
+
})
24
+
25
+
const addTodoMutation = useMutation({
26
+
mutationFn: (item: string) => todosAPI.createTodo({ item }),
27
+
onSuccess: () => {
28
+
queryClient.invalidateQueries({ queryKey: ['todos'] })
29
+
setNewTodo('')
30
+
toast.success('Todo added successfully!')
31
+
},
32
+
onError: () => {
33
+
toast.error('Failed to add todo')
34
+
},
35
+
})
36
+
37
+
const updateTodoMutation = useMutation({
38
+
mutationFn: ({ id, done }: { id: number; done: boolean }) =>
39
+
todosAPI.updateTodo(id, { done }),
40
+
onSuccess: () => {
41
+
queryClient.invalidateQueries({ queryKey: ['todos'] })
42
+
},
43
+
onError: () => {
44
+
toast.error('Failed to update todo')
45
+
},
46
+
})
47
+
48
+
const deleteTodoMutation = useMutation({
49
+
mutationFn: (id: number) => todosAPI.deleteTodo(id),
50
+
onSuccess: () => {
51
+
queryClient.invalidateQueries({ queryKey: ['todos'] })
52
+
toast.success('Todo deleted successfully!')
53
+
},
54
+
onError: () => {
55
+
toast.error('Failed to delete todo')
56
+
},
57
+
})
58
+
59
+
const clearAllMutation = useMutation({
60
+
mutationFn: () => todosAPI.clearTodos(),
61
+
onSuccess: () => {
62
+
queryClient.invalidateQueries({ queryKey: ['todos'] })
63
+
setShowClearConfirm(false)
64
+
toast.success('All todos cleared successfully!')
65
+
},
66
+
onError: () => {
67
+
toast.error('Failed to clear todos')
68
+
},
69
+
})
70
+
71
+
const handleAddTodo = (e: React.FormEvent) => {
72
+
e.preventDefault()
73
+
if (newTodo.trim()) {
74
+
addTodoMutation.mutate(newTodo.trim())
75
+
}
76
+
}
77
+
78
+
const handleToggleTodo = (id: number, done: boolean) => {
79
+
updateTodoMutation.mutate({ id, done: !done })
80
+
}
81
+
82
+
const handleDeleteTodo = (id: number) => {
83
+
deleteTodoMutation.mutate(id)
84
+
}
85
+
86
+
const handleClearAll = () => {
87
+
clearAllMutation.mutate()
88
+
}
89
+
90
+
const completedTodos = todos?.filter((todo: Todo) => todo.done) || []
91
+
const pendingTodos = todos?.filter((todo: Todo) => !todo.done) || []
92
+
93
+
if (isLoading) {
94
+
return (
95
+
<div className="flex items-center justify-center h-64">
96
+
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-discord-blurple"></div>
97
+
</div>
98
+
)
99
+
}
100
+
101
+
return (
102
+
<div className="space-y-6">
103
+
{/* Header */}
104
+
<div className="flex items-center justify-between">
105
+
<div>
106
+
<h1 className="text-2xl font-bold text-white">Todos</h1>
107
+
<p className="text-gray-400">
108
+
Manage your todo list. {todos?.length || 0} total, {completedTodos.length} completed
109
+
</p>
110
+
</div>
111
+
{todos && todos.length > 0 && (
112
+
<button
113
+
onClick={() => setShowClearConfirm(true)}
114
+
className="btn btn-danger"
115
+
disabled={clearAllMutation.isPending}
116
+
>
117
+
<Trash2 className="h-4 w-4 mr-2" />
118
+
Clear All
119
+
</button>
120
+
)}
121
+
</div>
122
+
123
+
{/* Add Todo Form */}
124
+
<div className="card p-6">
125
+
<form onSubmit={handleAddTodo} className="flex gap-3">
126
+
<input
127
+
type="text"
128
+
value={newTodo}
129
+
onChange={(e) => setNewTodo(e.target.value)}
130
+
placeholder="Add a new todo..."
131
+
className="input flex-1"
132
+
disabled={addTodoMutation.isPending}
133
+
/>
134
+
<button
135
+
type="submit"
136
+
disabled={!newTodo.trim() || addTodoMutation.isPending}
137
+
className="btn btn-primary"
138
+
>
139
+
<Plus className="h-4 w-4 mr-2" />
140
+
Add Todo
141
+
</button>
142
+
</form>
143
+
</div>
144
+
145
+
{/* Todos List */}
146
+
{todos && todos.length > 0 ? (
147
+
<div className="space-y-4">
148
+
{/* Pending Todos */}
149
+
{pendingTodos.length > 0 && (
150
+
<div className="bg-gray-900/50 p-6 rounded-lg border border-gray-700">
151
+
<h2 className="text-lg font-medium text-white mb-4">
152
+
Pending ({pendingTodos.length})
153
+
</h2>
154
+
<div className="space-y-3">
155
+
{pendingTodos.map((todo: Todo) => (
156
+
<div
157
+
key={todo.id}
158
+
className="flex items-center justify-between p-3 bg-gray-800/30 rounded-lg"
159
+
>
160
+
<div className="flex items-center space-x-3">
161
+
<button
162
+
onClick={() => handleToggleTodo(todo.id, todo.done)}
163
+
className="flex-shrink-0 w-5 h-5 border-2 border-gray-300 rounded hover:border-green-500 transition-colors"
164
+
disabled={updateTodoMutation.isPending}
165
+
>
166
+
{updateTodoMutation.isPending ? (
167
+
<div className="w-full h-full animate-spin rounded-full border-b border-gray-400"></div>
168
+
) : null}
169
+
</button>
170
+
<span className="text-white">{todo.item}</span>
171
+
</div>
172
+
<button
173
+
onClick={() => handleDeleteTodo(todo.id)}
174
+
className="text-red-600 hover:text-red-800 p-1"
175
+
disabled={deleteTodoMutation.isPending}
176
+
>
177
+
<Trash2 className="h-4 w-4" />
178
+
</button>
179
+
</div>
180
+
))}
181
+
</div>
182
+
</div>
183
+
)}
184
+
185
+
{/* Completed Todos */}
186
+
{completedTodos.length > 0 && (
187
+
<div className="card p-6">
188
+
<h2 className="text-lg font-medium text-white mb-4">
189
+
Completed ({completedTodos.length})
190
+
</h2>
191
+
<div className="space-y-3">
192
+
{completedTodos.map((todo: Todo) => (
193
+
<div
194
+
key={todo.id}
195
+
className="flex items-center justify-between p-3 bg-green-900/20 rounded-lg border border-green-700"
196
+
>
197
+
<div className="flex items-center space-x-3">
198
+
<button
199
+
onClick={() => handleToggleTodo(todo.id, todo.done)}
200
+
className="flex-shrink-0 w-5 h-5 bg-green-500 border-2 border-green-500 rounded flex items-center justify-center hover:bg-green-600 transition-colors"
201
+
disabled={updateTodoMutation.isPending}
202
+
>
203
+
<Check className="h-3 w-3 text-white" />
204
+
</button>
205
+
<span className="text-gray-400 line-through">{todo.item}</span>
206
+
</div>
207
+
<button
208
+
onClick={() => handleDeleteTodo(todo.id)}
209
+
className="text-red-600 hover:text-red-800 p-1"
210
+
disabled={deleteTodoMutation.isPending}
211
+
>
212
+
<Trash2 className="h-4 w-4" />
213
+
</button>
214
+
</div>
215
+
))}
216
+
</div>
217
+
</div>
218
+
)}
219
+
</div>
220
+
) : (
221
+
<div className="card p-12 text-center">
222
+
<AlertCircle className="h-12 w-12 text-gray-400 mx-auto mb-4" />
223
+
<h3 className="text-lg font-medium text-white mb-2">
224
+
No todos yet
225
+
</h3>
226
+
<p className="text-gray-400">
227
+
Create your first todo to get started!
228
+
</p>
229
+
</div>
230
+
)}
231
+
232
+
{/* Clear All Confirmation Modal */}
233
+
{showClearConfirm && (
234
+
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
235
+
<div className="bg-gray-900 rounded-lg p-6 max-w-md w-full mx-4">
236
+
<div className="flex items-center mb-4">
237
+
<AlertCircle className="h-6 w-6 text-red-600 mr-3" />
238
+
<h3 className="text-lg font-medium text-white">
239
+
Clear All Todos
240
+
</h3>
241
+
</div>
242
+
<p className="text-gray-400 mb-6">
243
+
Are you sure you want to clear all todos? This action cannot be undone.
244
+
</p>
245
+
<div className="flex justify-end space-x-3">
246
+
<button
247
+
onClick={() => setShowClearConfirm(false)}
248
+
className="btn btn-secondary"
249
+
disabled={clearAllMutation.isPending}
250
+
>
251
+
Cancel
252
+
</button>
253
+
<button
254
+
onClick={handleClearAll}
255
+
className="btn btn-danger"
256
+
disabled={clearAllMutation.isPending}
257
+
>
258
+
{clearAllMutation.isPending ? 'Clearing...' : 'Clear All'}
259
+
</button>
260
+
</div>
261
+
</div>
262
+
</div>
263
+
)}
264
+
</div>
265
+
)
266
+
}
267
+
268
+
export default TodosPage
+71
web/src/stores/authStore.ts
+71
web/src/stores/authStore.ts
···
1
+
import { create } from 'zustand';
2
+
import { persist } from 'zustand/middleware';
3
+
import axios from 'axios';
4
+
5
+
interface User {
6
+
id: string;
7
+
username: string;
8
+
discriminator: string | null;
9
+
avatar: string | null;
10
+
email?: string;
11
+
}
12
+
13
+
interface AuthState {
14
+
user: User | null;
15
+
token: string | null;
16
+
isAuthenticated: boolean;
17
+
login: (token: string, user: User) => void;
18
+
logout: () => void;
19
+
checkAuth: () => Promise<void>;
20
+
}
21
+
22
+
export const useAuthStore = create<AuthState>()(
23
+
persist(
24
+
(set, get) => ({
25
+
user: null,
26
+
token: null,
27
+
isAuthenticated: false,
28
+
29
+
login: (token: string, user: User) => {
30
+
localStorage.setItem('token', token);
31
+
axios.defaults.headers.common['Authorization'] = `Bearer ${token}`;
32
+
set({ token, user, isAuthenticated: true });
33
+
},
34
+
35
+
logout: () => {
36
+
localStorage.removeItem('token');
37
+
delete axios.defaults.headers.common['Authorization'];
38
+
set({ token: null, user: null, isAuthenticated: false });
39
+
},
40
+
41
+
checkAuth: async () => {
42
+
const token = localStorage.getItem('token');
43
+
if (!token) {
44
+
set({ isAuthenticated: false });
45
+
return;
46
+
}
47
+
48
+
try {
49
+
axios.defaults.headers.common['Authorization'] = `Bearer ${token}`;
50
+
const response = await axios.get('/api/auth/me');
51
+
set({
52
+
token,
53
+
user: response.data.user,
54
+
isAuthenticated: true,
55
+
});
56
+
} catch (error) {
57
+
console.error('Auth check failed:', error);
58
+
get().logout();
59
+
}
60
+
},
61
+
}),
62
+
{
63
+
name: 'auth-storage',
64
+
partialize: (state) => ({
65
+
token: state.token,
66
+
user: state.user,
67
+
isAuthenticated: state.isAuthenticated,
68
+
}),
69
+
}
70
+
)
71
+
);
+42
web/tailwind.config.js
+42
web/tailwind.config.js
···
1
+
/** @type {import('tailwindcss').Config} */
2
+
export default {
3
+
content: ['./index.html', './src/**/*.{js,ts,jsx,tsx}'],
4
+
theme: {
5
+
extend: {
6
+
colors: {
7
+
discord: {
8
+
blurple: '#5865F2',
9
+
dark: '#2C2F33',
10
+
gray: '#99AAB5',
11
+
red: '#F04747',
12
+
},
13
+
primary: {
14
+
50: '#eef2ff',
15
+
100: '#e0e7ff',
16
+
500: '#5865F2',
17
+
600: '#4f46e5',
18
+
700: '#4338ca',
19
+
},
20
+
gray: {
21
+
50: '#f9fafb',
22
+
100: '#f3f4f6',
23
+
200: '#e5e7eb',
24
+
300: '#d1d5db',
25
+
400: '#9ca3af',
26
+
500: '#6b7280',
27
+
600: '#4b5563',
28
+
700: '#374151',
29
+
800: '#1f2937',
30
+
900: '#111827',
31
+
},
32
+
},
33
+
fontFamily: {
34
+
sans: ['Inter', 'system-ui', 'sans-serif'],
35
+
},
36
+
borderRadius: {
37
+
lg: '8px',
38
+
},
39
+
},
40
+
},
41
+
plugins: [],
42
+
};
+25
web/tsconfig.json
+25
web/tsconfig.json
···
1
+
{
2
+
"compilerOptions": {
3
+
"target": "ES2020",
4
+
"useDefineForClassFields": true,
5
+
"lib": ["ES2020", "DOM", "DOM.Iterable"],
6
+
"module": "ESNext",
7
+
"skipLibCheck": true,
8
+
"moduleResolution": "bundler",
9
+
"allowImportingTsExtensions": true,
10
+
"resolveJsonModule": true,
11
+
"isolatedModules": true,
12
+
"noEmit": true,
13
+
"jsx": "react-jsx",
14
+
"strict": true,
15
+
"noUnusedLocals": true,
16
+
"noUnusedParameters": true,
17
+
"noFallthroughCasesInSwitch": true,
18
+
"baseUrl": ".",
19
+
"paths": {
20
+
"@/*": ["./src/*"]
21
+
}
22
+
},
23
+
"include": ["src"],
24
+
"references": [{ "path": "./tsconfig.node.json" }]
25
+
}
+10
web/tsconfig.node.json
+10
web/tsconfig.node.json
+25
web/vite.config.ts
+25
web/vite.config.ts
···
1
+
import { defineConfig } from 'vite';
2
+
import react from '@vitejs/plugin-react';
3
+
import path from 'path';
4
+
5
+
export default defineConfig({
6
+
plugins: [react()],
7
+
resolve: {
8
+
alias: {
9
+
'@': path.resolve(__dirname, './src'),
10
+
},
11
+
},
12
+
server: {
13
+
port: 3000,
14
+
proxy: {
15
+
'/api': {
16
+
target: 'http://localhost:2020',
17
+
changeOrigin: true,
18
+
},
19
+
},
20
+
},
21
+
build: {
22
+
outDir: 'dist',
23
+
sourcemap: true,
24
+
},
25
+
});