refactor(v4.0.0-alpha): beginning of a new era

This commit is contained in:
Amruth Pillai
2023-11-05 12:31:42 +01:00
parent 0ba6a444e2
commit 22933bd412
505 changed files with 81829 additions and 0 deletions

View File

@ -0,0 +1,20 @@
import { GithubLogo, GoogleLogo } from "@phosphor-icons/react";
import { Button } from "@reactive-resume/ui";
export const SocialAuth = () => (
<div className="grid grid-cols-2 gap-4">
<Button asChild size="lg" className="w-full !bg-[#222] !text-white hover:!bg-[#222]/80">
<a href="/api/auth/github">
<GoogleLogo className="mr-3 h-4 w-4" />
GitHub
</a>
</Button>
<Button asChild size="lg" className="w-full !bg-[#4285F4] !text-white hover:!bg-[#4285F4]/80">
<a href="/api/auth/google">
<GithubLogo className="mr-3 h-4 w-4" />
Google
</a>
</Button>
</div>
);

View File

@ -0,0 +1,102 @@
import { zodResolver } from "@hookform/resolvers/zod";
import { Warning } from "@phosphor-icons/react";
import { twoFactorBackupSchema } from "@reactive-resume/dto";
import { usePasswordToggle } from "@reactive-resume/hooks";
import {
Button,
Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
Input,
} from "@reactive-resume/ui";
import { AxiosError } from "axios";
import { useRef } from "react";
import { useForm } from "react-hook-form";
import { useNavigate } from "react-router-dom";
import { z } from "zod";
import { toast } from "@/client/hooks/use-toast";
import { useBackupOtp } from "@/client/services/auth";
type FormValues = z.infer<typeof twoFactorBackupSchema>;
export const BackupOtpPage = () => {
const navigate = useNavigate();
const { backupOtp, loading } = useBackupOtp();
const formRef = useRef<HTMLFormElement>(null);
usePasswordToggle(formRef);
const form = useForm<FormValues>({
resolver: zodResolver(twoFactorBackupSchema),
defaultValues: { code: "" },
});
const onSubmit = async (data: FormValues) => {
try {
await backupOtp(data);
navigate("/dashboard");
} catch (error) {
form.reset();
if (error instanceof AxiosError) {
const message = error.response?.data.message || error.message;
toast({
variant: "error",
icon: <Warning size={16} weight="bold" />,
title: "An error occurred while trying to sign in",
description: message,
});
}
}
};
return (
<div className="space-y-8">
<div className="space-y-1.5">
<h2 className="text-2xl font-semibold tracking-tight">Use your backup code</h2>
<h6 className="leading-relaxed opacity-60">
Enter one of the 10 backup codes you saved when you enabled two-factor authentication.
</h6>
</div>
<div>
<Form {...form}>
<form
ref={formRef}
className="flex flex-col gap-y-4"
onSubmit={form.handleSubmit(onSubmit)}
>
<FormField
name="code"
control={form.control}
render={({ field }) => (
<FormItem>
<FormLabel>Backup Code</FormLabel>
<FormControl>
<Input
pattern="[a-z0-9]{10}"
placeholder="a1b2c3d4e5"
title="may contain lowercase letters or numbers, and must be exactly 10 characters."
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<Button type="submit" disabled={loading} className="mt-4 w-full">
Sign in
</Button>
</form>
</Form>
</div>
</div>
);
};

View File

@ -0,0 +1,105 @@
import { zodResolver } from "@hookform/resolvers/zod";
import { Warning } from "@phosphor-icons/react";
import { forgotPasswordSchema } from "@reactive-resume/dto";
import {
Alert,
AlertDescription,
Button,
Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
Input,
} from "@reactive-resume/ui";
import { AxiosError } from "axios";
import { useState } from "react";
import { useForm } from "react-hook-form";
import { z } from "zod";
import { toast } from "@/client/hooks/use-toast";
import { useForgotPassword } from "@/client/services/auth";
type FormValues = z.infer<typeof forgotPasswordSchema>;
export const ForgotPasswordPage = () => {
const [submitted, setSubmitted] = useState<boolean>(false);
const { forgotPassword, loading } = useForgotPassword();
const form = useForm<FormValues>({
resolver: zodResolver(forgotPasswordSchema),
defaultValues: { email: "" },
});
const onSubmit = async (data: FormValues) => {
try {
await forgotPassword(data);
setSubmitted(true);
form.reset();
} catch (error) {
if (error instanceof AxiosError) {
const message = error.response?.data.message || error.message;
toast({
variant: "error",
icon: <Warning size={16} weight="bold" />,
title: "An error occurred while trying to send your password recovery email",
description: message,
});
}
}
};
if (submitted) {
return (
<div className="space-y-8">
<div className="space-y-4">
<h2 className="text-2xl font-semibold tracking-tight">You've got mail!</h2>
<Alert variant="success">
<AlertDescription className="pt-0">
A password reset link should have been sent to your inbox, if an account existed with
the email you provided.
</AlertDescription>
</Alert>
</div>
</div>
);
}
return (
<div className="space-y-8">
<div className="space-y-1.5">
<h2 className="text-2xl font-semibold tracking-tight">Forgot your password?</h2>
<h6 className="leading-relaxed opacity-75">
Enter your email address and we will send you a link to reset your password if the account
exists.
</h6>
</div>
<div>
<Form {...form}>
<form className="flex flex-col gap-y-4" onSubmit={form.handleSubmit(onSubmit)}>
<FormField
name="email"
control={form.control}
render={({ field }) => (
<FormItem>
<FormLabel>Email</FormLabel>
<FormControl>
<Input placeholder="john.doe@example.com" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<Button type="submit" disabled={loading} className="mt-4 w-full">
Send Email
</Button>
</form>
</Form>
</div>
</div>
);
};

View File

@ -0,0 +1,58 @@
import { useMemo } from "react";
import { Link, matchRoutes, Outlet, useLocation } from "react-router-dom";
import { Logo } from "@/client/components/logo";
import { SocialAuth } from "./_components/social-auth";
const authRoutes = [{ path: "/auth/login" }, { path: "/auth/register" }];
export const AuthLayout = () => {
const location = useLocation();
const isAuthRoute = useMemo(() => matchRoutes(authRoutes, location) !== null, [location]);
return (
<div className="flex h-screen w-screen">
<div className="flex w-full flex-col justify-center gap-y-8 px-12 sm:mx-auto sm:basis-[420px] sm:px-0 lg:basis-[480px] lg:px-12">
<Link to="/" className="h-24 w-24">
<Logo className="-ml-3" size={96} />
</Link>
<Outlet />
{isAuthRoute && (
<>
<div className="flex items-center gap-x-4">
<hr className="flex-1" />
<span className="text-xs font-medium">or continue with</span>
<hr className="flex-1" />
</div>
<SocialAuth />
</>
)}
</div>
<div className="relative hidden lg:block lg:flex-1">
<img
width={1920}
height={1080}
alt="Open books on a table"
className="h-screen w-full object-cover object-center"
src="/backgrounds/patrick-tomasso-Oaqk7qqNh_c-unsplash.jpg"
/>
<div className="absolute bottom-5 right-5 z-10 bg-primary/30 px-4 py-2 text-xs font-medium text-primary-foreground backdrop-blur-sm">
<a
href="https://unsplash.com/photos/Oaqk7qqNh_c"
target="_blank"
rel="noopener noreferrer nofollow"
>
Photograph by Patrick Tomasso
</a>
</div>
</div>
</div>
);
};

View File

@ -0,0 +1,126 @@
import { zodResolver } from "@hookform/resolvers/zod";
import { ArrowRight, Warning } from "@phosphor-icons/react";
import { loginSchema } from "@reactive-resume/dto";
import { usePasswordToggle } from "@reactive-resume/hooks";
import {
Button,
Form,
FormControl,
FormDescription,
FormField,
FormItem,
FormLabel,
FormMessage,
Input,
} from "@reactive-resume/ui";
import { AxiosError } from "axios";
import { useRef } from "react";
import { useForm } from "react-hook-form";
import { Link } from "react-router-dom";
import { z } from "zod";
import { useToast } from "@/client/hooks/use-toast";
import { useLogin } from "@/client/services/auth";
type FormValues = z.infer<typeof loginSchema>;
export const LoginPage = () => {
const { toast } = useToast();
const { login, loading } = useLogin();
const formRef = useRef<HTMLFormElement>(null);
usePasswordToggle(formRef);
const form = useForm<FormValues>({
resolver: zodResolver(loginSchema),
defaultValues: { identifier: "", password: "" },
});
const onSubmit = async (data: FormValues) => {
try {
await login(data);
} catch (error) {
form.reset();
if (error instanceof AxiosError) {
const message = error.response?.data.message || error.message;
toast({
variant: "error",
icon: <Warning size={16} weight="bold" />,
title: "An error occurred while trying to sign in",
description: message,
});
}
}
};
return (
<div className="space-y-8">
<div className="space-y-1.5">
<h2 className="text-2xl font-semibold tracking-tight">Sign in to your account</h2>
<h6>
<span className="opacity-75">Don't have an account?</span>
<Button asChild variant="link" className="px-1.5">
<Link to="/auth/register">
Create one now <ArrowRight className="ml-1" />
</Link>
</Button>
</h6>
</div>
<div>
<Form {...form}>
<form
ref={formRef}
className="flex flex-col gap-y-4"
onSubmit={form.handleSubmit(onSubmit)}
>
<FormField
name="identifier"
control={form.control}
render={({ field }) => (
<FormItem>
<FormLabel>Email</FormLabel>
<FormControl>
<Input placeholder="john.doe@example.com" {...field} />
</FormControl>
<FormDescription>You can also enter your username.</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
name="password"
control={form.control}
render={({ field }) => (
<FormItem>
<FormLabel>Password</FormLabel>
<FormControl>
<Input type="password" {...field} />
</FormControl>
<FormDescription>
Hold <code className="text-xs font-bold">Ctrl</code> to display your password
temporarily.
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<div className="mt-4 flex items-center gap-x-4">
<Button type="submit" disabled={loading} className="flex-1">
Sign in
</Button>
<Button asChild variant="link" className="px-4">
<Link to="/auth/forgot-password">Forgot Password?</Link>
</Button>
</div>
</form>
</Form>
</div>
</div>
);
};

View File

@ -0,0 +1,155 @@
import { zodResolver } from "@hookform/resolvers/zod";
import { ArrowRight, Warning } from "@phosphor-icons/react";
import { registerSchema } from "@reactive-resume/dto";
import { usePasswordToggle } from "@reactive-resume/hooks";
import {
Button,
Form,
FormControl,
FormDescription,
FormField,
FormItem,
FormLabel,
FormMessage,
Input,
} from "@reactive-resume/ui";
import { AxiosError } from "axios";
import { useRef } from "react";
import { useForm } from "react-hook-form";
import { Link, useNavigate } from "react-router-dom";
import { z } from "zod";
import { toast } from "@/client/hooks/use-toast";
import { useRegister } from "@/client/services/auth";
type FormValues = z.infer<typeof registerSchema>;
export const RegisterPage = () => {
const navigate = useNavigate();
const { register, loading } = useRegister();
const formRef = useRef<HTMLFormElement>(null);
usePasswordToggle(formRef);
const form = useForm<FormValues>({
resolver: zodResolver(registerSchema),
defaultValues: {
name: "",
username: "",
email: "",
password: "",
language: "en",
},
});
const onSubmit = async (data: FormValues) => {
try {
await register(data);
navigate("/auth/verify-email");
} catch (error) {
form.reset();
if (error instanceof AxiosError) {
const message = error.response?.data.message || error.message;
toast({
variant: "error",
icon: <Warning size={16} weight="bold" />,
title: "An error occurred while trying to sign up",
description: message,
});
}
}
};
return (
<div className="space-y-8">
<div className="space-y-1.5">
<h2 className="text-2xl font-semibold tracking-tight">Create a new account</h2>
<h6>
<span className="opacity-75">Already have an account?</span>
<Button asChild variant="link" className="px-1.5">
<Link to="/auth/login">
Sign in now <ArrowRight className="ml-1" />
</Link>
</Button>
</h6>
</div>
<div>
<Form {...form}>
<form
ref={formRef}
className="flex flex-col gap-y-4"
onSubmit={form.handleSubmit(onSubmit)}
>
<FormField
name="name"
control={form.control}
render={({ field }) => (
<FormItem>
<FormLabel>Name</FormLabel>
<FormControl>
<Input placeholder="John Doe" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
name="username"
control={form.control}
render={({ field }) => (
<FormItem>
<FormLabel>Username</FormLabel>
<FormControl>
<Input placeholder="john.doe" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
name="email"
control={form.control}
render={({ field }) => (
<FormItem>
<FormLabel>Email</FormLabel>
<FormControl>
<Input placeholder="john.doe@example.com" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
name="password"
control={form.control}
render={({ field }) => (
<FormItem>
<FormLabel>Password</FormLabel>
<FormControl>
<Input type="password" {...field} />
</FormControl>
<FormDescription>
Hold <code className="text-xs font-bold">Ctrl</code> to display your password
temporarily.
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<Button disabled={loading} className="mt-4 w-full">
Sign up
</Button>
</form>
</Form>
</div>
</div>
);
};

View File

@ -0,0 +1,110 @@
import { zodResolver } from "@hookform/resolvers/zod";
import { Warning } from "@phosphor-icons/react";
import { resetPasswordSchema } from "@reactive-resume/dto";
import { usePasswordToggle } from "@reactive-resume/hooks";
import {
Button,
Form,
FormControl,
FormDescription,
FormField,
FormItem,
FormLabel,
FormMessage,
Input,
} from "@reactive-resume/ui";
import { AxiosError } from "axios";
import { useEffect, useRef } from "react";
import { useForm } from "react-hook-form";
import { useNavigate, useSearchParams } from "react-router-dom";
import { z } from "zod";
import { toast } from "@/client/hooks/use-toast";
import { useResetPassword } from "@/client/services/auth";
type FormValues = z.infer<typeof resetPasswordSchema>;
export const ResetPasswordPage = () => {
const navigate = useNavigate();
const [searchParams] = useSearchParams();
const token = searchParams.get("token") || "";
const { resetPassword, loading } = useResetPassword();
const formRef = useRef<HTMLFormElement>(null);
usePasswordToggle(formRef);
const form = useForm<FormValues>({
resolver: zodResolver(resetPasswordSchema),
defaultValues: { token, password: "" },
});
const onSubmit = async (data: FormValues) => {
try {
await resetPassword(data);
navigate("/auth/login");
} catch (error) {
form.reset();
if (error instanceof AxiosError) {
const message = error.response?.data.message || error.message;
toast({
variant: "error",
icon: <Warning size={16} weight="bold" />,
title: "An error occurred while trying to reset your password",
description: message,
});
}
}
};
// Redirect the user to the forgot password page if the token is not present.
useEffect(() => {
if (!token) navigate("/auth/forgot-password");
}, [token, navigate]);
return (
<div className="space-y-8">
<div className="space-y-1.5">
<h2 className="text-2xl font-semibold tracking-tight">Reset your password</h2>
<h6 className="leading-relaxed opacity-75">
Enter a new password below, and make sure it's secure.
</h6>
</div>
<div>
<Form {...form}>
<form
ref={formRef}
className="flex flex-col gap-y-4"
onSubmit={form.handleSubmit(onSubmit)}
>
<FormField
name="password"
control={form.control}
render={({ field }) => (
<FormItem>
<FormLabel>Password</FormLabel>
<FormControl>
<Input type="password" {...field} />
</FormControl>
<FormDescription>
Hold <code className="text-xs font-bold">Ctrl</code> to display your password
temporarily.
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<Button type="submit" disabled={loading} className="mt-4 w-full">
Update Password
</Button>
</form>
</Form>
</div>
</div>
);
};

View File

@ -0,0 +1,80 @@
import { ArrowRight, Info, SealCheck, Warning } from "@phosphor-icons/react";
import { Alert, AlertDescription, AlertTitle, Button } from "@reactive-resume/ui";
import { AxiosError } from "axios";
import { useEffect } from "react";
import { Link, useNavigate, useSearchParams } from "react-router-dom";
import { useToast } from "@/client/hooks/use-toast";
import { queryClient } from "@/client/libs/query-client";
import { useVerifyEmail } from "@/client/services/auth";
export const VerifyEmailPage = () => {
const { toast } = useToast();
const navigate = useNavigate();
const [searchParams] = useSearchParams();
const token = searchParams.get("token");
const { verifyEmail, loading } = useVerifyEmail();
useEffect(() => {
const handleVerifyEmail = async (token: string) => {
try {
await verifyEmail({ token });
await queryClient.invalidateQueries({ queryKey: ["user"] });
toast({
variant: "success",
icon: <SealCheck size={16} weight="bold" />,
title: "Your email address has been verified successfully.",
});
navigate("/dashboard/resumes", { replace: true });
} catch (error) {
if (error instanceof AxiosError) {
const message = error.response?.data.message || error.message;
toast({
variant: "error",
icon: <Warning size={16} weight="bold" />,
title: "An error occurred while trying to verify your email address",
description: message,
});
}
}
};
if (!token) return;
handleVerifyEmail(token);
}, [token, navigate, verifyEmail]);
return (
<div className="space-y-6">
<div className="space-y-2">
<h2 className="text-2xl font-semibold tracking-tight">Verify your email address</h2>
<p className="leading-relaxed opacity-75">
You should have received an email from <strong>Reactive Resume</strong> with a link to
verify your account.
</p>
</div>
<Alert variant="info">
<Info size={18} />
<AlertTitle>Please note that this step is completely optional.</AlertTitle>
<AlertDescription>
We verify your email address only to ensure that we can send you a password reset link in
case you forget your password.
</AlertDescription>
</Alert>
<Button asChild disabled={loading}>
<Link to="/dashboard">
Continue to Dashboard
<ArrowRight className="ml-2" />
</Link>
</Button>
</div>
);
};

View File

@ -0,0 +1,104 @@
import { zodResolver } from "@hookform/resolvers/zod";
import { ArrowRight, Warning } from "@phosphor-icons/react";
import { twoFactorSchema } from "@reactive-resume/dto";
import { usePasswordToggle } from "@reactive-resume/hooks";
import {
Button,
Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
Input,
} from "@reactive-resume/ui";
import { AxiosError } from "axios";
import { useRef } from "react";
import { useForm } from "react-hook-form";
import { Link, useNavigate } from "react-router-dom";
import { z } from "zod";
import { toast } from "@/client/hooks/use-toast";
import { useVerifyOtp } from "@/client/services/auth";
type FormValues = z.infer<typeof twoFactorSchema>;
export const VerifyOtpPage = () => {
const navigate = useNavigate();
const { verifyOtp, loading } = useVerifyOtp();
const formRef = useRef<HTMLFormElement>(null);
usePasswordToggle(formRef);
const form = useForm<FormValues>({
resolver: zodResolver(twoFactorSchema),
defaultValues: { code: "" },
});
const onSubmit = async (data: FormValues) => {
try {
await verifyOtp(data);
navigate("/dashboard");
} catch (error) {
form.reset();
if (error instanceof AxiosError) {
const message = error.response?.data.message || error.message;
toast({
variant: "error",
icon: <Warning size={16} weight="bold" />,
title: "An error occurred while trying to sign in",
description: message,
});
}
}
};
return (
<div className="space-y-8">
<div className="space-y-1.5">
<h2 className="text-2xl font-semibold tracking-tight">Two Step Verification</h2>
<h6>
<span className="opacity-75">
Enter the one-time password provided by your authenticator app below.
</span>
<Button asChild variant="link" className="px-1.5">
<Link to="/auth/backup-otp">
Lost your device? <ArrowRight className="ml-1" />
</Link>
</Button>
</h6>
</div>
<div>
<Form {...form}>
<form
ref={formRef}
className="flex flex-col gap-y-4"
onSubmit={form.handleSubmit(onSubmit)}
>
<FormField
name="code"
control={form.control}
render={({ field }) => (
<FormItem>
<FormLabel>One-Time Password</FormLabel>
<FormControl>
<Input placeholder="123456" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<Button type="submit" disabled={loading} className="mt-4 w-full">
Sign in
</Button>
</form>
</Form>
</div>
</div>
);
};