aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--frontend/index.html1
-rw-r--r--frontend/package-lock.json701
-rw-r--r--frontend/package.json3
-rw-r--r--frontend/src/App.css229
-rw-r--r--frontend/src/api/Api.ts2
-rw-r--r--frontend/src/components/ConfirmDialog.tsx16
-rw-r--r--frontend/src/components/GameCategory.tsx15
-rw-r--r--frontend/src/components/GameEntry.tsx27
-rw-r--r--frontend/src/components/Leaderboards.tsx6
-rw-r--r--frontend/src/components/Login.tsx53
-rw-r--r--frontend/src/components/ModMenu.tsx1
-rw-r--r--frontend/src/components/Sidebar.tsx386
-rw-r--r--frontend/src/components/Summary.tsx1
-rw-r--r--frontend/src/components/UploadRunDialog.tsx1
-rw-r--r--frontend/src/images/Images.tsx4
-rw-r--r--frontend/src/images/svgs/steam.tsx7
-rw-r--r--frontend/src/pages/About.tsx4
-rw-r--r--frontend/src/pages/Games.tsx32
-rw-r--r--frontend/src/pages/Homepage.tsx12
-rw-r--r--frontend/src/pages/Maplist.tsx131
-rw-r--r--frontend/src/pages/Maps.tsx15
-rw-r--r--frontend/src/pages/Profile.tsx1
-rw-r--r--frontend/src/pages/Rules.tsx4
-rw-r--r--frontend/src/pages/User.tsx1
-rw-r--r--frontend/vite.config.ts5
25 files changed, 1227 insertions, 431 deletions
diff --git a/frontend/index.html b/frontend/index.html
index 8578eba..7538ee9 100644
--- a/frontend/index.html
+++ b/frontend/index.html
@@ -7,6 +7,7 @@
7 <meta name="viewport" content="width=device-width, initial-scale=1" /> 7 <meta name="viewport" content="width=device-width, initial-scale=1" />
8 <meta name="theme-color" content="#000000" /> 8 <meta name="theme-color" content="#000000" />
9 <link rel="apple-touch-icon" href="/logo192.png" /> 9 <link rel="apple-touch-icon" href="/logo192.png" />
10 <link href="/src/style.css" rel="stylesheet">
10 <link rel="manifest" href="/manifest.json" /> 11 <link rel="manifest" href="/manifest.json" />
11 <title>LPHUB</title> 12 <title>LPHUB</title>
12</head> 13</head>
diff --git a/frontend/package-lock.json b/frontend/package-lock.json
index 9bc560f..92fcc9b 100644
--- a/frontend/package-lock.json
+++ b/frontend/package-lock.json
@@ -9,6 +9,7 @@
9 "version": "0.1.0", 9 "version": "0.1.0",
10 "dependencies": { 10 "dependencies": {
11 "@nekz/sdp": "^0.9.0", 11 "@nekz/sdp": "^0.9.0",
12 "@tailwindcss/vite": "^4.1.11",
12 "@testing-library/jest-dom": "^5.17.0", 13 "@testing-library/jest-dom": "^5.17.0",
13 "@testing-library/react": "^13.4.0", 14 "@testing-library/react": "^13.4.0",
14 "@testing-library/user-event": "^13.5.0", 15 "@testing-library/user-event": "^13.5.0",
@@ -21,6 +22,7 @@
21 "react-helmet": "^6.1.0", 22 "react-helmet": "^6.1.0",
22 "react-markdown": "^9.0.1", 23 "react-markdown": "^9.0.1",
23 "react-router-dom": "^6.26.1", 24 "react-router-dom": "^6.26.1",
25 "tailwindcss": "^4.1.11",
24 "typescript": "^4.9.5", 26 "typescript": "^4.9.5",
25 "web-vitals": "^2.1.4" 27 "web-vitals": "^2.1.4"
26 }, 28 },
@@ -42,7 +44,6 @@
42 "version": "2.3.0", 44 "version": "2.3.0",
43 "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", 45 "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz",
44 "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==", 46 "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==",
45 "dev": true,
46 "license": "Apache-2.0", 47 "license": "Apache-2.0",
47 "dependencies": { 48 "dependencies": {
48 "@jridgewell/gen-mapping": "^0.3.5", 49 "@jridgewell/gen-mapping": "^0.3.5",
@@ -348,7 +349,6 @@
348 "cpu": [ 349 "cpu": [
349 "ppc64" 350 "ppc64"
350 ], 351 ],
351 "dev": true,
352 "license": "MIT", 352 "license": "MIT",
353 "optional": true, 353 "optional": true,
354 "os": [ 354 "os": [
@@ -365,7 +365,6 @@
365 "cpu": [ 365 "cpu": [
366 "arm" 366 "arm"
367 ], 367 ],
368 "dev": true,
369 "license": "MIT", 368 "license": "MIT",
370 "optional": true, 369 "optional": true,
371 "os": [ 370 "os": [
@@ -382,7 +381,6 @@
382 "cpu": [ 381 "cpu": [
383 "arm64" 382 "arm64"
384 ], 383 ],
385 "dev": true,
386 "license": "MIT", 384 "license": "MIT",
387 "optional": true, 385 "optional": true,
388 "os": [ 386 "os": [
@@ -399,7 +397,6 @@
399 "cpu": [ 397 "cpu": [
400 "x64" 398 "x64"
401 ], 399 ],
402 "dev": true,
403 "license": "MIT", 400 "license": "MIT",
404 "optional": true, 401 "optional": true,
405 "os": [ 402 "os": [
@@ -416,7 +413,6 @@
416 "cpu": [ 413 "cpu": [
417 "arm64" 414 "arm64"
418 ], 415 ],
419 "dev": true,
420 "license": "MIT", 416 "license": "MIT",
421 "optional": true, 417 "optional": true,
422 "os": [ 418 "os": [
@@ -433,7 +429,6 @@
433 "cpu": [ 429 "cpu": [
434 "x64" 430 "x64"
435 ], 431 ],
436 "dev": true,
437 "license": "MIT", 432 "license": "MIT",
438 "optional": true, 433 "optional": true,
439 "os": [ 434 "os": [
@@ -450,7 +445,6 @@
450 "cpu": [ 445 "cpu": [
451 "arm64" 446 "arm64"
452 ], 447 ],
453 "dev": true,
454 "license": "MIT", 448 "license": "MIT",
455 "optional": true, 449 "optional": true,
456 "os": [ 450 "os": [
@@ -467,7 +461,6 @@
467 "cpu": [ 461 "cpu": [
468 "x64" 462 "x64"
469 ], 463 ],
470 "dev": true,
471 "license": "MIT", 464 "license": "MIT",
472 "optional": true, 465 "optional": true,
473 "os": [ 466 "os": [
@@ -484,7 +477,6 @@
484 "cpu": [ 477 "cpu": [
485 "arm" 478 "arm"
486 ], 479 ],
487 "dev": true,
488 "license": "MIT", 480 "license": "MIT",
489 "optional": true, 481 "optional": true,
490 "os": [ 482 "os": [
@@ -501,7 +493,6 @@
501 "cpu": [ 493 "cpu": [
502 "arm64" 494 "arm64"
503 ], 495 ],
504 "dev": true,
505 "license": "MIT", 496 "license": "MIT",
506 "optional": true, 497 "optional": true,
507 "os": [ 498 "os": [
@@ -518,7 +509,6 @@
518 "cpu": [ 509 "cpu": [
519 "ia32" 510 "ia32"
520 ], 511 ],
521 "dev": true,
522 "license": "MIT", 512 "license": "MIT",
523 "optional": true, 513 "optional": true,
524 "os": [ 514 "os": [
@@ -535,7 +525,6 @@
535 "cpu": [ 525 "cpu": [
536 "loong64" 526 "loong64"
537 ], 527 ],
538 "dev": true,
539 "license": "MIT", 528 "license": "MIT",
540 "optional": true, 529 "optional": true,
541 "os": [ 530 "os": [
@@ -552,7 +541,6 @@
552 "cpu": [ 541 "cpu": [
553 "mips64el" 542 "mips64el"
554 ], 543 ],
555 "dev": true,
556 "license": "MIT", 544 "license": "MIT",
557 "optional": true, 545 "optional": true,
558 "os": [ 546 "os": [
@@ -569,7 +557,6 @@
569 "cpu": [ 557 "cpu": [
570 "ppc64" 558 "ppc64"
571 ], 559 ],
572 "dev": true,
573 "license": "MIT", 560 "license": "MIT",
574 "optional": true, 561 "optional": true,
575 "os": [ 562 "os": [
@@ -586,7 +573,6 @@
586 "cpu": [ 573 "cpu": [
587 "riscv64" 574 "riscv64"
588 ], 575 ],
589 "dev": true,
590 "license": "MIT", 576 "license": "MIT",
591 "optional": true, 577 "optional": true,
592 "os": [ 578 "os": [
@@ -603,7 +589,6 @@
603 "cpu": [ 589 "cpu": [
604 "s390x" 590 "s390x"
605 ], 591 ],
606 "dev": true,
607 "license": "MIT", 592 "license": "MIT",
608 "optional": true, 593 "optional": true,
609 "os": [ 594 "os": [
@@ -620,7 +605,6 @@
620 "cpu": [ 605 "cpu": [
621 "x64" 606 "x64"
622 ], 607 ],
623 "dev": true,
624 "license": "MIT", 608 "license": "MIT",
625 "optional": true, 609 "optional": true,
626 "os": [ 610 "os": [
@@ -637,7 +621,6 @@
637 "cpu": [ 621 "cpu": [
638 "arm64" 622 "arm64"
639 ], 623 ],
640 "dev": true,
641 "license": "MIT", 624 "license": "MIT",
642 "optional": true, 625 "optional": true,
643 "os": [ 626 "os": [
@@ -654,7 +637,6 @@
654 "cpu": [ 637 "cpu": [
655 "x64" 638 "x64"
656 ], 639 ],
657 "dev": true,
658 "license": "MIT", 640 "license": "MIT",
659 "optional": true, 641 "optional": true,
660 "os": [ 642 "os": [
@@ -671,7 +653,6 @@
671 "cpu": [ 653 "cpu": [
672 "arm64" 654 "arm64"
673 ], 655 ],
674 "dev": true,
675 "license": "MIT", 656 "license": "MIT",
676 "optional": true, 657 "optional": true,
677 "os": [ 658 "os": [
@@ -688,7 +669,6 @@
688 "cpu": [ 669 "cpu": [
689 "x64" 670 "x64"
690 ], 671 ],
691 "dev": true,
692 "license": "MIT", 672 "license": "MIT",
693 "optional": true, 673 "optional": true,
694 "os": [ 674 "os": [
@@ -705,7 +685,6 @@
705 "cpu": [ 685 "cpu": [
706 "arm64" 686 "arm64"
707 ], 687 ],
708 "dev": true,
709 "license": "MIT", 688 "license": "MIT",
710 "optional": true, 689 "optional": true,
711 "os": [ 690 "os": [
@@ -722,7 +701,6 @@
722 "cpu": [ 701 "cpu": [
723 "x64" 702 "x64"
724 ], 703 ],
725 "dev": true,
726 "license": "MIT", 704 "license": "MIT",
727 "optional": true, 705 "optional": true,
728 "os": [ 706 "os": [
@@ -739,7 +717,6 @@
739 "cpu": [ 717 "cpu": [
740 "arm64" 718 "arm64"
741 ], 719 ],
742 "dev": true,
743 "license": "MIT", 720 "license": "MIT",
744 "optional": true, 721 "optional": true,
745 "os": [ 722 "os": [
@@ -756,7 +733,6 @@
756 "cpu": [ 733 "cpu": [
757 "ia32" 734 "ia32"
758 ], 735 ],
759 "dev": true,
760 "license": "MIT", 736 "license": "MIT",
761 "optional": true, 737 "optional": true,
762 "os": [ 738 "os": [
@@ -773,7 +749,6 @@
773 "cpu": [ 749 "cpu": [
774 "x64" 750 "x64"
775 ], 751 ],
776 "dev": true,
777 "license": "MIT", 752 "license": "MIT",
778 "optional": true, 753 "optional": true,
779 "os": [ 754 "os": [
@@ -783,11 +758,22 @@
783 "node": ">=18" 758 "node": ">=18"
784 } 759 }
785 }, 760 },
761 "node_modules/@isaacs/fs-minipass": {
762 "version": "4.0.1",
763 "resolved": "https://registry.npmjs.org/@isaacs/fs-minipass/-/fs-minipass-4.0.1.tgz",
764 "integrity": "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==",
765 "license": "ISC",
766 "dependencies": {
767 "minipass": "^7.0.4"
768 },
769 "engines": {
770 "node": ">=18.0.0"
771 }
772 },
786 "node_modules/@jridgewell/gen-mapping": { 773 "node_modules/@jridgewell/gen-mapping": {
787 "version": "0.3.12", 774 "version": "0.3.12",
788 "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.12.tgz", 775 "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.12.tgz",
789 "integrity": "sha512-OuLGC46TjB5BbN1dH8JULVVZY4WTdkF7tV9Ys6wLL1rubZnCMstOhNHueU5bLCrnRuDhKPDM4g6sw4Bel5Gzqg==", 776 "integrity": "sha512-OuLGC46TjB5BbN1dH8JULVVZY4WTdkF7tV9Ys6wLL1rubZnCMstOhNHueU5bLCrnRuDhKPDM4g6sw4Bel5Gzqg==",
790 "dev": true,
791 "license": "MIT", 777 "license": "MIT",
792 "dependencies": { 778 "dependencies": {
793 "@jridgewell/sourcemap-codec": "^1.5.0", 779 "@jridgewell/sourcemap-codec": "^1.5.0",
@@ -798,7 +784,6 @@
798 "version": "3.1.2", 784 "version": "3.1.2",
799 "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", 785 "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
800 "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", 786 "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
801 "dev": true,
802 "license": "MIT", 787 "license": "MIT",
803 "engines": { 788 "engines": {
804 "node": ">=6.0.0" 789 "node": ">=6.0.0"
@@ -808,14 +793,12 @@
808 "version": "1.5.4", 793 "version": "1.5.4",
809 "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.4.tgz", 794 "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.4.tgz",
810 "integrity": "sha512-VT2+G1VQs/9oz078bLrYbecdZKs912zQlkelYpuf+SXF+QvZDYJlbx/LSx+meSAwdDFnF8FVXW92AVjjkVmgFw==", 795 "integrity": "sha512-VT2+G1VQs/9oz078bLrYbecdZKs912zQlkelYpuf+SXF+QvZDYJlbx/LSx+meSAwdDFnF8FVXW92AVjjkVmgFw==",
811 "dev": true,
812 "license": "MIT" 796 "license": "MIT"
813 }, 797 },
814 "node_modules/@jridgewell/trace-mapping": { 798 "node_modules/@jridgewell/trace-mapping": {
815 "version": "0.3.29", 799 "version": "0.3.29",
816 "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.29.tgz", 800 "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.29.tgz",
817 "integrity": "sha512-uw6guiW/gcAGPDhLmd77/6lW8QLeiV5RUTsAX46Db6oLhGaVj4lhnPwb184s1bkc8kdVg/+h988dro8GRDpmYQ==", 801 "integrity": "sha512-uw6guiW/gcAGPDhLmd77/6lW8QLeiV5RUTsAX46Db6oLhGaVj4lhnPwb184s1bkc8kdVg/+h988dro8GRDpmYQ==",
818 "dev": true,
819 "license": "MIT", 802 "license": "MIT",
820 "dependencies": { 803 "dependencies": {
821 "@jridgewell/resolve-uri": "^3.1.0", 804 "@jridgewell/resolve-uri": "^3.1.0",
@@ -851,7 +834,6 @@
851 "cpu": [ 834 "cpu": [
852 "arm" 835 "arm"
853 ], 836 ],
854 "dev": true,
855 "license": "MIT", 837 "license": "MIT",
856 "optional": true, 838 "optional": true,
857 "os": [ 839 "os": [
@@ -865,7 +847,6 @@
865 "cpu": [ 847 "cpu": [
866 "arm64" 848 "arm64"
867 ], 849 ],
868 "dev": true,
869 "license": "MIT", 850 "license": "MIT",
870 "optional": true, 851 "optional": true,
871 "os": [ 852 "os": [
@@ -879,7 +860,6 @@
879 "cpu": [ 860 "cpu": [
880 "arm64" 861 "arm64"
881 ], 862 ],
882 "dev": true,
883 "license": "MIT", 863 "license": "MIT",
884 "optional": true, 864 "optional": true,
885 "os": [ 865 "os": [
@@ -893,7 +873,6 @@
893 "cpu": [ 873 "cpu": [
894 "x64" 874 "x64"
895 ], 875 ],
896 "dev": true,
897 "license": "MIT", 876 "license": "MIT",
898 "optional": true, 877 "optional": true,
899 "os": [ 878 "os": [
@@ -907,7 +886,6 @@
907 "cpu": [ 886 "cpu": [
908 "arm64" 887 "arm64"
909 ], 888 ],
910 "dev": true,
911 "license": "MIT", 889 "license": "MIT",
912 "optional": true, 890 "optional": true,
913 "os": [ 891 "os": [
@@ -921,7 +899,6 @@
921 "cpu": [ 899 "cpu": [
922 "x64" 900 "x64"
923 ], 901 ],
924 "dev": true,
925 "license": "MIT", 902 "license": "MIT",
926 "optional": true, 903 "optional": true,
927 "os": [ 904 "os": [
@@ -935,7 +912,6 @@
935 "cpu": [ 912 "cpu": [
936 "arm" 913 "arm"
937 ], 914 ],
938 "dev": true,
939 "license": "MIT", 915 "license": "MIT",
940 "optional": true, 916 "optional": true,
941 "os": [ 917 "os": [
@@ -949,7 +925,6 @@
949 "cpu": [ 925 "cpu": [
950 "arm" 926 "arm"
951 ], 927 ],
952 "dev": true,
953 "license": "MIT", 928 "license": "MIT",
954 "optional": true, 929 "optional": true,
955 "os": [ 930 "os": [
@@ -963,7 +938,6 @@
963 "cpu": [ 938 "cpu": [
964 "arm64" 939 "arm64"
965 ], 940 ],
966 "dev": true,
967 "license": "MIT", 941 "license": "MIT",
968 "optional": true, 942 "optional": true,
969 "os": [ 943 "os": [
@@ -977,7 +951,6 @@
977 "cpu": [ 951 "cpu": [
978 "arm64" 952 "arm64"
979 ], 953 ],
980 "dev": true,
981 "license": "MIT", 954 "license": "MIT",
982 "optional": true, 955 "optional": true,
983 "os": [ 956 "os": [
@@ -991,7 +964,6 @@
991 "cpu": [ 964 "cpu": [
992 "loong64" 965 "loong64"
993 ], 966 ],
994 "dev": true,
995 "license": "MIT", 967 "license": "MIT",
996 "optional": true, 968 "optional": true,
997 "os": [ 969 "os": [
@@ -1005,7 +977,6 @@
1005 "cpu": [ 977 "cpu": [
1006 "ppc64" 978 "ppc64"
1007 ], 979 ],
1008 "dev": true,
1009 "license": "MIT", 980 "license": "MIT",
1010 "optional": true, 981 "optional": true,
1011 "os": [ 982 "os": [
@@ -1019,7 +990,6 @@
1019 "cpu": [ 990 "cpu": [
1020 "riscv64" 991 "riscv64"
1021 ], 992 ],
1022 "dev": true,
1023 "license": "MIT", 993 "license": "MIT",
1024 "optional": true, 994 "optional": true,
1025 "os": [ 995 "os": [
@@ -1033,7 +1003,6 @@
1033 "cpu": [ 1003 "cpu": [
1034 "riscv64" 1004 "riscv64"
1035 ], 1005 ],
1036 "dev": true,
1037 "license": "MIT", 1006 "license": "MIT",
1038 "optional": true, 1007 "optional": true,
1039 "os": [ 1008 "os": [
@@ -1047,7 +1016,6 @@
1047 "cpu": [ 1016 "cpu": [
1048 "s390x" 1017 "s390x"
1049 ], 1018 ],
1050 "dev": true,
1051 "license": "MIT", 1019 "license": "MIT",
1052 "optional": true, 1020 "optional": true,
1053 "os": [ 1021 "os": [
@@ -1061,7 +1029,6 @@
1061 "cpu": [ 1029 "cpu": [
1062 "x64" 1030 "x64"
1063 ], 1031 ],
1064 "dev": true,
1065 "license": "MIT", 1032 "license": "MIT",
1066 "optional": true, 1033 "optional": true,
1067 "os": [ 1034 "os": [
@@ -1075,7 +1042,6 @@
1075 "cpu": [ 1042 "cpu": [
1076 "x64" 1043 "x64"
1077 ], 1044 ],
1078 "dev": true,
1079 "license": "MIT", 1045 "license": "MIT",
1080 "optional": true, 1046 "optional": true,
1081 "os": [ 1047 "os": [
@@ -1089,7 +1055,6 @@
1089 "cpu": [ 1055 "cpu": [
1090 "arm64" 1056 "arm64"
1091 ], 1057 ],
1092 "dev": true,
1093 "license": "MIT", 1058 "license": "MIT",
1094 "optional": true, 1059 "optional": true,
1095 "os": [ 1060 "os": [
@@ -1103,7 +1068,6 @@
1103 "cpu": [ 1068 "cpu": [
1104 "ia32" 1069 "ia32"
1105 ], 1070 ],
1106 "dev": true,
1107 "license": "MIT", 1071 "license": "MIT",
1108 "optional": true, 1072 "optional": true,
1109 "os": [ 1073 "os": [
@@ -1117,13 +1081,274 @@
1117 "cpu": [ 1081 "cpu": [
1118 "x64" 1082 "x64"
1119 ], 1083 ],
1120 "dev": true,
1121 "license": "MIT", 1084 "license": "MIT",
1122 "optional": true, 1085 "optional": true,
1123 "os": [ 1086 "os": [
1124 "win32" 1087 "win32"
1125 ] 1088 ]
1126 }, 1089 },
1090 "node_modules/@tailwindcss/node": {
1091 "version": "4.1.11",
1092 "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.1.11.tgz",
1093 "integrity": "sha512-yzhzuGRmv5QyU9qLNg4GTlYI6STedBWRE7NjxP45CsFYYq9taI0zJXZBMqIC/c8fViNLhmrbpSFS57EoxUmD6Q==",
1094 "license": "MIT",
1095 "dependencies": {
1096 "@ampproject/remapping": "^2.3.0",
1097 "enhanced-resolve": "^5.18.1",
1098 "jiti": "^2.4.2",
1099 "lightningcss": "1.30.1",
1100 "magic-string": "^0.30.17",
1101 "source-map-js": "^1.2.1",
1102 "tailwindcss": "4.1.11"
1103 }
1104 },
1105 "node_modules/@tailwindcss/oxide": {
1106 "version": "4.1.11",
1107 "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.1.11.tgz",
1108 "integrity": "sha512-Q69XzrtAhuyfHo+5/HMgr1lAiPP/G40OMFAnws7xcFEYqcypZmdW8eGXaOUIeOl1dzPJBPENXgbjsOyhg2nkrg==",
1109 "hasInstallScript": true,
1110 "license": "MIT",
1111 "dependencies": {
1112 "detect-libc": "^2.0.4",
1113 "tar": "^7.4.3"
1114 },
1115 "engines": {
1116 "node": ">= 10"
1117 },
1118 "optionalDependencies": {
1119 "@tailwindcss/oxide-android-arm64": "4.1.11",
1120 "@tailwindcss/oxide-darwin-arm64": "4.1.11",
1121 "@tailwindcss/oxide-darwin-x64": "4.1.11",
1122 "@tailwindcss/oxide-freebsd-x64": "4.1.11",
1123 "@tailwindcss/oxide-linux-arm-gnueabihf": "4.1.11",
1124 "@tailwindcss/oxide-linux-arm64-gnu": "4.1.11",
1125 "@tailwindcss/oxide-linux-arm64-musl": "4.1.11",
1126 "@tailwindcss/oxide-linux-x64-gnu": "4.1.11",
1127 "@tailwindcss/oxide-linux-x64-musl": "4.1.11",
1128 "@tailwindcss/oxide-wasm32-wasi": "4.1.11",
1129 "@tailwindcss/oxide-win32-arm64-msvc": "4.1.11",
1130 "@tailwindcss/oxide-win32-x64-msvc": "4.1.11"
1131 }
1132 },
1133 "node_modules/@tailwindcss/oxide-android-arm64": {
1134 "version": "4.1.11",
1135 "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.1.11.tgz",
1136 "integrity": "sha512-3IfFuATVRUMZZprEIx9OGDjG3Ou3jG4xQzNTvjDoKmU9JdmoCohQJ83MYd0GPnQIu89YoJqvMM0G3uqLRFtetg==",
1137 "cpu": [
1138 "arm64"
1139 ],
1140 "license": "MIT",
1141 "optional": true,
1142 "os": [
1143 "android"
1144 ],
1145 "engines": {
1146 "node": ">= 10"
1147 }
1148 },
1149 "node_modules/@tailwindcss/oxide-darwin-arm64": {
1150 "version": "4.1.11",
1151 "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.1.11.tgz",
1152 "integrity": "sha512-ESgStEOEsyg8J5YcMb1xl8WFOXfeBmrhAwGsFxxB2CxY9evy63+AtpbDLAyRkJnxLy2WsD1qF13E97uQyP1lfQ==",
1153 "cpu": [
1154 "arm64"
1155 ],
1156 "license": "MIT",
1157 "optional": true,
1158 "os": [
1159 "darwin"
1160 ],
1161 "engines": {
1162 "node": ">= 10"
1163 }
1164 },
1165 "node_modules/@tailwindcss/oxide-darwin-x64": {
1166 "version": "4.1.11",
1167 "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.1.11.tgz",
1168 "integrity": "sha512-EgnK8kRchgmgzG6jE10UQNaH9Mwi2n+yw1jWmof9Vyg2lpKNX2ioe7CJdf9M5f8V9uaQxInenZkOxnTVL3fhAw==",
1169 "cpu": [
1170 "x64"
1171 ],
1172 "license": "MIT",
1173 "optional": true,
1174 "os": [
1175 "darwin"
1176 ],
1177 "engines": {
1178 "node": ">= 10"
1179 }
1180 },
1181 "node_modules/@tailwindcss/oxide-freebsd-x64": {
1182 "version": "4.1.11",
1183 "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.1.11.tgz",
1184 "integrity": "sha512-xdqKtbpHs7pQhIKmqVpxStnY1skuNh4CtbcyOHeX1YBE0hArj2romsFGb6yUmzkq/6M24nkxDqU8GYrKrz+UcA==",
1185 "cpu": [
1186 "x64"
1187 ],
1188 "license": "MIT",
1189 "optional": true,
1190 "os": [
1191 "freebsd"
1192 ],
1193 "engines": {
1194 "node": ">= 10"
1195 }
1196 },
1197 "node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": {
1198 "version": "4.1.11",
1199 "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.1.11.tgz",
1200 "integrity": "sha512-ryHQK2eyDYYMwB5wZL46uoxz2zzDZsFBwfjssgB7pzytAeCCa6glsiJGjhTEddq/4OsIjsLNMAiMlHNYnkEEeg==",
1201 "cpu": [
1202 "arm"
1203 ],
1204 "license": "MIT",
1205 "optional": true,
1206 "os": [
1207 "linux"
1208 ],
1209 "engines": {
1210 "node": ">= 10"
1211 }
1212 },
1213 "node_modules/@tailwindcss/oxide-linux-arm64-gnu": {
1214 "version": "4.1.11",
1215 "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.1.11.tgz",
1216 "integrity": "sha512-mYwqheq4BXF83j/w75ewkPJmPZIqqP1nhoghS9D57CLjsh3Nfq0m4ftTotRYtGnZd3eCztgbSPJ9QhfC91gDZQ==",
1217 "cpu": [
1218 "arm64"
1219 ],
1220 "license": "MIT",
1221 "optional": true,
1222 "os": [
1223 "linux"
1224 ],
1225 "engines": {
1226 "node": ">= 10"
1227 }
1228 },
1229 "node_modules/@tailwindcss/oxide-linux-arm64-musl": {
1230 "version": "4.1.11",
1231 "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.1.11.tgz",
1232 "integrity": "sha512-m/NVRFNGlEHJrNVk3O6I9ggVuNjXHIPoD6bqay/pubtYC9QIdAMpS+cswZQPBLvVvEF6GtSNONbDkZrjWZXYNQ==",
1233 "cpu": [
1234 "arm64"
1235 ],
1236 "license": "MIT",
1237 "optional": true,
1238 "os": [
1239 "linux"
1240 ],
1241 "engines": {
1242 "node": ">= 10"
1243 }
1244 },
1245 "node_modules/@tailwindcss/oxide-linux-x64-gnu": {
1246 "version": "4.1.11",
1247 "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.1.11.tgz",
1248 "integrity": "sha512-YW6sblI7xukSD2TdbbaeQVDysIm/UPJtObHJHKxDEcW2exAtY47j52f8jZXkqE1krdnkhCMGqP3dbniu1Te2Fg==",
1249 "cpu": [
1250 "x64"
1251 ],
1252 "license": "MIT",
1253 "optional": true,
1254 "os": [
1255 "linux"
1256 ],
1257 "engines": {
1258 "node": ">= 10"
1259 }
1260 },
1261 "node_modules/@tailwindcss/oxide-linux-x64-musl": {
1262 "version": "4.1.11",
1263 "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.1.11.tgz",
1264 "integrity": "sha512-e3C/RRhGunWYNC3aSF7exsQkdXzQ/M+aYuZHKnw4U7KQwTJotnWsGOIVih0s2qQzmEzOFIJ3+xt7iq67K/p56Q==",
1265 "cpu": [
1266 "x64"
1267 ],
1268 "license": "MIT",
1269 "optional": true,
1270 "os": [
1271 "linux"
1272 ],
1273 "engines": {
1274 "node": ">= 10"
1275 }
1276 },
1277 "node_modules/@tailwindcss/oxide-wasm32-wasi": {
1278 "version": "4.1.11",
1279 "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.1.11.tgz",
1280 "integrity": "sha512-Xo1+/GU0JEN/C/dvcammKHzeM6NqKovG+6921MR6oadee5XPBaKOumrJCXvopJ/Qb5TH7LX/UAywbqrP4lax0g==",
1281 "bundleDependencies": [
1282 "@napi-rs/wasm-runtime",
1283 "@emnapi/core",
1284 "@emnapi/runtime",
1285 "@tybys/wasm-util",
1286 "@emnapi/wasi-threads",
1287 "tslib"
1288 ],
1289 "cpu": [
1290 "wasm32"
1291 ],
1292 "license": "MIT",
1293 "optional": true,
1294 "dependencies": {
1295 "@emnapi/core": "^1.4.3",
1296 "@emnapi/runtime": "^1.4.3",
1297 "@emnapi/wasi-threads": "^1.0.2",
1298 "@napi-rs/wasm-runtime": "^0.2.11",
1299 "@tybys/wasm-util": "^0.9.0",
1300 "tslib": "^2.8.0"
1301 },
1302 "engines": {
1303 "node": ">=14.0.0"
1304 }
1305 },
1306 "node_modules/@tailwindcss/oxide-win32-arm64-msvc": {
1307 "version": "4.1.11",
1308 "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.1.11.tgz",
1309 "integrity": "sha512-UgKYx5PwEKrac3GPNPf6HVMNhUIGuUh4wlDFR2jYYdkX6pL/rn73zTq/4pzUm8fOjAn5L8zDeHp9iXmUGOXZ+w==",
1310 "cpu": [
1311 "arm64"
1312 ],
1313 "license": "MIT",
1314 "optional": true,
1315 "os": [
1316 "win32"
1317 ],
1318 "engines": {
1319 "node": ">= 10"
1320 }
1321 },
1322 "node_modules/@tailwindcss/oxide-win32-x64-msvc": {
1323 "version": "4.1.11",
1324 "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.1.11.tgz",
1325 "integrity": "sha512-YfHoggn1j0LK7wR82TOucWc5LDCguHnoS879idHekmmiR7g9HUtMw9MI0NHatS28u/Xlkfi9w5RJWgz2Dl+5Qg==",
1326 "cpu": [
1327 "x64"
1328 ],
1329 "license": "MIT",
1330 "optional": true,
1331 "os": [
1332 "win32"
1333 ],
1334 "engines": {
1335 "node": ">= 10"
1336 }
1337 },
1338 "node_modules/@tailwindcss/vite": {
1339 "version": "4.1.11",
1340 "resolved": "https://registry.npmjs.org/@tailwindcss/vite/-/vite-4.1.11.tgz",
1341 "integrity": "sha512-RHYhrR3hku0MJFRV+fN2gNbDNEh3dwKvY8XJvTxCSXeMOsCRSr+uKvDWQcbizrHgjML6ZmTE5OwMrl5wKcujCw==",
1342 "license": "MIT",
1343 "dependencies": {
1344 "@tailwindcss/node": "4.1.11",
1345 "@tailwindcss/oxide": "4.1.11",
1346 "tailwindcss": "4.1.11"
1347 },
1348 "peerDependencies": {
1349 "vite": "^5.2.0 || ^6 || ^7"
1350 }
1351 },
1127 "node_modules/@testing-library/dom": { 1352 "node_modules/@testing-library/dom": {
1128 "version": "10.4.0", 1353 "version": "10.4.0",
1129 "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.0.tgz", 1354 "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.0.tgz",
@@ -1354,7 +1579,7 @@
1354 "version": "20.19.9", 1579 "version": "20.19.9",
1355 "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.9.tgz", 1580 "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.9.tgz",
1356 "integrity": "sha512-cuVNgarYWZqxRJDQHEB58GEONhOK79QVR/qYx4S7kcUObQvUwvFnYxJuuHUKm2aieN9X3yZB4LZsuYNU1Qphsw==", 1581 "integrity": "sha512-cuVNgarYWZqxRJDQHEB58GEONhOK79QVR/qYx4S7kcUObQvUwvFnYxJuuHUKm2aieN9X3yZB4LZsuYNU1Qphsw==",
1357 "dev": true, 1582 "devOptional": true,
1358 "license": "MIT", 1583 "license": "MIT",
1359 "dependencies": { 1584 "dependencies": {
1360 "undici-types": "~6.21.0" 1585 "undici-types": "~6.21.0"
@@ -1695,6 +1920,15 @@
1695 "url": "https://github.com/sponsors/wooorm" 1920 "url": "https://github.com/sponsors/wooorm"
1696 } 1921 }
1697 }, 1922 },
1923 "node_modules/chownr": {
1924 "version": "3.0.0",
1925 "resolved": "https://registry.npmjs.org/chownr/-/chownr-3.0.0.tgz",
1926 "integrity": "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==",
1927 "license": "BlueOak-1.0.0",
1928 "engines": {
1929 "node": ">=18"
1930 }
1931 },
1698 "node_modules/color-convert": { 1932 "node_modules/color-convert": {
1699 "version": "2.0.1", 1933 "version": "2.0.1",
1700 "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", 1934 "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
@@ -1868,6 +2102,15 @@
1868 "node": ">=6" 2102 "node": ">=6"
1869 } 2103 }
1870 }, 2104 },
2105 "node_modules/detect-libc": {
2106 "version": "2.0.4",
2107 "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.4.tgz",
2108 "integrity": "sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA==",
2109 "license": "Apache-2.0",
2110 "engines": {
2111 "node": ">=8"
2112 }
2113 },
1871 "node_modules/devlop": { 2114 "node_modules/devlop": {
1872 "version": "1.1.0", 2115 "version": "1.1.0",
1873 "resolved": "https://registry.npmjs.org/devlop/-/devlop-1.1.0.tgz", 2116 "resolved": "https://registry.npmjs.org/devlop/-/devlop-1.1.0.tgz",
@@ -1917,6 +2160,19 @@
1917 "dev": true, 2160 "dev": true,
1918 "license": "ISC" 2161 "license": "ISC"
1919 }, 2162 },
2163 "node_modules/enhanced-resolve": {
2164 "version": "5.18.3",
2165 "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.3.tgz",
2166 "integrity": "sha512-d4lC8xfavMeBjzGr2vECC3fsGXziXZQyJxD868h2M/mBI3PwAuODxAkLkq5HYuvrPYcUtiLzsTo8U3PgX3Ocww==",
2167 "license": "MIT",
2168 "dependencies": {
2169 "graceful-fs": "^4.2.4",
2170 "tapable": "^2.2.0"
2171 },
2172 "engines": {
2173 "node": ">=10.13.0"
2174 }
2175 },
1920 "node_modules/es-define-property": { 2176 "node_modules/es-define-property": {
1921 "version": "1.0.1", 2177 "version": "1.0.1",
1922 "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", 2178 "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
@@ -1986,7 +2242,6 @@
1986 "version": "0.25.8", 2242 "version": "0.25.8",
1987 "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.8.tgz", 2243 "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.8.tgz",
1988 "integrity": "sha512-vVC0USHGtMi8+R4Kz8rt6JhEWLxsv9Rnu/lGYbPR8u47B+DCBksq9JarW0zOO7bs37hyOK1l2/oqtbciutL5+Q==", 2244 "integrity": "sha512-vVC0USHGtMi8+R4Kz8rt6JhEWLxsv9Rnu/lGYbPR8u47B+DCBksq9JarW0zOO7bs37hyOK1l2/oqtbciutL5+Q==",
1989 "dev": true,
1990 "hasInstallScript": true, 2245 "hasInstallScript": true,
1991 "license": "MIT", 2246 "license": "MIT",
1992 "bin": { 2247 "bin": {
@@ -2054,7 +2309,6 @@
2054 "version": "6.4.6", 2309 "version": "6.4.6",
2055 "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.6.tgz", 2310 "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.6.tgz",
2056 "integrity": "sha512-hiFoqpyZcfNm1yc4u8oWCf9A2c4D3QjCrks3zmoVKVxpQRzmPNar1hUJcBG2RQHvEVGDN+Jm81ZheVLAQMK6+w==", 2311 "integrity": "sha512-hiFoqpyZcfNm1yc4u8oWCf9A2c4D3QjCrks3zmoVKVxpQRzmPNar1hUJcBG2RQHvEVGDN+Jm81ZheVLAQMK6+w==",
2057 "dev": true,
2058 "license": "MIT", 2312 "license": "MIT",
2059 "peerDependencies": { 2313 "peerDependencies": {
2060 "picomatch": "^3 || ^4" 2314 "picomatch": "^3 || ^4"
@@ -2120,7 +2374,6 @@
2120 "version": "2.3.3", 2374 "version": "2.3.3",
2121 "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", 2375 "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
2122 "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", 2376 "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
2123 "dev": true,
2124 "hasInstallScript": true, 2377 "hasInstallScript": true,
2125 "license": "MIT", 2378 "license": "MIT",
2126 "optional": true, 2379 "optional": true,
@@ -2208,6 +2461,12 @@
2208 "url": "https://github.com/sponsors/ljharb" 2461 "url": "https://github.com/sponsors/ljharb"
2209 } 2462 }
2210 }, 2463 },
2464 "node_modules/graceful-fs": {
2465 "version": "4.2.11",
2466 "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
2467 "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==",
2468 "license": "ISC"
2469 },
2211 "node_modules/has-bigints": { 2470 "node_modules/has-bigints": {
2212 "version": "1.1.0", 2471 "version": "1.1.0",
2213 "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.1.0.tgz", 2472 "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.1.0.tgz",
@@ -2686,6 +2945,15 @@
2686 "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" 2945 "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0"
2687 } 2946 }
2688 }, 2947 },
2948 "node_modules/jiti": {
2949 "version": "2.5.1",
2950 "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.5.1.tgz",
2951 "integrity": "sha512-twQoecYPiVA5K/h6SxtORw/Bs3ar+mLUtoPSc7iMXzQzK8d7eJ/R09wmTwAjiamETn1cXYPGfNnu7DMoHgu12w==",
2952 "license": "MIT",
2953 "bin": {
2954 "jiti": "lib/jiti-cli.mjs"
2955 }
2956 },
2689 "node_modules/js-tokens": { 2957 "node_modules/js-tokens": {
2690 "version": "4.0.0", 2958 "version": "4.0.0",
2691 "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", 2959 "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
@@ -2718,6 +2986,234 @@
2718 "node": ">=6" 2986 "node": ">=6"
2719 } 2987 }
2720 }, 2988 },
2989 "node_modules/lightningcss": {
2990 "version": "1.30.1",
2991 "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.30.1.tgz",
2992 "integrity": "sha512-xi6IyHML+c9+Q3W0S4fCQJOym42pyurFiJUHEcEyHS0CeKzia4yZDEsLlqOFykxOdHpNy0NmvVO31vcSqAxJCg==",
2993 "license": "MPL-2.0",
2994 "dependencies": {
2995 "detect-libc": "^2.0.3"
2996 },
2997 "engines": {
2998 "node": ">= 12.0.0"
2999 },
3000 "funding": {
3001 "type": "opencollective",
3002 "url": "https://opencollective.com/parcel"
3003 },
3004 "optionalDependencies": {
3005 "lightningcss-darwin-arm64": "1.30.1",
3006 "lightningcss-darwin-x64": "1.30.1",
3007 "lightningcss-freebsd-x64": "1.30.1",
3008 "lightningcss-linux-arm-gnueabihf": "1.30.1",
3009 "lightningcss-linux-arm64-gnu": "1.30.1",
3010 "lightningcss-linux-arm64-musl": "1.30.1",
3011 "lightningcss-linux-x64-gnu": "1.30.1",
3012 "lightningcss-linux-x64-musl": "1.30.1",
3013 "lightningcss-win32-arm64-msvc": "1.30.1",
3014 "lightningcss-win32-x64-msvc": "1.30.1"
3015 }
3016 },
3017 "node_modules/lightningcss-darwin-arm64": {
3018 "version": "1.30.1",
3019 "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.30.1.tgz",
3020 "integrity": "sha512-c8JK7hyE65X1MHMN+Viq9n11RRC7hgin3HhYKhrMyaXflk5GVplZ60IxyoVtzILeKr+xAJwg6zK6sjTBJ0FKYQ==",
3021 "cpu": [
3022 "arm64"
3023 ],
3024 "license": "MPL-2.0",
3025 "optional": true,
3026 "os": [
3027 "darwin"
3028 ],
3029 "engines": {
3030 "node": ">= 12.0.0"
3031 },
3032 "funding": {
3033 "type": "opencollective",
3034 "url": "https://opencollective.com/parcel"
3035 }
3036 },
3037 "node_modules/lightningcss-darwin-x64": {
3038 "version": "1.30.1",
3039 "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.30.1.tgz",
3040 "integrity": "sha512-k1EvjakfumAQoTfcXUcHQZhSpLlkAuEkdMBsI/ivWw9hL+7FtilQc0Cy3hrx0AAQrVtQAbMI7YjCgYgvn37PzA==",
3041 "cpu": [
3042 "x64"
3043 ],
3044 "license": "MPL-2.0",
3045 "optional": true,
3046 "os": [
3047 "darwin"
3048 ],
3049 "engines": {
3050 "node": ">= 12.0.0"
3051 },
3052 "funding": {
3053 "type": "opencollective",
3054 "url": "https://opencollective.com/parcel"
3055 }
3056 },
3057 "node_modules/lightningcss-freebsd-x64": {
3058 "version": "1.30.1",
3059 "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.30.1.tgz",
3060 "integrity": "sha512-kmW6UGCGg2PcyUE59K5r0kWfKPAVy4SltVeut+umLCFoJ53RdCUWxcRDzO1eTaxf/7Q2H7LTquFHPL5R+Gjyig==",
3061 "cpu": [
3062 "x64"
3063 ],
3064 "license": "MPL-2.0",
3065 "optional": true,
3066 "os": [
3067 "freebsd"
3068 ],
3069 "engines": {
3070 "node": ">= 12.0.0"
3071 },
3072 "funding": {
3073 "type": "opencollective",
3074 "url": "https://opencollective.com/parcel"
3075 }
3076 },
3077 "node_modules/lightningcss-linux-arm-gnueabihf": {
3078 "version": "1.30.1",
3079 "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.30.1.tgz",
3080 "integrity": "sha512-MjxUShl1v8pit+6D/zSPq9S9dQ2NPFSQwGvxBCYaBYLPlCWuPh9/t1MRS8iUaR8i+a6w7aps+B4N0S1TYP/R+Q==",
3081 "cpu": [
3082 "arm"
3083 ],
3084 "license": "MPL-2.0",
3085 "optional": true,
3086 "os": [
3087 "linux"
3088 ],
3089 "engines": {
3090 "node": ">= 12.0.0"
3091 },
3092 "funding": {
3093 "type": "opencollective",
3094 "url": "https://opencollective.com/parcel"
3095 }
3096 },
3097 "node_modules/lightningcss-linux-arm64-gnu": {
3098 "version": "1.30.1",
3099 "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.30.1.tgz",
3100 "integrity": "sha512-gB72maP8rmrKsnKYy8XUuXi/4OctJiuQjcuqWNlJQ6jZiWqtPvqFziskH3hnajfvKB27ynbVCucKSm2rkQp4Bw==",
3101 "cpu": [
3102 "arm64"
3103 ],
3104 "license": "MPL-2.0",
3105 "optional": true,
3106 "os": [
3107 "linux"
3108 ],
3109 "engines": {
3110 "node": ">= 12.0.0"
3111 },
3112 "funding": {
3113 "type": "opencollective",
3114 "url": "https://opencollective.com/parcel"
3115 }
3116 },
3117 "node_modules/lightningcss-linux-arm64-musl": {
3118 "version": "1.30.1",
3119 "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.30.1.tgz",
3120 "integrity": "sha512-jmUQVx4331m6LIX+0wUhBbmMX7TCfjF5FoOH6SD1CttzuYlGNVpA7QnrmLxrsub43ClTINfGSYyHe2HWeLl5CQ==",
3121 "cpu": [
3122 "arm64"
3123 ],
3124 "license": "MPL-2.0",
3125 "optional": true,
3126 "os": [
3127 "linux"
3128 ],
3129 "engines": {
3130 "node": ">= 12.0.0"
3131 },
3132 "funding": {
3133 "type": "opencollective",
3134 "url": "https://opencollective.com/parcel"
3135 }
3136 },
3137 "node_modules/lightningcss-linux-x64-gnu": {
3138 "version": "1.30.1",
3139 "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.30.1.tgz",
3140 "integrity": "sha512-piWx3z4wN8J8z3+O5kO74+yr6ze/dKmPnI7vLqfSqI8bccaTGY5xiSGVIJBDd5K5BHlvVLpUB3S2YCfelyJ1bw==",
3141 "cpu": [
3142 "x64"
3143 ],
3144 "license": "MPL-2.0",
3145 "optional": true,
3146 "os": [
3147 "linux"
3148 ],
3149 "engines": {
3150 "node": ">= 12.0.0"
3151 },
3152 "funding": {
3153 "type": "opencollective",
3154 "url": "https://opencollective.com/parcel"
3155 }
3156 },
3157 "node_modules/lightningcss-linux-x64-musl": {
3158 "version": "1.30.1",
3159 "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.30.1.tgz",
3160 "integrity": "sha512-rRomAK7eIkL+tHY0YPxbc5Dra2gXlI63HL+v1Pdi1a3sC+tJTcFrHX+E86sulgAXeI7rSzDYhPSeHHjqFhqfeQ==",
3161 "cpu": [
3162 "x64"
3163 ],
3164 "license": "MPL-2.0",
3165 "optional": true,
3166 "os": [
3167 "linux"
3168 ],
3169 "engines": {
3170 "node": ">= 12.0.0"
3171 },
3172 "funding": {
3173 "type": "opencollective",
3174 "url": "https://opencollective.com/parcel"
3175 }
3176 },
3177 "node_modules/lightningcss-win32-arm64-msvc": {
3178 "version": "1.30.1",
3179 "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.30.1.tgz",
3180 "integrity": "sha512-mSL4rqPi4iXq5YVqzSsJgMVFENoa4nGTT/GjO2c0Yl9OuQfPsIfncvLrEW6RbbB24WtZ3xP/2CCmI3tNkNV4oA==",
3181 "cpu": [
3182 "arm64"
3183 ],
3184 "license": "MPL-2.0",
3185 "optional": true,
3186 "os": [
3187 "win32"
3188 ],
3189 "engines": {
3190 "node": ">= 12.0.0"
3191 },
3192 "funding": {
3193 "type": "opencollective",
3194 "url": "https://opencollective.com/parcel"
3195 }
3196 },
3197 "node_modules/lightningcss-win32-x64-msvc": {
3198 "version": "1.30.1",
3199 "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.30.1.tgz",
3200 "integrity": "sha512-PVqXh48wh4T53F/1CCu8PIPCxLzWyCnn/9T5W1Jpmdy5h9Cwd+0YQS6/LwhHXSafuc61/xg9Lv5OrCby6a++jg==",
3201 "cpu": [
3202 "x64"
3203 ],
3204 "license": "MPL-2.0",
3205 "optional": true,
3206 "os": [
3207 "win32"
3208 ],
3209 "engines": {
3210 "node": ">= 12.0.0"
3211 },
3212 "funding": {
3213 "type": "opencollective",
3214 "url": "https://opencollective.com/parcel"
3215 }
3216 },
2721 "node_modules/lodash": { 3217 "node_modules/lodash": {
2722 "version": "4.17.21", 3218 "version": "4.17.21",
2723 "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", 3219 "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
@@ -2765,6 +3261,15 @@
2765 "lz-string": "bin/bin.js" 3261 "lz-string": "bin/bin.js"
2766 } 3262 }
2767 }, 3263 },
3264 "node_modules/magic-string": {
3265 "version": "0.30.17",
3266 "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.17.tgz",
3267 "integrity": "sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==",
3268 "license": "MIT",
3269 "dependencies": {
3270 "@jridgewell/sourcemap-codec": "^1.5.0"
3271 }
3272 },
2768 "node_modules/math-intrinsics": { 3273 "node_modules/math-intrinsics": {
2769 "version": "1.1.0", 3274 "version": "1.1.0",
2770 "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", 3275 "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
@@ -3399,6 +3904,42 @@
3399 "node": ">=4" 3904 "node": ">=4"
3400 } 3905 }
3401 }, 3906 },
3907 "node_modules/minipass": {
3908 "version": "7.1.2",
3909 "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz",
3910 "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==",
3911 "license": "ISC",
3912 "engines": {
3913 "node": ">=16 || 14 >=14.17"
3914 }
3915 },
3916 "node_modules/minizlib": {
3917 "version": "3.0.2",
3918 "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-3.0.2.tgz",
3919 "integrity": "sha512-oG62iEk+CYt5Xj2YqI5Xi9xWUeZhDI8jjQmC5oThVH5JGCTgIjr7ciJDzC7MBzYd//WvR1OTmP5Q38Q8ShQtVA==",
3920 "license": "MIT",
3921 "dependencies": {
3922 "minipass": "^7.1.2"
3923 },
3924 "engines": {
3925 "node": ">= 18"
3926 }
3927 },
3928 "node_modules/mkdirp": {
3929 "version": "3.0.1",
3930 "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-3.0.1.tgz",
3931 "integrity": "sha512-+NsyUUAZDmo6YVHzL/stxSu3t9YS1iljliy3BSDrXJ/dkn1KYdmtZODGGjLcc9XLgVVpH4KshHB8XmZgMhaBXg==",
3932 "license": "MIT",
3933 "bin": {
3934 "mkdirp": "dist/cjs/src/bin.js"
3935 },
3936 "engines": {
3937 "node": ">=10"
3938 },
3939 "funding": {
3940 "url": "https://github.com/sponsors/isaacs"
3941 }
3942 },
3402 "node_modules/ms": { 3943 "node_modules/ms": {
3403 "version": "2.1.3", 3944 "version": "2.1.3",
3404 "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", 3945 "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
@@ -3409,7 +3950,6 @@
3409 "version": "3.3.11", 3950 "version": "3.3.11",
3410 "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", 3951 "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
3411 "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", 3952 "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==",
3412 "dev": true,
3413 "funding": [ 3953 "funding": [
3414 { 3954 {
3415 "type": "github", 3955 "type": "github",
@@ -3532,7 +4072,6 @@
3532 "version": "4.0.3", 4072 "version": "4.0.3",
3533 "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", 4073 "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
3534 "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", 4074 "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
3535 "dev": true,
3536 "license": "MIT", 4075 "license": "MIT",
3537 "engines": { 4076 "engines": {
3538 "node": ">=12" 4077 "node": ">=12"
@@ -3554,7 +4093,6 @@
3554 "version": "8.5.6", 4093 "version": "8.5.6",
3555 "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", 4094 "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz",
3556 "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", 4095 "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==",
3557 "dev": true,
3558 "funding": [ 4096 "funding": [
3559 { 4097 {
3560 "type": "opencollective", 4098 "type": "opencollective",
@@ -3853,7 +4391,6 @@
3853 "version": "4.46.2", 4391 "version": "4.46.2",
3854 "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.46.2.tgz", 4392 "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.46.2.tgz",
3855 "integrity": "sha512-WMmLFI+Boh6xbop+OAGo9cQ3OgX9MIg7xOQjn+pTCwOkk+FNDAeAemXkJ3HzDJrVXleLOFVa1ipuc1AmEx1Dwg==", 4393 "integrity": "sha512-WMmLFI+Boh6xbop+OAGo9cQ3OgX9MIg7xOQjn+pTCwOkk+FNDAeAemXkJ3HzDJrVXleLOFVa1ipuc1AmEx1Dwg==",
3856 "dev": true,
3857 "license": "MIT", 4394 "license": "MIT",
3858 "dependencies": { 4395 "dependencies": {
3859 "@types/estree": "1.0.8" 4396 "@types/estree": "1.0.8"
@@ -4033,7 +4570,6 @@
4033 "version": "1.2.1", 4570 "version": "1.2.1",
4034 "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", 4571 "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
4035 "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", 4572 "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
4036 "dev": true,
4037 "license": "BSD-3-Clause", 4573 "license": "BSD-3-Clause",
4038 "engines": { 4574 "engines": {
4039 "node": ">=0.10.0" 4575 "node": ">=0.10.0"
@@ -4109,11 +4645,51 @@
4109 "node": ">=8" 4645 "node": ">=8"
4110 } 4646 }
4111 }, 4647 },
4648 "node_modules/tailwindcss": {
4649 "version": "4.1.11",
4650 "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.11.tgz",
4651 "integrity": "sha512-2E9TBm6MDD/xKYe+dvJZAmg3yxIEDNRc0jwlNyDg/4Fil2QcSLjFKGVff0lAf1jjeaArlG/M75Ey/EYr/OJtBA==",
4652 "license": "MIT"
4653 },
4654 "node_modules/tapable": {
4655 "version": "2.2.2",
4656 "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.2.tgz",
4657 "integrity": "sha512-Re10+NauLTMCudc7T5WLFLAwDhQ0JWdrMK+9B2M8zR5hRExKmsRDCBA7/aV/pNJFltmBFO5BAMlQFi/vq3nKOg==",
4658 "license": "MIT",
4659 "engines": {
4660 "node": ">=6"
4661 }
4662 },
4663 "node_modules/tar": {
4664 "version": "7.4.3",
4665 "resolved": "https://registry.npmjs.org/tar/-/tar-7.4.3.tgz",
4666 "integrity": "sha512-5S7Va8hKfV7W5U6g3aYxXmlPoZVAwUMy9AOKyF2fVuZa2UD3qZjg578OrLRt8PcNN1PleVaL/5/yYATNL0ICUw==",
4667 "license": "ISC",
4668 "dependencies": {
4669 "@isaacs/fs-minipass": "^4.0.0",
4670 "chownr": "^3.0.0",
4671 "minipass": "^7.1.2",
4672 "minizlib": "^3.0.1",
4673 "mkdirp": "^3.0.1",
4674 "yallist": "^5.0.0"
4675 },
4676 "engines": {
4677 "node": ">=18"
4678 }
4679 },
4680 "node_modules/tar/node_modules/yallist": {
4681 "version": "5.0.0",
4682 "resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz",
4683 "integrity": "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==",
4684 "license": "BlueOak-1.0.0",
4685 "engines": {
4686 "node": ">=18"
4687 }
4688 },
4112 "node_modules/tinyglobby": { 4689 "node_modules/tinyglobby": {
4113 "version": "0.2.14", 4690 "version": "0.2.14",
4114 "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.14.tgz", 4691 "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.14.tgz",
4115 "integrity": "sha512-tX5e7OM1HnYr2+a2C/4V0htOcSQcoSTH9KgJnVvNm5zm/cyEWKJ7j7YutsH9CxMdtOkkLFy2AHrMci9IM8IPZQ==", 4692 "integrity": "sha512-tX5e7OM1HnYr2+a2C/4V0htOcSQcoSTH9KgJnVvNm5zm/cyEWKJ7j7YutsH9CxMdtOkkLFy2AHrMci9IM8IPZQ==",
4116 "dev": true,
4117 "license": "MIT", 4693 "license": "MIT",
4118 "dependencies": { 4694 "dependencies": {
4119 "fdir": "^6.4.4", 4695 "fdir": "^6.4.4",
@@ -4163,7 +4739,7 @@
4163 "version": "6.21.0", 4739 "version": "6.21.0",
4164 "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", 4740 "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
4165 "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", 4741 "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
4166 "dev": true, 4742 "devOptional": true,
4167 "license": "MIT" 4743 "license": "MIT"
4168 }, 4744 },
4169 "node_modules/unified": { 4745 "node_modules/unified": {
@@ -4316,7 +4892,6 @@
4316 "version": "7.1.1", 4892 "version": "7.1.1",
4317 "resolved": "https://registry.npmjs.org/vite/-/vite-7.1.1.tgz", 4893 "resolved": "https://registry.npmjs.org/vite/-/vite-7.1.1.tgz",
4318 "integrity": "sha512-yJ+Mp7OyV+4S+afWo+QyoL9jFWD11QFH0i5i7JypnfTcA1rmgxCbiA8WwAICDEtZ1Z1hzrVhN8R8rGTqkTY8ZQ==", 4894 "integrity": "sha512-yJ+Mp7OyV+4S+afWo+QyoL9jFWD11QFH0i5i7JypnfTcA1rmgxCbiA8WwAICDEtZ1Z1hzrVhN8R8rGTqkTY8ZQ==",
4319 "dev": true,
4320 "license": "MIT", 4895 "license": "MIT",
4321 "dependencies": { 4896 "dependencies": {
4322 "esbuild": "^0.25.0", 4897 "esbuild": "^0.25.0",
diff --git a/frontend/package.json b/frontend/package.json
index f9cab6b..e7b16f7 100644
--- a/frontend/package.json
+++ b/frontend/package.json
@@ -1,9 +1,11 @@
1{ 1{
2 "name": "frontend", 2 "name": "frontend",
3 "version": "0.1.0", 3 "version": "0.1.0",
4 "type": "module",
4 "private": true, 5 "private": true,
5 "dependencies": { 6 "dependencies": {
6 "@nekz/sdp": "^0.9.0", 7 "@nekz/sdp": "^0.9.0",
8 "@tailwindcss/vite": "^4.1.11",
7 "@testing-library/jest-dom": "^5.17.0", 9 "@testing-library/jest-dom": "^5.17.0",
8 "@testing-library/react": "^13.4.0", 10 "@testing-library/react": "^13.4.0",
9 "@testing-library/user-event": "^13.5.0", 11 "@testing-library/user-event": "^13.5.0",
@@ -16,6 +18,7 @@
16 "react-helmet": "^6.1.0", 18 "react-helmet": "^6.1.0",
17 "react-markdown": "^9.0.1", 19 "react-markdown": "^9.0.1",
18 "react-router-dom": "^6.26.1", 20 "react-router-dom": "^6.26.1",
21 "tailwindcss": "^4.1.11",
19 "typescript": "^4.9.5", 22 "typescript": "^4.9.5",
20 "web-vitals": "^2.1.4" 23 "web-vitals": "^2.1.4"
21 }, 24 },
diff --git a/frontend/src/App.css b/frontend/src/App.css
index 14a9972..a4c058b 100644
--- a/frontend/src/App.css
+++ b/frontend/src/App.css
@@ -1,3 +1,54 @@
1@import url('https://fonts.googleapis.com/css2?family=Barlow+Condensed:ital,wght@0,100;0,200;0,300;0,400;0,500;0,600;0,700;0,800;0,900;1,100;1,200;1,300;1,400;1,500;1,600;1,700;1,800;1,900&family=Montserrat+Alternates:ital,wght@0,100;0,200;0,300;0,400;0,500;0,600;0,700;0,800;0,900;1,100;1,200;1,300;1,400;1,500;1,600;1,700;1,800;1,900&display=swap');
2@import "tailwindcss";
3
4@theme {
5 --color-rosewater: #f2d5cf;
6 --color-flamingo: #eebebe;
7 --color-pink: #f4b8e4;
8 --color-mauve: #ca9ee6;
9 --color-red: #e78284;
10 --color-maroon: #ea999c;
11 --color-peach: #ef9f76;
12 --color-yellow: #e5c890;
13 --color-green: #a6d189;
14 --color-teal: #81c8be;
15 --color-sky: #99d1db;
16 --color-sapphire: #85c1dc;
17 --color-blue: #8caaee;
18 --color-lavender: #babbf1;
19 --color-text: #c6d0f5;
20 --color-subtext1: #b5bfe2;
21 --color-subtext0: #a5adce;
22 --color-overlay2: #949cbb;
23 --color-overlay1: #838ba7;
24 --color-overlay0: #737994;
25 --color-surface2: #626880;
26 --color-surface1: #51576d;
27 --color-surface0: #414559;
28 --color-base: #303446;
29 --color-mantle: #292c3c;
30 --color-crust: #232634;
31
32 --color-primary: var(--color-mauve);
33 --color-secondary: var(--color-blue);
34 --color-accent: var(--color-peach);
35 --color-background: var(--color-base);
36 --color-surface: var(--color-surface0);
37 --color-muted: var(--color-overlay0);
38 --color-border: var(--color-surface2);
39 --color-input: var(--color-surface1);
40 --color-foreground: var(--color-text);
41 --color-success: var(--color-green);
42 --color-warning: var(--color-yellow);
43 --color-error: var(--color-red);
44 --color-info: var(--color-blue);
45
46 --font-barlow-condensed-regular: 'BarlowCondensed-Regular';
47 --font-barlow-condensed-bold: 'BarlowCondensed-Bold';
48 --font-barlow-semicondensed-regular: 'BarlowSemiCondensed-Regular';
49 --font-barlow-semicondensed-semibold: 'BarlowSemiCondensed-SemiBold';
50}
51
1main { 52main {
2 overflow: auto; 53 overflow: auto;
3 overflow-x: hidden; 54 overflow-x: hidden;
@@ -10,33 +61,32 @@ main {
10 padding-right: 30px; 61 padding-right: 30px;
11 62
12 font-size: 40px; 63 font-size: 40px;
13 font-family: BarlowSemiCondensed-Regular; 64 font-family: var(--font-barlow-semicondensed-regular);
14 color: #cdcfdf; 65 color: var(--color-text);
15 66
16} 67}
17 68
18a { 69a {
19 color: inherit; 70 color: inherit;
20 width: fit-content;
21} 71}
22 72
23body { 73body {
24 overflow: hidden; 74 overflow: hidden;
25 background-color: #141520; 75 background-color: var(--color-crust);
26 margin: 0; 76 margin: 0;
27} 77}
28 78
29.loader { 79.loader {
30 animation: loader 1.2s ease infinite; 80 animation: loader 1.2s ease infinite;
31 background-size: 400% 300%; 81 background-size: 400% 300%;
32 background-image: linear-gradient(-90deg, #202232 0%, #202232 25%, #2a2c41 50%, #202232 75%, #202232 100%); 82 background-image: linear-gradient(-90deg, var(--color-mantle) 0%, var(--color-mantle) 25%, var(--color-surface1) 50%, var(--color-mantle) 75%, var(--color-mantle) 100%);
33 user-select: none; 83 user-select: none;
34} 84}
35 85
36.loader-text { 86.loader-text {
37 animation: loader 1.2s ease infinite; 87 animation: loader 1.2s ease infinite;
38 background-size: 400% 300%; 88 background-size: 400% 300%;
39 background-image: linear-gradient(-90deg, #202232 0%, #202232 25%, #2a2c41 50%, #202232 75%, #202232 100%); 89 background-image: linear-gradient(-90deg, var(--color-mantle) 0%, var(--color-mantle) 25%, var(--color-surface1) 50%, var(--color-mantle) 75%, var(--color-mantle) 100%);
40 user-select: none; 90 user-select: none;
41 color: #00000000; 91 color: #00000000;
42 border-radius: 1000px; 92 border-radius: 1000px;
@@ -76,6 +126,173 @@ body {
76 } 126 }
77} 127}
78 128
129/* Custom Tailwind utilities for Catppuccin Frappe theme */
130@layer utilities {
131 .bg-primary {
132 background-color: var(--color-primary);
133 }
134
135 .bg-secondary {
136 background-color: var(--color-secondary);
137 }
138
139 .bg-accent {
140 background-color: var(--color-accent);
141 }
142
143 .bg-background {
144 background-color: var(--color-background);
145 }
146
147 .bg-surface {
148 background-color: var(--color-surface);
149 }
150
151 .bg-muted {
152 background-color: var(--color-muted);
153 }
154
155 .text-primary {
156 color: var(--color-primary);
157 }
158
159 .text-secondary {
160 color: var(--color-secondary);
161 }
162
163 .text-accent {
164 color: var(--color-accent);
165 }
166
167 .text-foreground {
168 color: var(--color-foreground);
169 }
170
171 .text-muted {
172 color: var(--color-muted);
173 }
174
175 .border-primary {
176 border-color: var(--color-primary);
177 }
178
179 .border-secondary {
180 border-color: var(--color-secondary);
181 }
182
183 .border-muted {
184 border-color: var(--color-border);
185 }
186
187 .hover\:bg-primary:hover {
188 background-color: var(--color-primary);
189 }
190
191 .hover\:bg-secondary:hover {
192 background-color: var(--color-secondary);
193 }
194
195 .hover\:bg-surface:hover {
196 background-color: var(--color-surface);
197 }
198
199 .hover\:text-primary:hover {
200 color: var(--color-primary);
201 }
202
203 .focus\:ring-primary:focus {
204 --tw-ring-color: var(--color-primary);
205 }
206
207 .triangle {
208 width: 0;
209 height: 0;
210 border-left: 5px solid transparent;
211 border-right: 5px solid transparent;
212 border-bottom: 8px solid var(--color-foreground);
213 display: inline-block;
214 }
215
216 .sidebar-button-selected {
217 background-color: var(--color-primary) !important;
218 color: var(--color-background) !important;
219 }
220
221 .sidebar-button-deselected {
222 background-color: var(--color-surface) !important;
223 color: var(--color-foreground) !important;
224 }
225
226 .profileboard-record {
227 background-color: var(--color-surface);
228 border: 1px solid var(--color-border);
229 border-radius: 0.5rem;
230 padding: 0.75rem;
231 margin-bottom: 0.5rem;
232 transition: all 0.2s ease;
233 }
234
235 .profileboard-record:hover {
236 background-color: var(--color-surface1);
237 }
238
239 .difficulty-rating {
240 width: 20px;
241 height: 20px;
242 background-color: var(--color-muted);
243 border-radius: 50%;
244 margin: 0 2px;
245 display: inline-block;
246 }
247
248 .nav-button {
249 background-color: var(--color-surface);
250 color: var(--color-foreground);
251 border: 1px solid var(--color-border);
252 border-radius: 0.5rem;
253 padding: 0.5rem 1rem;
254 transition: all 0.2s ease;
255 display: inline-flex;
256 align-items: center;
257 gap: 0.5rem;
258 text-decoration: none;
259 }
260
261 .nav-button:hover {
262 background-color: var(--color-surface1);
263 }
264
265 .record {
266 background-color: var(--color-surface);
267 border: 1px solid var(--color-border);
268 border-radius: 0.5rem;
269 padding: 0.5rem;
270 margin: 0.25rem;
271 cursor: pointer;
272 transition: all 0.2s ease;
273 }
274
275 .record:hover {
276 background-color: var(--color-surface1);
277 }
278
279 .portal-count {
280 font-size: 3rem;
281 font-weight: bold;
282 color: var(--color-primary);
283 }
284
285 .titles {
286 background-color: var(--color-accent);
287 color: var(--color-background);
288 padding: 0.25rem 0.5rem;
289 border-radius: 1rem;
290 font-size: 0.875rem;
291 margin-right: 0.5rem;
292 display: inline-block;
293 }
294}
295
79@font-face { 296@font-face {
80 font-family: 'BarlowCondensed-Bold'; 297 font-family: 'BarlowCondensed-Bold';
81 src: local('BarlowCondensed-Bold'), url(./fonts/BarlowCondensed-Bold.ttf) format('truetype'); 298 src: local('BarlowCondensed-Bold'), url(./fonts/BarlowCondensed-Bold.ttf) format('truetype');
diff --git a/frontend/src/api/Api.ts b/frontend/src/api/Api.ts
index 0e1658c..b782d17 100644
--- a/frontend/src/api/Api.ts
+++ b/frontend/src/api/Api.ts
@@ -91,7 +91,7 @@ export const API = {
91 delete_map_summary(token, map_id, route_id), 91 delete_map_summary(token, map_id, route_id),
92}; 92};
93 93
94const BASE_API_URL: string = "/api/v1/"; 94const BASE_API_URL: string = "https://lp.portal2.sr/api/v1/"
95 95
96export function url(path: string): string { 96export function url(path: string): string {
97 return BASE_API_URL + path; 97 return BASE_API_URL + path;
diff --git a/frontend/src/components/ConfirmDialog.tsx b/frontend/src/components/ConfirmDialog.tsx
index c89d9ea..8f2ce7a 100644
--- a/frontend/src/components/ConfirmDialog.tsx
+++ b/frontend/src/components/ConfirmDialog.tsx
@@ -1,7 +1,5 @@
1import React from "react"; 1import React from "react";
2 2
3import "@css/Dialog.css";
4
5interface ConfirmDialogProps { 3interface ConfirmDialogProps {
6 title: string; 4 title: string;
7 subtitle: string; 5 subtitle: string;
@@ -16,17 +14,17 @@ const ConfirmDialog: React.FC<ConfirmDialogProps> = ({
16 onCancel, 14 onCancel,
17}) => { 15}) => {
18 return ( 16 return (
19 <div className="dimmer"> 17 <div className="fixed w-[200%] h-full bg-black bg-opacity-50 z-[4]">
20 <div className="dialog"> 18 <div className="fixed z-[4] top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2 bg-surface rounded-3xl overflow-hidden min-w-[350px] border border-border animate-[dialog_in_0.2s_cubic-bezier(0.075,0.82,0.165,1.1)] text-foreground font-[--font-barlow-semicondensed-regular]">
21 <div className="dialog-element dialog-header"> 19 <div className="p-2 text-2xl bg-mantle">
22 <span>{title}</span> 20 <span>{title}</span>
23 </div> 21 </div>
24 <div className="dialog-element dialog-description"> 22 <div className="p-2">
25 <span>{subtitle}</span> 23 <span>{subtitle}</span>
26 </div> 24 </div>
27 <div className="dialog-element dialog-btns-container"> 25 <div className="p-2 flex justify-end border-t-2 border-border bg-mantle">
28 <button onClick={onCancel}>Cancel</button> 26 <button className="mr-2 px-4 py-2 bg-muted text-foreground rounded hover:bg-overlay1 transition-colors" onClick={onCancel}>Cancel</button>
29 <button onClick={onConfirm}>Confirm</button> 27 <button className="px-4 py-2 bg-primary text-background rounded hover:bg-mauve transition-colors" onClick={onConfirm}>Confirm</button>
30 </div> 28 </div>
31 </div> 29 </div>
32 </div> 30 </div>
diff --git a/frontend/src/components/GameCategory.tsx b/frontend/src/components/GameCategory.tsx
index 2bb6d42..b18c9d9 100644
--- a/frontend/src/components/GameCategory.tsx
+++ b/frontend/src/components/GameCategory.tsx
@@ -2,7 +2,6 @@ import React from "react";
2import { Link } from "react-router-dom"; 2import { Link } from "react-router-dom";
3 3
4import { Game, GameCategoryPortals } from "@customTypes/Game"; 4import { Game, GameCategoryPortals } from "@customTypes/Game";
5import "@css/Games.css";
6 5
7interface GameCategoryProps { 6interface GameCategoryProps {
8 game: Game; 7 game: Game;
@@ -12,18 +11,12 @@ interface GameCategoryProps {
12const GameCategory: React.FC<GameCategoryProps> = ({ cat, game }) => { 11const GameCategory: React.FC<GameCategoryProps> = ({ cat, game }) => {
13 return ( 12 return (
14 <Link 13 <Link
15 className="games-page-item-body-item" 14 className="bg-surface text-center w-full h-[100px] rounded-3xl text-foreground m-3 hover:bg-surface1 transition-colors flex flex-col justify-between p-4"
16 to={"/games/" + game.id + "?cat=" + cat.category.id} 15 to={"/games/" + game.id + "?cat=" + cat.category.id}
17 > 16 >
18 <div> 17 <p className="text-3xl font-semibold">{cat.category.name}</p>
19 <span className="games-page-item-body-item-title"> 18 <br />
20 {cat.category.name} 19 <p className="font-bold text-4xl">{cat.portal_count}</p>
21 </span>
22 <br />
23 <span className="games-page-item-body-item-num">
24 {cat.portal_count}
25 </span>
26 </div>
27 </Link> 20 </Link>
28 ); 21 );
29}; 22};
diff --git a/frontend/src/components/GameEntry.tsx b/frontend/src/components/GameEntry.tsx
index 04c3483..f8fd179 100644
--- a/frontend/src/components/GameEntry.tsx
+++ b/frontend/src/components/GameEntry.tsx
@@ -2,7 +2,6 @@ import React from "react";
2import { Link } from "react-router-dom"; 2import { Link } from "react-router-dom";
3 3
4import { Game, GameCategoryPortals } from "@customTypes/Game"; 4import { Game, GameCategoryPortals } from "@customTypes/Game";
5import "@css/Games.css";
6 5
7import GameCategory from "@components/GameCategory"; 6import GameCategory from "@components/GameCategory";
8 7
@@ -18,23 +17,25 @@ const GameEntry: React.FC<GameEntryProps> = ({ game }) => {
18 }, [game.category_portals]); 17 }, [game.category_portals]);
19 18
20 return ( 19 return (
21 <Link to={"/games/" + game.id}> 20 <Link to={"/games/" + game.id} className="w-full">
22 <div className="games-page-item"> 21 <div className="w-full h-64 bg-mantle rounded-3xl overflow-hidden my-6">
23 <div className="games-page-item-header"> 22 <div className="w-full h-1/2 bg-cover overflow-hidden relative">
24 <div 23 <div
25 style={{ backgroundImage: `url(${game.image})` }} 24 style={{ backgroundImage: `url(${game.image})` }}
26 className="games-page-item-header-img" 25 className="w-full h-full backdrop-blur-sm blur-sm bg-cover"
27 ></div> 26 ></div>
28 <span> 27 <span className="absolute inset-0 flex justify-center items-center">
29 <b>{game.name}</b> 28 <b className="text-[56px] font-[--font-barlow-condensed-bold] text-white">{game.name}</b>
30 </span> 29 </span>
31 </div> 30 </div>
32 <div id={game.id as any as string} className="games-page-item-body"> 31 <div className="flex justify-center items-center h-1/2">
33 {catInfo.map((cat, index) => { 32 <div className="flex flex-row justify-between w-full">
34 return ( 33 {catInfo.map((cat, index) => {
35 <GameCategory cat={cat} game={game} key={index}></GameCategory> 34 return (
36 ); 35 <GameCategory key={index} cat={cat} game={game} />
37 })} 36 );
37 })}
38 </div>
38 </div> 39 </div>
39 </div> 40 </div>
40 </Link> 41 </Link>
diff --git a/frontend/src/components/Leaderboards.tsx b/frontend/src/components/Leaderboards.tsx
index b388aba..99481a2 100644
--- a/frontend/src/components/Leaderboards.tsx
+++ b/frontend/src/components/Leaderboards.tsx
@@ -36,7 +36,7 @@ const Leaderboards: React.FC<LeaderboardsProps> = ({ mapID }) => {
36 return ( 36 return (
37 <section id="section6" className="summary2"> 37 <section id="section6" className="summary2">
38 <h1 style={{ textAlign: "center" }}> 38 <h1 style={{ textAlign: "center" }}>
39 Map is not available for competitive boards. 39 Loading...
40 </h1> 40 </h1>
41 </section> 41 </section>
42 ); 42 );
@@ -195,6 +195,7 @@ const Leaderboards: React.FC<LeaderboardsProps> = ({ mapID }) => {
195 filter: 195 filter:
196 "hue-rotate(160deg) contrast(60%) saturate(1000%)", 196 "hue-rotate(160deg) contrast(60%) saturate(1000%)",
197 }} 197 }}
198 className="w-6 h-6 mx-4"
198 /> 199 />
199 </button> 200 </button>
200 <button 201 <button
@@ -209,6 +210,7 @@ const Leaderboards: React.FC<LeaderboardsProps> = ({ mapID }) => {
209 filter: 210 filter:
210 "hue-rotate(300deg) contrast(60%) saturate(1000%)", 211 "hue-rotate(300deg) contrast(60%) saturate(1000%)",
211 }} 212 }}
213 className="w-6 h-6"
212 /> 214 />
213 </button> 215 </button>
214 </span> 216 </span>
@@ -227,7 +229,7 @@ const Leaderboards: React.FC<LeaderboardsProps> = ({ mapID }) => {
227 (window.location.href = `/api/v1/demos?uuid=${r.demo_id}`) 229 (window.location.href = `/api/v1/demos?uuid=${r.demo_id}`)
228 } 230 }
229 > 231 >
230 <img src={DownloadIcon} alt="download" /> 232 <img src={DownloadIcon} alt="download" className="w-6 h-6 mr-4" />
231 </button> 233 </button>
232 </span> 234 </span>
233 ) 235 )
diff --git a/frontend/src/components/Login.tsx b/frontend/src/components/Login.tsx
index 1858c48..ba85aeb 100644
--- a/frontend/src/components/Login.tsx
+++ b/frontend/src/components/Login.tsx
@@ -1,18 +1,18 @@
1import React from "react"; 1import React from "react";
2import { Link, useNavigate } from "react-router-dom"; 2import { Link, useNavigate } from "react-router-dom";
3 3
4import { ExitIcon, UserIcon, LoginIcon } from "@images/Images"; 4import { ExitIcon, UserIcon, LoginIcon } from "../images/Images";
5import { UserProfile } from "@customTypes/Profile"; 5import { UserProfile } from "@customTypes/Profile";
6import { API } from "@api/Api"; 6import { API } from "@api/Api";
7import "@css/Login.css";
8 7
9interface LoginProps { 8interface LoginProps {
10 setToken: React.Dispatch<React.SetStateAction<string | undefined>>; 9 setToken: React.Dispatch<React.SetStateAction<string | undefined>>;
11 profile?: UserProfile; 10 profile?: UserProfile;
12 setProfile: React.Dispatch<React.SetStateAction<UserProfile | undefined>>; 11 setProfile: React.Dispatch<React.SetStateAction<UserProfile | undefined>>;
12 isOpen: boolean;
13} 13}
14 14
15const Login: React.FC<LoginProps> = ({ setToken, profile, setProfile }) => { 15const Login: React.FC<LoginProps> = ({ setToken, profile, setProfile, isOpen }) => {
16 const navigate = useNavigate(); 16 const navigate = useNavigate();
17 17
18 const _login = () => { 18 const _login = () => {
@@ -32,16 +32,16 @@ const Login: React.FC<LoginProps> = ({ setToken, profile, setProfile }) => {
32 <> 32 <>
33 {profile.profile ? ( 33 {profile.profile ? (
34 <> 34 <>
35 <Link to="/profile" tabIndex={-1} className="login"> 35 <Link to="/profile" tabIndex={-1} className="grid grid-cols-[50px_auto_200px]">
36 <button className="sidebar-button"> 36 <button className="grid grid-cols-[50px_auto] place-items-start text-left bg-inherit cursor-pointer border-none w-[310px] h-10 rounded-[20px] py-[0.3em] px-0 pl-[11px] transition-all duration-300">
37 <img 37 <img
38 className="avatar-img" 38 className="rounded-[50px]"
39 src={profile.avatar_link} 39 src={profile.avatar_link}
40 alt="" 40 alt=""
41 /> 41 />
42 <span>{profile.user_name}</span> 42 <span className="font-[--font-barlow-semicondensed-regular] text-lg text-foreground h-8 leading-7 transition-opacity duration-100 max-w-[22ch] overflow-hidden">{profile.user_name}</span>
43 </button> 43 </button>
44 <button className="logout-button" onClick={_logout}> 44 <button className="relative left-[210px] w-[50px] !pl-[10px] !bg-transparent" onClick={_logout}>
45 <img src={ExitIcon} alt="" /> 45 <img src={ExitIcon} alt="" />
46 <span /> 46 <span />
47 </button> 47 </button>
@@ -49,16 +49,16 @@ const Login: React.FC<LoginProps> = ({ setToken, profile, setProfile }) => {
49 </> 49 </>
50 ) : ( 50 ) : (
51 <> 51 <>
52 <Link to="/" tabIndex={-1} className="login"> 52 <Link to="/" tabIndex={-1} className="grid grid-cols-[50px_auto_200px]">
53 <button className="sidebar-button"> 53 <button className="grid grid-cols-[50px_auto] place-items-start text-left bg-inherit cursor-pointer border-none w-[310px] h-10 rounded-[20px] py-[0.3em] px-0 pl-[11px] transition-all duration-300">
54 <img 54 <img
55 className="avatar-img" 55 className="rounded-[50px]"
56 src={profile.avatar_link} 56 src={profile.avatar_link}
57 alt="" 57 alt=""
58 /> 58 />
59 <span>Loading Profile...</span> 59 <span className="font-[--font-barlow-semicondensed-regular] text-lg text-foreground h-8 leading-7 transition-opacity duration-100 max-w-[22ch] overflow-hidden">Loading Profile...</span>
60 </button> 60 </button>
61 <button disabled className="logout-button" onClick={_logout}> 61 <button disabled className="relative left-[210px] w-[50px] !pl-[10px] !bg-transparent hidden" onClick={_logout}>
62 <img src={ExitIcon} alt="" /> 62 <img src={ExitIcon} alt="" />
63 <span /> 63 <span />
64 </button> 64 </button>
@@ -67,11 +67,28 @@ const Login: React.FC<LoginProps> = ({ setToken, profile, setProfile }) => {
67 )} 67 )}
68 </> 68 </>
69 ) : ( 69 ) : (
70 <Link to="/api/v1/login" tabIndex={-1} className="login"> 70 <Link to="/api/v1/login" tabIndex={-1}>
71 <button className="sidebar-button" onClick={_login}> 71 <button
72 <img className="avatar-img" src={UserIcon} alt="" /> 72 className={`${
73 <span> 73 isOpen
74 <img src={LoginIcon} alt="Sign in through Steam" /> 74 ? "grid grid-cols-[50px_auto] place-items-start pl-[11px]"
75 : "flex items-center justify-center"
76 } text-left bg-inherit cursor-pointer border-none w-[310px] h-16 rounded-[20px] py-[0.3em] px-0 transition-all duration-300 ${isOpen ? "text-white" : "text-gray-400"}`}
77 onClick={_login}
78 >
79 <span className={`font-[--font-barlow-semicondensed-regular] text-lg h-12 leading-7 transition-opacity duration-100 ${isOpen ? " overflow-hidden" : ""}`}>
80 {isOpen ? (
81 <div className="bg-neutral-800 p-2 rounded-lg w-64 flex flex-row items-center justifyt-start gap-2 font-semibold">
82 <LoginIcon />
83 <span>
84 Login with Steam
85 </span>
86 </div>
87 ) : (
88 <div className="bg-neutral-800 p-2 rounded-lg w-">
89 <LoginIcon />
90 </div>
91 )}
75 </span> 92 </span>
76 </button> 93 </button>
77 </Link> 94 </Link>
diff --git a/frontend/src/components/ModMenu.tsx b/frontend/src/components/ModMenu.tsx
index 618d1a7..a0d7eb7 100644
--- a/frontend/src/components/ModMenu.tsx
+++ b/frontend/src/components/ModMenu.tsx
@@ -5,7 +5,6 @@ import { useNavigate } from "react-router-dom";
5import { MapSummary } from "@customTypes/Map"; 5import { MapSummary } from "@customTypes/Map";
6import { ModMenuContent } from "@customTypes/Content"; 6import { ModMenuContent } from "@customTypes/Content";
7import { API } from "@api/Api"; 7import { API } from "@api/Api";
8import "@css/ModMenu.css";
9import useConfirm from "@hooks/UseConfirm"; 8import useConfirm from "@hooks/UseConfirm";
10 9
11interface ModMenuProps { 10interface ModMenuProps {
diff --git a/frontend/src/components/Sidebar.tsx b/frontend/src/components/Sidebar.tsx
index b55d56b..88a5297 100644
--- a/frontend/src/components/Sidebar.tsx
+++ b/frontend/src/components/Sidebar.tsx
@@ -1,4 +1,4 @@
1import React, { useCallback } from "react"; 1import React, { useCallback, useRef } from "react";
2import { Link, useLocation } from "react-router-dom"; 2import { Link, useLocation } from "react-router-dom";
3 3
4import { 4import {
@@ -10,12 +10,11 @@ import {
10 PortalIcon, 10 PortalIcon,
11 SearchIcon, 11 SearchIcon,
12 UploadIcon, 12 UploadIcon,
13} from "@images/Images"; 13} from "../images/Images";
14import Login from "@components/Login"; 14import Login from "@components/Login";
15import { UserProfile } from "@customTypes/Profile"; 15import { UserProfile } from "@customTypes/Profile";
16import { Search } from "@customTypes/Search"; 16import { Search } from "@customTypes/Search";
17import { API } from "@api/Api"; 17import { API } from "@api/Api";
18import "@css/Sidebar.css";
19 18
20interface SidebarProps { 19interface SidebarProps {
21 setToken: React.Dispatch<React.SetStateAction<string | undefined>>; 20 setToken: React.Dispatch<React.SetStateAction<string | undefined>>;
@@ -24,6 +23,17 @@ interface SidebarProps {
24 onUploadRun: () => void; 23 onUploadRun: () => void;
25} 24}
26 25
26function OpenSidebarIcon(){
27 return (
28 <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className="lucide lucide-panel-right-close-icon lucide-panel-right-close"><rect width="18" height="18" x="3" y="3" rx="2"/><path d="M15 3v18"/><path d="m8 9 3 3-3 3"/></svg>
29 )
30}
31
32function ClosedSidebarIcon(){
33 return (
34<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className="lucide lucide-panel-right-open-icon lucide-panel-right-open"><rect width="18" height="18" x="3" y="3" rx="2"/><path d="M15 3v18"/><path d="m10 15-3-3 3-3"/></svg> )
35}
36
27const Sidebar: React.FC<SidebarProps> = ({ 37const Sidebar: React.FC<SidebarProps> = ({
28 setToken, 38 setToken,
29 profile, 39 profile,
@@ -34,100 +44,38 @@ const Sidebar: React.FC<SidebarProps> = ({
34 undefined 44 undefined
35 ); 45 );
36 const [isSidebarLocked, setIsSidebarLocked] = React.useState<boolean>(false); 46 const [isSidebarLocked, setIsSidebarLocked] = React.useState<boolean>(false);
37 const [isSidebarOpen, setSidebarOpen] = React.useState<boolean>(true); 47 const [isSidebarOpen, setSidebarOpen] = React.useState<boolean>(false);
48 const [selectedButtonIndex, setSelectedButtonIndex] = React.useState<number>(1);
38 49
39 const location = useLocation(); 50 const location = useLocation();
40 const path = location.pathname; 51 const path = location.pathname;
41 52
42 const _handle_sidebar_hide = useCallback(() => { 53 const sidebarRef = useRef<HTMLDivElement>(null);
43 var btn = document.querySelectorAll( 54 const searchbarRef = useRef<HTMLInputElement>(null);
44 "button.sidebar-button" 55 const uploadRunRef = useRef<HTMLButtonElement>(null);
45 ) as NodeListOf<HTMLElement>; 56 const sidebarButtonRefs = useRef<(HTMLButtonElement | null)[]>([]);
46 const span = document.querySelectorAll( 57
47 "button.sidebar-button>span" 58 const _handle_sidebar_toggle = useCallback(() => {
48 ) as NodeListOf<HTMLElement>; 59 if (!sidebarRef.current) return;
49 const side = document.querySelector("#sidebar-list") as HTMLElement;
50 const searchbar = document.querySelector("#searchbar") as HTMLInputElement;
51 const uploadRunBtn = document.querySelector(
52 "#upload-run"
53 ) as HTMLInputElement;
54 const uploadRunSpan = document.querySelector(
55 "#upload-run>span"
56 ) as HTMLInputElement;
57 60
58 if (isSidebarOpen) { 61 if (isSidebarOpen) {
59 if (profile) {
60 const login = document.querySelectorAll(
61 ".login>button"
62 )[1] as HTMLElement;
63 login.style.opacity = "1";
64 uploadRunBtn.style.width = "310px";
65 uploadRunBtn.style.padding = "0.4em 0 0 11px";
66 uploadRunSpan.style.opacity = "0";
67 setTimeout(() => {
68 uploadRunSpan.style.opacity = "1";
69 }, 100);
70 }
71 setSidebarOpen(false); 62 setSidebarOpen(false);
72 side.style.width = "320px";
73 btn.forEach((e, i) => {
74 e.style.width = "310px";
75 e.style.padding = "0.4em 0 0 11px";
76 setTimeout(() => {
77 span[i].style.opacity = "1";
78 }, 100);
79 });
80 side.style.zIndex = "2";
81 } else { 63 } else {
82 if (profile) {
83 const login = document.querySelectorAll(
84 ".login>button"
85 )[1] as HTMLElement;
86 login.style.opacity = "0";
87 uploadRunBtn.style.width = "40px";
88 uploadRunBtn.style.padding = "0.4em 0 0 5px";
89 uploadRunSpan.style.opacity = "0";
90 }
91 setSidebarOpen(true); 64 setSidebarOpen(true);
92 side.style.width = "40px"; 65 searchbarRef.current?.focus();
93 searchbar.focus();
94 btn.forEach((e, i) => {
95 e.style.width = "40px";
96 e.style.padding = "0.4em 0 0 5px";
97 span[i].style.opacity = "0";
98 });
99 setTimeout(() => {
100 side.style.zIndex = "0";
101 }, 300);
102 } 66 }
103 }, [isSidebarOpen, profile]); 67 }, [isSidebarOpen]);
104 68
105 const handle_sidebar_click = useCallback( 69 const handle_sidebar_click = useCallback(
106 (clicked_sidebar_idx: number) => { 70 (clicked_sidebar_idx: number) => {
107 const btn = document.querySelectorAll("button.sidebar-button"); 71 setSelectedButtonIndex(clicked_sidebar_idx);
108 if (isSidebarOpen) { 72 if (isSidebarOpen) {
109 setSidebarOpen(false); 73 setSidebarOpen(false);
110 _handle_sidebar_hide();
111 } 74 }
112 // clusterfuck
113 btn.forEach((e, i) => {
114 btn[i].classList.remove("sidebar-button-selected");
115 btn[i].classList.add("sidebar-button-deselected");
116 });
117 btn[clicked_sidebar_idx].classList.add("sidebar-button-selected");
118 btn[clicked_sidebar_idx].classList.remove("sidebar-button-deselected");
119 }, 75 },
120 [isSidebarOpen, _handle_sidebar_hide] 76 [isSidebarOpen]
121 ); 77 );
122 78
123 const _handle_sidebar_lock = () => {
124 if (!isSidebarLocked) {
125 _handle_sidebar_hide();
126 setIsSidebarLocked(true);
127 setTimeout(() => setIsSidebarLocked(false), 300);
128 }
129 };
130
131 const _handle_search_change = async (q: string) => { 79 const _handle_search_change = async (q: string) => {
132 const searchResponse = await API.get_search(q); 80 const searchResponse = await API.get_search(q);
133 setSearchData(searchResponse); 81 setSearchData(searchResponse);
@@ -135,149 +83,199 @@ const Sidebar: React.FC<SidebarProps> = ({
135 83
136 React.useEffect(() => { 84 React.useEffect(() => {
137 if (path === "/") { 85 if (path === "/") {
138 handle_sidebar_click(1); 86 setSelectedButtonIndex(1);
139 } else if (path.includes("games")) { 87 } else if (path.includes("games")) {
140 handle_sidebar_click(2); 88 setSelectedButtonIndex(2);
141 } else if (path.includes("rankings")) { 89 } else if (path.includes("rankings")) {
142 handle_sidebar_click(3); 90 setSelectedButtonIndex(3);
143 } 91 } else if (path.includes("profile")) {
144 // else if (path.includes("news")) { handle_sidebar_click(4) } 92 setSelectedButtonIndex(4);
145 // else if (path.includes("scorelog")) { handle_sidebar_click(5) }
146 else if (path.includes("profile")) {
147 handle_sidebar_click(4);
148 } else if (path.includes("rules")) { 93 } else if (path.includes("rules")) {
149 handle_sidebar_click(5); 94 setSelectedButtonIndex(5);
150 } else if (path.includes("about")) { 95 } else if (path.includes("about")) {
151 handle_sidebar_click(6); 96 setSelectedButtonIndex(6);
152 } 97 }
153 }, [path, handle_sidebar_click]); 98 }, [path]);
99
100 const getButtonClasses = (buttonIndex: number) => {
101 const baseClasses = "flex items-center gap-3 w-full text-left bg-inherit cursor-pointer border-none rounded-lg py-3 px-3 transition-all duration-300 hover:bg-surface1";
102 const selectedClasses = selectedButtonIndex === buttonIndex ? "bg-primary text-background" : "bg-transparent text-foreground";
103
104 return `${baseClasses} ${selectedClasses}`;
105 };
106
107 const iconClasses = "w-6 h-6 flex-shrink-0";
154 108
155 return ( 109 return (
156 <div id="sidebar"> 110 <div className={`fixed top-0 left-0 h-screen bg-surface border-r border-border transition-all duration-300 z-10 overflow-hidden ${
157 <Link to="/" tabIndex={-1}> 111 isSidebarOpen ? 'w-80' : 'w-20'
158 <div id="logo"> 112 }`}>
159 {" "} 113 <div className="flex items-center h-20 px-4 border-b border-border">
160 {/* logo */} 114 <Link to="/" tabIndex={-1} className="flex items-center flex-1 cursor-pointer select-none min-w-0">
161 <img src={LogoIcon} alt="" height={"80px"} /> 115 <img src={LogoIcon} alt="Logo" className="w-12 h-12 flex-shrink-0" />
162 <div id="logo-text"> 116 {isSidebarOpen && (
163 <span> 117 <div className="ml-3 font-[--font-barlow-condensed-regular] text-white min-w-0 overflow-hidden">
164 <b>PORTAL 2</b> 118 <div className="font-[--font-barlow-condensed-bold] text-2xl leading-6 truncate">
165 </span> 119 PORTAL 2
166 <br /> 120 </div>
167 <span>Least Portals Hub</span> 121 <div className="text-sm leading-4 truncate">
122 Least Portals Hub
123 </div>
124 </div>
125 )}
126 </Link>
127
128 <button
129 onClick={_handle_sidebar_toggle}
130 className="ml-2 p-2 rounded-lg hover:bg-surface1 transition-colors text-foreground"
131 title={isSidebarOpen ? "Close sidebar" : "Open sidebar"}
132 >
133 {isSidebarOpen ? <ClosedSidebarIcon /> : <OpenSidebarIcon />}
134 </button>
135 </div>
136
137 {/* Sidebar Content */}
138 <div
139 ref={sidebarRef}
140 className="flex flex-col h-[calc(100vh-80px)] overflow-y-auto overflow-x-hidden"
141 >
142 {isSidebarOpen && (
143 <div className="p-4 border-b border-border min-w-0">
144 <div className="flex items-center gap-3 mb-3">
145 <img src={SearchIcon} alt="Search" className={iconClasses} />
146 <span className="text-white font-[--font-barlow-semicondensed-regular] truncate">Search</span>
147 </div>
148
149 <div className="min-w-0">
150 <input
151 ref={searchbarRef}
152 type="text"
153 id="searchbar"
154 placeholder="Search for map or a player..."
155 onChange={e => _handle_search_change(e.target.value)}
156 className="w-full p-2 bg-input text-foreground border border-border rounded-lg text-sm min-w-0"
157 />
158
159 {searchData && (
160 <div className="mt-2 max-h-40 overflow-y-auto min-w-0">
161 {searchData?.maps.map((q, index) => (
162 <Link to={`/maps/${q.id}`} className="block p-2 mb-1 bg-surface1 rounded hover:bg-surface2 transition-colors min-w-0" key={index}>
163 <span className="block text-xs text-subtext1 truncate">{q.game}</span>
164 <span className="block text-xs text-subtext1 truncate">{q.chapter}</span>
165 <span className="block text-sm text-foreground truncate">{q.map}</span>
166 </Link>
167 ))}
168 {searchData?.players.map((q, index) => (
169 <Link
170 to={
171 profile && q.steam_id === profile.steam_id
172 ? `/profile`
173 : `/users/${q.steam_id}`
174 }
175 className="flex items-center p-2 mb-1 bg-surface1 rounded hover:bg-surface2 transition-colors min-w-0"
176 key={index}
177 >
178 <img src={q.avatar_link} alt="pfp" className="w-6 h-6 rounded-full mr-2 flex-shrink-0" />
179 <span className="text-sm text-foreground truncate">
180 {q.user_name}
181 </span>
182 </Link>
183 ))}
184 </div>
185 )}
186 </div>
168 </div> 187 </div>
188 )}
189
190 <div className="flex-1 p-4 min-w-0">
191 <nav className="space-y-2">
192 {[
193 {
194 to: "/",
195 refIndex: 1,
196 icon: HomeIcon,
197 alt: "Home",
198 label: "Home Page",
199 },
200 {
201 to: "/games",
202 refIndex: 2,
203 icon: PortalIcon,
204 alt: "Games",
205 label: "Games",
206 },
207 {
208 to: "/rankings",
209 refIndex: 3,
210 icon: FlagIcon,
211 alt: "Rankings",
212 label: "Rankings",
213 },
214 ].map(({ to, refIndex, icon, alt, label }) => (
215 <Link to={to} tabIndex={-1} key={refIndex}>
216 <button
217 ref={el => sidebarButtonRefs.current[refIndex] = el}
218 className={getButtonClasses(refIndex)}
219 onClick={() => handle_sidebar_click(refIndex)}
220 >
221 <img src={icon} alt={alt} className={iconClasses} />
222 {isSidebarOpen && (
223 <span className="text-white font-[--font-barlow-semicondensed-regular] truncate">
224 {label}
225 </span>
226 )}
227 </button>
228 </Link>
229 ))}
230 </nav>
169 </div> 231 </div>
170 </Link>
171 <div id="sidebar-list">
172 {" "}
173 {/* List */}
174 <div id="sidebar-toplist">
175 {" "}
176 {/* Top */}
177 <button
178 className="sidebar-button"
179 onClick={() => _handle_sidebar_lock()}
180 >
181 <img src={SearchIcon} alt="" />
182 <span>Search</span>
183 </button>
184 <span></span>
185 <Link to="/" tabIndex={-1}>
186 <button className="sidebar-button">
187 <img src={HomeIcon} alt="homepage" />
188 <span>Home&nbsp;Page</span>
189 </button>
190 </Link>
191 <Link to="/games" tabIndex={-1}>
192 <button className="sidebar-button">
193 <img src={PortalIcon} alt="games" />
194 <span>Games</span>
195 </button>
196 </Link>
197 <Link to="/rankings" tabIndex={-1}>
198 <button className="sidebar-button">
199 <img src={FlagIcon} alt="rankings" />
200 <span>Rankings</span>
201 </button>
202 </Link>
203 {/* <Link to="/news" tabIndex={-1}>
204 <button className='sidebar-button'><img src={NewsIcon} alt="news" /><span>News</span></button>
205 </Link> */}
206 {/* <Link to="/scorelog" tabIndex={-1}>
207 <button className='sidebar-button'><img src={TableIcon} alt="scorelogs" /><span>Score&nbsp;Logs</span></button>
208 </Link> */}
209 </div>
210 <div id="sidebar-bottomlist">
211 <span></span>
212 232
213 {profile && profile.profile ? ( 233 {/* Bottom Section */}
234 <div className="p-4 border-t border-border space-y-2 min-w-0">
235 {profile && profile.profile && (
214 <button 236 <button
237 ref={uploadRunRef}
215 id="upload-run" 238 id="upload-run"
216 className="submit-run-button" 239 className={getButtonClasses(-1)}
217 onClick={() => onUploadRun()} 240 onClick={() => onUploadRun()}
218 > 241 >
219 <img src={UploadIcon} alt="upload" /> 242 <img src={UploadIcon} alt="Upload" className={iconClasses} />
220 <span>Upload&nbsp;Record</span> 243 {isSidebarOpen && <span className="font-[--font-barlow-semicondensed-regular] truncate">Upload Record</span>}
221 </button> 244 </button>
222 ) : (
223 <span></span>
224 )} 245 )}
225 246
226 <Login 247 <div className={isSidebarOpen ? 'min-w-0' : 'flex justify-center'}>
227 setToken={setToken} 248 <Login
228 profile={profile} 249 setToken={setToken}
229 setProfile={setProfile} 250 profile={profile}
230 /> 251 setProfile={setProfile}
252 isOpen={isSidebarOpen}
253 />
254 </div>
231 255
232 <Link to="/rules" tabIndex={-1}> 256 <Link to="/rules" tabIndex={-1}>
233 <button className="sidebar-button"> 257 <button
234 <img src={BookIcon} alt="rules" /> 258 ref={el => sidebarButtonRefs.current[5] = el}
235 <span>Leaderboard&nbsp;Rules</span> 259 className={getButtonClasses(5)}
260 onClick={() => handle_sidebar_click(5)}
261 >
262 <img src={BookIcon} alt="Rules" className={iconClasses} />
263 {isSidebarOpen && <span className="font-[--font-barlow-semicondensed-regular] truncate">Leaderboard Rules</span>}
236 </button> 264 </button>
237 </Link> 265 </Link>
238 266
239 <Link to="/about" tabIndex={-1}> 267 <Link to="/about" tabIndex={-1}>
240 <button className="sidebar-button"> 268 <button
241 <img src={HelpIcon} alt="about" /> 269 ref={el => sidebarButtonRefs.current[6] = el}
242 <span>About&nbsp;LPHUB</span> 270 className={getButtonClasses(6)}
271 onClick={() => handle_sidebar_click(6)}
272 >
273 <img src={HelpIcon} alt="About" className={iconClasses} />
274 {isSidebarOpen && <span className="font-[--font-barlow-semicondensed-regular] truncate">About LPHUB</span>}
243 </button> 275 </button>
244 </Link> 276 </Link>
245 </div> 277 </div>
246 </div> 278 </div>
247 <div>
248 <input
249 type="text"
250 id="searchbar"
251 placeholder="Search for map or a player..."
252 onChange={e => _handle_search_change(e.target.value)}
253 />
254
255 <div id="search-data">
256 {searchData?.maps.map((q, index) => (
257 <Link to={`/maps/${q.id}`} className="search-map" key={index}>
258 <span>{q.game}</span>
259 <span>{q.chapter}</span>
260 <span>{q.map}</span>
261 </Link>
262 ))}
263 {searchData?.players.map((q, index) => (
264 <Link
265 to={
266 profile && q.steam_id === profile.steam_id
267 ? `/profile`
268 : `/users/${q.steam_id}`
269 }
270 className="search-player"
271 key={index}
272 >
273 <img src={q.avatar_link} alt="pfp"></img>
274 <span style={{ fontSize: `${36 - q.user_name.length * 0.8}px` }}>
275 {q.user_name}
276 </span>
277 </Link>
278 ))}
279 </div>
280 </div>
281 </div> 279 </div>
282 ); 280 );
283}; 281};
diff --git a/frontend/src/components/Summary.tsx b/frontend/src/components/Summary.tsx
index 61e52d4..cdecf30 100644
--- a/frontend/src/components/Summary.tsx
+++ b/frontend/src/components/Summary.tsx
@@ -2,7 +2,6 @@ import React from "react";
2import ReactMarkdown from "react-markdown"; 2import ReactMarkdown from "react-markdown";
3 3
4import { MapSummary } from "@customTypes/Map"; 4import { MapSummary } from "@customTypes/Map";
5import "@css/Maps.css";
6 5
7interface SummaryProps { 6interface SummaryProps {
8 selectedRun: number; 7 selectedRun: number;
diff --git a/frontend/src/components/UploadRunDialog.tsx b/frontend/src/components/UploadRunDialog.tsx
index d5eabcd..0034019 100644
--- a/frontend/src/components/UploadRunDialog.tsx
+++ b/frontend/src/components/UploadRunDialog.tsx
@@ -2,7 +2,6 @@ import React from "react";
2import { UploadRunContent } from "@customTypes/Content"; 2import { UploadRunContent } from "@customTypes/Content";
3import { ScoreboardTempUpdate, SourceDemoParser, NetMessages } from "@nekz/sdp"; 3import { ScoreboardTempUpdate, SourceDemoParser, NetMessages } from "@nekz/sdp";
4 4
5import "@css/UploadRunDialog.css";
6import { Game } from "@customTypes/Game"; 5import { Game } from "@customTypes/Game";
7import { API } from "@api/Api"; 6import { API } from "@api/Api";
8import { useNavigate } from "react-router-dom"; 7import { useNavigate } from "react-router-dom";
diff --git a/frontend/src/images/Images.tsx b/frontend/src/images/Images.tsx
index eb12588..6b46893 100644
--- a/frontend/src/images/Images.tsx
+++ b/frontend/src/images/Images.tsx
@@ -1,5 +1,5 @@
1import logo from "./png/logo.png"; 1import logo from "./png/logo.png";
2import login from "./png/login.png"; 2import { LoginIcon as Login } from "./svgs/steam.tsx";
3import img1 from "./png/1.png"; 3import img1 from "./png/1.png";
4import img2 from "./png/2.png"; 4import img2 from "./png/2.png";
5import img3 from "./png/3.png"; 5import img3 from "./png/3.png";
@@ -23,7 +23,7 @@ import img20 from "./png/20.png";
23import img21 from "./png/21.png"; 23import img21 from "./png/21.png";
24 24
25export const LogoIcon = logo; 25export const LogoIcon = logo;
26export const LoginIcon = login; 26export const LoginIcon = Login;
27 27
28export const SearchIcon = img1; 28export const SearchIcon = img1;
29export const HomeIcon = img2; 29export const HomeIcon = img2;
diff --git a/frontend/src/images/svgs/steam.tsx b/frontend/src/images/svgs/steam.tsx
new file mode 100644
index 0000000..0dc9a04
--- /dev/null
+++ b/frontend/src/images/svgs/steam.tsx
@@ -0,0 +1,7 @@
1export function LoginIcon(){
2 return (
3 <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" className="text-white" height={32} width={32}>
4 <path d="M504 256c0 137-111.2 248-248.4 248-113.8 0-209.6-76.3-239-180.4l95.2 39.3c6.4 32.1 34.9 56.4 68.9 56.4 39.2 0 71.9-32.4 70.2-73.5l84.5-60.2c52.1 1.3 95.8-40.9 95.8-93.5 0-51.6-42-93.5-93.7-93.5s-93.7 42-93.7 93.5l0 1.2-59.2 85.7c-15.5-.9-30.7 3.4-43.5 12.1L8 236.1C18.2 108.4 125.1 8 255.6 8 392.8 8 504 119 504 256zM163.7 384.3l-30.5-12.6c5.6 11.6 15.3 20.8 27.2 25.8 26.9 11.2 57.8-1.6 69-28.4 5.4-13 5.5-27.3 .1-40.3S214 305.6 201 300.2c-12.9-5.4-26.7-5.2-38.9-.6l31.5 13c19.8 8.2 29.2 30.9 20.9 50.7-8.3 19.9-31 29.2-50.8 21zM337.5 129.8a62.3 62.3 0 1 1 0 124.6 62.3 62.3 0 1 1 0-124.6zm.1 109a46.8 46.8 0 1 0 0-93.6 46.8 46.8 0 1 0 0 93.6z" fill="currentColor"/>
5 </svg>
6 )
7} \ No newline at end of file
diff --git a/frontend/src/pages/About.tsx b/frontend/src/pages/About.tsx
index 5a69bfe..a5bb291 100644
--- a/frontend/src/pages/About.tsx
+++ b/frontend/src/pages/About.tsx
@@ -2,8 +2,6 @@ import React from "react";
2import ReactMarkdown from "react-markdown"; 2import ReactMarkdown from "react-markdown";
3import { Helmet } from "react-helmet"; 3import { Helmet } from "react-helmet";
4 4
5import "@css/About.css";
6
7const About: React.FC = () => { 5const About: React.FC = () => {
8 const [aboutText, setAboutText] = React.useState<string>(""); 6 const [aboutText, setAboutText] = React.useState<string>("");
9 7
@@ -26,7 +24,7 @@ const About: React.FC = () => {
26 }, []); 24 }, []);
27 25
28 return ( 26 return (
29 <div id="about"> 27 <div className="p-8 text-foreground font-[--font-barlow-semicondensed-regular] prose prose-invert max-w-none">
30 <Helmet> 28 <Helmet>
31 <title>LPHUB | About</title> 29 <title>LPHUB | About</title>
32 </Helmet> 30 </Helmet>
diff --git a/frontend/src/pages/Games.tsx b/frontend/src/pages/Games.tsx
index d7dacde..1ef0f57 100644
--- a/frontend/src/pages/Games.tsx
+++ b/frontend/src/pages/Games.tsx
@@ -3,41 +3,23 @@ import { Helmet } from "react-helmet";
3 3
4import GameEntry from "@components/GameEntry"; 4import GameEntry from "@components/GameEntry";
5import { Game } from "@customTypes/Game"; 5import { Game } from "@customTypes/Game";
6import "@css/Maps.css";
7 6
8interface GamesProps { 7interface GamesProps {
9 games: Game[]; 8 games: Game[];
10} 9}
11 10
12const Games: React.FC<GamesProps> = ({ games }) => { 11const Games: React.FC<GamesProps> = ({ games }) => {
13 const _page_load = () => {
14 const loaders = document.querySelectorAll(".loader");
15 loaders.forEach(loader => {
16 (loader as HTMLElement).style.display = "none";
17 });
18 };
19
20 React.useEffect(() => {
21 document
22 .querySelectorAll(".games-page-item-body")
23 .forEach((game, index) => {
24 game.innerHTML = "";
25 });
26 _page_load();
27 }, []);
28
29 return ( 12 return (
30 <div className="games-page"> 13 <div className="ml-10 min-h-screen w-[calc(100%-320px)] text-foreground font-[--font-barlow-semicondensed-regular] overflow-y-auto scrollbar-thin">
31 <Helmet> 14 <Helmet>
32 <title>LPHUB | Games</title> 15 <title>LPHUB | Games</title>
33 </Helmet> 16 </Helmet>
34 <section> 17 <section className="py-12 px-12 w-full">
35 <div className="games-page-content"> 18 <h1 className="text-3xl font-bold mb-8">Games</h1>
36 <div className="games-page-item-content"> 19 <div className="flex flex-col w-full">
37 {games.map((game, index) => ( 20 {games.map((game, index) => (
38 <GameEntry game={game} key={index} /> 21 <GameEntry game={game} key={index} />
39 ))} 22 ))}
40 </div>
41 </div> 23 </div>
42 </section> 24 </section>
43 </div> 25 </div>
diff --git a/frontend/src/pages/Homepage.tsx b/frontend/src/pages/Homepage.tsx
index f0c5821..2d16b8d 100644
--- a/frontend/src/pages/Homepage.tsx
+++ b/frontend/src/pages/Homepage.tsx
@@ -3,23 +3,23 @@ import { Helmet } from "react-helmet";
3 3
4const Homepage: React.FC = () => { 4const Homepage: React.FC = () => {
5 return ( 5 return (
6 <main> 6 <main className="text-foreground font-[--font-barlow-semicondensed-regular]">
7 <Helmet> 7 <Helmet>
8 <title>LPHUB | Homepage</title> 8 <title>LPHUB | Homepage</title>
9 </Helmet> 9 </Helmet>
10 <section> 10 <section className="p-8">
11 <p /> 11 <p />
12 <h1>Welcome to Least Portals Hub!</h1> 12 <h1 className="text-5xl font-[--font-barlow-condensed-bold] mb-6 text-primary">Welcome to Least Portals Hub!</h1>
13 <p> 13 <p className="text-lg mb-4 leading-relaxed">
14 At the moment, LPHUB is in beta state. This means that the site has 14 At the moment, LPHUB is in beta state. This means that the site has
15 only the core functionalities enabled for providing both collaborative 15 only the core functionalities enabled for providing both collaborative
16 information and competitive leaderboards. 16 information and competitive leaderboards.
17 </p> 17 </p>
18 <p> 18 <p className="text-lg mb-4 leading-relaxed">
19 The website should feel intuitive to navigate around. For any type of 19 The website should feel intuitive to navigate around. For any type of
20 feedback, reach us at LPHUB Discord server. 20 feedback, reach us at LPHUB Discord server.
21 </p> 21 </p>
22 <p> 22 <p className="text-lg mb-4 leading-relaxed">
23 By using LPHUB, you agree that you have read the 'Leaderboard Rules' 23 By using LPHUB, you agree that you have read the 'Leaderboard Rules'
24 and the 'About LPHUB' pages. 24 and the 'About LPHUB' pages.
25 </p> 25 </p>
diff --git a/frontend/src/pages/Maplist.tsx b/frontend/src/pages/Maplist.tsx
index a7242ef..8343129 100644
--- a/frontend/src/pages/Maplist.tsx
+++ b/frontend/src/pages/Maplist.tsx
@@ -2,7 +2,6 @@ import React, { useEffect } from "react";
2import { Link, useLocation, useNavigate, useParams } from "react-router-dom"; 2import { Link, useLocation, useNavigate, useParams } from "react-router-dom";
3import { Helmet } from "react-helmet"; 3import { Helmet } from "react-helmet";
4 4
5import "@css/Maplist.css";
6import { API } from "@api/Api"; 5import { API } from "@api/Api";
7import { Game } from "@customTypes/Game"; 6import { Game } from "@customTypes/Game";
8import { GameChapter, GamesChapters } from "@customTypes/Chapters"; 7import { GameChapter, GamesChapters } from "@customTypes/Chapters";
@@ -92,44 +91,52 @@ const Maplist: React.FC = () => {
92 <Helmet> 91 <Helmet>
93 <title>LPHUB | Maplist</title> 92 <title>LPHUB | Maplist</title>
94 </Helmet> 93 </Helmet>
95 <section style={{ marginTop: "20px" }}> 94
95 <section className="mt-5">
96 <Link to="/games"> 96 <Link to="/games">
97 <button className="nav-button" style={{ borderRadius: "20px" }}> 97 <button className="nav-button rounded-[20px] h-10 bg-surface border-0 text-foreground text-lg font-[--font-barlow-semicondensed-regular] transition-colors duration-100 hover:bg-surface2 flex items-center px-2">
98 <i className="triangle"></i> 98 <i className="triangle mr-2"></i>
99 <span>Games List</span> 99 <span className="px-2">Games List</span>
100 </button> 100 </button>
101 </Link> 101 </Link>
102 </section> 102 </section>
103
103 {load ? ( 104 {load ? (
104 <div></div> 105 <div></div>
105 ) : ( 106 ) : (
106 <section> 107 <section>
107 <h1>{game?.name}</h1> 108 <h1 className="font-[--font-barlow-condensed-bold] text-6xl my-0 text-foreground">
109 {game?.name}
110 </h1>
111
108 <div 112 <div
113 className="text-center rounded-3xl overflow-hidden bg-cover bg-[25%] mt-3 relative"
109 style={{ backgroundImage: `url(${game?.image})` }} 114 style={{ backgroundImage: `url(${game?.image})` }}
110 className="game-header"
111 > 115 >
112 <div className="blur"> 116 <div className="backdrop-blur-sm flex flex-col w-full">
113 <div className="game-header-portal-count"> 117 <div className="h-full flex flex-col justify-center items-center">
114 <h2 className="portal-count"> 118 <h2 className="my-5 font-[--font-barlow-semicondensed-semibold] text-8xl text-foreground">
115 { 119 {
116 game?.category_portals.find( 120 game?.category_portals.find(
117 obj => obj.category.id === catNum + 1 121 obj => obj.category.id === catNum + 1
118 )?.portal_count 122 )?.portal_count
119 } 123 }
120 </h2> 124 </h2>
121 <h3>portals</h3> 125 <h3 className="font-[--font-barlow-semicondensed-regular] mx-2.5 text-4xl my-0 text-foreground">
126 portals
127 </h3>
122 </div> 128 </div>
123 <div className="game-header-categories"> 129
130 <div className="flex h-12 bg-surface gap-0.5">
124 {game?.category_portals.map((cat, index) => ( 131 {game?.category_portals.map((cat, index) => (
125 <button 132 <button
126 key={index} 133 key={index}
127 className={ 134 className={`border-0 text-foreground font-[--font-barlow-semicondensed-regular] text-xl cursor-pointer transition-all duration-100 w-full ${
128 currentlySelected === cat.category.id || 135 currentlySelected === cat.category.id ||
129 (cat.category.id - 1 === catNum && !hasClicked) 136 (cat.category.id - 1 === catNum && !hasClicked)
130 ? "game-cat-button selected" 137 ? "bg-surface"
131 : "game-cat-button" 138 : "bg-surface1 hover:bg-surface"
132 } 139 }`}
133 onClick={() => { 140 onClick={() => {
134 setCatNum(cat.category.id - 1); 141 setCatNum(cat.category.id - 1);
135 _update_currently_selected(cat.category.id); 142 _update_currently_selected(cat.category.id);
@@ -143,31 +150,32 @@ const Maplist: React.FC = () => {
143 </div> 150 </div>
144 151
145 <div> 152 <div>
146 <section className="chapter-select-container"> 153 <section>
147 <div> 154 <div>
148 <span 155 <span className="text-lg translate-y-1.5 block mt-2.5 text-foreground">
149 style={{
150 fontSize: "18px",
151 transform: "translateY(5px)",
152 display: "block",
153 marginTop: "10px",
154 }}
155 >
156 {curChapter?.chapter.name.split(" - ")[0]} 156 {curChapter?.chapter.name.split(" - ")[0]}
157 </span> 157 </span>
158 </div> 158 </div>
159 <div onClick={_handle_dropdown_click} className="dropdown"> 159 <div
160 <span>{curChapter?.chapter.name.split(" - ")[1]}</span> 160 onClick={_handle_dropdown_click}
161 <i className="triangle"></i> 161 className="cursor-pointer select-none flex w-fit items-center"
162 >
163 <span className="text-foreground text-2xl">
164 {curChapter?.chapter.name.split(" - ")[1]}
165 </span>
166 <i className="triangle translate-x-1.5 translate-y-2 -rotate-90"></i>
162 </div> 167 </div>
168 \
163 <div 169 <div
164 className="dropdown-elements" 170 className={`absolute z-[1000] bg-surface1 rounded-2xl overflow-hidden p-1 animate-in fade-in duration-100 ${
165 style={{ display: dropdownActive }} 171 dropdownActive === "none" ? "hidden" : "block"
172 }`}
166 > 173 >
167 {gameChapters?.chapters.map((chapter, i) => { 174 {gameChapters?.chapters.map((chapter, i) => {
168 return ( 175 return (
169 <div 176 <div
170 className="dropdown-element" 177 key={i}
178 className="cursor-pointer text-xl rounded-[2000px] p-1 hover:bg-surface text-foreground"
171 onClick={() => { 179 onClick={() => {
172 _fetch_chapters(chapter.id.toString()); 180 _fetch_chapters(chapter.id.toString());
173 _handle_dropdown_click(); 181 _handle_dropdown_click();
@@ -179,49 +187,52 @@ const Maplist: React.FC = () => {
179 })} 187 })}
180 </div> 188 </div>
181 </section> 189 </section>
182 <section className="maplist"> 190
191 <section className="grid grid-cols-4 gap-5 my-5">
183 {curChapter?.maps.map((map, i) => { 192 {curChapter?.maps.map((map, i) => {
184 return ( 193 return (
185 <div className="maplist-entry"> 194 <div key={i} className="bg-surface rounded-3xl overflow-hidden">
186 <Link to={`/maps/${map.id}`}> 195 <Link to={`/maps/${map.id}`}>
187 <span>{map.name}</span> 196 <span className="text-center text-xl w-full block my-1.5 text-foreground">
197 {map.name}
198 </span>
188 <div 199 <div
189 className="map-entry-image" 200 className="flex h-48 bg-cover relative"
190 style={{ backgroundImage: `url(${map.image})` }} 201 style={{ backgroundImage: `url(${map.image})` }}
191 > 202 >
192 <div className="blur map"> 203 <div className="backdrop-blur-sm w-full flex items-center justify-center">
193 <span> 204 <span className="text-4xl font-[--font-barlow-semicondensed-semibold] text-white mr-1.5">
194 {map.is_disabled 205 {map.is_disabled
195 ? map.category_portals[0].portal_count 206 ? map.category_portals[0].portal_count
196 : map.category_portals.find( 207 : map.category_portals.find(
197 obj => obj.category.id === catNum + 1 208 obj => obj.category.id === catNum + 1
198 )?.portal_count} 209 )?.portal_count}
199 </span> 210 </span>
200 <span>portals</span> 211 <span className="text-4xl font-[--font-barlow-semicondensed-regular] text-white">
212 portals
213 </span>
201 </div> 214 </div>
202 </div> 215 </div>
203 <div className="difficulty-bar"> 216
204 {/* <span>Difficulty:</span> */} 217 {/* Difficulty rating */}
205 <div 218 <div className="flex mx-2.5 my-4">
206 className={ 219 <div className="flex w-full items-center justify-center gap-1.5 rounded-[2000px] ml-0.5 translate-y-px">
207 map.difficulty === 0 220 {[1, 2, 3, 4, 5].map((point) => (
208 ? "one" 221 <div
209 : map.difficulty === 1 222 key={point}
210 ? "two" 223 className={`flex h-0.5 w-full rounded-3xl ${
211 : map.difficulty === 2 224 point <= (map.difficulty + 1)
212 ? "three" 225 ? map.difficulty === 0
213 : map.difficulty === 3 226 ? "bg-green-500"
214 ? "four" 227 : map.difficulty === 1 || map.difficulty === 2
215 : map.difficulty === 4 228 ? "bg-lime-500"
216 ? "five" 229 : map.difficulty === 3
217 : "one" 230 ? "bg-red-400"
218 } 231 : "bg-red-600"
219 > 232 : "bg-surface1"
220 <div className="difficulty-point"></div> 233 }`}
221 <div className="difficulty-point"></div> 234 />
222 <div className="difficulty-point"></div> 235 ))}
223 <div className="difficulty-point"></div>
224 <div className="difficulty-point"></div>
225 </div> 236 </div>
226 </div> 237 </div>
227 </Link> 238 </Link>
diff --git a/frontend/src/pages/Maps.tsx b/frontend/src/pages/Maps.tsx
index fbdb8f3..75753ac 100644
--- a/frontend/src/pages/Maps.tsx
+++ b/frontend/src/pages/Maps.tsx
@@ -2,14 +2,13 @@ import React from "react";
2import { Link, useLocation } from "react-router-dom"; 2import { Link, useLocation } from "react-router-dom";
3import { Helmet } from "react-helmet"; 3import { Helmet } from "react-helmet";
4 4
5import { PortalIcon, FlagIcon, ChatIcon } from "@images/Images"; 5import { PortalIcon, FlagIcon, ChatIcon } from "../images/Images";
6import Summary from "@components/Summary"; 6import Summary from "@components/Summary";
7import Leaderboards from "@components/Leaderboards"; 7import Leaderboards from "@components/Leaderboards";
8import Discussions from "@components/Discussions"; 8import Discussions from "@components/Discussions";
9import ModMenu from "@components/ModMenu"; 9import ModMenu from "@components/ModMenu";
10import { MapDiscussions, MapLeaderboard, MapSummary } from "@customTypes/Map"; 10import { MapDiscussions, MapLeaderboard, MapSummary } from "@customTypes/Map";
11import { API } from "@api/Api"; 11import { API } from "@api/Api";
12import "@css/Maps.css";
13 12
14interface MapProps { 13interface MapProps {
15 token?: string; 14 token?: string;
@@ -82,15 +81,15 @@ const Maps: React.FC<MapProps> = ({ token, isModerator }) => {
82 81
83 <section id="section2" className="summary1"> 82 <section id="section2" className="summary1">
84 <button className="nav-button"> 83 <button className="nav-button">
85 <img src={PortalIcon} alt="" /> 84 <img src={PortalIcon} alt="" className="w-6 h-6" />
86 <span>Summary</span> 85 <span>Summary</span>
87 </button> 86 </button>
88 <button className="nav-button"> 87 <button className="nav-button">
89 <img src={FlagIcon} alt="" /> 88 <img src={FlagIcon} alt="" className="w-6 h-6" />
90 <span>Leaderboards</span> 89 <span>Leaderboards</span>
91 </button> 90 </button>
92 <button className="nav-button"> 91 <button className="nav-button">
93 <img src={ChatIcon} alt="" /> 92 <img src={ChatIcon} alt="" className="w-6 h-6" />
94 <span>Discussions</span> 93 <span>Discussions</span>
95 </button> 94 </button>
96 </section> 95 </section>
@@ -151,15 +150,15 @@ const Maps: React.FC<MapProps> = ({ token, isModerator }) => {
151 150
152 <section id="section2" className="summary1"> 151 <section id="section2" className="summary1">
153 <button className="nav-button" onClick={() => setNavState(0)}> 152 <button className="nav-button" onClick={() => setNavState(0)}>
154 <img src={PortalIcon} alt="" /> 153 <img src={PortalIcon} alt="" className="w-6 h-6" />
155 <span>Summary</span> 154 <span>Summary</span>
156 </button> 155 </button>
157 <button className="nav-button" onClick={() => setNavState(1)}> 156 <button className="nav-button" onClick={() => setNavState(1)}>
158 <img src={FlagIcon} alt="" /> 157 <img src={FlagIcon} alt="" className="w-6 h-6" />
159 <span>Leaderboards</span> 158 <span>Leaderboards</span>
160 </button> 159 </button>
161 <button className="nav-button" onClick={() => setNavState(2)}> 160 <button className="nav-button" onClick={() => setNavState(2)}>
162 <img src={ChatIcon} alt="" /> 161 <img src={ChatIcon} alt="" className="w-6 h-6" />
163 <span>Discussions</span> 162 <span>Discussions</span>
164 </button> 163 </button>
165 </section> 164 </section>
diff --git a/frontend/src/pages/Profile.tsx b/frontend/src/pages/Profile.tsx
index e2d6000..f44f587 100644
--- a/frontend/src/pages/Profile.tsx
+++ b/frontend/src/pages/Profile.tsx
@@ -19,7 +19,6 @@ import { UserProfile } from "@customTypes/Profile";
19import { Game, GameChapters } from "@customTypes/Game"; 19import { Game, GameChapters } from "@customTypes/Game";
20import { Map } from "@customTypes/Map"; 20import { Map } from "@customTypes/Map";
21import { ticks_to_time } from "@utils/Time"; 21import { ticks_to_time } from "@utils/Time";
22import "@css/Profile.css";
23import { API } from "@api/Api"; 22import { API } from "@api/Api";
24import useConfirm from "@hooks/UseConfirm"; 23import useConfirm from "@hooks/UseConfirm";
25import useMessage from "@hooks/UseMessage"; 24import useMessage from "@hooks/UseMessage";
diff --git a/frontend/src/pages/Rules.tsx b/frontend/src/pages/Rules.tsx
index 91027a0..7cdc08b 100644
--- a/frontend/src/pages/Rules.tsx
+++ b/frontend/src/pages/Rules.tsx
@@ -2,8 +2,6 @@ import React from "react";
2import ReactMarkdown from "react-markdown"; 2import ReactMarkdown from "react-markdown";
3import { Helmet } from "react-helmet"; 3import { Helmet } from "react-helmet";
4 4
5import "@css/Rules.css";
6
7const Rules: React.FC = () => { 5const Rules: React.FC = () => {
8 const [rulesText, setRulesText] = React.useState<string>(""); 6 const [rulesText, setRulesText] = React.useState<string>("");
9 7
@@ -27,7 +25,7 @@ const Rules: React.FC = () => {
27 }, []); 25 }, []);
28 26
29 return ( 27 return (
30 <main> 28 <main className="p-8 text-foreground font-[--font-barlow-semicondensed-regular] prose prose-invert max-w-none">
31 <Helmet> 29 <Helmet>
32 <title>LPHUB | Rules</title> 30 <title>LPHUB | Rules</title>
33 </Helmet> 31 </Helmet>
diff --git a/frontend/src/pages/User.tsx b/frontend/src/pages/User.tsx
index 0198034..4b8a456 100644
--- a/frontend/src/pages/User.tsx
+++ b/frontend/src/pages/User.tsx
@@ -19,7 +19,6 @@ import { Game, GameChapters } from "@customTypes/Game";
19import { Map } from "@customTypes/Map"; 19import { Map } from "@customTypes/Map";
20import { API } from "@api/Api"; 20import { API } from "@api/Api";
21import { ticks_to_time } from "@utils/Time"; 21import { ticks_to_time } from "@utils/Time";
22import "@css/Profile.css";
23import useMessage from "@hooks/UseMessage"; 22import useMessage from "@hooks/UseMessage";
24 23
25interface UserProps { 24interface UserProps {
diff --git a/frontend/vite.config.ts b/frontend/vite.config.ts
index 3c2c9bd..aa41236 100644
--- a/frontend/vite.config.ts
+++ b/frontend/vite.config.ts
@@ -1,9 +1,10 @@
1import { defineConfig } from 'vite' 1import { defineConfig } from 'vite'
2import react from '@vitejs/plugin-react' 2import react from '@vitejs/plugin-react'
3import tailwindcss from '@tailwindcss/vite'
3import path from 'path' 4import path from 'path'
4 5
5export default defineConfig({ 6export default defineConfig({
6 plugins: [react()], 7 plugins: [react(), tailwindcss()],
7 resolve: { 8 resolve: {
8 alias: { 9 alias: {
9 '@api': path.resolve(__dirname, './src/api'), 10 '@api': path.resolve(__dirname, './src/api'),
@@ -20,7 +21,7 @@ export default defineConfig({
20 port: 3000, 21 port: 3000,
21 proxy: { 22 proxy: {
22 '/api': { 23 '/api': {
23 target: 'http://localhost:8080', 24 target: 'https://lp.pektezol.dev/',
24 changeOrigin: true, 25 changeOrigin: true,
25 }, 26 },
26 }, 27 },