Squashed commit of the following:

commit b2b0470a1d9267d042ec0ac66523c6635bf5b199
Author: Amruth Pillai <im.amruth@gmail.com>
Date:   Tue May 19 13:13:38 2026 +0200

    chore: update .gitignore to include .vite-hooks and modify pnpm-lock.yaml for dependencies

commit d28fadb5cd8706c874e616102878b4a394ec84c1
Author: Amruth Pillai <im.amruth@gmail.com>
Date:   Tue May 19 13:08:04 2026 +0200

    fix: remove timestamp conflict guard

commit c6998d9dbab19d09d3c8054feef1d2e4117555eb
Author: Amruth Pillai <im.amruth@gmail.com>
Date:   Tue May 19 12:11:51 2026 +0200

    chore(release): v5.1.5

commit f33d168711804880e1f12e88d24290aae16cc258
Author: Amruth Pillai <im.amruth@gmail.com>
Date:   Tue May 19 11:58:35 2026 +0200

    revert: compose.yml

commit d961e6535811a10c335525fb33a08d03e737278d
Author: Amruth Pillai <im.amruth@gmail.com>
Date:   Tue May 19 11:58:08 2026 +0200

    refactor(agent): replace 'revert' terminology with 'restore' for clarity, resolves #3086

commit 17f351171be218e33f01c469d95e4164d4c8dc57
Author: Amruth Pillai <im.amruth@gmail.com>
Date:   Tue May 19 11:10:41 2026 +0200

    refactor(pdf): simplify sidebar section filtering and update summary feature logic

commit d55179b9d76879e3204de185e8b53fadd0a107ed
Author: Amruth Pillai <im.amruth@gmail.com>
Date:   Tue May 19 09:53:37 2026 +0200

    chore: update pnpm-lock.yaml and turbo.json

commit 7cade6980e1a04352536bd44ef773f338c4ef599
Author: Amruth Pillai <im.amruth@gmail.com>
Date:   Tue May 19 09:38:30 2026 +0200

    fix(polyfill): add tested polyfill for Map Upsert methods

commit 26d175bb9c53d93225d1e907678445252c13d660
Merge: 1cf33dc6c 5b1297fa2
Author: Amruth Pillai <im.amruth@gmail.com>
Date:   Tue May 19 09:23:29 2026 +0200

    Merge remote-tracking branch 'origin/main' into feat/explore-hono-orpc-migration

    # Conflicts:
    #	packages/api/src/services/agent-url.ts
    #	packages/runtime-externals/package.json

commit 1cf33dc6c9d81735730ad656e16dab6501c6d6a1
Author: Amruth Pillai <im.amruth@gmail.com>
Date:   Tue May 19 09:22:12 2026 +0200

    chore: preserve branch changes before main sync

commit b380a4b00fdbcdd81ff4f8ef72b330fd027ccda5
Author: Amruth Pillai <im.amruth@gmail.com>
Date:   Mon May 18 07:50:28 2026 +0200

    chore: lot of fixes for monorepo migration

commit 8fcf0ec64e1c29572ebaff494338368bfcf75760
Author: Amruth Pillai <im.amruth@gmail.com>
Date:   Fri May 15 13:57:17 2026 +0200

    chore: update knip version and refine web app routing with new SEO endpoints

commit 234e68086ff15610a93877354c98e2c020364533
Author: Amruth Pillai <im.amruth@gmail.com>
Date:   Fri May 15 12:10:06 2026 +0200

    refactor(auth): update OAuth routes to include API prefix and remove unused schema endpoint

commit 91c84b9a8496b0ce21d71cae9f8b2a027638c9ac
Author: Amruth Pillai <im.amruth@gmail.com>
Date:   Fri May 15 11:54:29 2026 +0200

    chore: update dependencies and enhance PWA metadata in web app

commit 150117d4a5a9dd6cd92c64891aad8cae90f6a7af
Author: Amruth Pillai <im.amruth@gmail.com>
Date:   Fri May 15 11:12:35 2026 +0200

    docs: revise manifest-only pwa testing scope

commit 6b939a55661aec9dd8122b184e4b60a5c7325fb5
Author: Amruth Pillai <im.amruth@gmail.com>
Date:   Fri May 15 11:11:33 2026 +0200

    docs: add manifest-only pwa design

commit 1422e1fc96c400948b273210a1067251087d15d4
Author: Amruth Pillai <im.amruth@gmail.com>
Date:   Fri May 15 11:05:04 2026 +0200

    chore(dev): simplify server proxy config

commit bc2ff5a9f6fda41e6c40333c8f163aa23a6c5e48
Author: Amruth Pillai <im.amruth@gmail.com>
Date:   Fri May 15 11:04:50 2026 +0200

    docs: add unsafe oauth redirect plan

commit 445359ebe9b96c1515bf1c4c3f73ba8a8448ec12
Author: Amruth Pillai <im.amruth@gmail.com>
Date:   Fri May 15 11:04:34 2026 +0200

    feat(auth): add unsafe oauth redirect flag

commit 73fffdd24598e56b2793f7657919bc794835892e
Author: Amruth Pillai <im.amruth@gmail.com>
Date:   Fri May 15 10:55:02 2026 +0200

    docs: design unsafe oauth redirect flag

commit c0066aa19c15fc8a4c8e5179ed49889c117519f4
Author: Amruth Pillai <im.amruth@gmail.com>
Date:   Fri May 15 10:22:04 2026 +0200

    chore: update translation source paths

