diff --git a/client/src/assets/wave.gif b/client/src/assets/wave.gif new file mode 100644 index 0000000..acc50dd Binary files /dev/null and b/client/src/assets/wave.gif differ diff --git a/client/src/components/switch-auth.tsx b/client/src/components/switch-auth.tsx index 0e2f8a2..4531b1f 100644 --- a/client/src/components/switch-auth.tsx +++ b/client/src/components/switch-auth.tsx @@ -18,7 +18,11 @@ const SwitchAuth = ({ label, tag, href, disabled }: SwitchAuthProps) => { to={href} replace className={`font-bold underline-offset-4 hover:underline transition-all hover:drop-shadow-secondary-glow - ${disabled} ? "text-secondary/50 pointer-events-none" : "text-secondary" + ${ + disabled + ? "text-secondary/50 pointer-events-none" + : "text-secondary" + } `} > {tag} diff --git a/client/src/components/auth-title.tsx b/client/src/components/title.tsx similarity index 86% rename from client/src/components/auth-title.tsx rename to client/src/components/title.tsx index b11d078..0590b99 100644 --- a/client/src/components/auth-title.tsx +++ b/client/src/components/title.tsx @@ -1,9 +1,9 @@ -interface AuthTitleProps { +interface TitleProps { title: string; body?: string; } -const AuthTitle = ({ title, body }: AuthTitleProps) => { +const Title = ({ title, body }: TitleProps) => { return (

{ ); }; -export { AuthTitle }; +export { Title }; diff --git a/client/src/constants/assets.ts b/client/src/constants/assets.ts index 1581dc8..e860b58 100644 --- a/client/src/constants/assets.ts +++ b/client/src/constants/assets.ts @@ -1,3 +1,5 @@ +import wave from "@/assets/wave.gif"; import key from "@/assets/key.svg"; export const KEY = key; +export const WAVE = wave; diff --git a/client/src/constants/collections.ts b/client/src/constants/collections.ts index 8acdbe3..27a6ab2 100644 --- a/client/src/constants/collections.ts +++ b/client/src/constants/collections.ts @@ -85,7 +85,7 @@ export const SIGNIN_INPUTS = [ export const RESET_PASSWORD_INPUTS = [ { - name: "oldPassword", + name: "old-password", label: "Old Password", tooltip: "Enter your current password", placeholder: "e.g. m#P52s@ap$V", @@ -96,7 +96,7 @@ export const RESET_PASSWORD_INPUTS = [ maxLength: 128, }, { - name: "newPassword", + name: "new-password", label: "New Password", tooltip: "Create a password with at least 8 characters, including uppercase, lowercase, numbers, and special characters for security", diff --git a/client/src/lib/DTO/reset-dto.ts b/client/src/lib/DTO/reset-dto.ts index 1edc94e..2f32021 100644 --- a/client/src/lib/DTO/reset-dto.ts +++ b/client/src/lib/DTO/reset-dto.ts @@ -1,5 +1,5 @@ export type ResetDTO = { token: string; - oldpassword: string; - newpassword: string; + old_password: string; + new_password: string; }; diff --git a/client/src/lib/services/auth-service.ts b/client/src/lib/services/auth-service.ts index 1598787..8adcfba 100644 --- a/client/src/lib/services/auth-service.ts +++ b/client/src/lib/services/auth-service.ts @@ -4,7 +4,7 @@ import { UserResponse } from "@/lib/classes/user-response-class"; import { SignUpDTO } from "@/lib/DTO/sign-up-dto"; import { SignInDTO } from "@/lib/DTO/sign-in-dto"; import { AppRoutes } from "@/constants/routes"; -import { ResetDTO } from "../DTO/reset-dto"; +import { ResetDTO } from "@/lib/DTO/reset-dto"; const baseURL = import.meta.env.VITE_ENV === "development" @@ -28,9 +28,8 @@ export const AuthService = { }); } - return new UserResponse({ + return new GenericResponse({ message: data.message, - user: data.user, }); }, signIn: async (signIn: SignInDTO) => { @@ -52,8 +51,9 @@ export const AuthService = { }); } - return new GenericResponse({ + return new UserResponse({ message: data.message, + user: data.user, }); }, signOut: async () => { @@ -172,8 +172,8 @@ export const AuthService = { "Content-Type": "application/json", }, body: JSON.stringify({ - old_password: reset.oldpassword, - new_password: reset.newpassword, + old_password: reset.old_password, + new_password: reset.new_password, }), } ); diff --git a/client/src/lib/stores/user-store.ts b/client/src/lib/stores/user-store.ts new file mode 100644 index 0000000..bad3f9b --- /dev/null +++ b/client/src/lib/stores/user-store.ts @@ -0,0 +1,24 @@ +import { createJSONStorage, devtools, persist } from "zustand/middleware"; +import { create } from "zustand"; + +import { UserDTO } from "@/lib/DTO/user-dto"; + +interface UserStoreState { + user?: UserDTO; + setUser: (user?: UserDTO) => void; +} + +export const useUserStore = create()( + devtools( + persist( + (set) => ({ + user: undefined, + setUser: (user?: UserDTO) => set({ user }), + }), + { + name: "go-react-user", + storage: createJSONStorage(() => sessionStorage), + } + ) + ) +); diff --git a/client/src/lib/utils/date.ts b/client/src/lib/utils/date.ts new file mode 100644 index 0000000..971cb0c --- /dev/null +++ b/client/src/lib/utils/date.ts @@ -0,0 +1,16 @@ +export const formatDate = (dateString: string) => { + const date = new Date(dateString); + + if (isNaN(date.getTime())) { + return "Invalid Date"; + } + + return date.toLocaleString("en-US", { + year: "numeric", + month: "short", + day: "numeric", + hour: "2-digit", + minute: "2-digit", + hour12: true, + }); +}; diff --git a/client/src/pages/forgot-password/page.tsx b/client/src/pages/forgot-password/page.tsx index ab82818..d726969 100644 --- a/client/src/pages/forgot-password/page.tsx +++ b/client/src/pages/forgot-password/page.tsx @@ -1,4 +1,4 @@ -import { AuthTitle } from "@/components/auth-title"; +import { Title } from "@/components/title"; import { ForgotPasswordForm } from "./_components/forgot-password-form"; import { ForgotPasswordBack } from "./_components/forgot-password-back"; @@ -6,7 +6,7 @@ import { ForgotPasswordBack } from "./_components/forgot-password-back"; const ForgotPasswordPage = () => { return (
- diff --git a/client/src/pages/reset-password/_components/reset-password-form.tsx b/client/src/pages/reset-password/_components/reset-password-form.tsx index 2e121e2..2e33763 100644 --- a/client/src/pages/reset-password/_components/reset-password-form.tsx +++ b/client/src/pages/reset-password/_components/reset-password-form.tsx @@ -41,11 +41,13 @@ const ResetPasswordForm = () => { const formData = new FormData(e.currentTarget); - const resetPasswordData = Object.fromEntries( - formData.entries() - ) as ResetDTO; + const resetPasswordData: ResetDTO = { + token: token, + old_password: formData.get("old-password") as string, + new_password: formData.get("new-password") as string, + }; - resetPasswordData.token = token; + console.log(resetPasswordData); setGlobalLoading(true); diff --git a/client/src/pages/reset-password/page.tsx b/client/src/pages/reset-password/page.tsx index 621a711..72cfd31 100644 --- a/client/src/pages/reset-password/page.tsx +++ b/client/src/pages/reset-password/page.tsx @@ -1,4 +1,4 @@ -import { AuthTitle } from "@/components/auth-title"; +import { Title } from "@/components/title"; import { ResetPasswordForm } from "./_components/reset-password-form"; import { ResetPasswordBack } from "./_components/reset-password-back"; @@ -6,7 +6,7 @@ import { ResetPasswordBack } from "./_components/reset-password-back"; const ResetPasswordPage = () => { return (
- + <ResetPasswordForm /> diff --git a/client/src/pages/root/_components/content/index.tsx b/client/src/pages/root/_components/content/index.tsx new file mode 100644 index 0000000..821ffaa --- /dev/null +++ b/client/src/pages/root/_components/content/index.tsx @@ -0,0 +1,36 @@ +import { useUserStore } from "@/lib/stores/user-store"; +import { formatDate } from "@/lib/utils/date"; + +import { WelcomeUser } from "./welcome-user"; +import { InfoPair } from "./info-pair"; + +const Content = () => { + const { user } = useUserStore(); + + return ( + <section className="h-[calc(100dvh_-_56px)] flex-center max-w-screen-lg mx-auto px-4 lg:px-0"> + <div className="w-fit space-y-6"> + <WelcomeUser name={user?.username} /> + + <div className="w-full rounded-md border border-secondary/50 bg-secondary/15 drop-shadow-2xl"> + <InfoPair label="Email" value={user?.email_address} /> + <InfoPair + label="Last Visit" + value={formatDate(new Date(user!.last_signed_in).toDateString())} + /> + <InfoPair + label="Joined" + value={new Date(user!.created_at).toLocaleDateString("en-US", { + year: "numeric", + month: "long", + day: "numeric", + })} + /> + <InfoPair label="Verified" value={user!.is_verified ? "Yes" : "No"} /> + </div> + </div> + </section> + ); +}; + +export { Content }; diff --git a/client/src/pages/root/_components/content/info-pair.tsx b/client/src/pages/root/_components/content/info-pair.tsx new file mode 100644 index 0000000..9917178 --- /dev/null +++ b/client/src/pages/root/_components/content/info-pair.tsx @@ -0,0 +1,14 @@ +interface InfoPairProps { + label: string; + value?: string; +} + +const InfoPair = ({ label, value }: InfoPairProps) => { + return ( + <p className="font-medium flex items-center gap-x-2 px-6 py-4 border-b border-secondary/50"> + {label}:<span className="font-semibold text-secondary">{value}</span> + </p> + ); +}; + +export { InfoPair }; diff --git a/client/src/pages/root/_components/content/welcome-user.tsx b/client/src/pages/root/_components/content/welcome-user.tsx new file mode 100644 index 0000000..c0ed048 --- /dev/null +++ b/client/src/pages/root/_components/content/welcome-user.tsx @@ -0,0 +1,29 @@ +import { WAVE } from "@/constants/assets"; + +interface WelcomeUserProps { + name?: string; +} + +const WelcomeUser = ({ name }: WelcomeUserProps) => { + return ( + <div className="w-full rounded-md border border-primary/50 bg-primary/15 drop-shadow-2xl"> + <h2 + className="text-lg lg:text-2xl font-extrabold flex-center uppercase + text-primary drop-shadow-primary-glow animate-pulse p-4 gap-x-4" + > + <span className="text-foreground flex-center gap-x-4 relative"> + Hi there{" "} + <img src={WAVE} alt="wave" className="size-8 relative -top-[3px]" /> + </span> + {name} + </h2> + <div className="w-full bg-primary/25 p-2.5 flex-center border-t border-primary/50"> + <p className="text-xs lg:text-base text-center lg:max-w-[324px] text-foreground/60 font-semibold"> + Welcome to Go + React Auth + </p> + </div> + </div> + ); +}; + +export { WelcomeUser }; diff --git a/client/src/pages/root/_components/footer/index.tsx b/client/src/pages/root/_components/footer/index.tsx new file mode 100644 index 0000000..212ab8f --- /dev/null +++ b/client/src/pages/root/_components/footer/index.tsx @@ -0,0 +1,11 @@ +const Footer = () => { + return ( + <footer className="bg-secondary/10 backdrop-blur-lg border-t border-secondary/25 p-4"> + <p className="max-w-screen-lg mx-auto text-center text-sm font-medium text-foreground/60"> + © {new Date().getUTCFullYear()} Fingertips. All rights reserved. + </p> + </footer> + ); +}; + +export { Footer }; diff --git a/client/src/pages/root/_components/header/sign-out-button.tsx b/client/src/pages/root/_components/header/sign-out-button.tsx index a2b8f14..7057928 100644 --- a/client/src/pages/root/_components/header/sign-out-button.tsx +++ b/client/src/pages/root/_components/header/sign-out-button.tsx @@ -4,18 +4,21 @@ import { toast } from "sonner"; import { GenericResponse } from "@/lib/classes/generic-response-class"; import { AuthService } from "@/lib/services/auth-service"; +import { useUserStore } from "@/lib/stores/user-store"; import { useAuthStore } from "@/lib/stores/auth-store"; import IconButton from "@/components/icon-button"; import { SIGNOUTKEY } from "@/constants/keys"; const SignOutButton = () => { const { setAuthorized } = useAuthStore(); + const { setUser } = useUserStore(); const { mutate, isPending } = useMutation({ mutationKey: [SIGNOUTKEY], mutationFn: AuthService.signOut, onSuccess: (res: GenericResponse) => { toast.success(res.message); + setUser(undefined); setAuthorized(false); }, onError: ({ message }) => toast.error(message), diff --git a/client/src/pages/root/page.tsx b/client/src/pages/root/page.tsx index f734d65..32a1c28 100644 --- a/client/src/pages/root/page.tsx +++ b/client/src/pages/root/page.tsx @@ -1,12 +1,13 @@ +import { Footer } from "./_components/footer"; import { Header } from "./_components/header"; +import { Content } from "./_components/content"; const RootPage = () => { return ( <> <Header /> - <section className="h-full flex-center"> - <h1 className="font-bold text-2xl">Root Page</h1> - </section> + <Content /> + <Footer /> </> ); }; diff --git a/client/src/pages/sign-in/_components/sign-in-form.tsx b/client/src/pages/sign-in/_components/sign-in-form.tsx index afdb1fa..4350b43 100644 --- a/client/src/pages/sign-in/_components/sign-in-form.tsx +++ b/client/src/pages/sign-in/_components/sign-in-form.tsx @@ -3,11 +3,12 @@ import { useMutation } from "@tanstack/react-query"; import { FormEvent } from "react"; import { toast } from "sonner"; -import { GenericResponse } from "@/lib/classes/generic-response-class"; import { ErrorResponse } from "@/lib/classes/error-response-class"; +import { UserResponse } from "@/lib/classes/user-response-class"; import { AuthService } from "@/lib/services/auth-service"; import { SIGNIN_INPUTS } from "@/constants/collections"; import { useAuthStore } from "@/lib/stores/auth-store"; +import { useUserStore } from "@/lib/stores/user-store"; import { SignInDTO } from "@/lib/DTO/sign-in-dto"; import { Button } from "@/components/text-button"; import { AppRoutes } from "@/constants/routes"; @@ -21,22 +22,26 @@ const SignInForm = () => { setAuthorized, setLoading: setGlobalLoading, } = useAuthStore(); + const { setUser } = useUserStore(); const { mutate, isPending } = useMutation({ mutationKey: [SIGNINKEY], mutationFn: AuthService.signIn, - onSuccess: (res: GenericResponse) => { + onSuccess: (res: UserResponse) => { toast.success(res.message); + setUser(res.user); setAuthorized(true); setGlobalLoading(false); }, onError: (error: ErrorResponse) => { if (error.status == 403) { toast.error("Please verify to sign in"); + setUser(undefined); setGlobalLoading(false); navigate(AppRoutes.VerifyEmail); } else { toast.error(error.message); + setUser(undefined); setAuthorized(false); setGlobalLoading(false); } diff --git a/client/src/pages/sign-in/page.tsx b/client/src/pages/sign-in/page.tsx index f8d4317..fd4dd84 100644 --- a/client/src/pages/sign-in/page.tsx +++ b/client/src/pages/sign-in/page.tsx @@ -1,4 +1,4 @@ -import { AuthTitle } from "@/components/auth-title"; +import { Title } from "@/components/title"; import { Or } from "@/components/or"; import { NoAccountYet } from "./_components/no-account-yet"; @@ -7,7 +7,7 @@ import { SignInForm } from "./_components/sign-in-form"; const SignInPage = () => { return ( <section className="px-4 lg:px-0 h-full flex-center flex-col gap-y-6 w-fit mx-auto"> - <AuthTitle title="Access Your Account" /> + <Title title="Access Your Account" /> <SignInForm /> diff --git a/client/src/pages/sign-up/_components/sign-up-form.tsx b/client/src/pages/sign-up/_components/sign-up-form.tsx index 2c296ca..7eb6056 100644 --- a/client/src/pages/sign-up/_components/sign-up-form.tsx +++ b/client/src/pages/sign-up/_components/sign-up-form.tsx @@ -3,6 +3,7 @@ import { useNavigate } from "react-router-dom"; import { FormEvent, useState } from "react"; import { toast } from "sonner"; +import { GenericResponse } from "@/lib/classes/generic-response-class"; import { ErrorResponse } from "@/lib/classes/error-response-class"; import { AuthService } from "@/lib/services/auth-service"; import { SIGNUP_INPUTS } from "@/constants/collections"; @@ -21,8 +22,8 @@ const SignUpForm = () => { const { mutate, isPending } = useMutation({ mutationKey: [SIGNUPKEY], mutationFn: AuthService.signUp, - onSuccess: () => { - toast.success("Registered successfully"); + onSuccess: (res: GenericResponse) => { + toast.success(res.message); setGlobalLoading(false); navigate(AppRoutes.SignIn); }, diff --git a/client/src/pages/sign-up/page.tsx b/client/src/pages/sign-up/page.tsx index 7aef38e..f9a3817 100644 --- a/client/src/pages/sign-up/page.tsx +++ b/client/src/pages/sign-up/page.tsx @@ -1,4 +1,4 @@ -import { AuthTitle } from "@/components/auth-title"; +import { Title } from "@/components/title"; import { Or } from "@/components/or"; import { AlreadyHaveAccount } from "./_components/already-have-account"; @@ -7,7 +7,7 @@ import { SignUpForm } from "./_components/sign-up-form"; const SignUpPage = () => { return ( <section className="px-4 lg:px-0 h-full flex-center flex-col gap-y-6 w-fit mx-auto"> - <AuthTitle title="Create Account" /> + <Title title="Create Account" /> <SignUpForm /> diff --git a/client/src/pages/verify-email/page.tsx b/client/src/pages/verify-email/page.tsx index 57b1f04..1414231 100644 --- a/client/src/pages/verify-email/page.tsx +++ b/client/src/pages/verify-email/page.tsx @@ -1,4 +1,4 @@ -import { AuthTitle } from "@/components/auth-title"; +import { Title } from "@/components/title"; import { VerifyEmailForm } from "./_components/verify-email-form"; import { ResendCode } from "./_components/resend-code"; @@ -6,7 +6,7 @@ import { ResendCode } from "./_components/resend-code"; const VerifyEmailPage = () => { return ( <section className="px-4 lg:px-0 h-full flex-center flex-col gap-y-6 w-fit mx-auto"> - <AuthTitle title="Verify Email" /> + <Title title="Verify Email" /> <VerifyEmailForm /> diff --git a/controllers/auth_controller.go b/controllers/auth_controller.go index 15cbd5c..5422fd6 100644 --- a/controllers/auth_controller.go +++ b/controllers/auth_controller.go @@ -53,12 +53,7 @@ func SignUp(c fiber.Ctx) error { return c.Status(fiber.StatusInternalServerError).JSON(dto.ErrorDTO{Error: err.Error()}) } - return c.Status(fiber.StatusCreated).JSON( - dto.UserDTO{ - Message: "User created successfully", - User: user, - }, - ) + return c.Status(fiber.StatusCreated).JSON(dto.GenericDTO{Message: "User created successfully"}) } func SignIn(c fiber.Ctx) error { @@ -98,7 +93,10 @@ func SignIn(c fiber.Ctx) error { return c.Status(fiber.StatusInternalServerError).JSON(dto.ErrorDTO{Error: "Unable to save sign in credentials"}) } - return c.Status(fiber.StatusOK).JSON(dto.GenericDTO{Message: "Sign in successful"}) + return c.Status(fiber.StatusOK).JSON(dto.UserDTO{ + Message: "Sign in successful", + User: user, + }) } else { return c.Status(fiber.StatusForbidden).JSON(dto.ErrorDTO{Error: "User is not verified"}) }