mirror of
https://github.com/AmruthPillai/Reactive-Resume.git
synced 2026-09-29 16:24:22 +10:00
Rewrites the landing page, in-app microcopy, and public docs, then fixes what the rewrite exposed: stale template counts, a broken quickstart anchor, out-of-sync FAQ structured data, dead error-hint branches in the MCP tools, and wrong-sense translations across all 53 locales. Adds GLOSSARY.md so translators get the right sense of the ambiguous UI terms.
152 lines
4.6 KiB
TypeScript
152 lines
4.6 KiB
TypeScript
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 { 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 { toast } from "@reactive-resume/ui/components/toast";
|
|
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] = redirectPath.split("/").slice(1) as [string, string];
|
|
if (!username || !slug) throw navigate({ to: "/" });
|
|
|
|
const form = useAppForm({
|
|
defaultValues: { password: "" },
|
|
validators: { onSubmit: formSchema },
|
|
onSubmit: ({ value, formApi }) => {
|
|
const toastId = toast.add({ type: "loading", description: t`Verifying password...` });
|
|
|
|
verifyPassword(
|
|
{ username, slug, password: value.password },
|
|
{
|
|
onSuccess: () => {
|
|
toast.close(toastId);
|
|
void navigate({ to: redirectPath, replace: true });
|
|
},
|
|
onError: (error) => {
|
|
if (error instanceof ORPCError && error.code === "INVALID_PASSWORD") {
|
|
toast.close(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.add({
|
|
type: "error",
|
|
description: 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-semibold text-2xl tracking-tight">
|
|
<Trans>This resume is password protected</Trans>
|
|
</h1>
|
|
|
|
<div className="text-muted-foreground leading-relaxed">
|
|
<Trans>Enter the password the resume owner shared with you.</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>
|
|
</>
|
|
);
|
|
}
|