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 (
+ <>
+
+ >
+ );
+}
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 (
+ <>
+
+ >
+ );
+}
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,