diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..4c49bd7 --- /dev/null +++ b/.dockerignore @@ -0,0 +1 @@ +.env diff --git a/.env b/.env new file mode 100644 index 0000000..2dff607 --- /dev/null +++ b/.env @@ -0,0 +1 @@ +NEXT_PUBLIC_BACKEND_URL="http://localhost:4000" diff --git a/.releaserc.json b/.releaserc.json index 02f7401..df9c7ae 100644 --- a/.releaserc.json +++ b/.releaserc.json @@ -7,7 +7,10 @@ { "dockerImage": "gists-app", "dockerProject": "milou666", - "dockerCacheFrom": "milou666/gists-app" + "dockerCacheFrom": "milou666/gists-app", + "dockerArgs": { + "NEXT_PUBLIC_API_URL": "https://api-gists.courtcircuits.xyz" + } } ] ] diff --git a/package.json b/package.json index 1b9afd0..bed3ffb 100644 --- a/package.json +++ b/package.json @@ -11,21 +11,27 @@ "dependencies": { "@radix-ui/react-slot": "^1.1.0", "@radix-ui/react-toast": "^1.2.1", + "@reduxjs/toolkit": "^2.2.7", + "@tanstack/query-core": "5.21.4", + "@tanstack/react-query": "5.21.4", "class-variance-authority": "^0.7.0", "clsx": "^2.1.1", "gsap": "^3.12.5", "input-otp": "^1.2.4", + "ky": "^1.5.0", "lucide-react": "^0.419.0", "next": "14.2.5", "next-themes": "^0.3.0", "react": "^18", "react-dom": "^18", "react-hook-form": "^7.52.1", + "react-redux": "^9.1.2", "tailwind-merge": "^2.4.0", "tailwindcss-animate": "^1.0.7" }, "devDependencies": { "@codedependant/semantic-release-docker": "^5.0.3", + "@iconify/react": "^5.0.2", "@types/node": "^20", "@types/react": "^18", "@types/react-dom": "^18", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 4a466f2..7ab5a99 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -14,6 +14,15 @@ importers: '@radix-ui/react-toast': specifier: ^1.2.1 version: 1.2.1(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@reduxjs/toolkit': + specifier: ^2.2.7 + version: 2.2.7(react-redux@9.1.2(@types/react@18.3.3)(react@18.3.1)(redux@5.0.1))(react@18.3.1) + '@tanstack/query-core': + specifier: 5.21.4 + version: 5.21.4 + '@tanstack/react-query': + specifier: 5.21.4 + version: 5.21.4(react@18.3.1) class-variance-authority: specifier: ^0.7.0 version: 0.7.0 @@ -26,6 +35,9 @@ importers: input-otp: specifier: ^1.2.4 version: 1.2.4(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + ky: + specifier: ^1.5.0 + version: 1.6.0 lucide-react: specifier: ^0.419.0 version: 0.419.0(react@18.3.1) @@ -43,20 +55,26 @@ importers: version: 18.3.1(react@18.3.1) react-hook-form: specifier: ^7.52.1 - version: 7.52.1(react@18.3.1) + version: 7.52.2(react@18.3.1) + react-redux: + specifier: ^9.1.2 + version: 9.1.2(@types/react@18.3.3)(react@18.3.1)(redux@5.0.1) tailwind-merge: specifier: ^2.4.0 - version: 2.4.0 + version: 2.5.2 tailwindcss-animate: specifier: ^1.0.7 - version: 1.0.7(tailwindcss@3.4.7) + version: 1.0.7(tailwindcss@3.4.9) devDependencies: '@codedependant/semantic-release-docker': specifier: ^5.0.3 version: 5.0.3 + '@iconify/react': + specifier: ^5.0.2 + version: 5.0.2(react@18.3.1) '@types/node': specifier: ^20 - version: 20.14.13 + version: 20.14.15 '@types/react': specifier: ^18 version: 18.3.3 @@ -71,10 +89,10 @@ importers: version: 14.2.5(eslint@8.57.0)(typescript@5.5.4) postcss: specifier: ^8 - version: 8.4.40 + version: 8.4.41 tailwindcss: specifier: ^3.4.1 - version: 3.4.7 + version: 3.4.9 typescript: specifier: ^5 version: 5.5.4 @@ -119,6 +137,14 @@ packages: resolution: {integrity: sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==} deprecated: Use @eslint/object-schema instead + '@iconify/react@5.0.2': + resolution: {integrity: sha512-wtmstbYlEbo4NDxFxBJkhkf9gJBDqMGr7FaqLrAUMneRV3Z+fVHLJjOhWbkAF8xDQNFC/wcTYdrWo1lnRhmagQ==} + peerDependencies: + react: '>=16' + + '@iconify/types@2.0.0': + resolution: {integrity: sha512-+wluvCrRhXrhyOmRDJ3q8mux9JkKy5SJ/v8ol2tu4FVjyYvtEzkc/3pK15ET6RKg4b4w4BmTk1+gsCUhf21Ykg==} + '@isaacs/cliui@8.0.2': resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==} engines: {node: '>=12'} @@ -374,6 +400,17 @@ packages: '@types/react-dom': optional: true + '@reduxjs/toolkit@2.2.7': + resolution: {integrity: sha512-faI3cZbSdFb8yv9dhDTmGwclW0vk0z5o1cia+kf7gCbaCwHI5e+7tP57mJUv22pNcNbeA62GSrPpfrUfdXcQ6g==} + peerDependencies: + react: ^16.9.0 || ^17.0.0 || ^18 + react-redux: ^7.2.1 || ^8.1.3 || ^9.0.0 + peerDependenciesMeta: + react: + optional: true + react-redux: + optional: true + '@rushstack/eslint-patch@1.10.4': resolution: {integrity: sha512-WJgX9nzTqknM393q1QJDJmoW28kUfEnybeTfVNcNAPnIx210RXm2DiXiHzfNPJNIUUb1tJnz/l4QGtJ30PgWmA==} @@ -387,11 +424,19 @@ packages: '@swc/helpers@0.5.5': resolution: {integrity: sha512-KGYxvIOXcceOAbEk4bi/dVLEK9z8sZ0uBB3Il5b1rhfClSpcX0yfRO0KmTkqR2cnQDymwLB+25ZyMzICg/cm/A==} + '@tanstack/query-core@5.21.4': + resolution: {integrity: sha512-k3u4RcDAtcCurs8KVEIf52k4yUayc852v4ZQrtI8pkEii71riM9758A2WVGo5T/v4/X7b1RLON5g0aDvkoZYCA==} + + '@tanstack/react-query@5.21.4': + resolution: {integrity: sha512-tzl4mGerfPKmJsPDWbfKol0eBEk8bsgtMZJOwnbUvvSRnYZzS9OfX9CD/dbQLr+JjIvSekWKaDBTo3oXeeFhoQ==} + peerDependencies: + react: ^18.0.0 + '@types/json5@0.0.29': resolution: {integrity: sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==} - '@types/node@20.14.13': - resolution: {integrity: sha512-+bHoGiZb8UiQ0+WEtmph2IWQCjIqg8MDZMAV+ppRRhUZnquF5mQkP/9vpSwJClEiSM/C7fZZExPzfU0vJTyp8w==} + '@types/node@20.14.15': + resolution: {integrity: sha512-Fz1xDMCF/B00/tYSVMlmK7hVeLh7jE5f3B7X1/hmV0MJBwE27KlS7EvD/Yp+z1lm8mVhwV5w+n8jOZG8AfTlKw==} '@types/prop-types@15.7.12': resolution: {integrity: sha512-5zvhXYtRNRluoE/jAp4GVsSduVUzNWKkOZrCDBWYtE7biZywwdC2AcEzg+cSMLFRfVgeAFqpfNabiPjxFddV1Q==} @@ -402,6 +447,9 @@ packages: '@types/react@18.3.3': resolution: {integrity: sha512-hti/R0pS0q1/xx+TsI73XIqk26eBsISZ2R0wUijXIngRK9R/e7Xw/cXVxQK7R5JjW+SV4zGcn5hXjudkN/pLIw==} + '@types/use-sync-external-store@0.0.3': + resolution: {integrity: sha512-EwmlvuaxPNej9+T4v5AuBPJa2x2UOJVdjCtDHgcDqitUeOtjnJKJ+apYjVcAoBEMjKW1VVFGZLUb5+qqa09XFA==} + '@typescript-eslint/parser@7.2.0': resolution: {integrity: sha512-5FKsVcHTk6TafQKQbuIVkXq58Fnbkd2wDL4LB7AURN7RUOu1utVP+G8+6u3ZhEroW3DF6hyo3ZEXxgKgp4KeCg==} engines: {node: ^16.0.0 || >=18.0.0} @@ -564,8 +612,8 @@ packages: resolution: {integrity: sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==} engines: {node: '>= 6'} - caniuse-lite@1.0.30001644: - resolution: {integrity: sha512-YGvlOZB4QhZuiis+ETS0VXR+MExbFf4fZYYeMTEE0aTQd/RdIjkTyZjLrbYVKnHzppDvnOhritRVv+i7Go6mHw==} + caniuse-lite@1.0.30001651: + resolution: {integrity: sha512-9Cf+Xv1jJNe1xPZLGuUXLNkE1BoDkqRqYyFJ9TDYSqhduqA4hu4oR9HluGoWYQC/aj8WHjsGVV+bwkh0+tegRg==} chalk@4.1.2: resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} @@ -877,8 +925,8 @@ packages: for-each@0.3.3: resolution: {integrity: sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==} - foreground-child@3.2.1: - resolution: {integrity: sha512-PXUUyLqrR2XCWICfv6ukppP96sdFwWbNEnfEMt7jNsISjMsvaLNinAHNDYyvkyU+SZG2BTSbT5NjG+vZslfGTA==} + foreground-child@3.3.0: + resolution: {integrity: sha512-Ld2g8rrAyMYFXBhEqMz8ZAHBi4J4uS1i/CxGMDnjyFWddMXLVcDp051DZfu+t7+ab7Wv6SMqpWmyFIj5UbfFvg==} engines: {node: '>=14'} fs.realpath@1.0.0: @@ -994,10 +1042,13 @@ packages: resolution: {integrity: sha512-SEQu7vl8KjNL2eoGBLF3+wAjpsNfA9XMlXAYj/3EdaNfAlxKthD1xjEQfGOUhllCGGJVNY34bRr6lPINhNjyZw==} engines: {node: '>=8.12.0'} - ignore@5.3.1: - resolution: {integrity: sha512-5Fytz/IraMjqpwfd34ke28PTVMjZjJG2MPn5t7OE4eUCUNf8BAa7b5WUS9/Qvr6mwOQS7Mk6vdsMno5he+T8Xw==} + ignore@5.3.2: + resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} engines: {node: '>= 4'} + immer@10.1.1: + resolution: {integrity: sha512-s2MPrmjovJcoMaHtx6K11Ra7oD05NT97w1IC5zpMkT6Atjr7H8LjaDd81iIxUYpMKSRRNMJE703M1Fhr/TctHw==} + import-fresh@3.3.0: resolution: {integrity: sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==} engines: {node: '>=6'} @@ -1187,6 +1238,10 @@ packages: keyv@4.5.4: resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} + ky@1.6.0: + resolution: {integrity: sha512-MG7hlH26oShC4Lysk5TYzXshHLfEY52IJ0ofOeCsifquqTymbXCSTx+g4rXO30XYxoM6Y1ed5pNnpULe9Rx19A==} + engines: {node: '>=18'} + language-subtag-registry@0.3.23: resolution: {integrity: sha512-0K65Lea881pHotoGEa5gDlMxt3pctLi2RplBb7Ezh4rRdLEOtgi7n4EwK9lamnUCkKBqaeKRVebTq6BAxSkpXQ==} @@ -1451,8 +1506,8 @@ packages: peerDependencies: postcss: ^8.2.14 - postcss-selector-parser@6.1.1: - resolution: {integrity: sha512-b4dlw/9V8A71rLIDsSwVmak9z2DuBUB7CA1/wSdelNEzqsjoSPeADTWNO09lpH49Diy3/JIZ2bSPB1dI3LJCHg==} + postcss-selector-parser@6.1.2: + resolution: {integrity: sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==} engines: {node: '>=4'} postcss-value-parser@4.2.0: @@ -1462,8 +1517,8 @@ packages: resolution: {integrity: sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==} engines: {node: ^10 || ^12 || >=14} - postcss@8.4.40: - resolution: {integrity: sha512-YF2kKIUzAofPMpfH6hOi2cGnv/HrUlfucspc7pDyvv7kGdqXrfj8SCl/t8owkEgKEuu8ZcRjSOxFxVLqwChZ2Q==} + postcss@8.4.41: + resolution: {integrity: sha512-TesUflQ0WKZqAvg52PWL6kHgLKP6xB6heTOdoYM0Wt2UHyxNa4K25EZZMgKns3BH1RLVbZCREPpLY0rhnNoHVQ==} engines: {node: ^10 || ^12 || >=14} prelude-ls@1.2.1: @@ -1488,15 +1543,27 @@ packages: peerDependencies: react: ^18.3.1 - react-hook-form@7.52.1: - resolution: {integrity: sha512-uNKIhaoICJ5KQALYZ4TOaOLElyM+xipord+Ha3crEFhTntdLvWZqVY49Wqd/0GiVCA/f9NjemLeiNPjG7Hpurg==} - engines: {node: '>=12.22.0'} + react-hook-form@7.52.2: + resolution: {integrity: sha512-pqfPEbERnxxiNMPd0bzmt1tuaPcVccywFDpyk2uV5xCIBphHV5T8SVnX9/o3kplPE1zzKt77+YIoq+EMwJp56A==} + engines: {node: '>=18.0.0'} peerDependencies: react: ^16.8.0 || ^17 || ^18 || ^19 react-is@16.13.1: resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==} + react-redux@9.1.2: + resolution: {integrity: sha512-0OA4dhM1W48l3uzmv6B7TXPCGmokUU4p1M44DGN2/D9a1FjVPukVjER1PcPX97jIg6aUeLq1XJo1IpfbgULn0w==} + peerDependencies: + '@types/react': ^18.2.25 + react: ^18.0 + redux: ^5.0.0 + peerDependenciesMeta: + '@types/react': + optional: true + redux: + optional: true + react@18.3.1: resolution: {integrity: sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==} engines: {node: '>=0.10.0'} @@ -1508,6 +1575,14 @@ packages: resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==} engines: {node: '>=8.10.0'} + redux-thunk@3.1.0: + resolution: {integrity: sha512-NW2r5T6ksUKXCabzhL9z+h206HQw/NJkcLm1GPImRQ8IzfXwRGqjVhKJGauHirT0DAuyy6hjdnMZaRoAcy0Klw==} + peerDependencies: + redux: ^5.0.0 + + redux@5.0.1: + resolution: {integrity: sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==} + reflect.getprototypeof@1.0.6: resolution: {integrity: sha512-fmfw4XgoDke3kdI6h4xcUz1dG8uaiv5q9gcEwLS4Pnth2kxT+GZ7YehS1JTMGBQmtV7Y4GFGbs2re2NqhdozUg==} engines: {node: '>= 0.4'} @@ -1516,6 +1591,9 @@ packages: resolution: {integrity: sha512-NcDiDkTLuPR+++OCKB0nWafEmhg/Da8aUPLPMQbK+bxKKCm1/S5he+AqYa4PlMCVBalb4/yxIRub6qkEx5yJbw==} engines: {node: '>= 0.4'} + reselect@5.1.1: + resolution: {integrity: sha512-K/BG6eIky/SBpzfHZv/dd+9JBFiS4SWV7FIujVyJRux6e45+73RaUHXLmIR1f7WOMaQ0U1km6qwklRQxpJJY0w==} + resolve-from@4.0.0: resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} engines: {node: '>=4'} @@ -1685,16 +1763,16 @@ packages: resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} engines: {node: '>= 0.4'} - tailwind-merge@2.4.0: - resolution: {integrity: sha512-49AwoOQNKdqKPd9CViyH5wJoSKsCDjUlzL8DxuGp3P1FsGY36NJDAa18jLZcaHAUUuTj+JB8IAo8zWgBNvBF7A==} + tailwind-merge@2.5.2: + resolution: {integrity: sha512-kjEBm+pvD+6eAwzJL2Bi+02/9LFLal1Gs61+QB7HvTfQQ0aXwC5LGT8PEt1gS0CWKktKe6ysPTAy3cBC5MeiIg==} tailwindcss-animate@1.0.7: resolution: {integrity: sha512-bl6mpH3T7I3UFxuvDEXLxy/VuFxBk5bbzplh7tXI68mwMokNYd1t9qPBHlnyTwfa4JGC4zP516I1hYYtQ/vspA==} peerDependencies: tailwindcss: '>=3.0.0 || insiders' - tailwindcss@3.4.7: - resolution: {integrity: sha512-rxWZbe87YJb4OcSopb7up2Ba4U82BoiSGUdoDr3Ydrg9ckxFS/YWsvhN323GMcddgU65QRy7JndC7ahhInhvlQ==} + tailwindcss@3.4.9: + resolution: {integrity: sha512-1SEOvRr6sSdV5IDf9iC+NU4dhwdqzF4zKKq3sAbasUWHEM6lsMhX+eNN5gkPx1BvLFEnZQEUFbXnGj8Qlp83Pg==} engines: {node: '>=14.0.0'} hasBin: true @@ -1760,8 +1838,8 @@ packages: engines: {node: '>=14.17'} hasBin: true - uglify-js@3.19.1: - resolution: {integrity: sha512-y/2wiW+ceTYR2TSSptAhfnEtpLaQ4Ups5zrjB2d3kuVxHj16j/QJwPl5PvuGy9uARb39J0+iKxcRPvtpsx4A4A==} + uglify-js@3.19.2: + resolution: {integrity: sha512-S8KA6DDI47nQXJSi2ctQ629YzwOVs+bQML6DAtvy0wgNdpi+0ySpQK0g2pxBq2xfF2z3YCscu7NNA8nXT9PlIQ==} engines: {node: '>=0.8.0'} hasBin: true @@ -1774,6 +1852,11 @@ packages: uri-js@4.4.1: resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} + use-sync-external-store@1.2.2: + resolution: {integrity: sha512-PElTlVMwpblvbNqQ82d2n6RjStvdSoNe9FG28kNfz3WiXilJm4DdNkEzRhCZuIDwY8U08WVihhGR5iRqAwfDiw==} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 + util-deprecate@1.0.2: resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} @@ -1851,7 +1934,7 @@ snapshots: debug: 4.3.6 espree: 9.6.1 globals: 13.24.0 - ignore: 5.3.1 + ignore: 5.3.2 import-fresh: 3.3.0 js-yaml: 4.1.0 minimatch: 3.1.2 @@ -1873,6 +1956,13 @@ snapshots: '@humanwhocodes/object-schema@2.0.3': {} + '@iconify/react@5.0.2(react@18.3.1)': + dependencies: + '@iconify/types': 2.0.0 + react: 18.3.1 + + '@iconify/types@2.0.0': {} + '@isaacs/cliui@8.0.2': dependencies: string-width: 5.1.2 @@ -2077,6 +2167,16 @@ snapshots: '@types/react': 18.3.3 '@types/react-dom': 18.3.0 + '@reduxjs/toolkit@2.2.7(react-redux@9.1.2(@types/react@18.3.3)(react@18.3.1)(redux@5.0.1))(react@18.3.1)': + dependencies: + immer: 10.1.1 + redux: 5.0.1 + redux-thunk: 3.1.0(redux@5.0.1) + reselect: 5.1.1 + optionalDependencies: + react: 18.3.1 + react-redux: 9.1.2(@types/react@18.3.3)(react@18.3.1)(redux@5.0.1) + '@rushstack/eslint-patch@1.10.4': {} '@semantic-release/error@3.0.0': {} @@ -2088,9 +2188,16 @@ snapshots: '@swc/counter': 0.1.3 tslib: 2.6.3 + '@tanstack/query-core@5.21.4': {} + + '@tanstack/react-query@5.21.4(react@18.3.1)': + dependencies: + '@tanstack/query-core': 5.21.4 + react: 18.3.1 + '@types/json5@0.0.29': {} - '@types/node@20.14.13': + '@types/node@20.14.15': dependencies: undici-types: 5.26.5 @@ -2105,6 +2212,8 @@ snapshots: '@types/prop-types': 15.7.12 csstype: 3.1.3 + '@types/use-sync-external-store@0.0.3': {} + '@typescript-eslint/parser@7.2.0(eslint@8.57.0)(typescript@5.5.4)': dependencies: '@typescript-eslint/scope-manager': 7.2.0 @@ -2297,7 +2406,7 @@ snapshots: camelcase-css@2.0.1: {} - caniuse-lite@1.0.30001644: {} + caniuse-lite@1.0.30001651: {} chalk@4.1.2: dependencies: @@ -2554,7 +2663,7 @@ snapshots: eslint: 8.57.0 eslint-import-resolver-node: 0.3.9 eslint-import-resolver-typescript: 3.6.1(@typescript-eslint/parser@7.2.0(eslint@8.57.0)(typescript@5.5.4))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1(eslint@8.57.0))(eslint@8.57.0) - eslint-plugin-import: 2.29.1(@typescript-eslint/parser@7.2.0(eslint@8.57.0)(typescript@5.5.4))(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@7.2.0(eslint@8.57.0)(typescript@5.5.4))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1(eslint@8.57.0))(eslint@8.57.0))(eslint@8.57.0) + eslint-plugin-import: 2.29.1(@typescript-eslint/parser@7.2.0(eslint@8.57.0)(typescript@5.5.4))(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.0) eslint-plugin-jsx-a11y: 6.9.0(eslint@8.57.0) eslint-plugin-react: 7.35.0(eslint@8.57.0) eslint-plugin-react-hooks: 4.6.2(eslint@8.57.0) @@ -2578,7 +2687,7 @@ snapshots: enhanced-resolve: 5.17.1 eslint: 8.57.0 eslint-module-utils: 2.8.1(@typescript-eslint/parser@7.2.0(eslint@8.57.0)(typescript@5.5.4))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@7.2.0(eslint@8.57.0)(typescript@5.5.4))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1(eslint@8.57.0))(eslint@8.57.0))(eslint@8.57.0) - eslint-plugin-import: 2.29.1(@typescript-eslint/parser@7.2.0(eslint@8.57.0)(typescript@5.5.4))(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@7.2.0(eslint@8.57.0)(typescript@5.5.4))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1(eslint@8.57.0))(eslint@8.57.0))(eslint@8.57.0) + eslint-plugin-import: 2.29.1(@typescript-eslint/parser@7.2.0(eslint@8.57.0)(typescript@5.5.4))(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.0) fast-glob: 3.3.2 get-tsconfig: 4.7.6 is-core-module: 2.15.0 @@ -2600,7 +2709,7 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-plugin-import@2.29.1(@typescript-eslint/parser@7.2.0(eslint@8.57.0)(typescript@5.5.4))(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@7.2.0(eslint@8.57.0)(typescript@5.5.4))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1(eslint@8.57.0))(eslint@8.57.0))(eslint@8.57.0): + eslint-plugin-import@2.29.1(@typescript-eslint/parser@7.2.0(eslint@8.57.0)(typescript@5.5.4))(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.0): dependencies: array-includes: 3.1.8 array.prototype.findlastindex: 1.2.5 @@ -2707,7 +2816,7 @@ snapshots: glob-parent: 6.0.2 globals: 13.24.0 graphemer: 1.4.0 - ignore: 5.3.1 + ignore: 5.3.2 imurmurhash: 0.1.4 is-glob: 4.0.3 is-path-inside: 3.0.3 @@ -2796,7 +2905,7 @@ snapshots: dependencies: is-callable: 1.2.7 - foreground-child@3.2.1: + foreground-child@3.3.0: dependencies: cross-spawn: 7.0.3 signal-exit: 4.1.0 @@ -2849,7 +2958,7 @@ snapshots: glob@10.3.10: dependencies: - foreground-child: 3.2.1 + foreground-child: 3.3.0 jackspeak: 2.3.6 minimatch: 9.0.5 minipass: 7.1.2 @@ -2857,7 +2966,7 @@ snapshots: glob@10.4.5: dependencies: - foreground-child: 3.2.1 + foreground-child: 3.3.0 jackspeak: 3.4.3 minimatch: 9.0.5 minipass: 7.1.2 @@ -2887,7 +2996,7 @@ snapshots: array-union: 2.1.0 dir-glob: 3.0.1 fast-glob: 3.3.2 - ignore: 5.3.1 + ignore: 5.3.2 merge2: 1.4.1 slash: 3.0.0 @@ -2908,7 +3017,7 @@ snapshots: source-map: 0.6.1 wordwrap: 1.0.0 optionalDependencies: - uglify-js: 3.19.1 + uglify-js: 3.19.2 has-bigints@1.0.2: {} @@ -2932,7 +3041,9 @@ snapshots: human-signals@1.1.1: {} - ignore@5.3.1: {} + ignore@5.3.2: {} + + immer@10.1.1: {} import-fresh@3.3.0: dependencies: @@ -3117,6 +3228,8 @@ snapshots: dependencies: json-buffer: 3.0.1 + ky@1.6.0: {} + language-subtag-registry@0.3.23: {} language-tags@1.0.9: @@ -3203,7 +3316,7 @@ snapshots: '@next/env': 14.2.5 '@swc/helpers': 0.5.5 busboy: 1.6.0 - caniuse-lite: 1.0.30001644 + caniuse-lite: 1.0.30001651 graceful-fs: 4.2.11 postcss: 8.4.31 react: 18.3.1 @@ -3330,31 +3443,31 @@ snapshots: possible-typed-array-names@1.0.0: {} - postcss-import@15.1.0(postcss@8.4.40): + postcss-import@15.1.0(postcss@8.4.41): dependencies: - postcss: 8.4.40 + postcss: 8.4.41 postcss-value-parser: 4.2.0 read-cache: 1.0.0 resolve: 1.22.8 - postcss-js@4.0.1(postcss@8.4.40): + postcss-js@4.0.1(postcss@8.4.41): dependencies: camelcase-css: 2.0.1 - postcss: 8.4.40 + postcss: 8.4.41 - postcss-load-config@4.0.2(postcss@8.4.40): + postcss-load-config@4.0.2(postcss@8.4.41): dependencies: lilconfig: 3.1.2 yaml: 2.5.0 optionalDependencies: - postcss: 8.4.40 + postcss: 8.4.41 - postcss-nested@6.2.0(postcss@8.4.40): + postcss-nested@6.2.0(postcss@8.4.41): dependencies: - postcss: 8.4.40 - postcss-selector-parser: 6.1.1 + postcss: 8.4.41 + postcss-selector-parser: 6.1.2 - postcss-selector-parser@6.1.1: + postcss-selector-parser@6.1.2: dependencies: cssesc: 3.0.0 util-deprecate: 1.0.2 @@ -3367,7 +3480,7 @@ snapshots: picocolors: 1.0.1 source-map-js: 1.2.0 - postcss@8.4.40: + postcss@8.4.41: dependencies: nanoid: 3.3.7 picocolors: 1.0.1 @@ -3396,12 +3509,21 @@ snapshots: react: 18.3.1 scheduler: 0.23.2 - react-hook-form@7.52.1(react@18.3.1): + react-hook-form@7.52.2(react@18.3.1): dependencies: react: 18.3.1 react-is@16.13.1: {} + react-redux@9.1.2(@types/react@18.3.3)(react@18.3.1)(redux@5.0.1): + dependencies: + '@types/use-sync-external-store': 0.0.3 + react: 18.3.1 + use-sync-external-store: 1.2.2(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.3 + redux: 5.0.1 + react@18.3.1: dependencies: loose-envify: 1.4.0 @@ -3414,6 +3536,12 @@ snapshots: dependencies: picomatch: 2.3.1 + redux-thunk@3.1.0(redux@5.0.1): + dependencies: + redux: 5.0.1 + + redux@5.0.1: {} + reflect.getprototypeof@1.0.6: dependencies: call-bind: 1.0.7 @@ -3431,6 +3559,8 @@ snapshots: es-errors: 1.3.0 set-function-name: 2.0.2 + reselect@5.1.1: {} + resolve-from@4.0.0: {} resolve-pkg-maps@1.0.0: {} @@ -3614,13 +3744,13 @@ snapshots: supports-preserve-symlinks-flag@1.0.0: {} - tailwind-merge@2.4.0: {} + tailwind-merge@2.5.2: {} - tailwindcss-animate@1.0.7(tailwindcss@3.4.7): + tailwindcss-animate@1.0.7(tailwindcss@3.4.9): dependencies: - tailwindcss: 3.4.7 + tailwindcss: 3.4.9 - tailwindcss@3.4.7: + tailwindcss@3.4.9: dependencies: '@alloc/quick-lru': 5.2.0 arg: 5.0.2 @@ -3636,12 +3766,12 @@ snapshots: normalize-path: 3.0.0 object-hash: 3.0.0 picocolors: 1.0.1 - postcss: 8.4.40 - postcss-import: 15.1.0(postcss@8.4.40) - postcss-js: 4.0.1(postcss@8.4.40) - postcss-load-config: 4.0.2(postcss@8.4.40) - postcss-nested: 6.2.0(postcss@8.4.40) - postcss-selector-parser: 6.1.1 + postcss: 8.4.41 + postcss-import: 15.1.0(postcss@8.4.41) + postcss-js: 4.0.1(postcss@8.4.41) + postcss-load-config: 4.0.2(postcss@8.4.41) + postcss-nested: 6.2.0(postcss@8.4.41) + postcss-selector-parser: 6.1.2 resolve: 1.22.8 sucrase: 3.35.0 transitivePeerDependencies: @@ -3718,7 +3848,7 @@ snapshots: typescript@5.5.4: {} - uglify-js@3.19.1: + uglify-js@3.19.2: optional: true unbox-primitive@1.0.2: @@ -3734,6 +3864,10 @@ snapshots: dependencies: punycode: 2.3.1 + use-sync-external-store@1.2.2(react@18.3.1): + dependencies: + react: 18.3.1 + util-deprecate@1.0.2: {} which-boxed-primitive@1.0.2: diff --git a/src/app/layout.tsx b/src/app/layout.tsx index 268fc15..3e5ba0b 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -1,35 +1,39 @@ -import type { Metadata } from 'next' -import { Inter as FontSans } from 'next/font/google' -import './globals.css' -import { cn } from '@/lib/utils' -import BlurBackground from '@/components/ui/blur-background' -import ThemeWrapper from '@/components/theme/theme-wrapper' -import { Toaster } from '@/components/shadcn/toaster' -import { Providers } from '@/components/theme/theme-provider' +import type { Metadata } from "next"; +import { Inter as FontSans } from "next/font/google"; +import "./globals.css"; +import { cn, getBackendURL } from "@/lib/utils"; +import BlurBackground from "@/components/ui/blur-background"; +import ThemeWrapper from "@/components/theme/theme-wrapper"; +import { Toaster } from "@/components/shadcn/toaster"; +import { Providers } from "@/components/theme/theme-provider"; +import QueryProvider from "@/components/api/api-provider"; -const fontSans = FontSans({ subsets: ['latin'] }) +const fontSans = FontSans({ subsets: ["latin"] }); export const metadata: Metadata = { - title: 'Gists', - description: 'Create and share code snippets.', -} + title: "Gists", + description: "Create and share code snippets.", +}; export default function RootLayout({ children, }: Readonly<{ - children: React.ReactNode + children: React.ReactNode; }>) { + console.log(getBackendURL()); return ( <html lang="en" suppressHydrationWarning={true}> <body className={cn(fontSans.className)}> - <Providers> - <ThemeWrapper> - <div className="z-10 flex justify-center">{children}</div> - <BlurBackground /> - <Toaster /> - </ThemeWrapper> - </Providers> + <QueryProvider> + <Providers> + <ThemeWrapper> + <div className="z-10 flex justify-center">{children}</div> + <BlurBackground /> + <Toaster /> + </ThemeWrapper> + </Providers> + </QueryProvider> </body> </html> - ) + ); } diff --git a/src/app/login/login-feature.tsx b/src/app/login/login-feature.tsx new file mode 100644 index 0000000..bdb00bd --- /dev/null +++ b/src/app/login/login-feature.tsx @@ -0,0 +1,121 @@ +"use client"; + +import { useEffect, useState, useCallback } from "react"; +import { useForm, UseFormRegisterReturn } from "react-hook-form"; +import { useToast } from "@/components/shadcn/use-toast"; +import { getBackendURL } from "@/lib/utils"; +import { useLocalAuth } from "@/lib/queries/auth.queries"; +import Login from "./login-ui"; + +interface FormData { + email: string; +} + +export default function LoginFeature() { + const [step, setStep] = useState<"initial" | "emailInput" | "otpInput">( + "initial", + ); + const [otpValue, setOtpValue] = useState(""); + const { toast } = useToast(); + + const { mutate: sendEmail } = useLocalAuth(); + // const { mutate: verifyEmail, data: verified } = useLocalAuthVerify(); + + const { + register, + handleSubmit, + formState: { errors, isValid }, + } = useForm<FormData>({ + mode: "onChange", + }); + + const emailRegister = register("email", { + required: "Email is required", + pattern: { + value: /^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}$/i, + message: "Invalid email address", + }, + }); + + const handleEmailClick = useCallback(() => { + if (step === "initial") { + setStep("emailInput"); + } else if (step === "emailInput" && isValid) { + handleSubmit(onSubmit)(); + } + }, [step, isValid, handleSubmit]); + + const handleGitHubClick = useCallback(() => { + console.log("GitHub"); + window.location.href = getBackendURL() + "/auth/github"; + }, []); + + const handleGoogleClick = useCallback(() => { + console.log("CGoogle"); + + window.location.href = getBackendURL() + "/auth/google"; + }, []); + + const handleOtpChange = useCallback((value: string) => { + setOtpValue(value); + }, []); + + const handleContinueClick = useCallback(() => { + console.log("OTP:", otpValue); + const email = localStorage.getItem("email"); + if (!email) { + console.error("Email not found in local storage."); + return; + } + localStorage.removeItem("email"); + // verifyEmail({ email: email, token: otpValue }); + }, [otpValue]); + + const handleTryAgainClick = useCallback(() => { + console.log("A new OTP has been sent."); + toast({ + title: "A new one time password has been sent.", + description: "Please check your email.", + }); + }, [toast]); + + const handleBackToLoginClick = useCallback(() => { + setStep("initial"); + setOtpValue(""); + }, []); + + const onSubmit = (data: FormData) => { + console.log(data); + sendEmail(data.email); + localStorage.setItem("email", data.email); + setStep("otpInput"); + }; + + // useEffect(() => { + // if (verified) { + // console.log("Verified:", verified); + // toast({ + // title: "You have been verified.", + // }); + // + // redirect("/dashboard"); + // } + // }, [verified, toast]); + + return ( + <Login + step={step} + email={emailRegister as UseFormRegisterReturn<"email">} + otpValue={otpValue} + onEmailClick={handleEmailClick} + onGitHubClick={handleGitHubClick} + onGoogleClick={handleGoogleClick} + onOtpChange={handleOtpChange} + onContinueClick={handleContinueClick} + onTryAgainClick={handleTryAgainClick} + onBackToLoginClick={handleBackToLoginClick} + isEmailValid={isValid} + emailError={errors.email?.message} + /> + ); +} diff --git a/src/app/login/login-ui.tsx b/src/app/login/login-ui.tsx new file mode 100644 index 0000000..db31ef9 --- /dev/null +++ b/src/app/login/login-ui.tsx @@ -0,0 +1,210 @@ +import React, { useRef, useEffect, useState } from "react"; +import { gsap } from "gsap"; +import { Button } from "@/components/shadcn/button"; +import { Input } from "@/components/shadcn/input"; +import { + InputOTP, + InputOTPGroup, + InputOTPSlot, + InputOTPSeparator, +} from "@/components/shadcn/input-otp"; +import { UseFormRegisterReturn } from "react-hook-form"; +import ThemeSwitch from "@/components/theme/theme-switch"; +import { cn } from "@/lib/utils"; +import { Icon } from "@iconify/react"; + +interface LoginProps { + step: "initial" | "emailInput" | "otpInput"; + email: UseFormRegisterReturn<"email">; + otpValue: string; + onEmailClick: () => void; + onGitHubClick: () => void; + onGoogleClick: () => void; + onOtpChange: (value: string) => void; + onContinueClick: () => void; + onTryAgainClick: () => void; + onBackToLoginClick: () => void; + isEmailValid: boolean; + emailError?: string; +} + +export default function Login({ + step, + email, + otpValue, + onEmailClick, + onGitHubClick, + onGoogleClick, + onOtpChange, + onContinueClick, + onTryAgainClick, + onBackToLoginClick, + isEmailValid, + emailError, +}: LoginProps) { + const inputRef = useRef(null); + const otpContainerRef = useRef(null); + const loginContainerRef = useRef(null); + const [isAnimating, setIsAnimating] = useState(false); + const [shouldAnimate, setShouldAnimate] = useState(false); + + useEffect(() => { + setShouldAnimate(true); + }, []); + + useEffect(() => { + if (!shouldAnimate) return; + + if (step === "emailInput" && inputRef.current) { + gsap.fromTo( + inputRef.current, + { opacity: 0, y: -20 }, + { opacity: 1, y: 0, duration: 0.5, ease: "power2.out" }, + ); + } + + if (step === "otpInput" && otpContainerRef.current) { + gsap.fromTo( + otpContainerRef.current, + { opacity: 0, y: 20 }, + { opacity: 1, y: 0, duration: 0.5, ease: "power2.out" }, + ); + } + + if (step === "initial" && loginContainerRef.current) { + gsap.fromTo( + loginContainerRef.current, + { opacity: 0, y: 20 }, + { opacity: 1, y: 0, duration: 0.5, ease: "power2.out" }, + ); + } + }, [step, shouldAnimate]); + + const handleBackToLogin = () => { + setIsAnimating(true); + gsap.to(otpContainerRef.current, { + opacity: 0, + y: 20, + duration: 0.5, + ease: "power2.in", + onComplete: () => { + onBackToLoginClick(); + setIsAnimating(false); + }, + }); + }; + + const renderContent = () => { + if (step === "otpInput") { + return ( + <div + ref={otpContainerRef} + className="flex flex-col justify-center items-center gap-8 p-2" + > + <h2>Check your email</h2> + <div className="flex flex-col text-center gap-2"> + <span>We've sent a temporary login code.</span> + <span> + Please check your inbox at <b>{email.name}</b> + </span> + </div> + <div className="flex flex-col gap-8"> + <InputOTP maxLength={6} value={otpValue} onChange={onOtpChange}> + <InputOTPGroup> + <InputOTPSlot index={0} /> + <InputOTPSlot index={1} /> + </InputOTPGroup> + <InputOTPSeparator className="hidden md:block" /> + <InputOTPGroup> + <InputOTPSlot index={2} /> + <InputOTPSlot index={3} /> + </InputOTPGroup> + <InputOTPSeparator className="hidden md:block" /> + <InputOTPGroup> + <InputOTPSlot index={4} /> + <InputOTPSlot index={5} /> + </InputOTPGroup> + </InputOTP> + <Button onClick={onContinueClick} className="w-full"> + Continue + </Button> + </div> + <span> + Nothing received ?{" "} + <Button + onClick={onTryAgainClick} + variant={"link"} + size={"no-padding"} + className="text-primary underline" + > + Try again ! + </Button> + </span> + <Button + onClick={handleBackToLogin} + variant={"link"} + size={"no-padding"} + className="text-slate-500 font-bold hover:no-underline" + disabled={isAnimating} + > + Back to login + </Button> + </div> + ); + } + + return ( + <div + ref={loginContainerRef} + className="flex flex-col justify-center items-center gap-8 w-96 p-2" + > + <h2>Log in to Gists</h2> + <div className="flex flex-col gap-4 w-full"> + {step === "emailInput" && ( + <div ref={inputRef} style={{ opacity: shouldAnimate ? 0 : 1 }}> + <Input placeholder="Enter your email address..." {...email} /> + {emailError && ( + <p className="text-red-500 text-sm mt-1">{emailError}</p> + )} + </div> + )} + <Button + onClick={onEmailClick} + disabled={step === "emailInput" && !isEmailValid} + > + Continue with email + </Button> + {step === "emailInput" && ( + <div className="h-[1px] w-full bg-slate-800"></div> + )} + + <Button + variant={"secondary"} + onClick={onGitHubClick} + className="gap-1" + > + <Icon icon="mdi:github" width={22} height={22} /> + Continue with GitHub + </Button> + <Button + variant={"secondary"} + onClick={onGoogleClick} + className="gap-1" + > + <Icon icon="mdi:google" width={18} height={18} /> + Continue with Google + </Button> + </div> + </div> + ); + }; + + return ( + <div className="relative h-screen w-full flex justify-center items-center"> + <div className="absolute top-4 right-4 cursor-pointer"> + <ThemeSwitch /> + </div> + {renderContent()} + </div> + ); +} diff --git a/src/app/login/page.tsx b/src/app/login/page.tsx index f2983b6..2efa74a 100644 --- a/src/app/login/page.tsx +++ b/src/app/login/page.tsx @@ -1,5 +1,7 @@ -import LoginFeature from '@/pages/feature/login-feature' +"use client"; + +import LoginFeature from "./login-feature"; export default function LoginRoute() { - return <LoginFeature /> + return <LoginFeature />; } diff --git a/src/components/api/api-provider.tsx b/src/components/api/api-provider.tsx new file mode 100644 index 0000000..6134fe1 --- /dev/null +++ b/src/components/api/api-provider.tsx @@ -0,0 +1,15 @@ +"use client"; + +import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; +import { useState } from "react"; + +export default function QueryProvider({ + children, +}: { + children: React.ReactNode; +}) { + const [queryClient] = useState<QueryClient>(() => new QueryClient()); + return ( + <QueryClientProvider client={queryClient}>{children}</QueryClientProvider> + ); +} diff --git a/src/components/theme/theme-provider.tsx b/src/components/theme/theme-provider.tsx index ebabea3..1107645 100644 --- a/src/components/theme/theme-provider.tsx +++ b/src/components/theme/theme-provider.tsx @@ -1,10 +1,10 @@ -'use client' -import { ThemeProvider } from 'next-themes' +"use client"; +import { ThemeProvider } from "next-themes"; export function Providers({ children }: { children: React.ReactNode }) { return ( <ThemeProvider attribute="class" defaultTheme="dark" enableSystem> - {children}{' '} + {children}{" "} </ThemeProvider> - ) + ); } diff --git a/src/lib/queries/auth.queries.tsx b/src/lib/queries/auth.queries.tsx new file mode 100644 index 0000000..bde3853 --- /dev/null +++ b/src/lib/queries/auth.queries.tsx @@ -0,0 +1,76 @@ +"use client"; +import ky from "ky"; +import { getBackendURL } from "../utils"; +import { useMutation } from "@tanstack/react-query"; +import getQueryClient from "./queries"; + +const fetchLocalAuth = async ({ email }: { email: string }) => { + const json = await ky + .post(`${getBackendURL()}/auth/local/begin`, { + json: { email }, + }) + .json(); + + return json; //no need to assert type since the most important is the status code +}; + +const fetchLocalAuthVerify = async ({ + email, + token, +}: { + email: string; + token: string; +}) => { + const json = await ky + .post(`${getBackendURL()}/auth/local/verify`, { + json: { email, token }, + credentials: "include", + }) + .json(); + return json; +}; + +/** + * Start the local authentication process by sending an email to the user + * @param email, the user email + * @returns the mutation object, and error if any and a boolean indicating the loading state + */ +export const useLocalAuth = () => { + const { mutate, error, isPending } = useMutation({ + mutationFn: (email: string) => { + return fetchLocalAuth({ email }); + }, + }); + return { mutate, error, isPending }; +}; + +export const prefetchLocalAuth = async (email: string) => { + const queryClient = getQueryClient(); + return await queryClient.prefetchQuery({ + queryKey: ["localAuth", email], + queryFn: () => fetchLocalAuth({ email }), + }); +}; + +export const useLocalAuthVerify = () => { + const { mutate, error, isPending, data } = useMutation({ + mutationFn: ({ email, token }: { email: string; token: string }) => { + return fetchLocalAuthVerify({ email, token }); + }, + }); + return { mutate, error, isPending, data }; +}; + +export const prefetchLocalAuthVerify = async ({ + email, + token, +}: { + email: string; + token: string; +}) => { + const queryClient = getQueryClient(); + return await queryClient.prefetchQuery({ + queryKey: ["localAuthVerify", email, token], + queryFn: () => fetchLocalAuthVerify({ email, token }), + }); +}; diff --git a/src/lib/queries/queries.tsx b/src/lib/queries/queries.tsx new file mode 100644 index 0000000..0a5442f --- /dev/null +++ b/src/lib/queries/queries.tsx @@ -0,0 +1,8 @@ +"use client"; +import { QueryClient } from "@tanstack/react-query"; +import { cache } from "react"; + +export const getQueryClient = () => { + return new QueryClient(); +}; +export default getQueryClient; diff --git a/src/lib/utils.ts b/src/lib/utils.ts index d084cca..8c5ea97 100644 --- a/src/lib/utils.ts +++ b/src/lib/utils.ts @@ -1,6 +1,12 @@ -import { type ClassValue, clsx } from "clsx" -import { twMerge } from "tailwind-merge" +import { type ClassValue, clsx } from "clsx"; +import { twMerge } from "tailwind-merge"; export function cn(...inputs: ClassValue[]) { - return twMerge(clsx(inputs)) + return twMerge(clsx(inputs)); +} + +export function getBackendURL() { + return ( + process.env.NEXT_PUBLIC_BACKEND_URL || "https://api-gists.courtcircuits.xyz" + ); } diff --git a/src/middleware.ts b/src/middleware.ts new file mode 100644 index 0000000..df21206 --- /dev/null +++ b/src/middleware.ts @@ -0,0 +1,19 @@ +import type { NextRequest } from "next/server"; + +export function middleware(request: NextRequest) { + const accessToken = request.cookies.get("gists.access_token")?.value; + + console.log("accessToken", accessToken); + + if (accessToken && !request.nextUrl.pathname.startsWith("/dashboard")) { + return Response.redirect(new URL("/dashboard", request.url)); + } + + if (!accessToken && !request.nextUrl.pathname.startsWith("/login")) { + return Response.redirect(new URL("/login", request.url)); + } +} + +export const config = { + matcher: ["/((?!api|_next/static|_next/image|.*\\.png$).*)"], +}; diff --git a/src/pages/feature/login-feature.tsx b/src/pages/feature/login-feature.tsx deleted file mode 100644 index e3d14b6..0000000 --- a/src/pages/feature/login-feature.tsx +++ /dev/null @@ -1,88 +0,0 @@ -'use client' - -import { useState, useCallback } from 'react' -import { useForm, UseFormRegisterReturn } from 'react-hook-form' -import Login from '../ui/login' -import { useToast } from '@/components/shadcn/use-toast' - -interface FormData { - email: string -} - -export default function LoginFeature() { - const [step, setStep] = useState<'initial' | 'emailInput' | 'otpInput'>('initial') - const [otpValue, setOtpValue] = useState('') - const { toast } = useToast() - - const { - register, - handleSubmit, - formState: { errors, isValid }, - } = useForm<FormData>({ - mode: 'onChange', - }) - - const emailRegister = register('email', { - required: 'Email is required', - pattern: { - value: /^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}$/i, - message: 'Invalid email address', - }, - }) - - const handleEmailClick = useCallback(() => { - if (step === 'initial') { - setStep('emailInput') - } else if (step === 'emailInput' && isValid) { - handleSubmit(onSubmit)() - } - }, [step, isValid, handleSubmit]) - - const handleGitHubClick = useCallback(() => { - console.log('GitHub') - }, []) - - const handleGoogleClick = useCallback(() => { - console.log('CGoogle') - }, []) - - const handleOtpChange = useCallback((value: string) => { - setOtpValue(value) - }, []) - - const handleContinueClick = useCallback(() => { - console.log('OTP:', otpValue) - }, [otpValue]) - - const handleTryAgainClick = useCallback(() => { - console.log('A new OTP has been sent.') - toast({ title: 'A new one time password has been sent.', description: 'Please check your email.' }) - }, [toast]) - - const handleBackToLoginClick = useCallback(() => { - setStep('initial') - setOtpValue('') - }, []) - - const onSubmit = (data: FormData) => { - console.log(data) - setStep('otpInput') - } - - return ( - <Login - step={step} - email={emailRegister as UseFormRegisterReturn<'email'>} - otpValue={otpValue} - onEmailClick={handleEmailClick} - onGitHubClick={handleGitHubClick} - onGoogleClick={handleGoogleClick} - onOtpChange={handleOtpChange} - onContinueClick={handleContinueClick} - onTryAgainClick={handleTryAgainClick} - onBackToLoginClick={handleBackToLoginClick} - isEmailValid={isValid} - emailError={errors.email?.message} - /> - ) -} diff --git a/src/pages/ui/login.tsx b/src/pages/ui/login.tsx deleted file mode 100644 index 8dbf593..0000000 --- a/src/pages/ui/login.tsx +++ /dev/null @@ -1,158 +0,0 @@ -import React, { useRef, useEffect, useState } from 'react' -import { gsap } from 'gsap' -import { Button } from '@/components/shadcn/button' -import { Input } from '@/components/shadcn/input' -import { InputOTP, InputOTPGroup, InputOTPSlot, InputOTPSeparator } from '@/components/shadcn/input-otp' -import { UseFormRegisterReturn } from 'react-hook-form' -import ThemeSwitch from '@/components/theme/theme-switch' -import { cn } from '@/lib/utils' - -interface LoginProps { - step: 'initial' | 'emailInput' | 'otpInput' - email: UseFormRegisterReturn<'email'> - otpValue: string - onEmailClick: () => void - onGitHubClick: () => void - onGoogleClick: () => void - onOtpChange: (value: string) => void - onContinueClick: () => void - onTryAgainClick: () => void - onBackToLoginClick: () => void - isEmailValid: boolean - emailError?: string -} - -export default function Login({ - step, - email, - otpValue, - onEmailClick, - onGitHubClick, - onGoogleClick, - onOtpChange, - onContinueClick, - onTryAgainClick, - onBackToLoginClick, - isEmailValid, - emailError, -}: LoginProps) { - const inputRef = useRef(null) - const otpContainerRef = useRef(null) - const loginContainerRef = useRef(null) - const [isAnimating, setIsAnimating] = useState(false) - const [shouldAnimate, setShouldAnimate] = useState(false) - - useEffect(() => { - setShouldAnimate(true) - }, []) - - useEffect(() => { - if (!shouldAnimate) return - - if (step === 'emailInput' && inputRef.current) { - gsap.fromTo(inputRef.current, { opacity: 0, y: -20 }, { opacity: 1, y: 0, duration: 0.5, ease: 'power2.out' }) - } - - if (step === 'otpInput' && otpContainerRef.current) { - gsap.fromTo(otpContainerRef.current, { opacity: 0, y: 20 }, { opacity: 1, y: 0, duration: 0.5, ease: 'power2.out' }) - } - - if (step === 'initial' && loginContainerRef.current) { - gsap.fromTo(loginContainerRef.current, { opacity: 0, y: 20 }, { opacity: 1, y: 0, duration: 0.5, ease: 'power2.out' }) - } - }, [step, shouldAnimate]) - - const handleBackToLogin = () => { - setIsAnimating(true) - gsap.to(otpContainerRef.current, { - opacity: 0, - y: 20, - duration: 0.5, - ease: 'power2.in', - onComplete: () => { - onBackToLoginClick() - setIsAnimating(false) - }, - }) - } - - const renderContent = () => { - if (step === 'otpInput') { - return ( - <div ref={otpContainerRef} className="flex flex-col justify-center items-center gap-8 p-2"> - <h2>Check your email</h2> - <div className="flex flex-col text-center gap-2"> - <span>We've sent a temporary login code.</span> - <span> - Please check your inbox at <b>{email.name}</b> - </span> - </div> - <div className="flex flex-col gap-8"> - <InputOTP maxLength={6} value={otpValue} onChange={onOtpChange}> - <InputOTPGroup> - <InputOTPSlot index={0} /> - <InputOTPSlot index={1} /> - </InputOTPGroup> - <InputOTPSeparator className="hidden md:block" /> - <InputOTPGroup> - <InputOTPSlot index={2} /> - <InputOTPSlot index={3} /> - </InputOTPGroup> - <InputOTPSeparator className="hidden md:block" /> - <InputOTPGroup> - <InputOTPSlot index={4} /> - <InputOTPSlot index={5} /> - </InputOTPGroup> - </InputOTP> - <Button onClick={onContinueClick} className="w-full"> - Continue - </Button> - </div> - <span> - Nothing received ?{' '} - <Button onClick={onTryAgainClick} variant={'link'} size={'no-padding'} className="text-primary underline"> - Try again ! - </Button> - </span> - <Button onClick={handleBackToLogin} variant={'link'} size={'no-padding'} className="text-slate-500 font-bold hover:no-underline" disabled={isAnimating}> - Back to login - </Button> - </div> - ) - } - - return ( - <div ref={loginContainerRef} className="flex flex-col justify-center items-center gap-8 w-96 p-2"> - <h2>Log in to Gists</h2> - <div className="flex flex-col gap-4 w-full"> - {step === 'emailInput' && ( - <div ref={inputRef} style={{ opacity: shouldAnimate ? 0 : 1 }}> - <Input placeholder="Enter your email address..." {...email} /> - {emailError && <p className="text-red-500 text-sm mt-1">{emailError}</p>} - </div> - )} - <Button onClick={onEmailClick} disabled={step === 'emailInput' && !isEmailValid}> - {step === 'emailInput' ? 'Continue with Email' : 'Log in with Email'} - </Button> - {step === 'emailInput' && <div className="h-[1px] w-full bg-slate-800"></div>} - - <Button variant={'secondary'} onClick={onGitHubClick}> - Log in with GitHub - </Button> - <Button variant={'secondary'} onClick={onGoogleClick}> - Log in with Google - </Button> - </div> - </div> - ) - } - - return ( - <div className="relative h-screen w-full flex justify-center items-center"> - <div className="absolute top-4 right-4 cursor-pointer"> - <ThemeSwitch /> - </div> - {renderContent()} - </div> - ) -}