mirror of
https://github.com/AmruthPillai/Reactive-Resume.git
synced 2026-07-25 09:24:54 +10:00
08e9c80037
Normalize frontend error rendering and tighten input/path handling across auth, builder, dashboard, and shared components for more resilient UX behavior. Made-with: Cursor
284 lines
8.8 KiB
TypeScript
284 lines
8.8 KiB
TypeScript
import { zodResolver } from "@hookform/resolvers/zod";
|
|
import { t } from "@lingui/core/macro";
|
|
import { Trans } from "@lingui/react/macro";
|
|
import { CheckIcon, UserCircleIcon, WarningIcon } from "@phosphor-icons/react";
|
|
import { createFileRoute, useRouter } from "@tanstack/react-router";
|
|
import { AnimatePresence, motion } from "motion/react";
|
|
import { useMemo } from "react";
|
|
import { useForm } from "react-hook-form";
|
|
import { toast } from "sonner";
|
|
import { match } from "ts-pattern";
|
|
import z from "zod";
|
|
|
|
import { Button } from "@/components/ui/button";
|
|
import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from "@/components/ui/form";
|
|
import { Input } from "@/components/ui/input";
|
|
import { Separator } from "@/components/ui/separator";
|
|
import { authClient } from "@/integrations/auth/client";
|
|
import { getReadableErrorMessage } from "@/utils/error-message";
|
|
|
|
import { DashboardHeader } from "../-components/header";
|
|
|
|
export const Route = createFileRoute("/dashboard/settings/profile")({
|
|
component: RouteComponent,
|
|
});
|
|
|
|
const formSchema = z.object({
|
|
name: z.string().trim().min(1).max(64),
|
|
username: z
|
|
.string()
|
|
.trim()
|
|
.min(1)
|
|
.max(64)
|
|
.regex(/^[a-z0-9._-]+$/, {
|
|
message: "Username can only contain lowercase letters, numbers, dots, hyphens and underscores.",
|
|
}),
|
|
email: z.email().trim(),
|
|
});
|
|
|
|
type FormValues = z.infer<typeof formSchema>;
|
|
|
|
function RouteComponent() {
|
|
const router = useRouter();
|
|
const { session } = Route.useRouteContext();
|
|
|
|
const defaultValues = useMemo(() => {
|
|
return {
|
|
name: session.user.name,
|
|
username: session.user.username,
|
|
email: session.user.email,
|
|
};
|
|
}, [session.user]);
|
|
|
|
const form = useForm<FormValues>({
|
|
resolver: zodResolver(formSchema),
|
|
defaultValues,
|
|
});
|
|
|
|
const onCancel = () => {
|
|
form.reset(defaultValues);
|
|
};
|
|
|
|
const onSubmit = async (data: FormValues) => {
|
|
const { error } = await authClient.updateUser({
|
|
name: data.name,
|
|
username: data.username,
|
|
displayUsername: data.username,
|
|
});
|
|
|
|
if (error) {
|
|
toast.error(
|
|
getReadableErrorMessage(
|
|
error,
|
|
t({
|
|
comment: "Fallback toast when updating profile details fails",
|
|
message: "Failed to update your profile. Please try again.",
|
|
}),
|
|
),
|
|
);
|
|
return;
|
|
}
|
|
|
|
toast.success(t`Your profile has been updated successfully.`);
|
|
form.reset({ name: data.name, username: data.username, email: session.user.email });
|
|
void router.invalidate();
|
|
|
|
if (data.email !== session.user.email) {
|
|
const { error } = await authClient.changeEmail({
|
|
newEmail: data.email,
|
|
callbackURL: "/dashboard/settings/profile",
|
|
});
|
|
|
|
if (error) {
|
|
toast.error(
|
|
getReadableErrorMessage(
|
|
error,
|
|
t({
|
|
comment: "Fallback toast when requesting email change confirmation fails",
|
|
message: "Failed to request email change. Please try again.",
|
|
}),
|
|
),
|
|
);
|
|
return;
|
|
}
|
|
|
|
toast.success(
|
|
t`A confirmation link has been sent to your current email address. Please check your inbox to confirm the change.`,
|
|
);
|
|
form.reset({ name: data.name, username: data.username, email: session.user.email });
|
|
void router.invalidate();
|
|
}
|
|
};
|
|
|
|
const handleResendVerificationEmail = async () => {
|
|
const toastId = toast.loading(t`Resending verification email...`);
|
|
|
|
const { error } = await authClient.sendVerificationEmail({
|
|
email: session.user.email,
|
|
callbackURL: "/dashboard/settings/profile",
|
|
});
|
|
|
|
if (error) {
|
|
toast.error(
|
|
getReadableErrorMessage(
|
|
error,
|
|
t({
|
|
comment: "Fallback toast when resending account verification email fails",
|
|
message: "Failed to resend verification email. Please try again.",
|
|
}),
|
|
),
|
|
{ id: toastId },
|
|
);
|
|
return;
|
|
}
|
|
|
|
toast.success(
|
|
t`A new verification link has been sent to your email address. Please check your inbox to verify your account.`,
|
|
{ id: toastId },
|
|
);
|
|
void router.invalidate();
|
|
};
|
|
|
|
return (
|
|
<div className="space-y-4">
|
|
<DashboardHeader icon={UserCircleIcon} title={t`Profile`} />
|
|
|
|
<Separator />
|
|
|
|
<Form {...form}>
|
|
<motion.form
|
|
initial={{ opacity: 0, y: -20 }}
|
|
animate={{ opacity: 1, y: 0 }}
|
|
transition={{ duration: 0.25, ease: "easeOut" }}
|
|
className="grid max-w-xl gap-6 will-change-[transform,opacity]"
|
|
onSubmit={form.handleSubmit(onSubmit)}
|
|
>
|
|
<FormField
|
|
control={form.control}
|
|
name="name"
|
|
render={({ field }) => (
|
|
<FormItem>
|
|
<FormLabel>
|
|
<Trans>Name</Trans>
|
|
</FormLabel>
|
|
<FormControl
|
|
render={
|
|
<Input
|
|
min={3}
|
|
max={64}
|
|
autoComplete="name"
|
|
placeholder={t({
|
|
comment: "Example full name placeholder on profile settings form",
|
|
message: "John Doe",
|
|
})}
|
|
{...field}
|
|
/>
|
|
}
|
|
/>
|
|
<FormMessage />
|
|
</FormItem>
|
|
)}
|
|
/>
|
|
|
|
<FormField
|
|
control={form.control}
|
|
name="username"
|
|
render={({ field }) => (
|
|
<FormItem>
|
|
<FormLabel>
|
|
<Trans>Username</Trans>
|
|
</FormLabel>
|
|
<FormControl
|
|
render={
|
|
<Input
|
|
min={3}
|
|
max={64}
|
|
autoComplete="username"
|
|
placeholder={t({
|
|
comment: "Example username placeholder on profile settings form",
|
|
message: "john.doe",
|
|
})}
|
|
className="lowercase"
|
|
{...field}
|
|
/>
|
|
}
|
|
/>
|
|
<FormMessage />
|
|
</FormItem>
|
|
)}
|
|
/>
|
|
|
|
<FormField
|
|
control={form.control}
|
|
name="email"
|
|
render={({ field }) => (
|
|
<FormItem>
|
|
<FormLabel>
|
|
<Trans>Email Address</Trans>
|
|
</FormLabel>
|
|
<FormControl
|
|
render={
|
|
<Input
|
|
type="email"
|
|
autoComplete="email"
|
|
placeholder={t({
|
|
comment: "Example email placeholder on profile settings form",
|
|
message: "john.doe@example.com",
|
|
})}
|
|
className="lowercase"
|
|
{...field}
|
|
/>
|
|
}
|
|
/>
|
|
<FormMessage />
|
|
{match(session.user.emailVerified)
|
|
.with(true, () => (
|
|
<p className="flex items-center gap-x-1.5 text-xs text-green-700">
|
|
<CheckIcon />
|
|
<Trans>Verified</Trans>
|
|
</p>
|
|
))
|
|
.with(false, () => (
|
|
<p className="flex items-center gap-x-1.5 text-xs text-amber-600">
|
|
<WarningIcon className="size-3.5" />
|
|
<Trans>Unverified</Trans>
|
|
<span>|</span>
|
|
<Button
|
|
variant="link"
|
|
className="h-auto gap-x-1.5 p-0! text-xs text-inherit"
|
|
onClick={handleResendVerificationEmail}
|
|
>
|
|
<Trans>Resend verification email</Trans>
|
|
</Button>
|
|
</p>
|
|
))
|
|
.exhaustive()}
|
|
</FormItem>
|
|
)}
|
|
/>
|
|
|
|
<AnimatePresence initial={false} mode="popLayout">
|
|
{form.formState.isDirty && (
|
|
<motion.div
|
|
initial={{ opacity: 0, y: -8 }}
|
|
animate={{ opacity: 1, y: 0 }}
|
|
exit={{ opacity: 0, y: -8 }}
|
|
transition={{ duration: 0.16, ease: "easeOut" }}
|
|
className="flex items-center gap-x-4 justify-self-end will-change-[transform,opacity]"
|
|
>
|
|
<Button type="reset" variant="ghost" onClick={onCancel}>
|
|
<Trans comment="Profile settings form action to discard unsaved edits">Cancel</Trans>
|
|
</Button>
|
|
|
|
<Button type="submit">
|
|
<Trans>Save Changes</Trans>
|
|
</Button>
|
|
</motion.div>
|
|
)}
|
|
</AnimatePresence>
|
|
</motion.form>
|
|
</Form>
|
|
</div>
|
|
);
|
|
}
|