From 54a748ced71b71dbb07757b004e4c96969b192a5 Mon Sep 17 00:00:00 2001 From: Philipinho <16838612+Philipinho@users.noreply.github.com> Date: Sun, 27 Aug 2023 21:38:59 +0100 Subject: [PATCH] Implement frontend auth and user features - WIP --- frontend/package.json | 9 ++ frontend/src/app/(auth)/layout.tsx | 7 ++ frontend/src/app/(auth)/login/page.tsx | 49 ++++++++++ frontend/src/app/(auth)/signup/page.tsx | 52 +++++++++++ frontend/src/app/(dashboard)/home/page.tsx | 16 ++++ frontend/src/app/(dashboard)/layout.tsx | 21 +++++ frontend/src/app/(dashboard)/shell.tsx | 18 ++++ frontend/src/app/layout.tsx | 8 +- frontend/src/app/page.tsx | 4 +- frontend/src/components/icons.tsx | 48 ++++++++++ .../providers/tanstack-provider.tsx | 14 +++ frontend/src/components/ui/input.tsx | 25 +++++ frontend/src/components/ui/label.tsx | 26 ++++++ .../features/auth/atoms/auth-tokens-atom.ts | 14 +++ .../features/auth/components/legal-terms.tsx | 12 +++ .../features/auth/components/login-form.tsx | 92 +++++++++++++++++++ .../features/auth/components/sign-up-form.tsx | 92 +++++++++++++++++++ frontend/src/features/auth/hooks/use-auth.ts | 65 +++++++++++++ .../features/auth/services/auth-service.ts | 12 +++ .../src/features/auth/types/auth.types.ts | 18 ++++ .../features/user/atoms/current-user-atom.ts | 4 + .../features/user/hooks/use-current-user.ts | 13 +++ .../features/user/services/user-service.ts | 12 +++ .../src/features/user/types/user.types.ts | 20 ++++ frontend/src/features/user/user-provider.tsx | 25 +++++ .../workspace/types/workspace.types.ts | 14 +++ frontend/src/lib/api-client.ts | 53 +++++++++++ frontend/src/lib/jotai-helper.ts | 17 ++++ frontend/src/lib/routes.ts | 9 ++ frontend/tsconfig.json | 2 +- 30 files changed, 765 insertions(+), 6 deletions(-) create mode 100644 frontend/src/app/(auth)/layout.tsx create mode 100644 frontend/src/app/(auth)/login/page.tsx create mode 100644 frontend/src/app/(auth)/signup/page.tsx create mode 100644 frontend/src/app/(dashboard)/home/page.tsx create mode 100644 frontend/src/app/(dashboard)/layout.tsx create mode 100644 frontend/src/app/(dashboard)/shell.tsx create mode 100644 frontend/src/components/icons.tsx create mode 100644 frontend/src/components/providers/tanstack-provider.tsx create mode 100644 frontend/src/components/ui/input.tsx create mode 100644 frontend/src/components/ui/label.tsx create mode 100644 frontend/src/features/auth/atoms/auth-tokens-atom.ts create mode 100644 frontend/src/features/auth/components/legal-terms.tsx create mode 100644 frontend/src/features/auth/components/login-form.tsx create mode 100644 frontend/src/features/auth/components/sign-up-form.tsx create mode 100644 frontend/src/features/auth/hooks/use-auth.ts create mode 100644 frontend/src/features/auth/services/auth-service.ts create mode 100644 frontend/src/features/auth/types/auth.types.ts create mode 100644 frontend/src/features/user/atoms/current-user-atom.ts create mode 100644 frontend/src/features/user/hooks/use-current-user.ts create mode 100644 frontend/src/features/user/services/user-service.ts create mode 100644 frontend/src/features/user/types/user.types.ts create mode 100644 frontend/src/features/user/user-provider.tsx create mode 100644 frontend/src/features/workspace/types/workspace.types.ts create mode 100644 frontend/src/lib/api-client.ts create mode 100644 frontend/src/lib/jotai-helper.ts create mode 100644 frontend/src/lib/routes.ts diff --git a/frontend/package.json b/frontend/package.json index e497239..c7ec783 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -10,27 +10,36 @@ "lint": "next lint" }, "dependencies": { + "@hookform/resolvers": "^3.3.0", "@radix-ui/react-icons": "^1.3.0", + "@radix-ui/react-label": "^2.0.2", "@radix-ui/react-slot": "^1.0.2", "@radix-ui/react-toast": "^1.1.4", "@radix-ui/react-tooltip": "^1.0.6", + "@tanstack/react-query": "^4.33.0", "@types/node": "20.4.8", "@types/react": "18.2.18", "@types/react-dom": "18.2.7", "autoprefixer": "10.4.14", + "axios": "^1.4.0", "class-variance-authority": "^0.7.0", "clsx": "^2.0.0", "eslint": "8.46.0", "eslint-config-next": "13.4.13", "jotai": "^2.3.1", + "js-cookie": "^3.0.5", "next": "13.4.13", "next-themes": "^0.2.1", "postcss": "8.4.27", "react": "18.2.0", "react-dom": "18.2.0", + "react-hook-form": "^7.45.4", "tailwind-merge": "^1.14.0", "tailwindcss": "3.3.3", "tailwindcss-animate": "^1.0.6", "typescript": "5.1.6" + }, + "devDependencies": { + "@types/js-cookie": "^3.0.3" } } diff --git a/frontend/src/app/(auth)/layout.tsx b/frontend/src/app/(auth)/layout.tsx new file mode 100644 index 0000000..65aae78 --- /dev/null +++ b/frontend/src/app/(auth)/layout.tsx @@ -0,0 +1,7 @@ +interface AuthLayoutProps { + children: React.ReactNode +} + +export default function AuthLayout({ children }: AuthLayoutProps) { + return
{children}
+} diff --git a/frontend/src/app/(auth)/login/page.tsx b/frontend/src/app/(auth)/login/page.tsx new file mode 100644 index 0000000..3ba8c90 --- /dev/null +++ b/frontend/src/app/(auth)/login/page.tsx @@ -0,0 +1,49 @@ +"use client" + +import Link from "next/link"; +import { cn } from "@/lib/utils"; +import { buttonVariants } from "@/components/ui/button"; +import { Icons } from "@/components/icons"; +import { ChevronLeftIcon } from "@radix-ui/react-icons"; +import { LoginForm } from "@/features/auth/components/login-form"; +import LegalTerms from "@/features/auth/components/legal-terms"; + +export default function LoginPage() { + return ( +
+ + + <> + Back + + + +
+
+ +

+ Welcome back +

+

+ Enter your email and password to continue +

+
+ +

+ + Don't have an account? Sign Up + +

+ + + +
+
+ ); +} diff --git a/frontend/src/app/(auth)/signup/page.tsx b/frontend/src/app/(auth)/signup/page.tsx new file mode 100644 index 0000000..17dc762 --- /dev/null +++ b/frontend/src/app/(auth)/signup/page.tsx @@ -0,0 +1,52 @@ +"use client" + +import Link from "next/link"; +import { cn } from "@/lib/utils"; +import { buttonVariants } from "@/components/ui/button"; +import { Icons } from "@/components/icons"; +import { ChevronLeftIcon } from "@radix-ui/react-icons"; +import { SignUpForm } from "@/features/auth/components/sign-up-form"; +import LegalTerms from "@/features/auth/components/legal-terms"; + +export default function SignUpPage() { + + return ( +
+ + + <> + Back + + + +
+
+ +

+ Create an account +

+

+ Enter your name, email and password to signup +

+
+ + + +

+ + Already have an account? Sign In + +

+ + + +
+
+ ); +} diff --git a/frontend/src/app/(dashboard)/home/page.tsx b/frontend/src/app/(dashboard)/home/page.tsx new file mode 100644 index 0000000..7d45f29 --- /dev/null +++ b/frontend/src/app/(dashboard)/home/page.tsx @@ -0,0 +1,16 @@ +"use client"; + +import { useAtom } from "jotai"; +import { currentUserAtom } from "@/features/user/atoms/current-user-atom"; + +export default function Home() { + const [currentUser] = useAtom(currentUserAtom); + + return ( +
+
+ Hello {currentUser && currentUser.user.name}! +
+
+ ); +} diff --git a/frontend/src/app/(dashboard)/layout.tsx b/frontend/src/app/(dashboard)/layout.tsx new file mode 100644 index 0000000..dafe0ff --- /dev/null +++ b/frontend/src/app/(dashboard)/layout.tsx @@ -0,0 +1,21 @@ +"use client" + +import dynamic from "next/dynamic"; +import { UserProvider } from "@/features/user/user-provider"; + +const Shell = dynamic(() => import("./shell"), { + ssr: false, +}); + +export default function DashboardLayout({ children }: { + children: React.ReactNode +}) { + + return ( + + + {children} + + + ); +} diff --git a/frontend/src/app/(dashboard)/shell.tsx b/frontend/src/app/(dashboard)/shell.tsx new file mode 100644 index 0000000..e661e99 --- /dev/null +++ b/frontend/src/app/(dashboard)/shell.tsx @@ -0,0 +1,18 @@ +"use client" + +export default function Shell({ children }: { + children: React.ReactNode +}) { + + return ( +
+ +
+
+ {children} +
+
+ +
+ ); +} diff --git a/frontend/src/app/layout.tsx b/frontend/src/app/layout.tsx index 5dbc170..7ffd6e9 100644 --- a/frontend/src/app/layout.tsx +++ b/frontend/src/app/layout.tsx @@ -4,6 +4,7 @@ import { Inter } from 'next/font/google' import { cn } from "@/lib/utils"; import { ThemeProvider } from "@/components/providers/theme-provider"; import { Toaster } from "@/components/ui/toaster"; +import { TanstackProvider } from "@/components/providers/tanstack-provider"; const inter = Inter({ subsets: ['latin'] }) @@ -12,6 +13,7 @@ export const metadata: Metadata = { description: 'Generated by create next app', } + export default function RootLayout({ children, }: { @@ -22,8 +24,10 @@ export default function RootLayout({ - {children} - + + {children} + + diff --git a/frontend/src/app/page.tsx b/frontend/src/app/page.tsx index 619da76..07e3637 100644 --- a/frontend/src/app/page.tsx +++ b/frontend/src/app/page.tsx @@ -8,10 +8,8 @@ export default function Home() { Get started by editing  src/app/page.tsx

-
- +
-
diff --git a/frontend/src/components/icons.tsx b/frontend/src/components/icons.tsx new file mode 100644 index 0000000..b489aca --- /dev/null +++ b/frontend/src/components/icons.tsx @@ -0,0 +1,48 @@ +type IconProps = React.HTMLAttributes + +export const Icons = { + + logo: (props: IconProps) => ( + + + + + + ), + spinner: (props: IconProps) => ( + + + + ), +} diff --git a/frontend/src/components/providers/tanstack-provider.tsx b/frontend/src/components/providers/tanstack-provider.tsx new file mode 100644 index 0000000..fd6a426 --- /dev/null +++ b/frontend/src/components/providers/tanstack-provider.tsx @@ -0,0 +1,14 @@ +"use client" + +import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; +import React from "react"; + +const queryClient = new QueryClient(); + +export function TanstackProvider({ children }: React.PropsWithChildren) { + return ( + + {children} + + ); +} diff --git a/frontend/src/components/ui/input.tsx b/frontend/src/components/ui/input.tsx new file mode 100644 index 0000000..a92b8e0 --- /dev/null +++ b/frontend/src/components/ui/input.tsx @@ -0,0 +1,25 @@ +import * as React from "react" + +import { cn } from "@/lib/utils" + +export interface InputProps + extends React.InputHTMLAttributes {} + +const Input = React.forwardRef( + ({ className, type, ...props }, ref) => { + return ( + + ) + } +) +Input.displayName = "Input" + +export { Input } diff --git a/frontend/src/components/ui/label.tsx b/frontend/src/components/ui/label.tsx new file mode 100644 index 0000000..5341821 --- /dev/null +++ b/frontend/src/components/ui/label.tsx @@ -0,0 +1,26 @@ +"use client" + +import * as React from "react" +import * as LabelPrimitive from "@radix-ui/react-label" +import { cva, type VariantProps } from "class-variance-authority" + +import { cn } from "@/lib/utils" + +const labelVariants = cva( + "text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70" +) + +const Label = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef & + VariantProps +>(({ className, ...props }, ref) => ( + +)) +Label.displayName = LabelPrimitive.Root.displayName + +export { Label } diff --git a/frontend/src/features/auth/atoms/auth-tokens-atom.ts b/frontend/src/features/auth/atoms/auth-tokens-atom.ts new file mode 100644 index 0000000..4cd7d03 --- /dev/null +++ b/frontend/src/features/auth/atoms/auth-tokens-atom.ts @@ -0,0 +1,14 @@ +import Cookies from "js-cookie"; +import { createJSONStorage, atomWithStorage } from "jotai/utils"; +import { ITokens } from "@/features/auth/types/auth.types"; + + +const cookieStorage = createJSONStorage(() => { + return { + getItem: () => Cookies.get("authTokens"), + setItem: (key, value) => Cookies.set(key, value), + removeItem: (key) => Cookies.remove(key), + }; +}); + +export const authTokensAtom = atomWithStorage("authTokens", null, cookieStorage); diff --git a/frontend/src/features/auth/components/legal-terms.tsx b/frontend/src/features/auth/components/legal-terms.tsx new file mode 100644 index 0000000..9bb0db5 --- /dev/null +++ b/frontend/src/features/auth/components/legal-terms.tsx @@ -0,0 +1,12 @@ +import Link from "next/link"; + +export default function LegalTerms(){ + return ( +

+ By clicking continue, you agree to our{" "} + + Terms of Service{" "} and{" "} + Privacy Policy. +

+ ) +} diff --git a/frontend/src/features/auth/components/login-form.tsx b/frontend/src/features/auth/components/login-form.tsx new file mode 100644 index 0000000..ed8f9be --- /dev/null +++ b/frontend/src/features/auth/components/login-form.tsx @@ -0,0 +1,92 @@ +"use client"; + +import * as React from "react"; +import * as z from "zod"; + +import { cn } from "@/lib/utils"; +import { Button, buttonVariants } from "@/components/ui/button"; +import { Icons } from "@/components/icons"; +import { useForm } from "react-hook-form"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { Label } from "@/components/ui/label"; +import { Input } from "@/components/ui/input"; +import useAuth from "@/features/auth/hooks/use-auth"; +import { ILogin } from "@/features/auth/types/auth.types"; + +const formSchema = z.object({ + email: z.string({ required_error: "email is required" }).email({ message: "Invalid email address" }), + password: z.string({ required_error: "password is required" }), +}); + +interface UserAuthFormProps extends React.HTMLAttributes { +} + +export function LoginForm({ className, ...props }: UserAuthFormProps) { + const { register, handleSubmit, formState: { errors } } + = useForm({ resolver: zodResolver(formSchema) }); + + const { signIn, isLoading } = useAuth(); + + async function onSubmit(data: ILogin) { + await signIn(data); + } + + return ( + <> +
+
+
+ +
+ + + {errors?.email && ( +

+ {errors.email.message} +

+ )} +
+ +
+ + + {errors?.password && ( +

+ {errors.password.message} +

+ )} +
+ + +
+
+ +
+ + ); +} diff --git a/frontend/src/features/auth/components/sign-up-form.tsx b/frontend/src/features/auth/components/sign-up-form.tsx new file mode 100644 index 0000000..6583ce3 --- /dev/null +++ b/frontend/src/features/auth/components/sign-up-form.tsx @@ -0,0 +1,92 @@ +"use client"; + +import * as React from "react"; +import * as z from "zod"; + +import { cn } from "@/lib/utils"; +import { Button, buttonVariants } from "@/components/ui/button"; +import { Icons } from "@/components/icons"; +import { useForm } from "react-hook-form"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { Label } from "@/components/ui/label"; +import { Input } from "@/components/ui/input"; +import useAuth from "@/features/auth/hooks/use-auth"; +import { IRegister } from "@/features/auth/types/auth.types"; + +const formSchema = z.object({ + email: z.string({ required_error: "email is required" }).email({ message: "Invalid email address" }), + password: z.string({ required_error: "password is required" }), +}); + +interface UserAuthFormProps extends React.HTMLAttributes { +} + +export function SignUpForm({ className, ...props }: UserAuthFormProps) { + const { register, handleSubmit, formState: { errors } } + = useForm({ resolver: zodResolver(formSchema) }); + + const { signUp, isLoading } = useAuth(); + + async function onSubmit(data: IRegister) { + await signUp(data); + } + + return ( + <> +
+
+
+ +
+ + + {errors?.email && ( +

+ {errors.email.message} +

+ )} +
+ +
+ + + {errors?.password && ( +

+ {errors.password.message} +

+ )} +
+ + +
+
+ +
+ + ); +} diff --git a/frontend/src/features/auth/hooks/use-auth.ts b/frontend/src/features/auth/hooks/use-auth.ts new file mode 100644 index 0000000..f092fc5 --- /dev/null +++ b/frontend/src/features/auth/hooks/use-auth.ts @@ -0,0 +1,65 @@ +import { useState } from "react"; +import { toast } from "@/components/ui/use-toast"; +import { login, register } from "@/features/auth/services/auth-service"; +import { useRouter } from "next/navigation"; +import { useAtom } from "jotai"; +import { authTokensAtom } from "@/features/auth/atoms/auth-tokens-atom"; +import { currentUserAtom } from "@/features/user/atoms/current-user-atom"; +import { ILogin, IRegister } from "@/features/auth/types/auth.types"; +import { RESET } from "jotai/vanilla/utils/constants"; + +export default function useAuth() { + const [isLoading, setIsLoading] = useState(false); + const router = useRouter(); + + const [, setCurrentUser] = useAtom(currentUserAtom); + const [authToken, setAuthToken] = useAtom(authTokensAtom); + + const handleSignIn = async (data: ILogin) => { + setIsLoading(true); + + try { + const res = await login(data); + setIsLoading(false); + await setAuthToken(res.tokens); + + router.push("/home"); + } catch (err) { + setIsLoading(false); + toast({ + description: err.response?.data.message, + variant: "destructive", + }); + } + }; + + const handleSignUp = async (data: IRegister) => { + setIsLoading(true); + + try { + const res = await register(data); + setIsLoading(false); + + await setAuthToken(res.tokens); + + router.push("/home"); + } catch (err) { + setIsLoading(false); + toast({ + description: err.response?.data.message, + variant: "destructive", + }); + } + }; + + const hasTokens = () => { + return !!authToken; + }; + + const handleLogout = async () => { + await setAuthToken(RESET); + setCurrentUser(''); + } + + return { signIn: handleSignIn, signUp: handleSignUp, isLoading, hasTokens }; +} diff --git a/frontend/src/features/auth/services/auth-service.ts b/frontend/src/features/auth/services/auth-service.ts new file mode 100644 index 0000000..5ded352 --- /dev/null +++ b/frontend/src/features/auth/services/auth-service.ts @@ -0,0 +1,12 @@ +import api from "@/lib/api-client"; +import { ILogin, IRegister, ITokenResponse, ITokens } from "@/features/auth/types/auth.types"; + +export async function login(data: ILogin): Promise{ + const req = await api.post("/auth/login", data); + return req.data as ITokenResponse; +} + +export async function register(data: IRegister): Promise{ + const req = await api.post("/auth/register", data); + return req.data as ITokenResponse; +} diff --git a/frontend/src/features/auth/types/auth.types.ts b/frontend/src/features/auth/types/auth.types.ts new file mode 100644 index 0000000..cc8e01b --- /dev/null +++ b/frontend/src/features/auth/types/auth.types.ts @@ -0,0 +1,18 @@ +export interface ILogin { + email: string, + password: string +} + +export interface IRegister { + email: string, + password: string +} + +export interface ITokens { + accessToken: string, + refreshToken: string +} + +export interface ITokenResponse { + tokens: ITokens +} diff --git a/frontend/src/features/user/atoms/current-user-atom.ts b/frontend/src/features/user/atoms/current-user-atom.ts new file mode 100644 index 0000000..bc606e9 --- /dev/null +++ b/frontend/src/features/user/atoms/current-user-atom.ts @@ -0,0 +1,4 @@ +import { atom } from "jotai"; +import { ICurrentUserResponse } from "@/features/user/types/user.types"; + +export const currentUserAtom = atom(null); diff --git a/frontend/src/features/user/hooks/use-current-user.ts b/frontend/src/features/user/hooks/use-current-user.ts new file mode 100644 index 0000000..59966c7 --- /dev/null +++ b/frontend/src/features/user/hooks/use-current-user.ts @@ -0,0 +1,13 @@ +import { useQuery, UseQueryResult } from "@tanstack/react-query"; +import { getUserInfo } from "@/features/user/services/user-service"; +import { ICurrentUserResponse } from "@/features/user/types/user.types"; + +export default function useCurrentUser(): UseQueryResult { + return useQuery({ + queryKey: ["currentUser"], + queryFn: async () => { + return await getUserInfo(); + }, + }); + +} diff --git a/frontend/src/features/user/services/user-service.ts b/frontend/src/features/user/services/user-service.ts new file mode 100644 index 0000000..8c824d8 --- /dev/null +++ b/frontend/src/features/user/services/user-service.ts @@ -0,0 +1,12 @@ +import api from "@/lib/api-client"; +import { ICurrentUserResponse, IUser } from "@/features/user/types/user.types"; + +export async function getMe(): Promise{ + const req = await api.get("/user/me"); + return req.data as IUser; +} + +export async function getUserInfo(): Promise{ + const req = await api.get("/user/info"); + return req.data as ICurrentUserResponse; +} diff --git a/frontend/src/features/user/types/user.types.ts b/frontend/src/features/user/types/user.types.ts new file mode 100644 index 0000000..765c437 --- /dev/null +++ b/frontend/src/features/user/types/user.types.ts @@ -0,0 +1,20 @@ +import { IWorkspace } from "@/features/workspace/types/workspace.types"; + +export interface IUser { + id: string; + name: string; + email: string; + emailVerifiedAt: Date; + avatarUrl: string; + timezone: string; + settings: any; + lastLoginAt: string; + lastLoginIp: string; + createdAt: Date; + updatedAt: Date; +} + +export interface ICurrentUserResponse { + user: IUser, + workspace: IWorkspace +} diff --git a/frontend/src/features/user/user-provider.tsx b/frontend/src/features/user/user-provider.tsx new file mode 100644 index 0000000..9e6a0dc --- /dev/null +++ b/frontend/src/features/user/user-provider.tsx @@ -0,0 +1,25 @@ +"use client" + +import { useAtom } from "jotai"; +import { currentUserAtom } from "@/features/user/atoms/current-user-atom"; +import { useEffect } from "react"; +import useCurrentUser from "@/features/user/hooks/use-current-user"; + +export function UserProvider({ children }: React.PropsWithChildren) { + const [, setCurrentUser] = useAtom(currentUserAtom); + const { data, isLoading, error } = useCurrentUser(); + + useEffect(() => { + if (data && data.user){ + setCurrentUser(data); + } + }, [data, isLoading, setCurrentUser]); + + if (isLoading) return <>; + + if (error){ + return <>an error occurred + } + + return <>{children} +} diff --git a/frontend/src/features/workspace/types/workspace.types.ts b/frontend/src/features/workspace/types/workspace.types.ts new file mode 100644 index 0000000..c8c9ebb --- /dev/null +++ b/frontend/src/features/workspace/types/workspace.types.ts @@ -0,0 +1,14 @@ +export interface IWorkspace { + id: string; + name: string; + description: string; + logo: string; + hostname: string; + customDomain: string; + enableInvite: boolean; + inviteCode: string; + settings: any; + creatorId: string; + createdAt: Date; + updatedAt: Date; +} diff --git a/frontend/src/lib/api-client.ts b/frontend/src/lib/api-client.ts new file mode 100644 index 0000000..e7e4102 --- /dev/null +++ b/frontend/src/lib/api-client.ts @@ -0,0 +1,53 @@ +import axios, { AxiosInstance } from "axios"; +import Cookies from "js-cookie"; +import Routes from "@/lib/routes"; + +const api: AxiosInstance = axios.create({ + baseURL: process.env.NEXT_PUBLIC_BACKEND_API_URL, +}); + +api.interceptors.request.use(config => { + const tokenData = Cookies.get("authTokens"); + const accessToken = tokenData && JSON.parse(tokenData)?.accessToken; + + if (accessToken) { + config.headers.Authorization = `Bearer ${accessToken}`; + } + return config; + }, + error => { + return Promise.reject(error); + }, +); + +api.interceptors.response.use( + response => { + return response.data; + }, + error => { + if (error.response) { + switch (error.response.status) { + case 401: + // Handle unauthorized error + if (window.location.pathname != Routes.AUTH.LOGIN){ + window.location.href = Routes.AUTH.LOGIN; + } + break; + case 403: + // Handle forbidden error + break; + case 404: + // Handle not found error + break; + case 500: + // Handle internal server error + break; + default: + break; + } + } + return Promise.reject(error); + }, +); + +export default api; diff --git a/frontend/src/lib/jotai-helper.ts b/frontend/src/lib/jotai-helper.ts new file mode 100644 index 0000000..06d2525 --- /dev/null +++ b/frontend/src/lib/jotai-helper.ts @@ -0,0 +1,17 @@ +import { atom } from "jotai"; + +export function atomWithWebStorage(key: string, initialValue: Value, storage = localStorage) { + const storedValue = localStorage.getItem(key); + const isString = typeof initialValue === "string"; + + const storageValue = storedValue ? isString ? storedValue : storedValue === "true" : undefined; + + const baseAtom = atom(storageValue ?? initialValue); + return atom( + get => get(baseAtom) as Value, + (_get, set, nextValue: Value) => { + set(baseAtom, nextValue); + storage.setItem(key, nextValue!.toString()); + }, + ); +} diff --git a/frontend/src/lib/routes.ts b/frontend/src/lib/routes.ts new file mode 100644 index 0000000..c16f53b --- /dev/null +++ b/frontend/src/lib/routes.ts @@ -0,0 +1,9 @@ +const ROUTES = { + HOME: '/home', + AUTH: { + LOGIN: '/login', + SIGNUP: '/signup', + }, +}; + +export default ROUTES; diff --git a/frontend/tsconfig.json b/frontend/tsconfig.json index eb0b41d..44eea8a 100644 --- a/frontend/tsconfig.json +++ b/frontend/tsconfig.json @@ -4,7 +4,7 @@ "lib": ["dom", "dom.iterable", "esnext"], "allowJs": true, "skipLibCheck": true, - "strict": true, + "strict": false, "forceConsistentCasingInFileNames": true, "noEmit": true, "esModuleInterop": true,