diff --git a/apps/server/src/http/health.ts b/apps/server/src/http/health.ts index 0f7e8e8fb..27fd46073 100644 --- a/apps/server/src/http/health.ts +++ b/apps/server/src/http/health.ts @@ -37,9 +37,7 @@ async function checkDatabase() { return { status: "healthy" }; } -async function checkStorage() { - return getStorageService().healthcheck(); -} +const checkStorage = () => getStorageService().healthcheck(); export async function handleHealth() { const [database, storage] = await Promise.all([runCheck(checkDatabase), runCheck(checkStorage)]); diff --git a/apps/server/src/mcp/handler.ts b/apps/server/src/mcp/handler.ts index 1ccfd0a31..5443a8b08 100644 --- a/apps/server/src/mcp/handler.ts +++ b/apps/server/src/mcp/handler.ts @@ -7,7 +7,7 @@ export async function handleMcp(request: Request) { try { await authenticateRequest(request); - const server = await createMcpServer(request); + const server = createMcpServer(request); const transport = new WebStandardStreamableHTTPServerTransport({ enableJsonResponse: true, }); diff --git a/apps/server/src/mcp/server.ts b/apps/server/src/mcp/server.ts index 80e3a1735..6cbce3c61 100644 --- a/apps/server/src/mcp/server.ts +++ b/apps/server/src/mcp/server.ts @@ -22,7 +22,7 @@ function createRequestClient(request: Request): RouterClient { }); } -export async function createMcpServer(request: Request) { +export function createMcpServer(request: Request) { const server = new McpServer( { name: "reactive-resume", diff --git a/apps/server/src/openapi/metadata.ts b/apps/server/src/openapi/metadata.ts index 9972a92a4..8ace7869b 100644 --- a/apps/server/src/openapi/metadata.ts +++ b/apps/server/src/openapi/metadata.ts @@ -4,8 +4,8 @@ import { env } from "@reactive-resume/env/server"; import { buildMcpServerCard } from "@reactive-resume/mcp/server-card"; import { appVersion } from "../app-version"; -const oauthAuthorizationServerHandler = oauthProviderAuthServerMetadata(auth); -const openIdConfigurationHandler = oauthProviderOpenIdConfigMetadata(auth); +export const handleOAuthAuthorizationServer = oauthProviderAuthServerMetadata(auth); +export const handleOpenIdConfiguration = oauthProviderOpenIdConfigMetadata(auth); export function handleWellKnownFallback() { return new Response("OK", { status: 200 }); @@ -20,15 +20,7 @@ export function handleMcpServerCard() { }); } -export function handleOAuthAuthorizationServer(request: Request) { - return oauthAuthorizationServerHandler(request); -} - -export function handleOpenIdConfiguration(request: Request) { - return openIdConfigurationHandler(request); -} - -export async function handleOAuthProtectedResource() { +export function handleOAuthProtectedResource() { const metadata = { resource: env.APP_URL, bearer_methods_supported: ["header"], diff --git a/apps/web/src/components/input/chip-input.tsx b/apps/web/src/components/input/chip-input.tsx index 7284d5ef5..b0218a8fc 100644 --- a/apps/web/src/components/input/chip-input.tsx +++ b/apps/web/src/components/input/chip-input.tsx @@ -176,7 +176,7 @@ export function ChipInput({ }); if (nextValues.length === 0) return; - const newChips = Array.from(new Set([...chips, ...nextValues])); + const newChips = [...new Set([...chips, ...nextValues])]; setChips(newChips); }, [chips, setChips], @@ -269,7 +269,7 @@ export function ChipInput({ const oldIndex = chips.indexOf(active.id as string); const newIndex = chips.indexOf(over.id as string); if (oldIndex !== -1 && newIndex !== -1 && oldIndex !== newIndex) { - const newOrder = Array.from(chips); + const newOrder = [...chips]; const [removed] = newOrder.splice(oldIndex, 1); newOrder.splice(newIndex, 0, removed); handleReorder(newOrder); diff --git a/apps/web/src/components/input/icon-picker.tsx b/apps/web/src/components/input/icon-picker.tsx index 02c539ee3..d0d1b7db6 100644 --- a/apps/web/src/components/input/icon-picker.tsx +++ b/apps/web/src/components/input/icon-picker.tsx @@ -3,7 +3,7 @@ import type { CellComponentProps } from "react-window"; import { t } from "@lingui/core/macro"; import { ProhibitIcon } from "@phosphor-icons/react"; import Fuse from "fuse.js"; -import { memo, useCallback, useMemo, useState } from "react"; +import { useMemo, useState } from "react"; import { Grid } from "react-window"; import { icons } from "@reactive-resume/schema/icons"; import { Button } from "@reactive-resume/ui/components/button"; @@ -14,6 +14,7 @@ import { cn } from "@reactive-resume/utils/style"; const columnCount = 8; const columnWidth = 36; const rowHeight = 36; +const iconSearch = new Fuse(icons, { threshold: 0.35 }); type IconSearchInputProps = { value: string; @@ -21,7 +22,7 @@ type IconSearchInputProps = { className?: string; }; -function _IconSearchInput(props: IconSearchInputProps) { +function IconSearchInput(props: IconSearchInputProps) { return ( void; @@ -70,18 +67,9 @@ function IconCellComponent({ columnIndex, rowIndex, style, icons, onChange }: Ic ); } -function useIconSearch() { - const fuse = useMemo(() => new Fuse(icons, { threshold: 0.35 }), []); - - const search = useCallback( - (query: string): IconName[] => { - if (!query.trim()) return Array.from(icons); - return fuse.search(query).map((result) => result.item); - }, - [fuse], - ); - - return search; +function searchIcons(query: string): IconName[] { + if (!query.trim()) return [...icons]; + return iconSearch.search(query).map((result) => result.item); } type IconPickerProps = Omit, "value" | "onChange"> & { @@ -91,12 +79,10 @@ type IconPickerProps = Omit, "value" | "onCh }; export function IconPicker({ value, onChange, popoverProps, ...props }: IconPickerProps) { - const searchIcons = useIconSearch(); - const [search, setSearch] = useState(""); - const searchedIcons = useMemo(() => searchIcons(search), [search, searchIcons]); - const rowCount = useMemo(() => Math.ceil(searchedIcons.length / columnCount), [searchedIcons]); + const searchedIcons = useMemo(() => searchIcons(search), [search]); + const rowCount = Math.ceil(searchedIcons.length / columnCount); return ( diff --git a/apps/web/src/components/typography/get-next-weights.ts b/apps/web/src/components/typography/get-next-weights.ts index d2e80d826..ac9bbbf69 100644 --- a/apps/web/src/components/typography/get-next-weights.ts +++ b/apps/web/src/components/typography/get-next-weights.ts @@ -6,7 +6,7 @@ export function getNextWeights(fontFamily: string): Weight[] | null { const fontData = getFont(fontFamily); if (!fontData || !Array.isArray(fontData.weights) || fontData.weights.length === 0) return null; - const uniqueWeights = Array.from(new Set(fontData.weights)) as Weight[]; + const uniqueWeights = [...new Set(fontData.weights)] as Weight[]; // Try to pick 400 and 600 if available const weights: Weight[] = []; diff --git a/apps/web/src/dialogs/resume/index.tsx b/apps/web/src/dialogs/resume/index.tsx index 05f8d54d5..5bc5c8c44 100644 --- a/apps/web/src/dialogs/resume/index.tsx +++ b/apps/web/src/dialogs/resume/index.tsx @@ -284,7 +284,7 @@ export function DuplicateResumeDialog({ data }: DialogProps<"resume.duplicate">) const toastId = toast.loading(t`Duplicating your resume...`); duplicateResume(value, { - onSuccess: async (id) => { + onSuccess: (id) => { toast.success(t`Your resume has been duplicated successfully.`, { id: toastId }); closeDialog(); diff --git a/apps/web/src/dialogs/resume/sections/award.tsx b/apps/web/src/dialogs/resume/sections/award.tsx index a19e716a7..4d90a905a 100644 --- a/apps/web/src/dialogs/resume/sections/award.tsx +++ b/apps/web/src/dialogs/resume/sections/award.tsx @@ -7,8 +7,6 @@ import { awardItemSchema } from "@reactive-resume/schema/resume/data"; import { FormControl, FormItem, FormLabel, FormMessage } from "@reactive-resume/ui/components/form"; import { Input } from "@reactive-resume/ui/components/input"; import { Switch } from "@reactive-resume/ui/components/switch"; -import { RichInput } from "@/components/input/rich-input"; -import { URLInput } from "@/components/input/url-input"; import { useDialogStore } from "@/dialogs/store"; import { useUpdateResumeData } from "@/features/resume/builder/draft"; import { useFormBlocker } from "@/hooks/use-form-blocker"; @@ -38,7 +36,7 @@ export function CreateAwardDialog({ data }: DialogProps<"resume.sections.awards. const form = useAppForm({ defaultValues: makeSectionItem(defaultValues, data?.item), validators: { onSubmit: formSchema }, - onSubmit: async ({ value }) => { + onSubmit: ({ value }) => { updateResumeData((draft) => { createSectionItem(draft, "awards", value, data?.customSectionId); }); @@ -70,7 +68,7 @@ export function UpdateAwardDialog({ data }: DialogProps<"resume.sections.awards. const form = useAppForm({ defaultValues: data.item, validators: { onSubmit: formSchema }, - onSubmit: async ({ value }) => { + onSubmit: ({ value }) => { updateResumeData((draft) => { updateSectionItem(draft, "awards", value, data?.customSectionId); }); @@ -127,21 +125,9 @@ const AwardForm = withForm({ {(field) => Date} />} - - {(field) => ( - 0}> - - Website - - field.handleChange(v)} - hideLabelButton={inlineLink} - /> - - - )} - + + {(field) => Website} hideLabelButton={inlineLink} />} + {(field) => ( @@ -163,20 +149,9 @@ const AwardForm = withForm({ )} - - {(field) => ( - 0} - > - - Description - - field.handleChange(v)} />} /> - - - )} - + + {(field) => Description} formItemClassName="sm:col-span-full" />} + ); }, diff --git a/apps/web/src/dialogs/resume/sections/certification.tsx b/apps/web/src/dialogs/resume/sections/certification.tsx index 2d19c5cd1..0ed6ee47b 100644 --- a/apps/web/src/dialogs/resume/sections/certification.tsx +++ b/apps/web/src/dialogs/resume/sections/certification.tsx @@ -4,10 +4,8 @@ import { Trans } from "@lingui/react/macro"; import { PencilSimpleLineIcon, PlusIcon } from "@phosphor-icons/react"; import { useStore } from "@tanstack/react-form"; import { certificationItemSchema } from "@reactive-resume/schema/resume/data"; -import { FormControl, FormItem, FormLabel, FormMessage } from "@reactive-resume/ui/components/form"; +import { FormControl, FormItem, FormLabel } from "@reactive-resume/ui/components/form"; import { Switch } from "@reactive-resume/ui/components/switch"; -import { RichInput } from "@/components/input/rich-input"; -import { URLInput } from "@/components/input/url-input"; import { useDialogStore } from "@/dialogs/store"; import { useUpdateResumeData } from "@/features/resume/builder/draft"; import { useFormBlocker } from "@/hooks/use-form-blocker"; @@ -37,7 +35,7 @@ export function CreateCertificationDialog({ data }: DialogProps<"resume.sections const form = useAppForm({ defaultValues: makeSectionItem(defaultValues, data?.item), validators: { onSubmit: formSchema }, - onSubmit: async ({ value }) => { + onSubmit: ({ value }) => { updateResumeData((draft) => { createSectionItem(draft, "certifications", value, data?.customSectionId); }); @@ -69,7 +67,7 @@ export function UpdateCertificationDialog({ data }: DialogProps<"resume.sections const form = useAppForm({ defaultValues: data.item, validators: { onSubmit: formSchema }, - onSubmit: async ({ value }) => { + onSubmit: ({ value }) => { updateResumeData((draft) => { updateSectionItem(draft, "certifications", value, data?.customSectionId); }); @@ -107,21 +105,9 @@ const CertificationForm = withForm({ {(field) => Date} />} - - {(field) => ( - 0}> - - Website - - field.handleChange(v)} - hideLabelButton={inlineLink} - /> - - - )} - + + {(field) => Website} hideLabelButton={inlineLink} />} + {(field) => ( @@ -143,20 +129,9 @@ const CertificationForm = withForm({ )} - - {(field) => ( - 0} - > - - Description - - field.handleChange(v)} />} /> - - - )} - + + {(field) => Description} formItemClassName="sm:col-span-full" />} + ); }, diff --git a/apps/web/src/dialogs/resume/sections/cover-letter.tsx b/apps/web/src/dialogs/resume/sections/cover-letter.tsx index 9b832c6ac..ea22d497e 100644 --- a/apps/web/src/dialogs/resume/sections/cover-letter.tsx +++ b/apps/web/src/dialogs/resume/sections/cover-letter.tsx @@ -31,7 +31,7 @@ export function CreateCoverLetterDialog({ data }: DialogProps<"resume.sections.c const form = useAppForm({ defaultValues: makeSectionItem(defaultValues, data?.item), validators: { onSubmit: formSchema }, - onSubmit: async ({ value }) => { + onSubmit: ({ value }) => { updateResumeData((draft) => { if (data?.customSectionId) { const section = draft.customSections.find((s) => s.id === data.customSectionId); @@ -67,7 +67,7 @@ export function UpdateCoverLetterDialog({ data }: DialogProps<"resume.sections.c const form = useAppForm({ defaultValues: data.item, validators: { onSubmit: formSchema }, - onSubmit: async ({ value }) => { + onSubmit: ({ value }) => { updateResumeData((draft) => { if (data?.customSectionId) { const section = draft.customSections.find((s) => s.id === data.customSectionId); diff --git a/apps/web/src/dialogs/resume/sections/custom.tsx b/apps/web/src/dialogs/resume/sections/custom.tsx index 40f1b07e5..35242998a 100644 --- a/apps/web/src/dialogs/resume/sections/custom.tsx +++ b/apps/web/src/dialogs/resume/sections/custom.tsx @@ -88,7 +88,7 @@ export function CreateCustomSectionDialog({ data }: DialogProps<"resume.sections items: data?.items ?? [], }, validators: { onSubmit: formSchema }, - onSubmit: async ({ value }) => { + onSubmit: ({ value }) => { updateResumeData((draft) => { draft.customSections.push(value); const lastPageIndex = draft.metadata.layout.pages.length - 1; @@ -146,7 +146,7 @@ export function UpdateCustomSectionDialog({ data }: DialogProps<"resume.sections icon: data.icon ?? "", }, validators: { onSubmit: formSchema }, - onSubmit: async ({ value }) => { + onSubmit: ({ value }) => { updateResumeData((draft) => { const index = draft.customSections.findIndex((item) => item.id === value.id); if (index === -1) return; diff --git a/apps/web/src/dialogs/resume/sections/education.tsx b/apps/web/src/dialogs/resume/sections/education.tsx index 2253a266b..e5c982c9e 100644 --- a/apps/web/src/dialogs/resume/sections/education.tsx +++ b/apps/web/src/dialogs/resume/sections/education.tsx @@ -4,10 +4,8 @@ import { Trans } from "@lingui/react/macro"; import { PencilSimpleLineIcon, PlusIcon } from "@phosphor-icons/react"; import { useStore } from "@tanstack/react-form"; import { educationItemSchema } from "@reactive-resume/schema/resume/data"; -import { FormControl, FormItem, FormLabel, FormMessage } from "@reactive-resume/ui/components/form"; +import { FormControl, FormItem, FormLabel } from "@reactive-resume/ui/components/form"; import { Switch } from "@reactive-resume/ui/components/switch"; -import { RichInput } from "@/components/input/rich-input"; -import { URLInput } from "@/components/input/url-input"; import { useDialogStore } from "@/dialogs/store"; import { useUpdateResumeData } from "@/features/resume/builder/draft"; import { useFormBlocker } from "@/hooks/use-form-blocker"; @@ -40,7 +38,7 @@ export function CreateEducationDialog({ data }: DialogProps<"resume.sections.edu const form = useAppForm({ defaultValues: makeSectionItem(defaultValues, data?.item), validators: { onSubmit: formSchema }, - onSubmit: async ({ value }) => { + onSubmit: ({ value }) => { updateResumeData((draft) => { createSectionItem(draft, "education", value, data?.customSectionId); }); @@ -72,7 +70,7 @@ export function UpdateEducationDialog({ data }: DialogProps<"resume.sections.edu const form = useAppForm({ defaultValues: data.item, validators: { onSubmit: formSchema }, - onSubmit: async ({ value }) => { + onSubmit: ({ value }) => { updateResumeData((draft) => { updateSectionItem(draft, "education", value, data?.customSectionId); }); @@ -116,24 +114,15 @@ const EducationForm = withForm({ {(field) => Period} />} - + {(field) => ( - 0} - > - - Website - - field.handleChange(v)} - hideLabelButton={inlineLink} - /> - - + Website} + formItemClassName="sm:col-span-full" + hideLabelButton={inlineLink} + /> )} - + {(field) => ( @@ -155,20 +144,9 @@ const EducationForm = withForm({ )} - - {(field) => ( - 0} - > - - Description - - field.handleChange(v)} />} /> - - - )} - + + {(field) => Description} formItemClassName="sm:col-span-full" />} + ); }, diff --git a/apps/web/src/dialogs/resume/sections/experience.tsx b/apps/web/src/dialogs/resume/sections/experience.tsx index 26bba3047..8e029b214 100644 --- a/apps/web/src/dialogs/resume/sections/experience.tsx +++ b/apps/web/src/dialogs/resume/sections/experience.tsx @@ -12,7 +12,6 @@ import { Input } from "@reactive-resume/ui/components/input"; import { Switch } from "@reactive-resume/ui/components/switch"; import { generateId } from "@reactive-resume/utils/string"; import { RichInput } from "@/components/input/rich-input"; -import { URLInput } from "@/components/input/url-input"; import { useDialogStore } from "@/dialogs/store"; import { useUpdateResumeData } from "@/features/resume/builder/draft"; import { useFormBlocker } from "@/hooks/use-form-blocker"; @@ -44,7 +43,7 @@ export function CreateExperienceDialog({ data }: DialogProps<"resume.sections.ex const form = useAppForm({ defaultValues: makeSectionItem(defaultValues, data?.item), validators: { onSubmit: formSchema }, - onSubmit: async ({ value }) => { + onSubmit: ({ value }) => { updateResumeData((draft) => { createSectionItem(draft, "experience", value, data?.customSectionId); }); @@ -76,7 +75,7 @@ export function UpdateExperienceDialog({ data }: DialogProps<"resume.sections.ex const form = useAppForm({ defaultValues: data.item, validators: { onSubmit: formSchema }, - onSubmit: async ({ value }) => { + onSubmit: ({ value }) => { updateResumeData((draft) => { updateSectionItem(draft, "experience", value, data?.customSectionId); }); @@ -122,24 +121,15 @@ const ExperienceForm = withForm({ {(field) => Period} />} - + {(field) => ( - 0} - > - - Website - - field.handleChange(v)} - hideLabelButton={inlineLink} - /> - - + Website} + formItemClassName="sm:col-span-full" + hideLabelButton={inlineLink} + /> )} - + {(field) => ( @@ -219,20 +209,9 @@ const ExperienceForm = withForm({ {/* Single Role Description — only show when no roles are defined */} {!hasRoles && ( - - {(field) => ( - 0} - > - - Description - - field.handleChange(v)} />} /> - - - )} - + + {(field) => Description} formItemClassName="sm:col-span-full" />} + )} ); diff --git a/apps/web/src/dialogs/resume/sections/interest.tsx b/apps/web/src/dialogs/resume/sections/interest.tsx index b62e9d288..b308b92f3 100644 --- a/apps/web/src/dialogs/resume/sections/interest.tsx +++ b/apps/web/src/dialogs/resume/sections/interest.tsx @@ -39,7 +39,7 @@ export function CreateInterestDialog({ data }: DialogProps<"resume.sections.inte const form = useAppForm({ defaultValues: makeSectionItem(defaultValues, data?.item), validators: { onSubmit: formSchema }, - onSubmit: async ({ value }) => { + onSubmit: ({ value }) => { updateResumeData((draft) => { createSectionItem(draft, "interests", value, data?.customSectionId); }); @@ -71,7 +71,7 @@ export function UpdateInterestDialog({ data }: DialogProps<"resume.sections.inte const form = useAppForm({ defaultValues: data.item, validators: { onSubmit: formSchema }, - onSubmit: async ({ value }) => { + onSubmit: ({ value }) => { updateResumeData((draft) => { updateSectionItem(draft, "interests", value, data?.customSectionId); }); diff --git a/apps/web/src/dialogs/resume/sections/language.tsx b/apps/web/src/dialogs/resume/sections/language.tsx index 14a2c3fdb..5d7ff45e1 100644 --- a/apps/web/src/dialogs/resume/sections/language.tsx +++ b/apps/web/src/dialogs/resume/sections/language.tsx @@ -34,7 +34,7 @@ export function CreateLanguageDialog({ data }: DialogProps<"resume.sections.lang const form = useAppForm({ defaultValues: makeSectionItem(defaultValues, data?.item), validators: { onSubmit: formSchema }, - onSubmit: async ({ value }) => { + onSubmit: ({ value }) => { updateResumeData((draft) => { createSectionItem(draft, "languages", value, data?.customSectionId); }); @@ -66,7 +66,7 @@ export function UpdateLanguageDialog({ data }: DialogProps<"resume.sections.lang const form = useAppForm({ defaultValues: data.item, validators: { onSubmit: formSchema }, - onSubmit: async ({ value }) => { + onSubmit: ({ value }) => { updateResumeData((draft) => { updateSectionItem(draft, "languages", value, data?.customSectionId); }); diff --git a/apps/web/src/dialogs/resume/sections/profile.tsx b/apps/web/src/dialogs/resume/sections/profile.tsx index b318a7351..8c68aeeea 100644 --- a/apps/web/src/dialogs/resume/sections/profile.tsx +++ b/apps/web/src/dialogs/resume/sections/profile.tsx @@ -17,7 +17,6 @@ import { Switch } from "@reactive-resume/ui/components/switch"; import { cn } from "@reactive-resume/utils/style"; import { ColorPicker } from "@/components/input/color-picker"; import { IconPicker } from "@/components/input/icon-picker"; -import { URLInput } from "@/components/input/url-input"; import { useDialogStore } from "@/dialogs/store"; import { useUpdateResumeData } from "@/features/resume/builder/draft"; import { useFormBlocker } from "@/hooks/use-form-blocker"; @@ -47,7 +46,7 @@ export function CreateProfileDialog({ data }: DialogProps<"resume.sections.profi const form = useAppForm({ defaultValues: makeSectionItem(defaultValues, data?.item), validators: { onSubmit: formSchema }, - onSubmit: async ({ value }) => { + onSubmit: ({ value }) => { updateResumeData((draft) => { createSectionItem(draft, "profiles", value, data?.customSectionId); }); @@ -79,7 +78,7 @@ export function UpdateProfileDialog({ data }: DialogProps<"resume.sections.profi const form = useAppForm({ defaultValues: data.item, validators: { onSubmit: formSchema }, - onSubmit: async ({ value }) => { + onSubmit: ({ value }) => { updateResumeData((draft) => { updateSectionItem(draft, "profiles", value, data?.customSectionId); }); @@ -211,24 +210,15 @@ const ProfileForm = withForm({ )} - + {(field) => ( - 0} - > - - Website - - field.handleChange(v)} - hideLabelButton={inlineLink} - /> - - + Website} + formItemClassName="sm:col-span-full" + hideLabelButton={inlineLink} + /> )} - + {(field) => ( diff --git a/apps/web/src/dialogs/resume/sections/project.tsx b/apps/web/src/dialogs/resume/sections/project.tsx index 3e051ce00..ae9fd43bd 100644 --- a/apps/web/src/dialogs/resume/sections/project.tsx +++ b/apps/web/src/dialogs/resume/sections/project.tsx @@ -4,10 +4,8 @@ import { Trans } from "@lingui/react/macro"; import { PencilSimpleLineIcon, PlusIcon } from "@phosphor-icons/react"; import { useStore } from "@tanstack/react-form"; import { projectItemSchema } from "@reactive-resume/schema/resume/data"; -import { FormControl, FormItem, FormLabel, FormMessage } from "@reactive-resume/ui/components/form"; +import { FormControl, FormItem, FormLabel } from "@reactive-resume/ui/components/form"; import { Switch } from "@reactive-resume/ui/components/switch"; -import { RichInput } from "@/components/input/rich-input"; -import { URLInput } from "@/components/input/url-input"; import { useDialogStore } from "@/dialogs/store"; import { useUpdateResumeData } from "@/features/resume/builder/draft"; import { useFormBlocker } from "@/hooks/use-form-blocker"; @@ -36,7 +34,7 @@ export function CreateProjectDialog({ data }: DialogProps<"resume.sections.proje const form = useAppForm({ defaultValues: makeSectionItem(defaultValues, data?.item), validators: { onSubmit: formSchema }, - onSubmit: async ({ value }) => { + onSubmit: ({ value }) => { updateResumeData((draft) => { createSectionItem(draft, "projects", value, data?.customSectionId); }); @@ -68,7 +66,7 @@ export function UpdateProjectDialog({ data }: DialogProps<"resume.sections.proje const form = useAppForm({ defaultValues: data.item, validators: { onSubmit: formSchema }, - onSubmit: async ({ value }) => { + onSubmit: ({ value }) => { updateResumeData((draft) => { updateSectionItem(draft, "projects", value, data?.customSectionId); }); @@ -104,24 +102,15 @@ const ProjectForm = withForm({ {(field) => Period} />} - + {(field) => ( - 0} - > - - Website - - field.handleChange(v)} - hideLabelButton={inlineLink} - /> - - + Website} + formItemClassName="sm:col-span-full" + hideLabelButton={inlineLink} + /> )} - + {(field) => ( @@ -143,20 +132,9 @@ const ProjectForm = withForm({ )} - - {(field) => ( - 0} - > - - Description - - field.handleChange(v)} />} /> - - - )} - + + {(field) => Description} formItemClassName="sm:col-span-full" />} + ); }, diff --git a/apps/web/src/dialogs/resume/sections/publication.tsx b/apps/web/src/dialogs/resume/sections/publication.tsx index 5d62b31e9..bed592372 100644 --- a/apps/web/src/dialogs/resume/sections/publication.tsx +++ b/apps/web/src/dialogs/resume/sections/publication.tsx @@ -4,10 +4,8 @@ import { Trans } from "@lingui/react/macro"; import { PencilSimpleLineIcon, PlusIcon } from "@phosphor-icons/react"; import { useStore } from "@tanstack/react-form"; import { publicationItemSchema } from "@reactive-resume/schema/resume/data"; -import { FormControl, FormItem, FormLabel, FormMessage } from "@reactive-resume/ui/components/form"; +import { FormControl, FormItem, FormLabel } from "@reactive-resume/ui/components/form"; import { Switch } from "@reactive-resume/ui/components/switch"; -import { RichInput } from "@/components/input/rich-input"; -import { URLInput } from "@/components/input/url-input"; import { useDialogStore } from "@/dialogs/store"; import { useUpdateResumeData } from "@/features/resume/builder/draft"; import { useFormBlocker } from "@/hooks/use-form-blocker"; @@ -37,7 +35,7 @@ export function CreatePublicationDialog({ data }: DialogProps<"resume.sections.p const form = useAppForm({ defaultValues: makeSectionItem(defaultValues, data?.item), validators: { onSubmit: formSchema }, - onSubmit: async ({ value }) => { + onSubmit: ({ value }) => { updateResumeData((draft) => { createSectionItem(draft, "publications", value, data?.customSectionId); }); @@ -69,7 +67,7 @@ export function UpdatePublicationDialog({ data }: DialogProps<"resume.sections.p const form = useAppForm({ defaultValues: data.item, validators: { onSubmit: formSchema }, - onSubmit: async ({ value }) => { + onSubmit: ({ value }) => { updateResumeData((draft) => { updateSectionItem(draft, "publications", value, data?.customSectionId); }); @@ -109,21 +107,9 @@ const PublicationForm = withForm({ {(field) => Date} />} - - {(field) => ( - 0}> - - Website - - field.handleChange(v)} - hideLabelButton={inlineLink} - /> - - - )} - + + {(field) => Website} hideLabelButton={inlineLink} />} + {(field) => ( @@ -145,20 +131,9 @@ const PublicationForm = withForm({ )} - - {(field) => ( - 0} - > - - Description - - field.handleChange(v)} />} /> - - - )} - + + {(field) => Description} formItemClassName="sm:col-span-full" />} + ); }, diff --git a/apps/web/src/dialogs/resume/sections/reference.tsx b/apps/web/src/dialogs/resume/sections/reference.tsx index 11efdfcd9..69e37a55e 100644 --- a/apps/web/src/dialogs/resume/sections/reference.tsx +++ b/apps/web/src/dialogs/resume/sections/reference.tsx @@ -4,10 +4,8 @@ import { Trans } from "@lingui/react/macro"; import { PencilSimpleLineIcon, PlusIcon } from "@phosphor-icons/react"; import { useStore } from "@tanstack/react-form"; import { referenceItemSchema } from "@reactive-resume/schema/resume/data"; -import { FormControl, FormItem, FormLabel, FormMessage } from "@reactive-resume/ui/components/form"; +import { FormControl, FormItem, FormLabel } from "@reactive-resume/ui/components/form"; import { Switch } from "@reactive-resume/ui/components/switch"; -import { RichInput } from "@/components/input/rich-input"; -import { URLInput } from "@/components/input/url-input"; import { useDialogStore } from "@/dialogs/store"; import { useUpdateResumeData } from "@/features/resume/builder/draft"; import { useFormBlocker } from "@/hooks/use-form-blocker"; @@ -37,7 +35,7 @@ export function CreateReferenceDialog({ data }: DialogProps<"resume.sections.ref const form = useAppForm({ defaultValues: makeSectionItem(defaultValues, data?.item), validators: { onSubmit: formSchema }, - onSubmit: async ({ value }) => { + onSubmit: ({ value }) => { updateResumeData((draft) => { createSectionItem(draft, "references", value, data?.customSectionId); }); @@ -69,7 +67,7 @@ export function UpdateReferenceDialog({ data }: DialogProps<"resume.sections.ref const form = useAppForm({ defaultValues: data.item, validators: { onSubmit: formSchema }, - onSubmit: async ({ value }) => { + onSubmit: ({ value }) => { updateResumeData((draft) => { updateSectionItem(draft, "references", value, data?.customSectionId); }); @@ -107,21 +105,9 @@ const ReferenceForm = withForm({ {(field) => Phone} />} - - {(field) => ( - 0}> - - Website - - field.handleChange(v)} - hideLabelButton={inlineLink} - /> - - - )} - + + {(field) => Website} hideLabelButton={inlineLink} />} + {(field) => ( @@ -143,20 +129,9 @@ const ReferenceForm = withForm({ )} - - {(field) => ( - 0} - > - - Description - - field.handleChange(v)} />} /> - - - )} - + + {(field) => Description} formItemClassName="sm:col-span-full" />} + ); }, diff --git a/apps/web/src/dialogs/resume/sections/skill.tsx b/apps/web/src/dialogs/resume/sections/skill.tsx index a41781230..e830aaf7b 100644 --- a/apps/web/src/dialogs/resume/sections/skill.tsx +++ b/apps/web/src/dialogs/resume/sections/skill.tsx @@ -43,7 +43,7 @@ export function CreateSkillDialog({ data }: DialogProps<"resume.sections.skills. const form = useAppForm({ defaultValues: makeSectionItem(defaultValues, data?.item), validators: { onSubmit: formSchema }, - onSubmit: async ({ value }) => { + onSubmit: ({ value }) => { updateResumeData((draft) => { createSectionItem(draft, "skills", value, data?.customSectionId); }); @@ -75,7 +75,7 @@ export function UpdateSkillDialog({ data }: DialogProps<"resume.sections.skills. const form = useAppForm({ defaultValues: data.item, validators: { onSubmit: formSchema }, - onSubmit: async ({ value }) => { + onSubmit: ({ value }) => { updateResumeData((draft) => { updateSectionItem(draft, "skills", value, data?.customSectionId); }); diff --git a/apps/web/src/dialogs/resume/sections/summary-item.tsx b/apps/web/src/dialogs/resume/sections/summary-item.tsx index ae9b31716..a370f8c2b 100644 --- a/apps/web/src/dialogs/resume/sections/summary-item.tsx +++ b/apps/web/src/dialogs/resume/sections/summary-item.tsx @@ -30,7 +30,7 @@ export function CreateSummaryItemDialog({ data }: DialogProps<"resume.sections.s const form = useAppForm({ defaultValues: makeSectionItem(defaultValues, data?.item), validators: { onSubmit: formSchema }, - onSubmit: async ({ value }) => { + onSubmit: ({ value }) => { updateResumeData((draft) => { if (data?.customSectionId) { const section = draft.customSections.find((s) => s.id === data.customSectionId); @@ -66,7 +66,7 @@ export function UpdateSummaryItemDialog({ data }: DialogProps<"resume.sections.s const form = useAppForm({ defaultValues: data.item, validators: { onSubmit: formSchema }, - onSubmit: async ({ value }) => { + onSubmit: ({ value }) => { updateResumeStore((draft) => { if (data?.customSectionId) { const section = draft.customSections.find((s) => s.id === data.customSectionId); diff --git a/apps/web/src/dialogs/resume/sections/volunteer.tsx b/apps/web/src/dialogs/resume/sections/volunteer.tsx index e4db14e04..b45eb7f6b 100644 --- a/apps/web/src/dialogs/resume/sections/volunteer.tsx +++ b/apps/web/src/dialogs/resume/sections/volunteer.tsx @@ -4,10 +4,8 @@ import { Trans } from "@lingui/react/macro"; import { PencilSimpleLineIcon, PlusIcon } from "@phosphor-icons/react"; import { useStore } from "@tanstack/react-form"; import { volunteerItemSchema } from "@reactive-resume/schema/resume/data"; -import { FormControl, FormItem, FormLabel, FormMessage } from "@reactive-resume/ui/components/form"; +import { FormControl, FormItem, FormLabel } from "@reactive-resume/ui/components/form"; import { Switch } from "@reactive-resume/ui/components/switch"; -import { RichInput } from "@/components/input/rich-input"; -import { URLInput } from "@/components/input/url-input"; import { useDialogStore } from "@/dialogs/store"; import { useUpdateResumeData } from "@/features/resume/builder/draft"; import { useFormBlocker } from "@/hooks/use-form-blocker"; @@ -37,7 +35,7 @@ export function CreateVolunteerDialog({ data }: DialogProps<"resume.sections.vol const form = useAppForm({ defaultValues: makeSectionItem(defaultValues, data?.item), validators: { onSubmit: formSchema }, - onSubmit: async ({ value }) => { + onSubmit: ({ value }) => { updateResumeData((draft) => { createSectionItem(draft, "volunteer", value, data?.customSectionId); }); @@ -69,7 +67,7 @@ export function UpdateVolunteerDialog({ data }: DialogProps<"resume.sections.vol const form = useAppForm({ defaultValues: data.item, validators: { onSubmit: formSchema }, - onSubmit: async ({ value }) => { + onSubmit: ({ value }) => { updateResumeData((draft) => { updateSectionItem(draft, "volunteer", value, data?.customSectionId); }); @@ -109,21 +107,9 @@ const VolunteerForm = withForm({ {(field) => Period} />} - - {(field) => ( - 0}> - - Website - - field.handleChange(v)} - hideLabelButton={inlineLink} - /> - - - )} - + + {(field) => Website} hideLabelButton={inlineLink} />} + {(field) => ( @@ -145,20 +131,9 @@ const VolunteerForm = withForm({ )} - - {(field) => ( - 0} - > - - Description - - field.handleChange(v)} />} /> - - - )} - + + {(field) => Description} formItemClassName="sm:col-span-full" />} + ); }, diff --git a/apps/web/src/features/applications/components/application-detail-sheet.tsx b/apps/web/src/features/applications/components/application-detail-sheet.tsx index 05bc56186..25e25828a 100644 --- a/apps/web/src/features/applications/components/application-detail-sheet.tsx +++ b/apps/web/src/features/applications/components/application-detail-sheet.tsx @@ -40,7 +40,7 @@ const stageOf = (status: ApplicationStatus) => STAGES.find((s) => s.value === st const dateInputValue = (value: Date | string) => { const date = new Date(value); - return `${date.getUTCFullYear()}-${String(date.getUTCMonth() + 1).padStart(2, "0")}-${String(date.getUTCDate()).padStart(2, "0")}`; + return Number.isNaN(date.getTime()) ? "" : date.toISOString().slice(0, 10); }; const formatDate = (value: Date | string) => diff --git a/apps/web/src/features/applications/components/application-form-sheet.tsx b/apps/web/src/features/applications/components/application-form-sheet.tsx index f309eda8c..a8c30b037 100644 --- a/apps/web/src/features/applications/components/application-form-sheet.tsx +++ b/apps/web/src/features/applications/components/application-form-sheet.tsx @@ -27,10 +27,7 @@ import { FileAttachmentField } from "./file-attachment-field"; // Preset source suggestions surfaced via a ; the field itself stays free-text. const SOURCE_OPTIONS = ["LinkedIn", "Indeed", "Company Website", "Referral", "Recruiter", "Other"]; -const todayInputValue = () => { - const now = new Date(); - return `${now.getUTCFullYear()}-${String(now.getUTCMonth() + 1).padStart(2, "0")}-${String(now.getUTCDate()).padStart(2, "0")}`; -}; +const todayInputValue = () => new Date().toISOString().slice(0, 10); const emptyForm = () => ({ company: "", diff --git a/apps/web/src/features/auth/pages/resume-password.tsx b/apps/web/src/features/auth/pages/resume-password.tsx index dafe5b071..1fdc388df 100644 --- a/apps/web/src/features/auth/pages/resume-password.tsx +++ b/apps/web/src/features/auth/pages/resume-password.tsx @@ -4,7 +4,6 @@ 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"; @@ -29,16 +28,13 @@ export function ResumePasswordPage({ redirectPath }: Props) { 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 [username, slug] = redirectPath.split("/").slice(1) as [string, string]; + if (!username || !slug) throw navigate({ to: "/" }); const form = useAppForm({ defaultValues: { password: "" }, validators: { onSubmit: formSchema }, - onSubmit: async ({ value, formApi }) => { + onSubmit: ({ value, formApi }) => { const toastId = toast.loading(t`Verifying password...`); verifyPassword( diff --git a/apps/web/src/features/auth/pages/verify-2fa-backup.tsx b/apps/web/src/features/auth/pages/verify-2fa-backup.tsx deleted file mode 100644 index 397efa902..000000000 --- a/apps/web/src/features/auth/pages/verify-2fa-backup.tsx +++ /dev/null @@ -1,111 +0,0 @@ -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 ( - <> -
-

- Verify with a Backup Code -

-
- Enter one of your saved backup codes to access your account -
-
- -
{ - event.preventDefault(); - event.stopPropagation(); - void form.handleSubmit(); - }} - > - - {(field) => ( - 0} - > - field.handleChange(event.target.value)} - /> - } - /> - - - )} - - -
- -
-
- - ); -} diff --git a/apps/web/src/features/auth/pages/verify-2fa.tsx b/apps/web/src/features/auth/pages/verify-2fa.tsx index be96688a6..dcf9a5ea7 100644 --- a/apps/web/src/features/auth/pages/verify-2fa.tsx +++ b/apps/web/src/features/auth/pages/verify-2fa.tsx @@ -10,31 +10,44 @@ import { Input } from "@reactive-resume/ui/components/input"; import { authClient } from "@/libs/auth/client"; import { useAppForm } from "@/libs/tanstack-form"; -const formSchema = z.object({ +const totpSchema = z.object({ code: z.string().length(6, "Code must be 6 digits"), }); -export function VerifyTwoFactorPage() { +const backupCodeSchema = z.object({ + code: z.string().trim(), +}); + +type TwoFactorVerificationPageProps = { + backupCode?: boolean; +}; + +function TwoFactorVerificationPage({ backupCode = false }: TwoFactorVerificationPageProps) { const router = useRouter(); const navigate = useNavigate(); const form = useAppForm({ defaultValues: { code: "" }, - validators: { onSubmit: formSchema }, + validators: { onSubmit: backupCode ? backupCodeSchema : totpSchema }, onSubmit: async ({ value }) => { - const toastId = toast.loading(t`Verifying code...`); - - const { error } = await authClient.twoFactor.verifyTotp({ - code: value.code, - }); + const toastId = toast.loading(backupCode ? t`Verifying backup code...` : t`Verifying code...`); + const code = backupCode ? `${value.code.slice(0, 5)}-${value.code.slice(5)}` : value.code; + const { error } = backupCode + ? await authClient.twoFactor.verifyBackupCode({ code }) + : await authClient.twoFactor.verifyTotp({ 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.", - }), + (backupCode + ? t({ + comment: "Fallback toast when verifying a backup two-factor authentication code fails", + message: "Failed to verify your backup code. Please try again.", + }) + : t({ + comment: "Fallback toast when verifying a two-factor authentication code fails", + message: "Failed to verify your code. Please try again.", + })), { id: toastId }, ); return; @@ -50,10 +63,14 @@ export function VerifyTwoFactorPage() { <>

- Two-Factor Authentication + {backupCode ? Verify with a Backup Code : Two-Factor Authentication}

- Enter the verification code from your authenticator app + {backupCode ? ( + Enter one of your saved backup codes to access your account + ) : ( + Enter the verification code from your authenticator app + )}
@@ -74,8 +91,8 @@ export function VerifyTwoFactorPage() { + - Back to Login + {backupCode ? ( + Go Back + ) : ( + Back to Login + )} } /> - diff --git a/apps/web/src/features/settings/authentication/components/social-provider.tsx b/apps/web/src/features/settings/authentication/components/social-provider.tsx index bd45bbacf..c54669d43 100644 --- a/apps/web/src/features/settings/authentication/components/social-provider.tsx +++ b/apps/web/src/features/settings/authentication/components/social-provider.tsx @@ -2,25 +2,11 @@ import type { AuthProvider } from "@reactive-resume/auth/types"; import { Trans } from "@lingui/react/macro"; import { LinkBreakIcon, LinkIcon } from "@phosphor-icons/react"; import { m } from "motion/react"; -import { useCallback } from "react"; import { Button } from "@reactive-resume/ui/components/button"; import { Separator } from "@reactive-resume/ui/components/separator"; +import { ActionButton } from "./action-button"; import { getProviderIcon, getProviderName, useAuthAccounts, useAuthProviderActions } from "./hooks"; -// ponytail: shared hover/tap wrapper — identical in both branches, match(boolean) removed -function ActionButton({ children }: { children: React.ReactNode }) { - return ( - - {children} - - ); -} - type SocialProviderSectionProps = { provider: AuthProvider; name?: string; @@ -37,15 +23,6 @@ export function SocialProviderSection({ provider, name, animationDelay = 0 }: So const account = getAccountByProviderId(provider); const isConnected = hasAccount(provider); - const handleLink = useCallback(async () => { - await link(provider); - }, [link, provider]); - - const handleUnlink = useCallback(async () => { - if (!account?.accountId) return; - await unlink(provider, account.accountId); - }, [account, unlink, provider]); - return ( {isConnected ? ( - ) : ( - diff --git a/apps/web/src/features/settings/authentication/components/two-factor.tsx b/apps/web/src/features/settings/authentication/components/two-factor.tsx index a977586a6..e05f35241 100644 --- a/apps/web/src/features/settings/authentication/components/two-factor.tsx +++ b/apps/web/src/features/settings/authentication/components/two-factor.tsx @@ -1,27 +1,13 @@ import { Trans } from "@lingui/react/macro"; import { KeyIcon, LockOpenIcon, ToggleLeftIcon, ToggleRightIcon } from "@phosphor-icons/react"; import { m } from "motion/react"; -import { useCallback } from "react"; import { Button } from "@reactive-resume/ui/components/button"; import { Separator } from "@reactive-resume/ui/components/separator"; import { useDialogStore } from "@/dialogs/store"; import { authClient } from "@/libs/auth/client"; +import { ActionButton } from "./action-button"; import { useAuthAccounts } from "./hooks"; -// ponytail: shared hover/tap wrapper — identical in both branches, match(boolean) removed -function ActionButton({ children }: { children: React.ReactNode }) { - return ( - - {children} - - ); -} - export function TwoFactorSection() { const { openDialog } = useDialogStore(); const { hasAccount } = useAuthAccounts(); @@ -30,14 +16,6 @@ export function TwoFactorSection() { const hasPassword = hasAccount("credential"); const hasTwoFactor = session?.user.twoFactorEnabled ?? false; - const handleTwoFactorAction = useCallback(() => { - if (hasTwoFactor) { - openDialog("auth.two-factor.disable", undefined); - } else { - openDialog("auth.two-factor.enable", undefined); - } - }, [hasTwoFactor, openDialog]); - if (!hasPassword) return null; return ( @@ -56,7 +34,10 @@ export function TwoFactorSection() { -