From 33ab130cd720e67323433f51eedabbc6e6303bab Mon Sep 17 00:00:00 2001 From: Noel Simbolon <84700640+noelsimbolon@users.noreply.github.com> Date: Fri, 17 Nov 2023 03:12:20 +0700 Subject: [PATCH] feat: auth --- .env.example | 1 + .env.production.example | 1 + Dockerfile | 2 + nginx.conf | 21 +++++ src/App.css | 74 ++++++++--------- src/assets/images/logo.svg | 14 +--- src/index.css | 8 ++ src/layouts/AnonymousLayout.tsx | 27 ++++++- src/layouts/MainLayout.tsx | 8 +- src/layouts/NotMatchLayout.tsx | 12 +-- src/pages/LoginPage.tsx | 138 +++++++++++++++++++++++++++++++- src/pages/NotMatch.tsx | 8 +- src/pages/SignUpPage.tsx | 132 ++++++++++++++++++++++++++++++ src/routes/routes.ts | 82 ++++++++++--------- src/utils/token.ts | 11 +++ 15 files changed, 432 insertions(+), 107 deletions(-) create mode 100644 .env.example create mode 100644 .env.production.example create mode 100644 nginx.conf create mode 100644 src/pages/SignUpPage.tsx create mode 100644 src/utils/token.ts diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..712705f --- /dev/null +++ b/.env.example @@ -0,0 +1 @@ +VITE_REST_API_URL= \ No newline at end of file diff --git a/.env.production.example b/.env.production.example new file mode 100644 index 0000000..712705f --- /dev/null +++ b/.env.production.example @@ -0,0 +1 @@ +VITE_REST_API_URL= \ No newline at end of file diff --git a/Dockerfile b/Dockerfile index c827fa7..e4bed56 100644 --- a/Dockerfile +++ b/Dockerfile @@ -12,6 +12,8 @@ RUN npm run build FROM nginx:1.24.0-alpine +COPY nginx.conf /etc/nginx/nginx.conf + COPY --from=build /tonality/tonality-client/dist /usr/share/nginx/html EXPOSE 80 diff --git a/nginx.conf b/nginx.conf new file mode 100644 index 0000000..a8aabfc --- /dev/null +++ b/nginx.conf @@ -0,0 +1,21 @@ +events {} + +http { + include /etc/nginx/mime.types; # Include MIME types + + server { + listen 80; + + location / { + root /usr/share/nginx/html; + index index.html; + try_files $uri $uri/ /index.html; # Handle client-side routing + } + } + + # Specify MIME types for JavaScript and CSS files + types { + text/javascript js; + text/css css; + } +} diff --git a/src/App.css b/src/App.css index b9d355d..8a02e97 100644 --- a/src/App.css +++ b/src/App.css @@ -1,42 +1,42 @@ -#root { - max-width: 1280px; - margin: 0 auto; - padding: 2rem; - text-align: center; -} +/*#root {*/ +/* max-width: 1280px;*/ +/* margin: 0 auto;*/ +/* padding: 2rem;*/ +/* text-align: center;*/ +/*}*/ -.logo { - height: 6em; - padding: 1.5em; - will-change: filter; - transition: filter 300ms; -} -.logo:hover { - filter: drop-shadow(0 0 2em #646cffaa); -} -.logo.react:hover { - filter: drop-shadow(0 0 2em #61dafbaa); -} +/*.logo {*/ +/* height: 6em;*/ +/* padding: 1.5em;*/ +/* will-change: filter;*/ +/* transition: filter 300ms;*/ +/*}*/ +/*.logo:hover {*/ +/* filter: drop-shadow(0 0 2em #646cffaa);*/ +/*}*/ +/*.logo.react:hover {*/ +/* filter: drop-shadow(0 0 2em #61dafbaa);*/ +/*}*/ -@keyframes logo-spin { - from { - transform: rotate(0deg); - } - to { - transform: rotate(360deg); - } -} +/*@keyframes logo-spin {*/ +/* from {*/ +/* transform: rotate(0deg);*/ +/* }*/ +/* to {*/ +/* transform: rotate(360deg);*/ +/* }*/ +/*}*/ -@media (prefers-reduced-motion: no-preference) { - a:nth-of-type(2) .logo { - animation: logo-spin infinite 20s linear; - } -} +/*@media (prefers-reduced-motion: no-preference) {*/ +/* a:nth-of-type(2) .logo {*/ +/* animation: logo-spin infinite 20s linear;*/ +/* }*/ +/*}*/ -.card { - padding: 2em; -} +/*.card {*/ +/* padding: 2em;*/ +/*}*/ -.read-the-docs { - color: #888; -} +/*.read-the-docs {*/ +/* color: #888;*/ +/*}*/ diff --git a/src/assets/images/logo.svg b/src/assets/images/logo.svg index 4753de8..3e7c6da 100644 --- a/src/assets/images/logo.svg +++ b/src/assets/images/logo.svg @@ -1,12 +1,4 @@ -<?xml version="1.0" encoding="utf-8"?> -<!-- Generator: Adobe Illustrator 27.8.1, SVG Export Plug-In . SVG Version: 6.00 Build 0) --> -<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" - viewBox="0 0 512 512" style="enable-background:new 0 0 512 512;" xml:space="preserve"> -<style type="text/css"> - .st0{fill:#FFFFFF;} -</style> -<path class="st0" d="M0,256C0,114.6,114.6,0,256,0s256,114.6,256,256S397.4,512,256,512S0,397.4,0,256z M256,288 - c-17.7,0-32-14.3-32-32s14.3-32,32-32s32,14.3,32,32S273.7,288,256,288z M160,256c0,53,43,96,96,96s96-43,96-96s-43-96-96-96 - S160,203,160,256z M96,240c0-35,17.5-71.1,45.2-98.8S205,96,240,96c8.8,0,16-7.2,16-16s-7.2-16-16-16c-45.4,0-89.2,22.3-121.5,54.5 - S64,194.6,64,240c0,8.8,7.2,16,16,16S96,248.8,96,240z"/> +<svg width="176" height="50" viewBox="0 0 176 50" fill="none" xmlns="http://www.w3.org/2000/svg"> +<path d="M0 25C0 18.3696 2.63392 12.0107 7.32233 7.32233C12.0107 2.63392 18.3696 0 25 0C31.6304 0 37.9893 2.63392 42.6777 7.32233C47.3661 12.0107 50 18.3696 50 25C50 31.6304 47.3661 37.9893 42.6777 42.6777C37.9893 47.3661 31.6304 50 25 50C18.3696 50 12.0107 47.3661 7.32233 42.6777C2.63392 37.9893 0 31.6304 0 25ZM25 28.125C24.1712 28.125 23.3763 27.7958 22.7903 27.2097C22.2042 26.6237 21.875 25.8288 21.875 25C21.875 24.1712 22.2042 23.3763 22.7903 22.7903C23.3763 22.2042 24.1712 21.875 25 21.875C25.8288 21.875 26.6237 22.2042 27.2097 22.7903C27.7958 23.3763 28.125 24.1712 28.125 25C28.125 25.8288 27.7958 26.6237 27.2097 27.2097C26.6237 27.7958 25.8288 28.125 25 28.125ZM15.625 25C15.625 27.4864 16.6127 29.871 18.3709 31.6291C20.129 33.3873 22.5136 34.375 25 34.375C27.4864 34.375 29.871 33.3873 31.6291 31.6291C33.3873 29.871 34.375 27.4864 34.375 25C34.375 22.5136 33.3873 20.129 31.6291 18.3709C29.871 16.6127 27.4864 15.625 25 15.625C22.5136 15.625 20.129 16.6127 18.3709 18.3709C16.6127 20.129 15.625 22.5136 15.625 25ZM9.375 23.4375C9.375 20.0195 11.084 16.4941 13.7891 13.7891C16.4941 11.084 20.0195 9.375 23.4375 9.375C24.2969 9.375 25 8.67188 25 7.8125C25 6.95312 24.2969 6.25 23.4375 6.25C19.0039 6.25 14.7266 8.42773 11.5723 11.5723C8.41797 14.7168 6.25 19.0039 6.25 23.4375C6.25 24.2969 6.95312 25 7.8125 25C8.67188 25 9.375 24.2969 9.375 23.4375Z" fill="white"/> +<path d="M59.0653 17.9851V14.1818H76.9844V17.9851H70.3047V36H65.745V17.9851H59.0653ZM84.8706 36.3196C83.2157 36.3196 81.7846 35.968 80.5772 35.2649C79.377 34.5547 78.4501 33.5675 77.7967 32.3033C77.1433 31.032 76.8166 29.5582 76.8166 27.8821C76.8166 26.1918 77.1433 24.7145 77.7967 23.4503C78.4501 22.179 79.377 21.1918 80.5772 20.4886C81.7846 19.7784 83.2157 19.4233 84.8706 19.4233C86.5254 19.4233 87.9529 19.7784 89.1532 20.4886C90.3606 21.1918 91.291 22.179 91.9444 23.4503C92.5978 24.7145 92.9245 26.1918 92.9245 27.8821C92.9245 29.5582 92.5978 31.032 91.9444 32.3033C91.291 33.5675 90.3606 34.5547 89.1532 35.2649C87.9529 35.968 86.5254 36.3196 84.8706 36.3196ZM84.8919 32.804C85.6447 32.804 86.2733 32.5909 86.7775 32.1648C87.2818 31.7315 87.6618 31.142 87.9174 30.3963C88.1802 29.6506 88.3116 28.8018 88.3116 27.8501C88.3116 26.8984 88.1802 26.0497 87.9174 25.304C87.6618 24.5582 87.2818 23.9687 86.7775 23.5355C86.2733 23.1023 85.6447 22.8857 84.8919 22.8857C84.1319 22.8857 83.4927 23.1023 82.9743 23.5355C82.4629 23.9687 82.0758 24.5582 81.813 25.304C81.5574 26.0497 81.4295 26.8984 81.4295 27.8501C81.4295 28.8018 81.5574 29.6506 81.813 30.3963C82.0758 31.142 82.4629 31.7315 82.9743 32.1648C83.4927 32.5909 84.1319 32.804 84.8919 32.804ZM100.414 26.5398V36H95.8755V19.6364H100.201V22.5234H100.393C100.755 21.5717 101.362 20.8189 102.214 20.2649C103.067 19.7038 104.1 19.4233 105.314 19.4233C106.451 19.4233 107.442 19.6719 108.287 20.169C109.132 20.6662 109.789 21.3764 110.258 22.2997C110.726 23.2159 110.961 24.3097 110.961 25.581V36H106.422V26.3906C106.43 25.3892 106.174 24.608 105.655 24.0469C105.137 23.4787 104.423 23.1946 103.514 23.1946C102.903 23.1946 102.363 23.326 101.895 23.5888C101.433 23.8516 101.071 24.2351 100.808 24.7393C100.552 25.2365 100.421 25.8366 100.414 26.5398ZM119.204 36.3089C118.16 36.3089 117.229 36.1278 116.413 35.7656C115.596 35.3963 114.95 34.853 114.474 34.1357C114.005 33.4112 113.771 32.5092 113.771 31.4297C113.771 30.5206 113.938 29.7571 114.271 29.1392C114.605 28.5213 115.06 28.0241 115.635 27.6477C116.21 27.2713 116.864 26.9872 117.595 26.7955C118.334 26.6037 119.108 26.4688 119.918 26.3906C120.869 26.2912 121.636 26.1989 122.219 26.1136C122.801 26.0213 123.224 25.8864 123.487 25.7088C123.749 25.5312 123.881 25.2685 123.881 24.9205V24.8565C123.881 24.1818 123.668 23.6598 123.241 23.2905C122.822 22.9212 122.226 22.7365 121.452 22.7365C120.635 22.7365 119.985 22.9176 119.502 23.2798C119.019 23.6349 118.7 24.0824 118.543 24.6222L114.346 24.2812C114.559 23.2869 114.978 22.4276 115.603 21.7031C116.228 20.9716 117.034 20.4105 118.021 20.0199C119.016 19.6222 120.166 19.4233 121.473 19.4233C122.382 19.4233 123.252 19.5298 124.083 19.7429C124.921 19.956 125.663 20.2862 126.31 20.7337C126.963 21.1811 127.478 21.7564 127.854 22.4595C128.231 23.1555 128.419 23.9901 128.419 24.9631V36H124.115V33.7308H123.987C123.724 34.2422 123.373 34.6932 122.933 35.0838C122.492 35.4673 121.963 35.7692 121.345 35.9893C120.727 36.2024 120.013 36.3089 119.204 36.3089ZM120.504 33.1768C121.171 33.1768 121.761 33.0455 122.272 32.7827C122.783 32.5128 123.185 32.1506 123.476 31.696C123.767 31.2415 123.913 30.7266 123.913 30.1513V28.4148C123.771 28.5071 123.575 28.5923 123.327 28.6705C123.085 28.7415 122.812 28.8089 122.506 28.8729C122.201 28.9297 121.896 28.983 121.59 29.0327C121.285 29.0753 121.008 29.1143 120.759 29.1499C120.227 29.228 119.761 29.3523 119.364 29.5227C118.966 29.6932 118.657 29.924 118.437 30.2152C118.217 30.4993 118.107 30.8544 118.107 31.2805C118.107 31.8984 118.33 32.3707 118.778 32.6974C119.232 33.017 119.808 33.1768 120.504 33.1768ZM136.478 14.1818V36H131.94V14.1818H136.478ZM140.114 36V19.6364H144.652V36H140.114ZM142.394 17.527C141.719 17.527 141.14 17.3033 140.657 16.8558C140.181 16.4013 139.943 15.858 139.943 15.2259C139.943 14.6009 140.181 14.0646 140.657 13.6172C141.14 13.1626 141.719 12.9354 142.394 12.9354C143.068 12.9354 143.644 13.1626 144.119 13.6172C144.602 14.0646 144.844 14.6009 144.844 15.2259C144.844 15.858 144.602 16.4013 144.119 16.8558C143.644 17.3033 143.068 17.527 142.394 17.527ZM156.991 19.6364V23.0455H147.137V19.6364H156.991ZM149.374 15.7159H153.913V30.9716C153.913 31.3906 153.977 31.7173 154.104 31.9517C154.232 32.179 154.41 32.3388 154.637 32.4311C154.871 32.5234 155.141 32.5696 155.447 32.5696C155.66 32.5696 155.873 32.5518 156.086 32.5163C156.299 32.4737 156.462 32.4418 156.576 32.4205L157.29 35.7976C157.063 35.8686 156.743 35.9503 156.331 36.0426C155.919 36.142 155.418 36.2024 154.829 36.2237C153.735 36.2663 152.776 36.1207 151.952 35.7869C151.136 35.4531 150.5 34.9347 150.045 34.2315C149.591 33.5284 149.367 32.6406 149.374 31.5682V15.7159ZM162.75 42.1364C162.174 42.1364 161.635 42.0902 161.13 41.9979C160.633 41.9126 160.221 41.8026 159.895 41.6676L160.917 38.2798C161.45 38.4432 161.929 38.532 162.355 38.5462C162.789 38.5604 163.162 38.4609 163.474 38.2479C163.794 38.0348 164.053 37.6726 164.252 37.1612L164.518 36.4688L158.648 19.6364H163.421L166.809 31.6534H166.979L170.399 19.6364H175.203L168.843 37.7685C168.538 38.6491 168.123 39.4162 167.597 40.0696C167.078 40.7301 166.422 41.2379 165.626 41.593C164.831 41.9553 163.872 42.1364 162.75 42.1364Z" fill="white"/> </svg> diff --git a/src/index.css b/src/index.css index b5c61c9..bc0f310 100644 --- a/src/index.css +++ b/src/index.css @@ -1,3 +1,11 @@ +@import url('https://fonts.googleapis.com/css2?family=Inter:wght@100;200;300;400;500;600;700;800;900&family=Lato&display=swap'); + @tailwind base; @tailwind components; @tailwind utilities; + +@layer base { + html { + font-family: "Inter", system-ui, sans-serif; + } +} diff --git a/src/layouts/AnonymousLayout.tsx b/src/layouts/AnonymousLayout.tsx index 14ba62a..82adf2e 100644 --- a/src/layouts/AnonymousLayout.tsx +++ b/src/layouts/AnonymousLayout.tsx @@ -1,9 +1,30 @@ -import {Outlet} from "react-router-dom"; +import { Outlet } from "react-router-dom"; +import { Toaster } from "@/components/ui/toaster.tsx"; +import logo from "@/assets/images/logo.svg"; const AnonymousLayout = () => { return ( - <Outlet/> + <> + <Toaster /> + <div className="flex"> + <div className="flex flex-col justify-between bg-neutral-900 h-screen w-1/2"> + <div className="m-5"> + <img src={logo} alt="Tonality Logo"></img> + </div> + <div className="m-5"> + <p className="text-neutral-100"> + "Music is the universal language that transcends borders, + cultures, and time, speaking to the very core of our humanity, + where words alone often fall short." + </p> + </div> + </div> + <div className="h-screen w-1/2 bg-neutral-950 flex items-center justify-center"> + <Outlet /> + </div> + </div> + </> ); }; -export default AnonymousLayout; \ No newline at end of file +export default AnonymousLayout; diff --git a/src/layouts/MainLayout.tsx b/src/layouts/MainLayout.tsx index 8cbab69..f06545b 100644 --- a/src/layouts/MainLayout.tsx +++ b/src/layouts/MainLayout.tsx @@ -1,15 +1,15 @@ -import Sidebar from "@/components/Sidebar.tsx"; +import Sidebar from "@/components/sidebar.tsx"; import { Outlet } from "react-router-dom"; const MainLayout = () => { return ( <div className="wrapper"> - <Sidebar/> - <div className='ml-20 pr-10'> + <Sidebar /> + <div className="ml-20 pr-10"> <Outlet /> </div> </div> ); }; -export default MainLayout; \ No newline at end of file +export default MainLayout; diff --git a/src/layouts/NotMatchLayout.tsx b/src/layouts/NotMatchLayout.tsx index 6c1d898..1907ae0 100644 --- a/src/layouts/NotMatchLayout.tsx +++ b/src/layouts/NotMatchLayout.tsx @@ -1,7 +1,7 @@ -import {Outlet} from "react-router-dom"; +import { Outlet } from "react-router-dom"; -export const NotMatchLayout = () => { - return ( - <Outlet/> - ); -}; \ No newline at end of file +const NotMatchLayout = () => { + return <Outlet />; +}; + +export default NotMatchLayout; diff --git a/src/pages/LoginPage.tsx b/src/pages/LoginPage.tsx index 038e8eb..6504d95 100644 --- a/src/pages/LoginPage.tsx +++ b/src/pages/LoginPage.tsx @@ -1,9 +1,141 @@ +"use client"; + +import * as z from "zod"; +import { useForm } from "react-hook-form"; +import { zodResolver } from "@hookform/resolvers/zod"; +import axios from "axios"; +import { + Form, + FormControl, + FormField, + FormItem, + FormLabel, + FormMessage, +} from "@/components/ui/form.tsx"; +import { Input } from "@/components/ui/input.tsx"; +import { Button } from "@/components/ui/button.tsx"; +import { Link } from "react-router-dom"; +import { storeAccessToken } from "@/utils/token.ts"; +import { StatusCodes } from "http-status-codes"; + +const restApiUrl: string = import.meta.env.VITE_REST_API_URL; + +const isUsernameInvalid = async (username: string): Promise<boolean> => { + const res = await axios.post(restApiUrl + "username-availability", { + username: username, + }); + + const responseBody: { usernameAvailable: string } = res.data; + + return responseBody.usernameAvailable === "false"; +}; + +const loginFormSchema = z.object({ + username: z + .string() + .min(2, { + message: "Username has a minimum length of 2.", + }) + .max(50, { + message: "Username has a maximum length of 50.", + }) + .refine(isUsernameInvalid, { + message: "User does not exist.", + }), + password: z + .string() + .min(8, { + message: "Password has a minimum length of 8.", + }) + .max(255, { + message: "Password has a maximum length of 255.", + }), +}); + const LoginPage = () => { + // Define form + const loginForm = useForm<z.infer<typeof loginFormSchema>>({ + resolver: zodResolver(loginFormSchema), + defaultValues: { + username: "", + password: "", + }, + }); + + // Define submit handler + async function onSubmit(values: z.infer<typeof loginFormSchema>) { + try { + const res = await axios.post( + restApiUrl + "login", + { + username: values.username, + password: values.password, + }, + { + withCredentials: true, + }, + ); + + storeAccessToken(res.data.accessToken); + } catch (err) { + if ( + axios.isAxiosError(err) && + err.response?.status === StatusCodes.UNAUTHORIZED + ) { + loginForm.setError("password", { + message: "Invalid password.", + }); + } else { + // Handle other errors, such as network issues or server errors + console.error("Error occurred during login:", err); + } + } + } + return ( - <div> - Login Page + <div className="flex flex-col"> + <Form {...loginForm}> + <form onSubmit={loginForm.handleSubmit(onSubmit)} className="space-y-8"> + <FormField + control={loginForm.control} + name="username" + render={({ field }) => ( + <FormItem> + <FormLabel>Username</FormLabel> + <FormControl> + <Input placeholder="" {...field} /> + </FormControl> + <FormMessage /> + </FormItem> + )} + /> + + <FormField + control={loginForm.control} + name="password" + render={({ field }) => ( + <FormItem> + <FormLabel>Password</FormLabel> + <FormControl> + <Input placeholder="" {...field} /> + </FormControl> + <FormMessage /> + </FormItem> + )} + /> + <Button type="submit">Login</Button> + </form> + </Form> + + <div className="text-neutral-100 mt-6"> + Don't have an account?{" "} + <Link to="/signup" className="underline"> + Sign up for Tonality + </Link> + . + </div> </div> ); }; -export default LoginPage; \ No newline at end of file +export default LoginPage; diff --git a/src/pages/NotMatch.tsx b/src/pages/NotMatch.tsx index f7d2400..65355e3 100644 --- a/src/pages/NotMatch.tsx +++ b/src/pages/NotMatch.tsx @@ -2,15 +2,11 @@ const NotMatch = () => { return ( <> <div className="h-screen flex flex-col items-center justify-center"> - <h1 className="font-bold text-3xl text-neutral-100"> - Oops! - </h1> + <h1 className="font-bold text-3xl text-neutral-100">Oops!</h1> <p className="my-5 text-neutral-100"> Sorry, an unexpected error has occurred. </p> - <p className="text-neutral-100"> - Not Found - </p> + <p className="text-neutral-100 italic">Not Found</p> </div> </> ); diff --git a/src/pages/SignUpPage.tsx b/src/pages/SignUpPage.tsx new file mode 100644 index 0000000..d5eb1f6 --- /dev/null +++ b/src/pages/SignUpPage.tsx @@ -0,0 +1,132 @@ +"use client"; + +import { zodResolver } from "@hookform/resolvers/zod"; +import { useForm } from "react-hook-form"; +import * as z from "zod"; +import { + Form, + FormControl, + FormField, + FormItem, + FormLabel, + FormMessage, +} from "@/components/ui/form.tsx"; +import { Input } from "@/components/ui/input.tsx"; +import { Button } from "@/components/ui/button.tsx"; +import { Link, useNavigate } from "react-router-dom"; +import { StatusCodes } from "http-status-codes"; +import axios from "axios"; +import { useToast } from "@/components/ui/use-toast.ts"; + +const restApiUrl: string = import.meta.env.VITE_REST_API_URL; + +const isUsernameAvailable = async (username: string): Promise<boolean> => { + const res = await axios.post(restApiUrl + "username-availability", { + username: username, + }); + + const responseBody: { usernameAvailable: string } = res.data; + + return responseBody.usernameAvailable === "true"; +}; + +const signUpFormSchema = z.object({ + username: z + .string() + .min(2, { + message: "Username must have a minimum length of 2.", + }) + .max(50, { + message: "Username must have a maximum length of 50.", + }) + .refine(isUsernameAvailable, { + message: "Username already exists.", + }), + password: z + .string() + .min(8, { + message: "Password must have a minimum length of 8.", + }) + .max(255, { + message: "Password must have a maximum length of 255.", + }), +}); + +const SignUpPage = () => { + const { toast } = useToast(); + const navigate = useNavigate(); + + // Define form + const signUpForm = useForm<z.infer<typeof signUpFormSchema>>({ + resolver: zodResolver(signUpFormSchema), + defaultValues: { + username: "", + password: "", + }, + }); + + // Define submit handler + async function onSubmit(values: z.infer<typeof signUpFormSchema>) { + const res = await axios.post(restApiUrl + "signup", { + username: values.username, + password: values.password, + }); + + if (res.status === StatusCodes.OK) { + toast({ + description: "You have successfully signed up for Tonality!", + }); + navigate("/login"); + } + } + + return ( + <div className="flex flex-col"> + <Form {...signUpForm}> + <form + onSubmit={signUpForm.handleSubmit(onSubmit)} + className="space-y-8" + > + <FormField + control={signUpForm.control} + name="username" + render={({ field }) => ( + <FormItem> + <FormLabel>Username</FormLabel> + <FormControl> + <Input placeholder="" {...field} /> + </FormControl> + <FormMessage /> + </FormItem> + )} + /> + + <FormField + control={signUpForm.control} + name="password" + render={({ field }) => ( + <FormItem> + <FormLabel>Password</FormLabel> + <FormControl> + <Input placeholder="" {...field} /> + </FormControl> + <FormMessage /> + </FormItem> + )} + /> + <Button type="submit">Sign Up</Button> + </form> + </Form> + + <div className="text-neutral-100 mt-6"> + Already have an account?{" "} + <Link to="/login" className="underline"> + Login here + </Link> + . + </div> + </div> + ); +}; + +export default SignUpPage; diff --git a/src/routes/routes.ts b/src/routes/routes.ts index 46a6e98..c8704b2 100644 --- a/src/routes/routes.ts +++ b/src/routes/routes.ts @@ -1,5 +1,6 @@ import AnonymousLayout from "@/layouts/AnonymousLayout.tsx"; import LoginPage from "@/pages/LoginPage.tsx"; +import SignUpPage from "@/pages/SignUpPage.tsx"; import MainLayout from "@/layouts/MainLayout.tsx"; import AlbumPage from "@/pages/AlbumPage.tsx"; import AddAlbumPage from "@/pages/AddAlbumPage.tsx"; @@ -7,7 +8,7 @@ import EditAlbumPage from "@/pages/EditAlbumPage.tsx"; import SongsPage from "@/pages/SongsPage.tsx"; import AddSongPage from "@/pages/AddSongPage.tsx"; import EditSongPage from "@/pages/EditSongPage.tsx"; -import {NotMatchLayout} from "@/layouts/NotMatchLayout.tsx"; +import NotMatchLayout from "@/layouts/NotMatchLayout.tsx"; import NotMatch from "@/pages/NotMatch.tsx"; import DeleteAlbumDialog from "@/components/delete-dialog-album"; import DeleteSongDialog from "@/components/delete-dialog-song"; @@ -17,90 +18,97 @@ export const routes = [ layout: AnonymousLayout, routes: [ { - name: 'login', - title: 'Login page', + name: "login", + title: "Login to Tonality", component: LoginPage, path: "/login", isPublic: true, - } - ] + }, + { + name: "signup", + title: "Sign Up for Tonality", + component: SignUpPage, + path: "/signup", + isPublic: true, + }, + ], }, { layout: MainLayout, routes: [ { - name: 'album', - title: 'Album', + name: "album", + title: "Album", hasSiderLink: true, routes: [ { - name: 'album', - title: 'Album page', + name: "album", + title: "Album page", component: AlbumPage, path: "/album", }, { - name: 'add-album', - title: 'Add Album page', + name: "add-album", + title: "Add Album page", component: AddAlbumPage, path: "/add-album", }, { - name: 'edit-album', - title: 'Edit Album page', + name: "edit-album", + title: "Edit Album page", component: EditAlbumPage, path: "/:albumId/edit-album", }, { - name: 'delete-album', - title: 'Delete Album page', + name: "delete-album", + title: "Delete Album page", component: DeleteAlbumDialog, path: "/:albumId/delete-album/", - } - ] + }, + ], }, { - name: 'song', - title: 'Song', + name: "song", + title: "Song", hasSiderLink: true, routes: [ { - name: 'song', - title: 'Song page', + name: "song", + title: "Song page", component: SongsPage, path: "/:albumId/songs", }, { - name: 'add-song', - title: 'Add Song page', + name: "add-song", + title: "Add Song page", component: AddSongPage, path: "/:albumId/add-song", }, { - name: 'edit-song', - title: 'Edit Song page', + name: "edit-song", + title: "Edit Song page", component: EditSongPage, path: "/:albumId/edit-song/:songId", }, { - name: 'delete-song', - title: 'Delete Song page', + name: "delete-song", + title: "Delete Song page", component: DeleteSongDialog, path: "/:albumId/delete-song/:songId", - } - ] - } - ] + }, + ], + }, + ], }, { layout: NotMatchLayout, routes: [ { - name: 'not-match', - title: 'Not Match', + name: "not-match", + title: "Not Match", component: NotMatch, path: "*", - } - ] - } -] + }, + ], + }, +]; diff --git a/src/utils/token.ts b/src/utils/token.ts new file mode 100644 index 0000000..25ed7b4 --- /dev/null +++ b/src/utils/token.ts @@ -0,0 +1,11 @@ +// Stores access token in session storage +const storeAccessToken = (accessToken: string): void => { + sessionStorage.setItem("accessToken", accessToken); +}; + +// Removes access token from session storage +const removeAccessToken = (): void => { + sessionStorage.removeItem("accessToken"); +} + +export { storeAccessToken, removeAccessToken } \ No newline at end of file -- GitLab