commit 9033da082418d252aafd6c2eed72f71f014be3d9
Author: Amruth Pillai <im.amruth@gmail.com>
Date:   Fri May 15 10:09:25 2026 +0200

    refactor(arch): react spa + hono migration

commit 6f27936c11bda895977dc63ee550c3346d4ce24b
Author: Amruth Pillai <im.amruth@gmail.com>
Date:   Fri May 15 01:10:47 2026 +0200

    docs: add docker nightly tagging design

commit ecc1fd9a88a0ee1dca2f1977dfc17f74527fe1da
Author: Amruth Pillai <im.amruth@gmail.com>
Date:   Thu May 14 20:05:44 2026 +0200

    feat: migrate to hono spa server
This commit is contained in:
Amruth Pillai
2026-05-19 13:14:21 +02:00
parent 5b1297fa2b
commit 62f8270b3e
518 changed files with 29398 additions and 26871 deletions
@@ -0,0 +1,189 @@
import type { RouterOutput } from "@/libs/orpc/client";
import { t } from "@lingui/core/macro";
import { Trans } from "@lingui/react/macro";
import { FingerprintIcon, GithubLogoIcon, GoogleLogoIcon, LinkedinLogoIcon, VaultIcon } from "@phosphor-icons/react";
import { useQuery } from "@tanstack/react-query";
import { useRouter } from "@tanstack/react-router";
import { toast } from "sonner";
import { Button } from "@reactive-resume/ui/components/button";
import { Skeleton } from "@reactive-resume/ui/components/skeleton";
import { cn } from "@reactive-resume/utils/style";
import { authClient } from "@/libs/auth/client";
import { orpc } from "@/libs/orpc/client";
type SocialAuthProps = {
requestSignUp?: boolean;
};
type SocialSignInOptions = {
provider: string;
callbackURL: string;
requestSignUp?: true;
};
function getSocialSignInOptions(provider: string, requestSignUp: boolean): SocialSignInOptions {
const options: SocialSignInOptions = { provider, callbackURL: "/dashboard" };
if (requestSignUp) options.requestSignUp = true;
return options;
}
export function SocialAuth({ requestSignUp = false }: SocialAuthProps) {
const { data: providers = {}, isLoading } = useQuery(orpc.auth.providers.list.queryOptions());
return (
<>
<div className="flex items-center gap-x-2">
<hr className="flex-1" />
<span className="font-medium text-xs tracking-wide">
<Trans context="Choose to authenticate with a social provider (Google, GitHub, etc.) instead of email and password">
or continue with
</Trans>
</span>
<hr className="flex-1" />
</div>
{isLoading ? <SocialAuthSkeleton /> : <SocialAuthButtons providers={providers} requestSignUp={requestSignUp} />}
</>
);
}
function SocialAuthSkeleton() {
return (
<div className="grid grid-cols-2 gap-4">
<Skeleton className="h-9 w-full" />
<Skeleton className="h-9 w-full" />
<Skeleton className="h-9 w-full" />
<Skeleton className="h-9 w-full" />
</div>
);
}
type SocialAuthButtonsProps = {
providers: RouterOutput["auth"]["providers"]["list"];
requestSignUp: boolean;
};
function SocialAuthButtons({ providers, requestSignUp }: SocialAuthButtonsProps) {
const router = useRouter();
const handleSocialLogin = async (provider: string) => {
const toastId = toast.loading(t`Signing in...`);
const { error } = await authClient.signIn.social(getSocialSignInOptions(provider, requestSignUp));
if (error) {
toast.error(
error.message ||
t({
comment: "Fallback toast when social sign-in fails without a provider error message",
message: "Failed to sign in. Please try again.",
}),
{ id: toastId },
);
return;
}
toast.dismiss(toastId);
await router.invalidate();
};
const handleOAuthLogin = async () => {
const toastId = toast.loading(t`Signing in...`);
const { error } = await authClient.signIn.oauth2({
providerId: "custom",
callbackURL: "/dashboard",
});
if (error) {
toast.error(
error.message ||
t({
comment: "Fallback toast when custom OAuth sign-in fails without a provider error message",
message: "Failed to sign in. Please try again.",
}),
{ id: toastId },
);
return;
}
toast.dismiss(toastId);
await router.invalidate();
};
const handlePasskeyLogin = async () => {
const toastId = toast.loading(t`Signing in...`);
const { error } = await authClient.signIn.passkey({ autoFill: false });
if (error) {
toast.error(
error.message ||
t({
comment: "Fallback toast when passkey sign-in fails without an error message",
message: "Failed to sign in. Please try again.",
}),
{ id: toastId },
);
return;
}
toast.dismiss(toastId);
await router.invalidate();
};
return (
<div className="grid grid-cols-2 gap-4">
<Button
variant="secondary"
onClick={handleOAuthLogin}
className={cn("hidden", "custom" in providers && "inline-flex")}
>
<VaultIcon />
{providers.custom}
</Button>
<Button
variant="secondary"
onClick={handlePasskeyLogin}
className={cn("hidden", "passkey" in providers && "inline-flex")}
>
<FingerprintIcon />
<Trans comment="Label for passkey sign-in button">Passkey</Trans>
</Button>
<Button
onClick={() => handleSocialLogin("google")}
className={cn(
"hidden flex-1 bg-[#4285F4] text-white hover:bg-[#4285F4]/80",
"google" in providers && "inline-flex",
)}
>
<GoogleLogoIcon />
<Trans comment="Brand name label for Google social sign-in button">Google</Trans>
</Button>
<Button
onClick={() => handleSocialLogin("github")}
className={cn(
"hidden flex-1 bg-[#2b3137] text-white hover:bg-[#2b3137]/80",
"github" in providers && "inline-flex",
)}
>
<GithubLogoIcon />
<Trans comment="Brand name label for GitHub social sign-in button">GitHub</Trans>
</Button>
<Button
onClick={() => handleSocialLogin("linkedin")}
className={cn(
"hidden flex-1 bg-[#0A66C2] text-white hover:bg-[#0A66C2]/80",
"linkedin" in providers && "inline-flex",
)}
>
<LinkedinLogoIcon />
<Trans comment="Brand name label for LinkedIn social sign-in button">LinkedIn</Trans>
</Button>
</div>
);
}
+12
View File
@@ -0,0 +1,12 @@
import { Outlet } from "@tanstack/react-router";
import { BrandIcon } from "@reactive-resume/ui/components/brand-icon";
export function AuthLayout() {
return (
<div className="mx-auto flex h-svh w-dvw max-w-sm flex-col justify-center space-y-6 px-4 xs:px-0">
<BrandIcon className="mb-4 size-20 self-center" />
<Outlet />
</div>
);
}
@@ -0,0 +1,141 @@
import { t } from "@lingui/core/macro";
import { Trans } from "@lingui/react/macro";
import { ArrowRightIcon } from "@phosphor-icons/react";
import { Link } from "@tanstack/react-router";
import { useState } from "react";
import { toast } from "sonner";
import z from "zod";
import { Button } from "@reactive-resume/ui/components/button";
import { FormControl, FormItem, FormLabel, FormMessage } from "@reactive-resume/ui/components/form";
import { Input } from "@reactive-resume/ui/components/input";
import { authClient } from "@/libs/auth/client";
import { useAppForm } from "@/libs/tanstack-form";
const formSchema = z.object({
email: z.email(),
});
export function ForgotPasswordPage() {
const [submitted, setSubmitted] = useState(false);
const form = useAppForm({
defaultValues: { email: "" },
validators: { onSubmit: formSchema },
onSubmit: async ({ value }) => {
const toastId = toast.loading(t`Sending password reset email...`);
const { error } = await authClient.requestPasswordReset({
email: value.email,
redirectTo: "/auth/reset-password",
});
if (error) {
toast.error(
error.message ||
t({
comment: "Fallback toast when requesting password reset email fails without backend message",
message: "Failed to send password reset email. Please try again.",
}),
{ id: toastId },
);
return;
}
setSubmitted(true);
toast.dismiss(toastId);
},
});
if (submitted) return <PostForgotPasswordScreen />;
return (
<>
<div className="space-y-1 text-center">
<h1 className="font-bold text-2xl tracking-tight">
<Trans>Forgot your password?</Trans>
</h1>
<div className="text-muted-foreground">
<Trans>
Remember your password?{" "}
<Button
variant="link"
className="h-auto gap-1.5 px-1! py-0"
nativeButton={false}
render={
<Link to="/auth/login">
<Trans comment="Call-to-action link from forgot-password page to login page">Sign in now</Trans>{" "}
<ArrowRightIcon />
</Link>
}
/>
</Trans>
</div>
</div>
<form
className="space-y-6"
onSubmit={(event) => {
event.preventDefault();
event.stopPropagation();
void form.handleSubmit();
}}
>
<form.Field name="email">
{(field) => (
<FormItem hasError={field.state.meta.isTouched && field.state.meta.errors.length > 0}>
<FormLabel>
<Trans comment="Label for email input on forgot-password form">Email Address</Trans>
</FormLabel>
<FormControl
render={
<Input
type="email"
autoComplete="email"
placeholder={t({
comment: "Example email placeholder on forgot-password form",
message: "john.doe@example.com",
})}
name={field.name}
value={field.state.value}
onBlur={field.handleBlur}
onChange={(event) => field.handleChange(event.target.value)}
/>
}
/>
<FormMessage errors={field.state.meta.errors} />
</FormItem>
)}
</form.Field>
<Button type="submit" className="w-full">
<Trans comment="Primary action button label on forgot-password form">Send Password Reset Email</Trans>
</Button>
</form>
</>
);
}
function PostForgotPasswordScreen() {
return (
<>
<div className="space-y-1 text-center">
<h1 className="font-bold text-2xl tracking-tight">
<Trans>You've got mail!</Trans>
</h1>
<p className="text-muted-foreground">
<Trans>Check your email for a link to reset your password.</Trans>
</p>
</div>
<Button
nativeButton={false}
render={
<a href="mailto:">
<Trans comment="Button label to open the user's default email app">Open Email Client</Trans>
</a>
}
/>
</>
);
}
+239
View File
@@ -0,0 +1,239 @@
import { t } from "@lingui/core/macro";
import { Trans } from "@lingui/react/macro";
import { ArrowRightIcon, EyeIcon, EyeSlashIcon } from "@phosphor-icons/react";
import { useQuery } from "@tanstack/react-query";
import { Link, useNavigate, useRouter } from "@tanstack/react-router";
import { useEffect, useRef } from "react";
import { toast } from "sonner";
import { useToggle } from "usehooks-ts";
import z from "zod";
import { Button } from "@reactive-resume/ui/components/button";
import { FormControl, FormDescription, FormItem, FormLabel, FormMessage } from "@reactive-resume/ui/components/form";
import { Input } from "@reactive-resume/ui/components/input";
import { authClient } from "@/libs/auth/client";
import { orpc } from "@/libs/orpc/client";
import { useAppForm } from "@/libs/tanstack-form";
import { SocialAuth } from "../components/social-auth";
const formSchema = z.object({
identifier: z.string().trim().toLowerCase(),
password: z.string().trim().min(6).max(64),
});
type Props = {
disableEmailAuth: boolean;
disableSignups: boolean;
};
export function LoginPage({ disableEmailAuth, disableSignups }: Props) {
const router = useRouter();
const navigate = useNavigate();
const hasStartedConditionalPasskeyRef = useRef(false);
const [showPassword, toggleShowPassword] = useToggle(false);
const { data: providers = {} } = useQuery(orpc.auth.providers.list.queryOptions());
const form = useAppForm({
defaultValues: { identifier: "", password: "" },
validators: { onSubmit: formSchema },
onSubmit: async ({ value }) => {
const toastId = toast.loading(t`Signing in...`);
try {
const isEmail = value.identifier.includes("@");
const result = isEmail
? await authClient.signIn.email({ email: value.identifier, password: value.password })
: await authClient.signIn.username({ username: value.identifier, password: value.password });
if (result.error) {
toast.error(
result.error.message ||
t({
comment: "Fallback toast when sign-in fails and no server error message is available",
message: "Failed to sign in. Please try again.",
}),
{ id: toastId },
);
return;
}
const requiresTwoFactor =
result.data &&
typeof result.data === "object" &&
"twoFactorRedirect" in result.data &&
result.data.twoFactorRedirect;
if (requiresTwoFactor) {
toast.dismiss(toastId);
void navigate({ to: "/auth/verify-2fa", replace: true });
return;
}
toast.dismiss(toastId);
await router.invalidate();
void navigate({ to: "/dashboard", replace: true });
} catch {
toast.error(t`Failed to sign in. Please try again.`, { id: toastId });
}
},
});
useEffect(() => {
if (!("passkey" in providers)) return;
if (typeof window === "undefined") return;
if (!("PublicKeyCredential" in window)) return;
if (!PublicKeyCredential.isConditionalMediationAvailable) return;
if (hasStartedConditionalPasskeyRef.current) return;
hasStartedConditionalPasskeyRef.current = true;
void PublicKeyCredential.isConditionalMediationAvailable().then(async (isAvailable) => {
if (!isAvailable) return;
const { error } = await authClient.signIn.passkey({ autoFill: true });
if (error) return;
await router.invalidate();
});
}, [providers, router]);
return (
<>
<div className="space-y-1 text-center">
<h1 className="font-bold text-2xl tracking-tight">
<Trans comment="Title on the login page">Sign in to your account</Trans>
</h1>
{!disableSignups && (
<div className="text-muted-foreground">
<Trans>
Don't have an account?{" "}
<Button
variant="link"
nativeButton={false}
className="h-auto gap-1.5 px-1! py-0"
render={
<Link to="/auth/register">
<Trans comment="Call-to-action link from login page to account registration page">
Create one now
</Trans>{" "}
<ArrowRightIcon />
</Link>
}
/>
</Trans>
</div>
)}
</div>
{!disableEmailAuth && (
<form
className="space-y-6"
onSubmit={(event) => {
event.preventDefault();
event.stopPropagation();
void form.handleSubmit();
}}
>
<form.Field name="identifier">
{(field) => (
<FormItem hasError={field.state.meta.isTouched && field.state.meta.errors.length > 0}>
<FormLabel>
<Trans comment="Label for login identifier input that accepts email or username">Email Address</Trans>
</FormLabel>
<FormControl
render={
<Input
autoComplete="section-login username webauthn"
placeholder={t({
comment: "Example email placeholder for login identifier field",
message: "john.doe@example.com",
})}
className="lowercase"
name={field.name}
value={field.state.value}
onBlur={field.handleBlur}
onChange={(event) => field.handleChange(event.target.value)}
/>
}
/>
<FormMessage errors={field.state.meta.errors} />
<FormDescription>
<Trans>You can also use your username to login.</Trans>
</FormDescription>
</FormItem>
)}
</form.Field>
<form.Field name="password">
{(field) => (
<FormItem hasError={field.state.meta.isTouched && field.state.meta.errors.length > 0}>
<div className="flex items-center justify-between">
<FormLabel>
<Trans comment="Label for password input on login form">Password</Trans>
</FormLabel>
<Button
tabIndex={-1}
variant="link"
nativeButton={false}
className="h-auto p-0 text-xs leading-none"
render={
<Link to="/auth/forgot-password">
<Trans comment="Link label to password reset page from login form">Forgot Password?</Trans>
</Link>
}
/>
</div>
<div className="flex items-center gap-x-1.5">
<FormControl
render={
<Input
min={6}
max={64}
type={showPassword ? "text" : "password"}
autoComplete="section-login current-password"
name={field.name}
value={field.state.value}
onBlur={field.handleBlur}
onChange={(event) => field.handleChange(event.target.value)}
/>
}
/>
<Button
size="icon"
variant="ghost"
onClick={toggleShowPassword}
aria-label={
showPassword
? t({
comment: "Accessible label for button that hides the password in login form",
message: "Hide password",
})
: t({
comment: "Accessible label for button that reveals the password in login form",
message: "Show password",
})
}
>
{showPassword ? <EyeIcon /> : <EyeSlashIcon />}
</Button>
</div>
<FormMessage errors={field.state.meta.errors} />
</FormItem>
)}
</form.Field>
<Button type="submit" className="w-full">
<Trans comment="Primary action button label on login form">Sign in</Trans>
</Button>
</form>
)}
<SocialAuth />
</>
);
}
@@ -0,0 +1,282 @@
import { t } from "@lingui/core/macro";
import { Trans } from "@lingui/react/macro";
import { ArrowRightIcon, EyeIcon, EyeSlashIcon } from "@phosphor-icons/react";
import { Link } from "@tanstack/react-router";
import { useState } from "react";
import { toast } from "sonner";
import { useToggle } from "usehooks-ts";
import z from "zod";
import { Alert, AlertDescription, AlertTitle } from "@reactive-resume/ui/components/alert";
import { Button } from "@reactive-resume/ui/components/button";
import { FormControl, FormItem, FormLabel, FormMessage } from "@reactive-resume/ui/components/form";
import { Input } from "@reactive-resume/ui/components/input";
import { authClient } from "@/libs/auth/client";
import { useAppForm } from "@/libs/tanstack-form";
import { SocialAuth } from "../components/social-auth";
const formSchema = z.object({
name: z.string().min(3).max(64),
username: z
.string()
.min(3)
.max(64)
.trim()
.toLowerCase()
.regex(/^[a-z0-9._-]+$/, {
message: "Username can only contain lowercase letters, numbers, dots, hyphens and underscores.",
}),
email: z.email().toLowerCase(),
password: z.string().min(6).max(64),
});
type Props = {
disableEmailAuth: boolean;
};
export function RegisterPage({ disableEmailAuth }: Props) {
const [submitted, setSubmitted] = useState(false);
const [showPassword, toggleShowPassword] = useToggle(false);
const form = useAppForm({
defaultValues: { name: "", username: "", email: "", password: "" },
validators: { onSubmit: formSchema },
onSubmit: async ({ value }) => {
const toastId = toast.loading(t`Signing up...`);
const { error } = await authClient.signUp.email({
name: value.name,
email: value.email,
password: value.password,
username: value.username,
displayUsername: value.username,
callbackURL: "/dashboard",
});
if (error) {
toast.error(
error.message ||
t({
comment: "Fallback toast when account registration fails without a server error message",
message: "Failed to create your account. Please try again.",
}),
{ id: toastId },
);
return;
}
setSubmitted(true);
toast.dismiss(toastId);
},
});
if (submitted) return <PostSignupScreen />;
return (
<>
<div className="space-y-1 text-center">
<h1 className="font-bold text-2xl tracking-tight">
<Trans>Create a new account</Trans>
</h1>
<div className="text-muted-foreground">
<Trans>
Already have an account?{" "}
<Button
variant="link"
nativeButton={false}
className="h-auto gap-1.5 px-1! py-0"
render={
<Link to="/auth/login">
<Trans comment="Call-to-action link from registration page to login page">Sign in now</Trans>{" "}
<ArrowRightIcon />
</Link>
}
/>
</Trans>
</div>
</div>
{!disableEmailAuth && (
<form
className="space-y-6"
onSubmit={(event) => {
event.preventDefault();
event.stopPropagation();
void form.handleSubmit();
}}
>
<form.Field name="name">
{(field) => (
<FormItem hasError={field.state.meta.isTouched && field.state.meta.errors.length > 0}>
<FormLabel>
<Trans comment="Label for full name input on registration form">Name</Trans>
</FormLabel>
<FormControl
render={
<Input
min={3}
max={64}
autoComplete="section-register name"
placeholder={t({
comment: "Example full name placeholder on registration form",
message: "John Doe",
})}
name={field.name}
value={field.state.value}
onBlur={field.handleBlur}
onChange={(event) => field.handleChange(event.target.value)}
/>
}
/>
<FormMessage errors={field.state.meta.errors} />
</FormItem>
)}
</form.Field>
<form.Field name="username">
{(field) => (
<FormItem hasError={field.state.meta.isTouched && field.state.meta.errors.length > 0}>
<FormLabel>
<Trans comment="Label for username input on registration form">Username</Trans>
</FormLabel>
<FormControl
render={
<Input
min={3}
max={64}
autoComplete="section-register username"
placeholder={t({
comment: "Example username placeholder on registration form",
message: "john.doe",
})}
className="lowercase"
name={field.name}
value={field.state.value}
onBlur={field.handleBlur}
onChange={(event) => field.handleChange(event.target.value)}
/>
}
/>
<FormMessage errors={field.state.meta.errors} />
</FormItem>
)}
</form.Field>
<form.Field name="email">
{(field) => (
<FormItem hasError={field.state.meta.isTouched && field.state.meta.errors.length > 0}>
<FormLabel>
<Trans comment="Label for email input on registration form">Email Address</Trans>
</FormLabel>
<FormControl
render={
<Input
type="email"
autoComplete="section-register email"
placeholder={t({
comment: "Example email placeholder on registration form",
message: "john.doe@example.com",
})}
className="lowercase"
name={field.name}
value={field.state.value}
onBlur={field.handleBlur}
onChange={(event) => field.handleChange(event.target.value)}
/>
}
/>
<FormMessage errors={field.state.meta.errors} />
</FormItem>
)}
</form.Field>
<form.Field name="password">
{(field) => (
<FormItem hasError={field.state.meta.isTouched && field.state.meta.errors.length > 0}>
<FormLabel>
<Trans comment="Label for password input on registration form">Password</Trans>
</FormLabel>
<div className="flex items-center gap-x-1.5">
<FormControl
render={
<Input
min={6}
max={64}
type={showPassword ? "text" : "password"}
autoComplete="section-register new-password"
name={field.name}
value={field.state.value}
onBlur={field.handleBlur}
onChange={(event) => field.handleChange(event.target.value)}
/>
}
/>
<Button
size="icon"
variant="ghost"
onClick={toggleShowPassword}
aria-label={
showPassword
? t({
comment: "Accessible label for button that hides password in registration form",
message: "Hide password",
})
: t({
comment: "Accessible label for button that reveals password in registration form",
message: "Show password",
})
}
>
{showPassword ? <EyeIcon /> : <EyeSlashIcon />}
</Button>
</div>
<FormMessage errors={field.state.meta.errors} />
</FormItem>
)}
</form.Field>
<Button type="submit" className="w-full">
<Trans comment="Primary action button label on registration form">Sign up</Trans>
</Button>
</form>
)}
<SocialAuth requestSignUp />
</>
);
}
function PostSignupScreen() {
return (
<>
<div className="space-y-1 text-center">
<h1 className="font-bold text-2xl tracking-tight">
<Trans>You've got mail!</Trans>
</h1>
<p className="text-muted-foreground">
<Trans>Check your email for a link to verify your account.</Trans>
</p>
</div>
<Alert>
<AlertTitle>
<Trans>This step is optional, but recommended.</Trans>
</AlertTitle>
<AlertDescription>
<Trans>Verifying your email is required when resetting your password.</Trans>
</AlertDescription>
</Alert>
<Button
nativeButton={false}
render={
<Link to="/dashboard">
<Trans comment="Button label to continue to dashboard after successful registration">Continue</Trans>{" "}
<ArrowRightIcon />
</Link>
}
/>
</>
);
}
@@ -0,0 +1,126 @@
import { t } from "@lingui/core/macro";
import { Trans } from "@lingui/react/macro";
import { EyeIcon, EyeSlashIcon } from "@phosphor-icons/react";
import { useNavigate } from "@tanstack/react-router";
import { toast } from "sonner";
import { useToggle } from "usehooks-ts";
import z from "zod";
import { Button } from "@reactive-resume/ui/components/button";
import { FormControl, FormItem, FormLabel, FormMessage } from "@reactive-resume/ui/components/form";
import { Input } from "@reactive-resume/ui/components/input";
import { authClient } from "@/libs/auth/client";
import { useAppForm } from "@/libs/tanstack-form";
const formSchema = z.object({
password: z.string().min(6).max(64),
});
type Props = {
token: string;
};
export function ResetPasswordPage({ token }: Props) {
const navigate = useNavigate();
const [showPassword, toggleShowPassword] = useToggle(false);
const form = useAppForm({
defaultValues: { password: "" },
validators: { onSubmit: formSchema },
onSubmit: async ({ value }) => {
const toastId = toast.loading(t`Resetting your password...`);
const { error } = await authClient.resetPassword({ token, newPassword: value.password });
if (error) {
toast.error(
error.message ||
t({
comment: "Fallback toast when resetting password fails and no backend message is available",
message: "Failed to reset your password. Please try again.",
}),
{ id: toastId },
);
return;
}
toast.success(t`Your password has been reset successfully. You can now sign in with your new password.`, {
id: toastId,
});
void navigate({ to: "/auth/login" });
},
});
return (
<>
<div className="space-y-1 text-center">
<h1 className="font-bold text-2xl tracking-tight">
<Trans>Reset your password</Trans>
</h1>
<div className="text-muted-foreground">
<Trans>Please enter a new password for your account</Trans>
</div>
</div>
<form
className="space-y-6"
onSubmit={(event) => {
event.preventDefault();
event.stopPropagation();
void form.handleSubmit();
}}
>
<form.Field name="password">
{(field) => (
<FormItem hasError={field.state.meta.isTouched && field.state.meta.errors.length > 0}>
<FormLabel>
<Trans comment="Label for new password input on reset-password form">New Password</Trans>
</FormLabel>
<div className="flex items-center gap-x-1.5">
<FormControl
render={
<Input
min={6}
max={64}
type={showPassword ? "text" : "password"}
autoComplete="new-password"
name={field.name}
value={field.state.value}
onBlur={field.handleBlur}
onChange={(event) => field.handleChange(event.target.value)}
/>
}
/>
<Button
size="icon"
variant="ghost"
onClick={toggleShowPassword}
aria-label={
showPassword
? t({
comment: "Accessible label for button that hides password in reset-password form",
message: "Hide password",
})
: t({
comment: "Accessible label for button that reveals password in reset-password form",
message: "Show password",
})
}
>
{showPassword ? <EyeIcon /> : <EyeSlashIcon />}
</Button>
</div>
<FormMessage errors={field.state.meta.errors} />
</FormItem>
)}
</form.Field>
<Button type="submit" className="w-full">
<Trans comment="Primary action button label on reset-password form">Reset Password</Trans>
</Button>
</form>
</>
);
}
@@ -0,0 +1,154 @@
import { t } from "@lingui/core/macro";
import { Trans } from "@lingui/react/macro";
import { ORPCError } from "@orpc/client";
import { EyeIcon, EyeSlashIcon, LockOpenIcon } from "@phosphor-icons/react";
import { useMutation } from "@tanstack/react-query";
import { useNavigate } from "@tanstack/react-router";
import { useMemo } from "react";
import { toast } from "sonner";
import { useToggle } from "usehooks-ts";
import z from "zod";
import { Button } from "@reactive-resume/ui/components/button";
import { FormControl, FormItem, FormLabel, FormMessage } from "@reactive-resume/ui/components/form";
import { Input } from "@reactive-resume/ui/components/input";
import { getReadableErrorMessage } from "@/libs/error-message";
import { orpc } from "@/libs/orpc/client";
import { useAppForm } from "@/libs/tanstack-form";
const formSchema = z.object({
password: z.string().min(6).max(64),
});
type Props = {
redirectPath: string;
};
export function ResumePasswordPage({ redirectPath }: Props) {
const navigate = useNavigate();
const [showPassword, toggleShowPassword] = useToggle(false);
const { mutate: verifyPassword } = useMutation(orpc.resume.verifyPassword.mutationOptions());
const [username, slug] = useMemo(() => {
const [username, slug] = redirectPath.split("/").slice(1) as [string, string];
if (!username || !slug) throw navigate({ to: "/" });
return [username, slug];
}, [redirectPath, navigate]);
const form = useAppForm({
defaultValues: { password: "" },
validators: { onSubmit: formSchema },
onSubmit: async ({ value, formApi }) => {
const toastId = toast.loading(t`Verifying password...`);
verifyPassword(
{ username, slug, password: value.password },
{
onSuccess: () => {
toast.dismiss(toastId);
void navigate({ to: redirectPath, replace: true });
},
onError: (error) => {
if (error instanceof ORPCError && error.code === "INVALID_PASSWORD") {
toast.dismiss(toastId);
formApi.setFieldMeta("password", (meta) => ({
...meta,
isTouched: true,
errors: [{ message: t`The password you entered is incorrect` }],
errorMap: {
...meta.errorMap,
onSubmit: { message: t`The password you entered is incorrect` },
},
}));
} else {
toast.error(
getReadableErrorMessage(
error,
t({
comment: "Fallback toast when resume password verification fails unexpectedly",
message: "Failed to verify the password. Please try again.",
}),
),
{ id: toastId },
);
}
},
},
);
},
});
return (
<>
<div className="space-y-4 text-center">
<h1 className="font-bold text-2xl tracking-tight">
<Trans>The resume you are trying to access is password protected</Trans>
</h1>
<div className="text-muted-foreground leading-relaxed">
<Trans>Please enter the password shared with you by the owner of the resume to continue.</Trans>
</div>
</div>
<form
className="space-y-6"
onSubmit={(event) => {
event.preventDefault();
event.stopPropagation();
void form.handleSubmit();
}}
>
<form.Field name="password">
{(field) => (
<FormItem hasError={field.state.meta.isTouched && field.state.meta.errors.length > 0}>
<FormLabel>
<Trans comment="Label for password input on protected resume access form">Password</Trans>
</FormLabel>
<div className="flex items-center gap-x-1.5">
<FormControl
render={
<Input
min={6}
max={64}
type={showPassword ? "text" : "password"}
autoComplete="new-password"
name={field.name}
value={field.state.value}
onBlur={field.handleBlur}
onChange={(event) => field.handleChange(event.target.value)}
/>
}
/>
<Button
size="icon"
variant="ghost"
onClick={toggleShowPassword}
aria-label={
showPassword
? t({
comment: "Accessible label for button that hides password on protected resume screen",
message: "Hide password",
})
: t({
comment: "Accessible label for button that reveals password on protected resume screen",
message: "Show password",
})
}
>
{showPassword ? <EyeIcon /> : <EyeSlashIcon />}
</Button>
</div>
<FormMessage errors={field.state.meta.errors} />
</FormItem>
)}
</form.Field>
<Button type="submit" className="w-full">
<LockOpenIcon />
<Trans comment="Primary action button label to unlock a password-protected resume">Unlock</Trans>
</Button>
</form>
</>
);
}
@@ -0,0 +1,111 @@
import { t } from "@lingui/core/macro";
import { Trans } from "@lingui/react/macro";
import { ArrowLeftIcon, CheckIcon } from "@phosphor-icons/react";
import { Link, useNavigate, useRouter } from "@tanstack/react-router";
import { toast } from "sonner";
import z from "zod";
import { Button } from "@reactive-resume/ui/components/button";
import { FormControl, FormItem, FormMessage } from "@reactive-resume/ui/components/form";
import { Input } from "@reactive-resume/ui/components/input";
import { authClient } from "@/libs/auth/client";
import { useAppForm } from "@/libs/tanstack-form";
const formSchema = z.object({
code: z.string().trim(),
});
export function VerifyTwoFactorBackupPage() {
const router = useRouter();
const navigate = useNavigate();
const form = useAppForm({
defaultValues: { code: "" },
validators: { onSubmit: formSchema },
onSubmit: async ({ value }) => {
const toastId = toast.loading(t`Verifying backup code...`);
const formattedCode = `${value.code.slice(0, 5)}-${value.code.slice(5)}`;
const { error } = await authClient.twoFactor.verifyBackupCode({ code: formattedCode });
if (error) {
toast.error(
error.message ||
t({
comment: "Fallback toast when verifying a backup two-factor authentication code fails",
message: "Failed to verify your backup code. Please try again.",
}),
{ id: toastId },
);
return;
}
toast.dismiss(toastId);
await router.invalidate();
void navigate({ to: "/dashboard", replace: true });
},
});
return (
<>
<div className="space-y-1 text-center">
<h1 className="font-bold text-2xl tracking-tight">
<Trans>Verify with a Backup Code</Trans>
</h1>
<div className="text-muted-foreground">
<Trans>Enter one of your saved backup codes to access your account</Trans>
</div>
</div>
<form
className="grid gap-6"
onSubmit={(event) => {
event.preventDefault();
event.stopPropagation();
void form.handleSubmit();
}}
>
<form.Field name="code">
{(field) => (
<FormItem
className="justify-self-center"
hasError={field.state.meta.isTouched && field.state.meta.errors.length > 0}
>
<FormControl
render={
<Input
maxLength={10}
className="max-w-xs"
name={field.name}
value={field.state.value}
onBlur={field.handleBlur}
onChange={(event) => field.handleChange(event.target.value)}
/>
}
/>
<FormMessage errors={field.state.meta.errors} />
</FormItem>
)}
</form.Field>
<div className="flex gap-x-2">
<Button
variant="outline"
className="flex-1"
nativeButton={false}
render={
<Link to="/auth/verify-2fa">
<ArrowLeftIcon />
<Trans comment="Secondary navigation button on backup-code verification screen">Go Back</Trans>
</Link>
}
/>
<Button type="submit" className="flex-1">
<CheckIcon />
<Trans comment="Primary action button to submit backup code">Verify</Trans>
</Button>
</div>
</form>
</>
);
}
@@ -0,0 +1,126 @@
import { t } from "@lingui/core/macro";
import { Trans } from "@lingui/react/macro";
import { ArrowLeftIcon, CheckIcon } from "@phosphor-icons/react";
import { Link, useNavigate, useRouter } from "@tanstack/react-router";
import { toast } from "sonner";
import z from "zod";
import { Button } from "@reactive-resume/ui/components/button";
import { FormControl, FormItem, FormMessage } from "@reactive-resume/ui/components/form";
import { Input } from "@reactive-resume/ui/components/input";
import { authClient } from "@/libs/auth/client";
import { useAppForm } from "@/libs/tanstack-form";
const formSchema = z.object({
code: z.string().length(6, "Code must be 6 digits"),
});
export function VerifyTwoFactorPage() {
const router = useRouter();
const navigate = useNavigate();
const form = useAppForm({
defaultValues: { code: "" },
validators: { onSubmit: formSchema },
onSubmit: async ({ value }) => {
const toastId = toast.loading(t`Verifying code...`);
const { error } = await authClient.twoFactor.verifyTotp({
code: value.code,
});
if (error) {
toast.error(
error.message ||
t({
comment: "Fallback toast when verifying a two-factor authentication code fails",
message: "Failed to verify your code. Please try again.",
}),
{ id: toastId },
);
return;
}
toast.dismiss(toastId);
await router.invalidate();
void navigate({ to: "/dashboard", replace: true });
},
});
return (
<>
<div className="space-y-1 text-center">
<h1 className="font-bold text-2xl tracking-tight">
<Trans>Two-Factor Authentication</Trans>
</h1>
<div className="text-muted-foreground">
<Trans>Enter the verification code from your authenticator app</Trans>
</div>
</div>
<form
className="grid gap-6"
onSubmit={(event) => {
event.preventDefault();
event.stopPropagation();
void form.handleSubmit();
}}
>
<form.Field name="code">
{(field) => (
<FormItem
className="justify-self-center"
hasError={field.state.meta.isTouched && field.state.meta.errors.length > 0}
>
<FormControl
render={
<Input
type="number"
maxLength={6}
className="max-w-xs"
name={field.name}
value={field.state.value}
onBlur={field.handleBlur}
onChange={(event) => field.handleChange(event.target.value)}
/>
}
/>
<FormMessage errors={field.state.meta.errors} />
</FormItem>
)}
</form.Field>
<div className="flex gap-x-2">
<Button
variant="outline"
className="flex-1"
nativeButton={false}
render={
<Link to="/auth/login">
<ArrowLeftIcon />
<Trans comment="Secondary navigation button on 2FA verification screen">Back to Login</Trans>
</Link>
}
/>
<Button type="submit" className="flex-1">
<CheckIcon />
<Trans comment="Primary action button to submit 2FA code">Verify</Trans>
</Button>
</div>
</form>
<Button
variant="link"
nativeButton={false}
className="h-auto justify-self-center p-0 text-sm"
render={
<Link to="/auth/verify-2fa-backup">
<Trans comment="Link to backup-code verification flow when authenticator app is unavailable">
Lost access to your authenticator?
</Trans>
</Link>
}
/>
</>
);
}