diff --git a/frontend/package.json b/frontend/package.json index ee4ac70..fad731e 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -26,9 +26,12 @@ "@radix-ui/react-slot": "^1.0.2", "@tanstack/react-query": "^5.22.2", "@tanstack/react-table": "^8.12.0", + "@tremor/react": "^3.14.1", + "@upstash/redis": "^1.28.4", "class-variance-authority": "^0.7.0", "clsx": "^2.1.0", "cmdk": "^0.2.1", + "date-fns": "^3.3.1", "dayjs": "^1.11.10", "framer-motion": "^11.0.6", "lucide-react": "^0.334.0", @@ -36,6 +39,7 @@ "next": "14.1.0", "next-themes": "^0.2.1", "react": "^18", + "react-country-flag": "^3.1.0", "react-dom": "^18", "react-hook-form": "^7.50.1", "react-timer-hook": "^3.0.7", diff --git a/frontend/pnpm-lock.yaml b/frontend/pnpm-lock.yaml index 31b3219..8e994bc 100644 --- a/frontend/pnpm-lock.yaml +++ b/frontend/pnpm-lock.yaml @@ -56,6 +56,12 @@ dependencies: '@tanstack/react-table': specifier: ^8.12.0 version: 8.12.0(react-dom@18.2.0)(react@18.2.0) + '@tremor/react': + specifier: ^3.14.1 + version: 3.14.1(react-dom@18.2.0)(react@18.2.0)(tailwindcss@3.4.1) + '@upstash/redis': + specifier: ^1.28.4 + version: 1.28.4 class-variance-authority: specifier: ^0.7.0 version: 0.7.0 @@ -65,6 +71,9 @@ dependencies: cmdk: specifier: ^0.2.1 version: 0.2.1(@types/react@18.2.57)(react-dom@18.2.0)(react@18.2.0) + date-fns: + specifier: ^3.3.1 + version: 3.3.1 dayjs: specifier: ^1.11.10 version: 1.11.10 @@ -86,6 +95,9 @@ dependencies: react: specifier: ^18 version: 18.2.0 + react-country-flag: + specifier: ^3.1.0 + version: 3.1.0(react@18.2.0) react-dom: specifier: ^18 version: 18.2.0(react@18.2.0) @@ -314,6 +326,17 @@ packages: '@floating-ui/utils': 0.2.1 dev: false + /@floating-ui/react-dom@1.3.0(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-htwHm67Ji5E/pROEAr7f8IKFShuiCKHwUC/UY4vC3I5jiSvGFAYnSYiZO5MlGmads+QqvUkR9ANHEguGrDv72g==} + peerDependencies: + react: '>=16.8.0' + react-dom: '>=16.8.0' + dependencies: + '@floating-ui/dom': 1.6.3 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + dev: false + /@floating-ui/react-dom@2.0.8(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-HOdqOt3R3OGeTKidaLvJKcgg75S6tibQ3Tif4eyd91QnIJWr0NLvoXFpJA/j8HqkFSL68GDca9AuyWEHlhyClw==} peerDependencies: @@ -325,10 +348,45 @@ packages: react-dom: 18.2.0(react@18.2.0) dev: false + /@floating-ui/react@0.19.2(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-JyNk4A0Ezirq8FlXECvRtQOX/iBe5Ize0W/pLkrZjfHW9GUV7Xnq6zm6fyZuQzaHHqEnVizmvlA96e1/CkZv+w==} + peerDependencies: + react: '>=16.8.0' + react-dom: '>=16.8.0' + dependencies: + '@floating-ui/react-dom': 1.3.0(react-dom@18.2.0)(react@18.2.0) + aria-hidden: 1.2.3 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + tabbable: 6.2.0 + dev: false + /@floating-ui/utils@0.2.1: resolution: {integrity: sha512-9TANp6GPoMtYzQdt54kfAyMmz1+osLlXdg2ENroU7zzrtflTLrrC/lgrIfaSe+Wu0b89GKccT7vxXA0MoAIO+Q==} dev: false + /@headlessui/react@1.7.18(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-4i5DOrzwN4qSgNsL4Si61VMkUcWbcSKueUV7sFhpHzQcSShdlHENE5+QBntMSRvHt8NyoFO2AGG8si9lq+w4zQ==} + engines: {node: '>=10'} + peerDependencies: + react: ^16 || ^17 || ^18 + react-dom: ^16 || ^17 || ^18 + dependencies: + '@tanstack/react-virtual': 3.1.3(react-dom@18.2.0)(react@18.2.0) + client-only: 0.0.1 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + dev: false + + /@headlessui/tailwindcss@0.2.0(tailwindcss@3.4.1): + resolution: {integrity: sha512-fpL830Fln1SykOCboExsWr3JIVeQKieLJ3XytLe/tt1A0XzqUthOftDmjcCYLW62w7mQI7wXcoPXr3tZ9QfGxw==} + engines: {node: '>=10'} + peerDependencies: + tailwindcss: ^3.0 + dependencies: + tailwindcss: 3.4.1 + dev: false + /@hookform/resolvers@3.3.4(react-hook-form@7.50.1): resolution: {integrity: sha512-o5cgpGOuJYrd+iMKvkttOclgwRW86EsWJZZRC23prf0uU2i48Htq4PuT73AVb9ionFyZrwYEITuOFGF+BydEtQ==} peerDependencies: @@ -1587,11 +1645,46 @@ packages: react-dom: 18.2.0(react@18.2.0) dev: false + /@tanstack/react-virtual@3.1.3(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-YCzcbF/Ws/uZ0q3Z6fagH+JVhx4JLvbSflgldMgLsuvB8aXjZLLb3HvrEVxY480F9wFlBiXlvQxOyXb5ENPrNA==} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 + react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 + dependencies: + '@tanstack/virtual-core': 3.1.3 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + dev: false + /@tanstack/table-core@8.12.0: resolution: {integrity: sha512-cq/ylWVrOwixmwNXQjgZaQw1Izf7+nPxjczum7paAnMtwPg1S2qRAJU+Jb8rEBUWm69voC/zcChmePlk2hc6ug==} engines: {node: '>=12'} dev: false + /@tanstack/virtual-core@3.1.3: + resolution: {integrity: sha512-Y5B4EYyv1j9V8LzeAoOVeTg0LI7Fo5InYKgAjkY1Pu9GjtUwX/EKxNcU7ng3sKr99WEf+bPTcktAeybyMOYo+g==} + dev: false + + /@tremor/react@3.14.1(react-dom@18.2.0)(react@18.2.0)(tailwindcss@3.4.1): + resolution: {integrity: sha512-0LMxFIeBXsAaPnR6mXRK4fbZaTNLFfVngFpoOt+6Tf797k/c6yUkB48/QPB5vO02qzkV74D91hng9r6HwfDW5g==} + peerDependencies: + react: ^18.0.0 + react-dom: '>=16.6.0' + dependencies: + '@floating-ui/react': 0.19.2(react-dom@18.2.0)(react@18.2.0) + '@headlessui/react': 1.7.18(react-dom@18.2.0)(react@18.2.0) + '@headlessui/tailwindcss': 0.2.0(tailwindcss@3.4.1) + date-fns: 2.30.0 + react: 18.2.0 + react-day-picker: 8.10.0(date-fns@2.30.0)(react@18.2.0) + react-dom: 18.2.0(react@18.2.0) + react-transition-state: 2.1.1(react-dom@18.2.0)(react@18.2.0) + recharts: 2.12.2(react-dom@18.2.0)(react@18.2.0) + tailwind-merge: 1.14.0 + transitivePeerDependencies: + - tailwindcss + dev: false + /@types/body-parser@1.19.5: resolution: {integrity: sha512-fB3Zu92ucau0iQ0JMCFQE7b/dv8Ot07NI3KaZIkIUNXq82k4eBAqUaneXfleGY9JWskeS9y+u0nXMyspcuQrCg==} dependencies: @@ -1614,6 +1707,48 @@ packages: '@types/node': 20.11.19 dev: false + /@types/d3-array@3.2.1: + resolution: {integrity: sha512-Y2Jn2idRrLzUfAKV2LyRImR+y4oa2AntrgID95SHJxuMUrkNXmanDSed71sRNZysveJVt1hLLemQZIady0FpEg==} + dev: false + + /@types/d3-color@3.1.3: + resolution: {integrity: sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==} + dev: false + + /@types/d3-ease@3.0.2: + resolution: {integrity: sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==} + dev: false + + /@types/d3-interpolate@3.0.4: + resolution: {integrity: sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==} + dependencies: + '@types/d3-color': 3.1.3 + dev: false + + /@types/d3-path@3.1.0: + resolution: {integrity: sha512-P2dlU/q51fkOc/Gfl3Ul9kicV7l+ra934qBFXCFhrZMOL6du1TM0pm1ThYvENukyOn5h9v+yMJ9Fn5JK4QozrQ==} + dev: false + + /@types/d3-scale@4.0.8: + resolution: {integrity: sha512-gkK1VVTr5iNiYJ7vWDI+yUFFlszhNMtVeneJ6lUTKPjprsvLLI9/tgEGiXJOnlINJA8FyA88gfnQsHbybVZrYQ==} + dependencies: + '@types/d3-time': 3.0.3 + dev: false + + /@types/d3-shape@3.1.6: + resolution: {integrity: sha512-5KKk5aKGu2I+O6SONMYSNflgiP0WfZIQvVUMan50wHsLG1G94JlxEVnCpQARfTtzytuY0p/9PXXZb3I7giofIA==} + dependencies: + '@types/d3-path': 3.1.0 + dev: false + + /@types/d3-time@3.0.3: + resolution: {integrity: sha512-2p6olUZ4w3s+07q3Tm2dbiMZy5pCDfYwtLXXHUnVzXgQlZ/OyPtUz6OL382BkOuGlLXqfT+wqv8Fw2v8/0geBw==} + dev: false + + /@types/d3-timer@3.0.2: + resolution: {integrity: sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==} + dev: false + /@types/express-serve-static-core@4.17.43: resolution: {integrity: sha512-oaYtiBirUOPQGSWNGPWnzyAFJ0BP3cwvN4oWZQY+zUBwpVIGsKUkpBpSztp74drYcjavs7SKFZ4DX1V2QeN8rg==} dependencies: @@ -1804,6 +1939,12 @@ packages: resolution: {integrity: sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==} dev: true + /@upstash/redis@1.28.4: + resolution: {integrity: sha512-UalkSAny/dz1m8giEhD3Y5ru1o+CPHI32wFyS3MyzDzj2TRvEN+lTw+mPwi20ojk0H2gs8TBW3qsrvwuLLy+pA==} + dependencies: + crypto-js: 4.2.0 + dev: false + /acorn-jsx@5.3.2(acorn@8.11.3): resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} peerDependencies: @@ -2194,6 +2335,10 @@ packages: shebang-command: 2.0.0 which: 2.0.2 + /crypto-js@4.2.0: + resolution: {integrity: sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q==} + dev: false + /cssesc@3.0.0: resolution: {integrity: sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==} engines: {node: '>=4'} @@ -2206,10 +2351,92 @@ packages: /csstype@3.1.3: resolution: {integrity: sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==} + /d3-array@3.2.4: + resolution: {integrity: sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==} + engines: {node: '>=12'} + dependencies: + internmap: 2.0.3 + dev: false + + /d3-color@3.1.0: + resolution: {integrity: sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==} + engines: {node: '>=12'} + dev: false + + /d3-ease@3.0.1: + resolution: {integrity: sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==} + engines: {node: '>=12'} + dev: false + + /d3-format@3.1.0: + resolution: {integrity: sha512-YyUI6AEuY/Wpt8KWLgZHsIU86atmikuoOmCfommt0LYHiQSPjvX2AcFc38PX0CBpr2RCyZhjex+NS/LPOv6YqA==} + engines: {node: '>=12'} + dev: false + + /d3-interpolate@3.0.1: + resolution: {integrity: sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==} + engines: {node: '>=12'} + dependencies: + d3-color: 3.1.0 + dev: false + + /d3-path@3.1.0: + resolution: {integrity: sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==} + engines: {node: '>=12'} + dev: false + + /d3-scale@4.0.2: + resolution: {integrity: sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==} + engines: {node: '>=12'} + dependencies: + d3-array: 3.2.4 + d3-format: 3.1.0 + d3-interpolate: 3.0.1 + d3-time: 3.1.0 + d3-time-format: 4.1.0 + dev: false + + /d3-shape@3.2.0: + resolution: {integrity: sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==} + engines: {node: '>=12'} + dependencies: + d3-path: 3.1.0 + dev: false + + /d3-time-format@4.1.0: + resolution: {integrity: sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==} + engines: {node: '>=12'} + dependencies: + d3-time: 3.1.0 + dev: false + + /d3-time@3.1.0: + resolution: {integrity: sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==} + engines: {node: '>=12'} + dependencies: + d3-array: 3.2.4 + dev: false + + /d3-timer@3.0.1: + resolution: {integrity: sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==} + engines: {node: '>=12'} + dev: false + /damerau-levenshtein@1.0.8: resolution: {integrity: sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA==} dev: true + /date-fns@2.30.0: + resolution: {integrity: sha512-fnULvOpxnC5/Vg3NCiWelDsLiUc9bRwAPs/+LfTLNvetFCtCTN+yQz15C/fs4AwX1R9K5GLtLfn8QW+dWisaAw==} + engines: {node: '>=0.11'} + dependencies: + '@babel/runtime': 7.23.9 + dev: false + + /date-fns@3.3.1: + resolution: {integrity: sha512-y8e109LYGgoQDveiEBD3DYXKba1jWf5BA8YU1FL5Tvm0BTdEfy54WLCwnuYWZNnzzvALy/QQ4Hov+Q9RVRv+Zw==} + dev: false + /dayjs@1.11.10: resolution: {integrity: sha512-vjAczensTgRcqDERK0SR2XMwsF/tSvnvlv6VcF2GIhg6Sx4yOIt/irsr1RDJsKiIyBzJDpCoXiWWq28MqH2cnQ==} dev: false @@ -2237,6 +2464,10 @@ packages: ms: 2.1.2 dev: true + /decimal.js-light@2.5.1: + resolution: {integrity: sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg==} + dev: false + /deep-is@0.1.4: resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} dev: true @@ -2305,6 +2536,13 @@ packages: esutils: 2.0.3 dev: true + /dom-helpers@5.2.1: + resolution: {integrity: sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==} + dependencies: + '@babel/runtime': 7.23.9 + csstype: 3.1.3 + dev: false + /dot-case@3.0.4: resolution: {integrity: sha512-Kv5nKlh6yRrdrGvxeJ2e5y2eRUpkUosIW4A2AS38zwSz27zu7ufDwQPi5Jhs3XAlGNetl3bmnGhQsMtkKJnj3w==} dependencies: @@ -2726,10 +2964,19 @@ packages: engines: {node: '>=0.10.0'} dev: true + /eventemitter3@4.0.7: + resolution: {integrity: sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==} + dev: false + /fast-deep-equal@3.1.3: resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} dev: true + /fast-equals@5.0.1: + resolution: {integrity: sha512-WF1Wi8PwwSY7/6Kx0vKXtw8RwuSGoM1bvDaJbu7MxDlR1vovZjIAKrnzyrThgAjm6JDTu0fVgWXDlMGspodfoQ==} + engines: {node: '>=6.0.0'} + dev: false + /fast-glob@3.3.2: resolution: {integrity: sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==} engines: {node: '>=8.6.0'} @@ -3043,6 +3290,11 @@ packages: side-channel: 1.0.5 dev: true + /internmap@2.0.3: + resolution: {integrity: sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==} + engines: {node: '>=12'} + dev: false + /invariant@2.2.4: resolution: {integrity: sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==} dependencies: @@ -3329,6 +3581,10 @@ packages: resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} dev: true + /lodash@4.17.21: + resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==} + dev: false + /loose-envify@1.4.0: resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==} hasBin: true @@ -3757,7 +4013,6 @@ packages: loose-envify: 1.4.0 object-assign: 4.1.1 react-is: 16.13.1 - dev: true /punycode@2.3.1: resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} @@ -3783,6 +4038,25 @@ packages: engines: {node: '>=8'} dev: false + /react-country-flag@3.1.0(react@18.2.0): + resolution: {integrity: sha512-JWQFw1efdv9sTC+TGQvTKXQg1NKbDU2mBiAiRWcKM9F1sK+/zjhP2yGmm8YDddWyZdXVkR8Md47rPMJmo4YO5g==} + engines: {node: '>=12'} + peerDependencies: + react: '>=16' + dependencies: + react: 18.2.0 + dev: false + + /react-day-picker@8.10.0(date-fns@2.30.0)(react@18.2.0): + resolution: {integrity: sha512-mz+qeyrOM7++1NCb1ARXmkjMkzWVh2GL9YiPbRjKe0zHccvekk4HE+0MPOZOrosn8r8zTHIIeOUXTmXRqmkRmg==} + peerDependencies: + date-fns: ^2.28.0 || ^3.0.0 + react: ^16.8.0 || ^17.0.0 || ^18.0.0 + dependencies: + date-fns: 2.30.0 + react: 18.2.0 + dev: false + /react-dom@18.2.0(react@18.2.0): resolution: {integrity: sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g==} peerDependencies: @@ -3804,7 +4078,6 @@ packages: /react-is@16.13.1: resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==} - dev: true /react-remove-scroll-bar@2.3.5(@types/react@18.2.57)(react@18.2.0): resolution: {integrity: sha512-3cqjOqg6s0XbOjWvmasmqHch+RLxIEk2r/70rzGXuz3iIGQsQheEQyqYCBb5EECoD01Vo2SIbDqW4paLeLTASw==} @@ -3860,6 +4133,19 @@ packages: use-sidecar: 1.1.2(@types/react@18.2.57)(react@18.2.0) dev: false + /react-smooth@4.0.0(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-2NMXOBY1uVUQx1jBeENGA497HK20y6CPGYL1ZnJLeoQ8rrc3UfmOM82sRxtzpcoCkUMy4CS0RGylfuVhuFjBgg==} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 + react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 + dependencies: + fast-equals: 5.0.1 + prop-types: 15.8.1 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + react-transition-group: 4.4.5(react-dom@18.2.0)(react@18.2.0) + dev: false + /react-style-singleton@2.2.1(@types/react@18.2.57)(react@18.2.0): resolution: {integrity: sha512-ZWj0fHEMyWkHzKYUr2Bs/4zU6XLmq9HsgBURm7g5pAVfyn49DgUiNgY2d4lXRlYSiCif9YBGpQleewkcqddc7g==} engines: {node: '>=10'} @@ -3885,6 +4171,30 @@ packages: react: 18.2.0 dev: false + /react-transition-group@4.4.5(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==} + peerDependencies: + react: '>=16.6.0' + react-dom: '>=16.6.0' + dependencies: + '@babel/runtime': 7.23.9 + dom-helpers: 5.2.1 + loose-envify: 1.4.0 + prop-types: 15.8.1 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + dev: false + + /react-transition-state@2.1.1(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-kQx5g1FVu9knoz1T1WkapjUgFz08qQ/g1OmuWGi3/AoEFfS0kStxrPlZx81urjCXdz2d+1DqLpU6TyLW/Ro04Q==} + peerDependencies: + react: '>=16.8.0' + react-dom: '>=16.8.0' + dependencies: + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + dev: false + /react@18.2.0: resolution: {integrity: sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==} engines: {node: '>=0.10.0'} @@ -3903,6 +4213,31 @@ packages: dependencies: picomatch: 2.3.1 + /recharts-scale@0.4.5: + resolution: {integrity: sha512-kivNFO+0OcUNu7jQquLXAxz1FIwZj8nrj+YkOKc5694NbjCvcT6aSZiIzNzd2Kul4o4rTto8QVR9lMNtxD4G1w==} + dependencies: + decimal.js-light: 2.5.1 + dev: false + + /recharts@2.12.2(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-9bpxjXSF5g81YsKkTSlaX7mM4b6oYI1mIYck6YkUcWuL3tomADccI51/6thY4LmvhYuRTwpfrOvE80Zc3oBRfQ==} + engines: {node: '>=14'} + peerDependencies: + react: ^16.0.0 || ^17.0.0 || ^18.0.0 + react-dom: ^16.0.0 || ^17.0.0 || ^18.0.0 + dependencies: + clsx: 2.1.0 + eventemitter3: 4.0.7 + lodash: 4.17.21 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + react-is: 16.13.1 + react-smooth: 4.0.0(react-dom@18.2.0)(react@18.2.0) + recharts-scale: 0.4.5 + tiny-invariant: 1.3.3 + victory-vendor: 36.9.1 + dev: false + /reflect.getprototypeof@1.0.5: resolution: {integrity: sha512-62wgfC8dJWrmxv44CA36pLDnP6KKl3Vhxb7PL+8+qrrFMMoJij4vgiMP8zV4O8+CBMXY1mHxI5fITGHXFHVmQQ==} engines: {node: '>= 0.4'} @@ -4230,6 +4565,14 @@ packages: use-sync-external-store: 1.2.0(react@18.2.0) dev: false + /tabbable@6.2.0: + resolution: {integrity: sha512-Cat63mxsVJlzYvN51JmVXIgNoUokrIaT2zLclCXjRd8boZ0004U4KCs/sToJ75C6sdlByWxpYnb5Boif1VSFew==} + dev: false + + /tailwind-merge@1.14.0: + resolution: {integrity: sha512-3mFKyCo/MBcgyOTlrY8T7odzZFx+w+qKSMAmdFzRvqBfLlSigU6TZnlFHK0lkMwj9Bj8OYU+9yW9lmGuS0QEnQ==} + dev: false + /tailwind-merge@2.2.1: resolution: {integrity: sha512-o+2GTLkthfa5YUt4JxPfzMIpQzZ3adD1vLVkvKE1Twl9UAhGsEbIZhHHZVRttyW177S8PDJI3bTQNaebyofK3Q==} dependencies: @@ -4294,6 +4637,10 @@ packages: dependencies: any-promise: 1.3.0 + /tiny-invariant@1.3.3: + resolution: {integrity: sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==} + dev: false + /to-no-case@1.0.2: resolution: {integrity: sha512-Z3g735FxuZY8rodxV4gH7LxClE4H0hTIyHNIHdk+vpQxjLm0cwnKXq/OFVZ76SOQmto7txVcwSCwkU5kqp+FKg==} dev: false @@ -4492,6 +4839,25 @@ packages: - '@types/react-dom' dev: false + /victory-vendor@36.9.1: + resolution: {integrity: sha512-+pZIP+U3pEJdDCeFmsXwHzV7vNHQC/eIbHklfe2ZCZqayYRH7lQbHcVgsJ0XOOv27hWs4jH4MONgXxHMObTMSA==} + dependencies: + '@types/d3-array': 3.2.1 + '@types/d3-ease': 3.0.2 + '@types/d3-interpolate': 3.0.4 + '@types/d3-scale': 4.0.8 + '@types/d3-shape': 3.1.6 + '@types/d3-time': 3.0.3 + '@types/d3-timer': 3.0.2 + d3-array: 3.2.4 + d3-ease: 3.0.1 + d3-interpolate: 3.0.1 + d3-scale: 4.0.2 + d3-shape: 3.2.0 + d3-time: 3.1.0 + d3-timer: 3.0.1 + dev: false + /webcrypto-core@1.7.8: resolution: {integrity: sha512-eBR98r9nQXTqXt/yDRtInszPMjTaSAMJAFDg2AHsgrnczawT1asx9YNBX6k5p+MekbPF4+s/UJJrr88zsTqkSg==} dependencies: diff --git a/frontend/src/app/(analytics)/analytics/page.tsx b/frontend/src/app/(analytics)/analytics/page.tsx new file mode 100644 index 0000000..e2666b1 --- /dev/null +++ b/frontend/src/app/(analytics)/analytics/page.tsx @@ -0,0 +1,78 @@ +import AnalyticsDashboard from "@/components/analytics-dashboard"; +import { getDate } from "@/utils"; +import { analytics } from "@/utils/analytics"; + +const Page = async () => { + const TRACKING_DAYS = 7; + + const pageviews = await analytics.retrieveDays("pageview", TRACKING_DAYS); + + const totalPageviews = pageviews.reduce((acc, curr) => { + return ( + acc + + curr.events.reduce((acc, curr) => { + return acc + Object.values(curr)[0]!; + }, 0) + ); + }, 0); + + const avgVisitorsPerDay = (totalPageviews / TRACKING_DAYS).toFixed(1); + + const amtVisitorsToday = pageviews + .filter((ev) => ev.date === getDate()) + .reduce((acc, curr) => { + return ( + acc + + curr.events.reduce((acc, curr) => acc + Object.values(curr)[0]!, 0) + ); + }, 0); + + const topCountriesMap = new Map(); + + for (let i = 0; i < pageviews.length; i++) { + const day = pageviews[i]; + if (!day) continue; + + for (let j = 0; j < day.events.length; j++) { + const event = day.events[j]; + if (!event) continue; + + const key = Object.keys(event)[0]!; + const value = Object.values(event)[0]!; + + const parsedKey = JSON.parse(key); + const country = parsedKey?.country; + + if (country) { + if (topCountriesMap.has(country)) { + const prevValue = topCountriesMap.get(country)!; + topCountriesMap.set(country, prevValue + value); + } else { + topCountriesMap.set(country, value); + } + } + } + } + + const topCountries = [...topCountriesMap.entries()] + .sort((a, b) => { + if (a[1] > b[1]) return -1; + else return 1; + }) + .slice(0, 5); + + return ( +
+
+ +
+
+ ); +}; + +export default Page; diff --git a/frontend/src/app/(quiz-attempt)/_components/quiz-question.tsx b/frontend/src/app/(quiz-attempt)/_components/quiz-question.tsx index e71bc2e..1da81db 100644 --- a/frontend/src/app/(quiz-attempt)/_components/quiz-question.tsx +++ b/frontend/src/app/(quiz-attempt)/_components/quiz-question.tsx @@ -1,418 +1,413 @@ -'use client'; -import { zodResolver } from '@hookform/resolvers/zod'; -import { useState } from 'react'; -import { useForm } from 'react-hook-form'; -import { z } from 'zod'; +"use client"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { useState } from "react"; +import { useForm } from "react-hook-form"; +import { z } from "zod"; -import { saveQuizAnswer } from '@/components/actions/attempt'; -import { QuizProgress } from './quiz-progress'; +import { saveQuizAnswer } from "@/components/actions/attempt"; +import { QuizProgress } from "./quiz-progress"; -import { Button } from '@/components/ui/button'; -import { Checkbox } from '@/components/ui/checkbox'; +import { Button } from "@/components/ui/button"; +import { Checkbox } from "@/components/ui/checkbox"; import { - Form, - FormControl, - FormField, - FormItem, - FormLabel, - FormMessage, -} from '@/components/ui/form'; -import { RadioGroup, RadioGroupItem } from '@/components/ui/radio-group'; -import { Textarea } from '@/components/ui/textarea'; + Form, + FormControl, + FormField, + FormItem, + FormLabel, + FormMessage, +} from "@/components/ui/form"; +import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group"; +import { Textarea } from "@/components/ui/textarea"; export interface QuizMultiselectQuestionProps { - question_text: string; - question_type: - | 'single_select_mcq' - | 'multi_select_mcq' - | 'open_text_question'; - mcq_options: { - id: string; - option_text: string; - }[]; - total_questions: number; - current_question: number; - handleSubmit: () => void; - attempt_id: string; - question_id: string; - isLastQuestion: boolean; + question_text: string; + question_type: + | "single_select_mcq" + | "multi_select_mcq" + | "open_text_question"; + mcq_options: { + id: string; + option_text: string; + }[]; + total_questions: number; + current_question: number; + handleSubmit: () => void; + attempt_id: string; + question_id: string; + isLastQuestion: boolean; } export const QuizMultiselectQuestion = ({ - mcq_options, - question_text, - question_type, - current_question, - total_questions, - handleSubmit, - attempt_id, - question_id, - isLastQuestion, + mcq_options, + question_text, + question_type, + current_question, + total_questions, + handleSubmit, + attempt_id, + question_id, + isLastQuestion, }: QuizMultiselectQuestionProps) => { - const [isLoading, setIsLoading] = useState(false); + const [isLoading, setIsLoading] = useState(false); - const FormSchema = z.object({ - selectedOptions: z.array( - z.string({ required_error: 'You need to select at least one option.' }) - ), - }); + const FormSchema = z.object({ + selectedOptions: z.array( + z.string({ required_error: "You need to select at least one option." }) + ), + }); - const form = useForm>({ - resolver: zodResolver(FormSchema), - defaultValues: { - selectedOptions: [], - }, - }); + const form = useForm>({ + resolver: zodResolver(FormSchema), + defaultValues: { + selectedOptions: [], + }, + }); - async function onSubmit(data: z.infer) { - try { - setIsLoading(true); + async function onSubmit(data: z.infer) { + try { + setIsLoading(true); - const res = await saveQuizAnswer({ - attemptId: attempt_id, - questionType: question_type, - selectedOptions: data.selectedOptions, - questionId: question_id, - }); + const res = await saveQuizAnswer({ + attemptId: attempt_id, + questionType: question_type, + selectedOptions: data.selectedOptions, + questionId: question_id, + }); - if (res.error) { - console.error('Error submitting answer:', res.error); - return; - } + if (res.error) { + console.error("Error submitting answer:", res.error); + return; + } - console.log('Answer submitted:', res.data); + console.log("Answer submitted:", res.data); - handleSubmit(); + handleSubmit(); - form.reset(); - } catch (error) { - console.error('Error submitting answer:', error); - } finally { - setIsLoading(false); - } - } + form.reset(); + } catch (error) { + console.error("Error submitting answer:", error); + } finally { + setIsLoading(false); + } + } - return ( -
-
- - ( - - - {question_text} - - {mcq_options.map((item) => ( - { - return ( - - - { - return checked - ? field.onChange([...field.value, item.id]) - : field.onChange( - field.value?.filter( - (value) => value !== item.id - ) - ); - }} - /> - - - {item.option_text} - - - ); - }} - /> - ))} - - - )} - /> + return ( +
+ + + ( + + + {question_text} + + {mcq_options.map((item) => ( + { + return ( + + + { + return checked + ? field.onChange([...field.value, item.id]) + : field.onChange( + field.value?.filter( + (value) => value !== item.id + ) + ); + }} + /> + + + {item.option_text} + + + ); + }} + /> + ))} + + + )} + /> -
- - -
- - -
- ); +
+ + +
+ + +
+ ); }; export interface QuizOpentextQuestionProps { - question_text: string; - question_type: - | 'single_select_mcq' - | 'multi_select_mcq' - | 'open_text_question'; - mcq_options: { - id: string; - option_text: string; - }[]; - total_questions: number; - current_question: number; - handleSubmit: () => void; - attempt_id: string; - question_id: string; - isLastQuestion: boolean; + question_text: string; + question_type: + | "single_select_mcq" + | "multi_select_mcq" + | "open_text_question"; + mcq_options: { + id: string; + option_text: string; + }[]; + total_questions: number; + current_question: number; + handleSubmit: () => void; + attempt_id: string; + question_id: string; + isLastQuestion: boolean; } export const QuizOpentextQuestion = ({ - question_text, - question_type, - current_question, - total_questions, - handleSubmit, - attempt_id, - question_id, - isLastQuestion, + question_text, + question_type, + current_question, + total_questions, + handleSubmit, + attempt_id, + question_id, + isLastQuestion, }: QuizOpentextQuestionProps) => { - const [isLoading, setIsLoading] = useState(false); - const FormSchema = z.object({ - freeformAnswer: z.string({ - required_error: 'You need to provide an answer.', - }), - }); - const form = useForm>({ - resolver: zodResolver(FormSchema), - }); + const [isLoading, setIsLoading] = useState(false); + const FormSchema = z.object({ + freeformAnswer: z.string({ + required_error: "You need to provide an answer.", + }), + }); + const form = useForm>({ + resolver: zodResolver(FormSchema), + }); - async function onSubmit(data: z.infer) { - try { - setIsLoading(true); - const res = await saveQuizAnswer({ - attemptId: attempt_id, - questionType: question_type, - answerText: data.freeformAnswer, - questionId: question_id, - }); + async function onSubmit(data: z.infer) { + try { + setIsLoading(true); + const res = await saveQuizAnswer({ + attemptId: attempt_id, + questionType: question_type, + answerText: data.freeformAnswer, + questionId: question_id, + }); - if (res.error) { - console.error('Error submitting answer:', res.error); - return; - } + if (res.error) { + console.error("Error submitting answer:", res.error); + return; + } - console.log('Answer submitted:', res.data); - form.setValue('freeformAnswer', ''); - form.reset(); - handleSubmit(); - } catch (error) { - console.error('Error submitting answer:', error); - } finally { - setIsLoading(false); - } - } + console.log("Answer submitted:", res.data); + form.setValue("freeformAnswer", ""); + form.reset(); + handleSubmit(); + } catch (error) { + console.error("Error submitting answer:", error); + } finally { + setIsLoading(false); + } + } - return ( -
-
- - ( - - - {question_text} - - {question_type === 'open_text_question' && ( - -