mirror of
https://github.com/AmruthPillai/Reactive-Resume.git
synced 2026-07-14 14:57:00 +10:00
Feature: Create a new useFormBlocker hook to block the user from closing a dialog or navigating away from a page if the form is dirty (#2654)
* feat: add useFormBlocker hook for dialog dirty state protection * feat: add useFormBlocker hook for dialog dirty state protection - Create useFormBlocker hook that blocks dialog closing when forms have unsaved changes - Use onPointerDownOutside and onEscapeKeyDown to intercept close attempts - Show confirmation dialog with Leave/Stay options using useConfirm - Integrate with CreateResumeDialog, UpdateResumeDialog, and DuplicateResumeDialog - All strings are translatable via Lingui * Feature: Create a new `useFormBlocker` hook to block the user from closing a dialog or navigating away from a page if the form is dirty.
This commit is contained in:
@@ -14,6 +14,7 @@ import { DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTit
|
||||
import { Form, FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage } from "@/components/ui/form";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { InputGroup, InputGroupAddon, InputGroupButton, InputGroupInput } from "@/components/ui/input-group";
|
||||
import { useFormBlocker } from "@/hooks/use-form-blocker";
|
||||
import { authClient } from "@/integrations/auth/client";
|
||||
import { type DialogProps, useDialogStore } from "../store";
|
||||
|
||||
@@ -45,6 +46,8 @@ const CreateApiKeyForm = ({ setApiKey }: CreateApiKeyFormProps) => {
|
||||
},
|
||||
});
|
||||
|
||||
const { blockEvents } = useFormBlocker(form);
|
||||
|
||||
const onSubmit = async (values: FormValues) => {
|
||||
const toastId = toast.loading(t`Creating your API key...`);
|
||||
|
||||
@@ -63,7 +66,7 @@ const CreateApiKeyForm = ({ setApiKey }: CreateApiKeyFormProps) => {
|
||||
};
|
||||
|
||||
return (
|
||||
<DialogContent>
|
||||
<DialogContent {...blockEvents}>
|
||||
<DialogHeader>
|
||||
<DialogTitle className="flex items-center gap-x-2">
|
||||
<PlusIcon />
|
||||
|
||||
@@ -11,6 +11,7 @@ import { Button } from "@/components/ui/button";
|
||||
import { DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from "@/components/ui/dialog";
|
||||
import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from "@/components/ui/form";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { useFormBlocker } from "@/hooks/use-form-blocker";
|
||||
import { authClient } from "@/integrations/auth/client";
|
||||
import { type DialogProps, useDialogStore } from "../store";
|
||||
|
||||
@@ -41,6 +42,8 @@ export function ChangePasswordDialog(_: DialogProps<"auth.change-password">) {
|
||||
},
|
||||
});
|
||||
|
||||
const { blockEvents } = useFormBlocker(form);
|
||||
|
||||
const onSubmit = async (data: FormValues) => {
|
||||
const toastId = toast.loading(t`Updating your password...`);
|
||||
|
||||
@@ -60,7 +63,7 @@ export function ChangePasswordDialog(_: DialogProps<"auth.change-password">) {
|
||||
};
|
||||
|
||||
return (
|
||||
<DialogContent>
|
||||
<DialogContent {...blockEvents}>
|
||||
<DialogHeader>
|
||||
<DialogTitle className="flex items-center gap-x-2">
|
||||
<PasswordIcon />
|
||||
|
||||
@@ -11,6 +11,7 @@ import { Button } from "@/components/ui/button";
|
||||
import { DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from "@/components/ui/dialog";
|
||||
import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from "@/components/ui/form";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { useFormBlocker } from "@/hooks/use-form-blocker";
|
||||
import { authClient } from "@/integrations/auth/client";
|
||||
import { type DialogProps, useDialogStore } from "../store";
|
||||
|
||||
@@ -32,6 +33,8 @@ export function DisableTwoFactorDialog(_: DialogProps<"auth.two-factor.disable">
|
||||
},
|
||||
});
|
||||
|
||||
const { blockEvents } = useFormBlocker(form);
|
||||
|
||||
const onSubmit = async (data: FormValues) => {
|
||||
const toastId = toast.loading(t`Disabling two-factor authentication...`);
|
||||
|
||||
@@ -49,7 +52,7 @@ export function DisableTwoFactorDialog(_: DialogProps<"auth.two-factor.disable">
|
||||
};
|
||||
|
||||
return (
|
||||
<DialogContent>
|
||||
<DialogContent {...blockEvents}>
|
||||
<DialogHeader>
|
||||
<DialogTitle className="flex items-center gap-x-2">
|
||||
<LockOpenIcon />
|
||||
|
||||
@@ -16,6 +16,7 @@ import { DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTit
|
||||
import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from "@/components/ui/form";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { InputOTP, InputOTPGroup, InputOTPSlot } from "@/components/ui/input-otp";
|
||||
import { useFormBlocker } from "@/hooks/use-form-blocker";
|
||||
import { authClient } from "@/integrations/auth/client";
|
||||
import { type DialogProps, useDialogStore } from "../store";
|
||||
|
||||
@@ -55,6 +56,17 @@ export function EnableTwoFactorDialog(_: DialogProps<"auth.two-factor.enable">)
|
||||
},
|
||||
});
|
||||
|
||||
const enableFormState = enableForm.formState;
|
||||
const verifyFormState = verifyForm.formState;
|
||||
|
||||
const { blockEvents, requestClose } = useFormBlocker(enableForm, {
|
||||
shouldBlock: () => {
|
||||
if (step === "enable") return enableFormState.isDirty && !enableFormState.isSubmitting;
|
||||
if (step === "verify") return verifyFormState.isDirty && !verifyFormState.isSubmitting;
|
||||
return false;
|
||||
},
|
||||
});
|
||||
|
||||
const onEnableSubmit = async (values: EnableFormValues) => {
|
||||
const toastId = toast.loading(t`Enabling two-factor authentication...`);
|
||||
|
||||
@@ -136,7 +148,7 @@ export function EnableTwoFactorDialog(_: DialogProps<"auth.two-factor.enable">)
|
||||
};
|
||||
|
||||
return (
|
||||
<DialogContent className="max-w-md">
|
||||
<DialogContent className="max-w-md" {...blockEvents}>
|
||||
<DialogHeader>
|
||||
<DialogTitle>
|
||||
{match(step)
|
||||
@@ -257,7 +269,7 @@ export function EnableTwoFactorDialog(_: DialogProps<"auth.two-factor.enable">)
|
||||
/>
|
||||
|
||||
<DialogFooter className="gap-x-2">
|
||||
<Button type="button" variant="outline" onClick={closeDialog}>
|
||||
<Button type="button" variant="outline" onClick={requestClose}>
|
||||
<Trans>Cancel</Trans>
|
||||
</Button>
|
||||
<Button type="submit">
|
||||
|
||||
@@ -14,6 +14,7 @@ import { DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTit
|
||||
import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from "@/components/ui/form";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Spinner } from "@/components/ui/spinner";
|
||||
import { useFormBlocker } from "@/hooks/use-form-blocker";
|
||||
import { useAIStore } from "@/integrations/ai/store";
|
||||
import { JSONResumeImporter } from "@/integrations/import/json-resume";
|
||||
import { ReactiveResumeJSONImporter } from "@/integrations/import/reactive-resume-json";
|
||||
@@ -98,6 +99,8 @@ export function ImportResumeDialog(_: DialogProps<"resume.import">) {
|
||||
form.setValue("file", file, { shouldDirty: true });
|
||||
};
|
||||
|
||||
const { blockEvents } = useFormBlocker(form);
|
||||
|
||||
const onSubmit = async (values: FormValues) => {
|
||||
if (values.type === "") return;
|
||||
|
||||
@@ -182,7 +185,7 @@ export function ImportResumeDialog(_: DialogProps<"resume.import">) {
|
||||
};
|
||||
|
||||
return (
|
||||
<DialogContent>
|
||||
<DialogContent {...blockEvents}>
|
||||
<DialogHeader>
|
||||
<DialogTitle className="flex items-center gap-x-2">
|
||||
<DownloadSimpleIcon />
|
||||
|
||||
@@ -21,6 +21,7 @@ import {
|
||||
import { Form, FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage } from "@/components/ui/form";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { InputGroup, InputGroupAddon, InputGroupInput, InputGroupText } from "@/components/ui/input-group";
|
||||
import { useFormBlocker } from "@/hooks/use-form-blocker";
|
||||
import { authClient } from "@/integrations/auth/client";
|
||||
import { orpc, type RouterInput } from "@/integrations/orpc/client";
|
||||
import { generateId, generateRandomName, slugify } from "@/utils/string";
|
||||
@@ -56,6 +57,8 @@ export function CreateResumeDialog(_: DialogProps<"resume.create">) {
|
||||
form.setValue("slug", slugify(name), { shouldDirty: true });
|
||||
}, [form, name]);
|
||||
|
||||
const { blockEvents } = useFormBlocker(form);
|
||||
|
||||
const onSubmit = (data: FormValues) => {
|
||||
const toastId = toast.loading(t`Creating your resume...`);
|
||||
|
||||
@@ -100,7 +103,7 @@ export function CreateResumeDialog(_: DialogProps<"resume.create">) {
|
||||
};
|
||||
|
||||
return (
|
||||
<DialogContent>
|
||||
<DialogContent {...blockEvents}>
|
||||
<DialogHeader>
|
||||
<DialogTitle className="flex items-center gap-x-2">
|
||||
<PlusIcon />
|
||||
@@ -165,6 +168,8 @@ export function UpdateResumeDialog({ data }: DialogProps<"resume.update">) {
|
||||
form.setValue("slug", slugify(name), { shouldDirty: true });
|
||||
}, [form, name]);
|
||||
|
||||
const { blockEvents } = useFormBlocker(form);
|
||||
|
||||
const onSubmit = (data: FormValues) => {
|
||||
const toastId = toast.loading(t`Updating your resume...`);
|
||||
|
||||
@@ -185,7 +190,7 @@ export function UpdateResumeDialog({ data }: DialogProps<"resume.update">) {
|
||||
};
|
||||
|
||||
return (
|
||||
<DialogContent>
|
||||
<DialogContent {...blockEvents}>
|
||||
<DialogHeader>
|
||||
<DialogTitle className="flex items-center gap-x-2">
|
||||
<PencilSimpleLineIcon />
|
||||
@@ -234,6 +239,8 @@ export function DuplicateResumeDialog({ data }: DialogProps<"resume.duplicate">)
|
||||
form.setValue("slug", slugify(name), { shouldDirty: true });
|
||||
}, [form, name]);
|
||||
|
||||
const { blockEvents } = useFormBlocker(form);
|
||||
|
||||
const onSubmit = (values: FormValues) => {
|
||||
const toastId = toast.loading(t`Duplicating your resume...`);
|
||||
|
||||
@@ -253,7 +260,7 @@ export function DuplicateResumeDialog({ data }: DialogProps<"resume.duplicate">)
|
||||
};
|
||||
|
||||
return (
|
||||
<DialogContent>
|
||||
<DialogContent {...blockEvents}>
|
||||
<DialogHeader>
|
||||
<DialogTitle className="flex items-center gap-x-2">
|
||||
<PencilSimpleLineIcon />
|
||||
|
||||
@@ -12,6 +12,7 @@ import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from "
|
||||
import { Input } from "@/components/ui/input";
|
||||
import type { DialogProps } from "@/dialogs/store";
|
||||
import { useDialogStore } from "@/dialogs/store";
|
||||
import { useFormBlocker } from "@/hooks/use-form-blocker";
|
||||
import { awardItemSchema } from "@/schema/resume/data";
|
||||
import { generateId } from "@/utils/string";
|
||||
|
||||
@@ -48,8 +49,10 @@ export function CreateAwardDialog({ data }: DialogProps<"resume.sections.awards.
|
||||
closeDialog();
|
||||
};
|
||||
|
||||
const { blockEvents, requestClose } = useFormBlocker(form);
|
||||
|
||||
return (
|
||||
<DialogContent>
|
||||
<DialogContent {...blockEvents}>
|
||||
<DialogHeader>
|
||||
<DialogTitle className="flex items-center gap-x-2">
|
||||
<PlusIcon />
|
||||
@@ -63,7 +66,7 @@ export function CreateAwardDialog({ data }: DialogProps<"resume.sections.awards.
|
||||
<AwardForm />
|
||||
|
||||
<DialogFooter className="sm:col-span-full">
|
||||
<Button variant="ghost" onClick={closeDialog}>
|
||||
<Button variant="ghost" onClick={requestClose}>
|
||||
<Trans>Cancel</Trans>
|
||||
</Button>
|
||||
|
||||
@@ -109,8 +112,10 @@ export function UpdateAwardDialog({ data }: DialogProps<"resume.sections.awards.
|
||||
closeDialog();
|
||||
};
|
||||
|
||||
const { blockEvents, requestClose } = useFormBlocker(form);
|
||||
|
||||
return (
|
||||
<DialogContent>
|
||||
<DialogContent {...blockEvents}>
|
||||
<DialogHeader>
|
||||
<DialogTitle className="flex items-center gap-x-2">
|
||||
<PencilSimpleLineIcon />
|
||||
@@ -124,7 +129,7 @@ export function UpdateAwardDialog({ data }: DialogProps<"resume.sections.awards.
|
||||
<AwardForm />
|
||||
|
||||
<DialogFooter className="sm:col-span-full">
|
||||
<Button variant="ghost" onClick={closeDialog}>
|
||||
<Button variant="ghost" onClick={requestClose}>
|
||||
<Trans>Cancel</Trans>
|
||||
</Button>
|
||||
|
||||
|
||||
@@ -12,6 +12,7 @@ import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from "
|
||||
import { Input } from "@/components/ui/input";
|
||||
import type { DialogProps } from "@/dialogs/store";
|
||||
import { useDialogStore } from "@/dialogs/store";
|
||||
import { useFormBlocker } from "@/hooks/use-form-blocker";
|
||||
import { certificationItemSchema } from "@/schema/resume/data";
|
||||
import { generateId } from "@/utils/string";
|
||||
|
||||
@@ -48,8 +49,10 @@ export function CreateCertificationDialog({ data }: DialogProps<"resume.sections
|
||||
closeDialog();
|
||||
};
|
||||
|
||||
const { blockEvents, requestClose } = useFormBlocker(form);
|
||||
|
||||
return (
|
||||
<DialogContent>
|
||||
<DialogContent {...blockEvents}>
|
||||
<DialogHeader>
|
||||
<DialogTitle className="flex items-center gap-x-2">
|
||||
<PlusIcon />
|
||||
@@ -63,7 +66,7 @@ export function CreateCertificationDialog({ data }: DialogProps<"resume.sections
|
||||
<CertificationForm />
|
||||
|
||||
<DialogFooter className="sm:col-span-full">
|
||||
<Button variant="ghost" onClick={closeDialog}>
|
||||
<Button variant="ghost" onClick={requestClose}>
|
||||
<Trans>Cancel</Trans>
|
||||
</Button>
|
||||
|
||||
@@ -109,8 +112,10 @@ export function UpdateCertificationDialog({ data }: DialogProps<"resume.sections
|
||||
closeDialog();
|
||||
};
|
||||
|
||||
const { blockEvents, requestClose } = useFormBlocker(form);
|
||||
|
||||
return (
|
||||
<DialogContent>
|
||||
<DialogContent {...blockEvents}>
|
||||
<DialogHeader>
|
||||
<DialogTitle className="flex items-center gap-x-2">
|
||||
<PencilSimpleLineIcon />
|
||||
@@ -124,7 +129,7 @@ export function UpdateCertificationDialog({ data }: DialogProps<"resume.sections
|
||||
<CertificationForm />
|
||||
|
||||
<DialogFooter className="sm:col-span-full">
|
||||
<Button variant="ghost" onClick={closeDialog}>
|
||||
<Button variant="ghost" onClick={requestClose}>
|
||||
<Trans>Cancel</Trans>
|
||||
</Button>
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from "
|
||||
import { Input } from "@/components/ui/input";
|
||||
import type { DialogProps } from "@/dialogs/store";
|
||||
import { useDialogStore } from "@/dialogs/store";
|
||||
import { useFormBlocker } from "@/hooks/use-form-blocker";
|
||||
import { customSectionSchema, type SectionType } from "@/schema/resume/data";
|
||||
import { generateId } from "@/utils/string";
|
||||
|
||||
@@ -59,8 +60,10 @@ export function CreateCustomSectionDialog({ data }: DialogProps<"resume.sections
|
||||
closeDialog();
|
||||
};
|
||||
|
||||
const { blockEvents, requestClose } = useFormBlocker(form);
|
||||
|
||||
return (
|
||||
<DialogContent>
|
||||
<DialogContent {...blockEvents}>
|
||||
<DialogHeader>
|
||||
<DialogTitle className="flex items-center gap-x-2">
|
||||
<PlusIcon />
|
||||
@@ -74,7 +77,7 @@ export function CreateCustomSectionDialog({ data }: DialogProps<"resume.sections
|
||||
<CustomSectionForm />
|
||||
|
||||
<DialogFooter className="sm:col-span-full">
|
||||
<Button variant="ghost" onClick={closeDialog}>
|
||||
<Button variant="ghost" onClick={requestClose}>
|
||||
<Trans>Cancel</Trans>
|
||||
</Button>
|
||||
|
||||
@@ -113,8 +116,10 @@ export function UpdateCustomSectionDialog({ data }: DialogProps<"resume.sections
|
||||
closeDialog();
|
||||
};
|
||||
|
||||
const { blockEvents, requestClose } = useFormBlocker(form);
|
||||
|
||||
return (
|
||||
<DialogContent>
|
||||
<DialogContent {...blockEvents}>
|
||||
<DialogHeader>
|
||||
<DialogTitle className="flex items-center gap-x-2">
|
||||
<PencilSimpleLineIcon />
|
||||
@@ -128,7 +133,7 @@ export function UpdateCustomSectionDialog({ data }: DialogProps<"resume.sections
|
||||
<CustomSectionForm isUpdate />
|
||||
|
||||
<DialogFooter className="sm:col-span-full">
|
||||
<Button variant="ghost" onClick={closeDialog}>
|
||||
<Button variant="ghost" onClick={requestClose}>
|
||||
<Trans>Cancel</Trans>
|
||||
</Button>
|
||||
|
||||
|
||||
@@ -12,6 +12,7 @@ import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from "
|
||||
import { Input } from "@/components/ui/input";
|
||||
import type { DialogProps } from "@/dialogs/store";
|
||||
import { useDialogStore } from "@/dialogs/store";
|
||||
import { useFormBlocker } from "@/hooks/use-form-blocker";
|
||||
import { educationItemSchema } from "@/schema/resume/data";
|
||||
import { generateId } from "@/utils/string";
|
||||
|
||||
@@ -51,8 +52,10 @@ export function CreateEducationDialog({ data }: DialogProps<"resume.sections.edu
|
||||
closeDialog();
|
||||
};
|
||||
|
||||
const { blockEvents, requestClose } = useFormBlocker(form);
|
||||
|
||||
return (
|
||||
<DialogContent>
|
||||
<DialogContent {...blockEvents}>
|
||||
<DialogHeader>
|
||||
<DialogTitle className="flex items-center gap-x-2">
|
||||
<PlusIcon />
|
||||
@@ -66,7 +69,7 @@ export function CreateEducationDialog({ data }: DialogProps<"resume.sections.edu
|
||||
<EducationForm />
|
||||
|
||||
<DialogFooter className="sm:col-span-full">
|
||||
<Button variant="ghost" onClick={closeDialog}>
|
||||
<Button variant="ghost" onClick={requestClose}>
|
||||
<Trans>Cancel</Trans>
|
||||
</Button>
|
||||
|
||||
@@ -115,8 +118,10 @@ export function UpdateEducationDialog({ data }: DialogProps<"resume.sections.edu
|
||||
closeDialog();
|
||||
};
|
||||
|
||||
const { blockEvents, requestClose } = useFormBlocker(form);
|
||||
|
||||
return (
|
||||
<DialogContent>
|
||||
<DialogContent {...blockEvents}>
|
||||
<DialogHeader>
|
||||
<DialogTitle className="flex items-center gap-x-2">
|
||||
<PencilSimpleLineIcon />
|
||||
@@ -130,7 +135,7 @@ export function UpdateEducationDialog({ data }: DialogProps<"resume.sections.edu
|
||||
<EducationForm />
|
||||
|
||||
<DialogFooter className="sm:col-span-full">
|
||||
<Button variant="ghost" onClick={closeDialog}>
|
||||
<Button variant="ghost" onClick={requestClose}>
|
||||
<Trans>Cancel</Trans>
|
||||
</Button>
|
||||
|
||||
|
||||
@@ -12,6 +12,7 @@ import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from "
|
||||
import { Input } from "@/components/ui/input";
|
||||
import type { DialogProps } from "@/dialogs/store";
|
||||
import { useDialogStore } from "@/dialogs/store";
|
||||
import { useFormBlocker } from "@/hooks/use-form-blocker";
|
||||
import { experienceItemSchema } from "@/schema/resume/data";
|
||||
import { generateId } from "@/utils/string";
|
||||
|
||||
@@ -49,8 +50,10 @@ export function CreateExperienceDialog({ data }: DialogProps<"resume.sections.ex
|
||||
closeDialog();
|
||||
};
|
||||
|
||||
const { blockEvents, requestClose } = useFormBlocker(form);
|
||||
|
||||
return (
|
||||
<DialogContent>
|
||||
<DialogContent {...blockEvents}>
|
||||
<DialogHeader>
|
||||
<DialogTitle className="flex items-center gap-x-2">
|
||||
<PlusIcon />
|
||||
@@ -64,7 +67,7 @@ export function CreateExperienceDialog({ data }: DialogProps<"resume.sections.ex
|
||||
<ExperienceForm />
|
||||
|
||||
<DialogFooter className="sm:col-span-full">
|
||||
<Button variant="ghost" onClick={closeDialog}>
|
||||
<Button variant="ghost" onClick={requestClose}>
|
||||
<Trans>Cancel</Trans>
|
||||
</Button>
|
||||
|
||||
@@ -111,8 +114,10 @@ export function UpdateExperienceDialog({ data }: DialogProps<"resume.sections.ex
|
||||
closeDialog();
|
||||
};
|
||||
|
||||
const { blockEvents, requestClose } = useFormBlocker(form);
|
||||
|
||||
return (
|
||||
<DialogContent>
|
||||
<DialogContent {...blockEvents}>
|
||||
<DialogHeader>
|
||||
<DialogTitle className="flex items-center gap-x-2">
|
||||
<PencilSimpleLineIcon />
|
||||
@@ -126,7 +131,7 @@ export function UpdateExperienceDialog({ data }: DialogProps<"resume.sections.ex
|
||||
<ExperienceForm />
|
||||
|
||||
<DialogFooter className="sm:col-span-full">
|
||||
<Button variant="ghost" onClick={closeDialog}>
|
||||
<Button variant="ghost" onClick={requestClose}>
|
||||
<Trans>Cancel</Trans>
|
||||
</Button>
|
||||
|
||||
|
||||
@@ -13,6 +13,7 @@ import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from "
|
||||
import { Input } from "@/components/ui/input";
|
||||
import type { DialogProps } from "@/dialogs/store";
|
||||
import { useDialogStore } from "@/dialogs/store";
|
||||
import { useFormBlocker } from "@/hooks/use-form-blocker";
|
||||
import { interestItemSchema } from "@/schema/resume/data";
|
||||
import { generateId } from "@/utils/string";
|
||||
import { cn } from "@/utils/style";
|
||||
@@ -48,8 +49,10 @@ export function CreateInterestDialog({ data }: DialogProps<"resume.sections.inte
|
||||
closeDialog();
|
||||
};
|
||||
|
||||
const { blockEvents, requestClose } = useFormBlocker(form);
|
||||
|
||||
return (
|
||||
<DialogContent>
|
||||
<DialogContent {...blockEvents}>
|
||||
<DialogHeader>
|
||||
<DialogTitle className="flex items-center gap-x-2">
|
||||
<PlusIcon />
|
||||
@@ -63,7 +66,7 @@ export function CreateInterestDialog({ data }: DialogProps<"resume.sections.inte
|
||||
<InterestForm />
|
||||
|
||||
<DialogFooter className="sm:col-span-full">
|
||||
<Button variant="ghost" onClick={closeDialog}>
|
||||
<Button variant="ghost" onClick={requestClose}>
|
||||
<Trans>Cancel</Trans>
|
||||
</Button>
|
||||
|
||||
@@ -107,8 +110,10 @@ export function UpdateInterestDialog({ data }: DialogProps<"resume.sections.inte
|
||||
closeDialog();
|
||||
};
|
||||
|
||||
const { blockEvents, requestClose } = useFormBlocker(form);
|
||||
|
||||
return (
|
||||
<DialogContent>
|
||||
<DialogContent {...blockEvents}>
|
||||
<DialogHeader>
|
||||
<DialogTitle className="flex items-center gap-x-2">
|
||||
<PencilSimpleLineIcon />
|
||||
@@ -122,7 +127,7 @@ export function UpdateInterestDialog({ data }: DialogProps<"resume.sections.inte
|
||||
<InterestForm />
|
||||
|
||||
<DialogFooter className="sm:col-span-full">
|
||||
<Button variant="ghost" onClick={closeDialog}>
|
||||
<Button variant="ghost" onClick={requestClose}>
|
||||
<Trans>Cancel</Trans>
|
||||
</Button>
|
||||
|
||||
|
||||
@@ -12,6 +12,7 @@ import { Input } from "@/components/ui/input";
|
||||
import { Slider } from "@/components/ui/slider";
|
||||
import type { DialogProps } from "@/dialogs/store";
|
||||
import { useDialogStore } from "@/dialogs/store";
|
||||
import { useFormBlocker } from "@/hooks/use-form-blocker";
|
||||
import { languageItemSchema } from "@/schema/resume/data";
|
||||
import { generateId } from "@/utils/string";
|
||||
|
||||
@@ -46,8 +47,10 @@ export function CreateLanguageDialog({ data }: DialogProps<"resume.sections.lang
|
||||
closeDialog();
|
||||
};
|
||||
|
||||
const { blockEvents, requestClose } = useFormBlocker(form);
|
||||
|
||||
return (
|
||||
<DialogContent>
|
||||
<DialogContent {...blockEvents}>
|
||||
<DialogHeader>
|
||||
<DialogTitle className="flex items-center gap-x-2">
|
||||
<PlusIcon />
|
||||
@@ -61,7 +64,7 @@ export function CreateLanguageDialog({ data }: DialogProps<"resume.sections.lang
|
||||
<LanguageForm />
|
||||
|
||||
<DialogFooter className="sm:col-span-full">
|
||||
<Button variant="ghost" onClick={closeDialog}>
|
||||
<Button variant="ghost" onClick={requestClose}>
|
||||
<Trans>Cancel</Trans>
|
||||
</Button>
|
||||
|
||||
@@ -105,8 +108,10 @@ export function UpdateLanguageDialog({ data }: DialogProps<"resume.sections.lang
|
||||
closeDialog();
|
||||
};
|
||||
|
||||
const { blockEvents, requestClose } = useFormBlocker(form);
|
||||
|
||||
return (
|
||||
<DialogContent>
|
||||
<DialogContent {...blockEvents}>
|
||||
<DialogHeader>
|
||||
<DialogTitle className="flex items-center gap-x-2">
|
||||
<PencilSimpleLineIcon />
|
||||
@@ -120,7 +125,7 @@ export function UpdateLanguageDialog({ data }: DialogProps<"resume.sections.lang
|
||||
<LanguageForm />
|
||||
|
||||
<DialogFooter className="sm:col-span-full">
|
||||
<Button variant="ghost" onClick={closeDialog}>
|
||||
<Button variant="ghost" onClick={requestClose}>
|
||||
<Trans>Cancel</Trans>
|
||||
</Button>
|
||||
|
||||
|
||||
@@ -13,6 +13,7 @@ import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from "
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { InputGroup, InputGroupAddon, InputGroupInput, InputGroupText } from "@/components/ui/input-group";
|
||||
import { type DialogProps, useDialogStore } from "@/dialogs/store";
|
||||
import { useFormBlocker } from "@/hooks/use-form-blocker";
|
||||
import { profileItemSchema } from "@/schema/resume/data";
|
||||
import { generateId } from "@/utils/string";
|
||||
import { cn } from "@/utils/style";
|
||||
@@ -49,8 +50,10 @@ export function CreateProfileDialog({ data }: DialogProps<"resume.sections.profi
|
||||
closeDialog();
|
||||
};
|
||||
|
||||
const { blockEvents, requestClose } = useFormBlocker(form);
|
||||
|
||||
return (
|
||||
<DialogContent>
|
||||
<DialogContent {...blockEvents}>
|
||||
<DialogHeader>
|
||||
<DialogTitle className="flex items-center gap-x-2">
|
||||
<PlusIcon />
|
||||
@@ -64,7 +67,7 @@ export function CreateProfileDialog({ data }: DialogProps<"resume.sections.profi
|
||||
<ProfileForm />
|
||||
|
||||
<DialogFooter className="sm:col-span-full">
|
||||
<Button variant="ghost" onClick={closeDialog}>
|
||||
<Button variant="ghost" onClick={requestClose}>
|
||||
<Trans>Cancel</Trans>
|
||||
</Button>
|
||||
|
||||
@@ -109,8 +112,10 @@ export function UpdateProfileDialog({ data }: DialogProps<"resume.sections.profi
|
||||
closeDialog();
|
||||
};
|
||||
|
||||
const { blockEvents, requestClose } = useFormBlocker(form);
|
||||
|
||||
return (
|
||||
<DialogContent>
|
||||
<DialogContent {...blockEvents}>
|
||||
<DialogHeader>
|
||||
<DialogTitle className="flex items-center gap-x-2">
|
||||
<PencilSimpleLineIcon />
|
||||
@@ -124,7 +129,7 @@ export function UpdateProfileDialog({ data }: DialogProps<"resume.sections.profi
|
||||
<ProfileForm />
|
||||
|
||||
<DialogFooter className="sm:col-span-full">
|
||||
<Button variant="ghost" onClick={closeDialog}>
|
||||
<Button variant="ghost" onClick={requestClose}>
|
||||
<Trans>Cancel</Trans>
|
||||
</Button>
|
||||
|
||||
|
||||
@@ -12,6 +12,7 @@ import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from "
|
||||
import { Input } from "@/components/ui/input";
|
||||
import type { DialogProps } from "@/dialogs/store";
|
||||
import { useDialogStore } from "@/dialogs/store";
|
||||
import { useFormBlocker } from "@/hooks/use-form-blocker";
|
||||
import { projectItemSchema } from "@/schema/resume/data";
|
||||
import { generateId } from "@/utils/string";
|
||||
|
||||
@@ -47,8 +48,10 @@ export function CreateProjectDialog({ data }: DialogProps<"resume.sections.proje
|
||||
closeDialog();
|
||||
};
|
||||
|
||||
const { blockEvents, requestClose } = useFormBlocker(form);
|
||||
|
||||
return (
|
||||
<DialogContent>
|
||||
<DialogContent {...blockEvents}>
|
||||
<DialogHeader>
|
||||
<DialogTitle className="flex items-center gap-x-2">
|
||||
<PlusIcon />
|
||||
@@ -62,7 +65,7 @@ export function CreateProjectDialog({ data }: DialogProps<"resume.sections.proje
|
||||
<ProjectForm />
|
||||
|
||||
<DialogFooter className="sm:col-span-full">
|
||||
<Button variant="ghost" onClick={closeDialog}>
|
||||
<Button variant="ghost" onClick={requestClose}>
|
||||
<Trans>Cancel</Trans>
|
||||
</Button>
|
||||
|
||||
@@ -107,8 +110,10 @@ export function UpdateProjectDialog({ data }: DialogProps<"resume.sections.proje
|
||||
closeDialog();
|
||||
};
|
||||
|
||||
const { blockEvents, requestClose } = useFormBlocker(form);
|
||||
|
||||
return (
|
||||
<DialogContent>
|
||||
<DialogContent {...blockEvents}>
|
||||
<DialogHeader>
|
||||
<DialogTitle className="flex items-center gap-x-2">
|
||||
<PencilSimpleLineIcon />
|
||||
@@ -122,7 +127,7 @@ export function UpdateProjectDialog({ data }: DialogProps<"resume.sections.proje
|
||||
<ProjectForm />
|
||||
|
||||
<DialogFooter className="sm:col-span-full">
|
||||
<Button variant="ghost" onClick={closeDialog}>
|
||||
<Button variant="ghost" onClick={requestClose}>
|
||||
<Trans>Cancel</Trans>
|
||||
</Button>
|
||||
|
||||
|
||||
@@ -12,6 +12,7 @@ import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from "
|
||||
import { Input } from "@/components/ui/input";
|
||||
import type { DialogProps } from "@/dialogs/store";
|
||||
import { useDialogStore } from "@/dialogs/store";
|
||||
import { useFormBlocker } from "@/hooks/use-form-blocker";
|
||||
import { publicationItemSchema } from "@/schema/resume/data";
|
||||
import { generateId } from "@/utils/string";
|
||||
|
||||
@@ -48,8 +49,10 @@ export function CreatePublicationDialog({ data }: DialogProps<"resume.sections.p
|
||||
closeDialog();
|
||||
};
|
||||
|
||||
const { blockEvents, requestClose } = useFormBlocker(form);
|
||||
|
||||
return (
|
||||
<DialogContent>
|
||||
<DialogContent {...blockEvents}>
|
||||
<DialogHeader>
|
||||
<DialogTitle className="flex items-center gap-x-2">
|
||||
<PlusIcon />
|
||||
@@ -63,7 +66,7 @@ export function CreatePublicationDialog({ data }: DialogProps<"resume.sections.p
|
||||
<PublicationForm />
|
||||
|
||||
<DialogFooter className="sm:col-span-full">
|
||||
<Button variant="ghost" onClick={closeDialog}>
|
||||
<Button variant="ghost" onClick={requestClose}>
|
||||
<Trans>Cancel</Trans>
|
||||
</Button>
|
||||
|
||||
@@ -109,8 +112,10 @@ export function UpdatePublicationDialog({ data }: DialogProps<"resume.sections.p
|
||||
closeDialog();
|
||||
};
|
||||
|
||||
const { blockEvents, requestClose } = useFormBlocker(form);
|
||||
|
||||
return (
|
||||
<DialogContent>
|
||||
<DialogContent {...blockEvents}>
|
||||
<DialogHeader>
|
||||
<DialogTitle className="flex items-center gap-x-2">
|
||||
<PencilSimpleLineIcon />
|
||||
@@ -124,7 +129,7 @@ export function UpdatePublicationDialog({ data }: DialogProps<"resume.sections.p
|
||||
<PublicationForm />
|
||||
|
||||
<DialogFooter className="sm:col-span-full">
|
||||
<Button variant="ghost" onClick={closeDialog}>
|
||||
<Button variant="ghost" onClick={requestClose}>
|
||||
<Trans>Cancel</Trans>
|
||||
</Button>
|
||||
|
||||
|
||||
@@ -12,6 +12,7 @@ import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from "
|
||||
import { Input } from "@/components/ui/input";
|
||||
import type { DialogProps } from "@/dialogs/store";
|
||||
import { useDialogStore } from "@/dialogs/store";
|
||||
import { useFormBlocker } from "@/hooks/use-form-blocker";
|
||||
import { referenceItemSchema } from "@/schema/resume/data";
|
||||
import { generateId } from "@/utils/string";
|
||||
|
||||
@@ -48,8 +49,10 @@ export function CreateReferenceDialog({ data }: DialogProps<"resume.sections.ref
|
||||
closeDialog();
|
||||
};
|
||||
|
||||
const { blockEvents, requestClose } = useFormBlocker(form);
|
||||
|
||||
return (
|
||||
<DialogContent>
|
||||
<DialogContent {...blockEvents}>
|
||||
<DialogHeader>
|
||||
<DialogTitle className="flex items-center gap-x-2">
|
||||
<PlusIcon />
|
||||
@@ -63,7 +66,7 @@ export function CreateReferenceDialog({ data }: DialogProps<"resume.sections.ref
|
||||
<ReferenceForm />
|
||||
|
||||
<DialogFooter className="sm:col-span-full">
|
||||
<Button variant="ghost" onClick={closeDialog}>
|
||||
<Button variant="ghost" onClick={requestClose}>
|
||||
<Trans>Cancel</Trans>
|
||||
</Button>
|
||||
|
||||
@@ -109,8 +112,10 @@ export function UpdateReferenceDialog({ data }: DialogProps<"resume.sections.ref
|
||||
closeDialog();
|
||||
};
|
||||
|
||||
const { blockEvents, requestClose } = useFormBlocker(form);
|
||||
|
||||
return (
|
||||
<DialogContent>
|
||||
<DialogContent {...blockEvents}>
|
||||
<DialogHeader>
|
||||
<DialogTitle className="flex items-center gap-x-2">
|
||||
<PencilSimpleLineIcon />
|
||||
@@ -124,7 +129,7 @@ export function UpdateReferenceDialog({ data }: DialogProps<"resume.sections.ref
|
||||
<ReferenceForm />
|
||||
|
||||
<DialogFooter className="sm:col-span-full">
|
||||
<Button variant="ghost" onClick={closeDialog}>
|
||||
<Button variant="ghost" onClick={requestClose}>
|
||||
<Trans>Cancel</Trans>
|
||||
</Button>
|
||||
|
||||
|
||||
@@ -15,6 +15,7 @@ import { Input } from "@/components/ui/input";
|
||||
import { Slider } from "@/components/ui/slider";
|
||||
import type { DialogProps } from "@/dialogs/store";
|
||||
import { useDialogStore } from "@/dialogs/store";
|
||||
import { useFormBlocker } from "@/hooks/use-form-blocker";
|
||||
import { skillItemSchema } from "@/schema/resume/data";
|
||||
import { generateId } from "@/utils/string";
|
||||
import { cn } from "@/utils/style";
|
||||
@@ -52,8 +53,10 @@ export function CreateSkillDialog({ data }: DialogProps<"resume.sections.skills.
|
||||
closeDialog();
|
||||
};
|
||||
|
||||
const { blockEvents, requestClose } = useFormBlocker(form);
|
||||
|
||||
return (
|
||||
<DialogContent>
|
||||
<DialogContent {...blockEvents}>
|
||||
<DialogHeader>
|
||||
<DialogTitle className="flex items-center gap-x-2">
|
||||
<PlusIcon />
|
||||
@@ -67,7 +70,7 @@ export function CreateSkillDialog({ data }: DialogProps<"resume.sections.skills.
|
||||
<SkillForm />
|
||||
|
||||
<DialogFooter className="sm:col-span-full">
|
||||
<Button variant="ghost" onClick={closeDialog}>
|
||||
<Button variant="ghost" onClick={requestClose}>
|
||||
<Trans>Cancel</Trans>
|
||||
</Button>
|
||||
|
||||
@@ -113,8 +116,10 @@ export function UpdateSkillDialog({ data }: DialogProps<"resume.sections.skills.
|
||||
closeDialog();
|
||||
};
|
||||
|
||||
const { blockEvents, requestClose } = useFormBlocker(form);
|
||||
|
||||
return (
|
||||
<DialogContent>
|
||||
<DialogContent {...blockEvents}>
|
||||
<DialogHeader>
|
||||
<DialogTitle className="flex items-center gap-x-2">
|
||||
<PencilSimpleLineIcon />
|
||||
@@ -128,7 +133,7 @@ export function UpdateSkillDialog({ data }: DialogProps<"resume.sections.skills.
|
||||
<SkillForm />
|
||||
|
||||
<DialogFooter className="sm:col-span-full">
|
||||
<Button variant="ghost" onClick={closeDialog}>
|
||||
<Button variant="ghost" onClick={requestClose}>
|
||||
<Trans>Cancel</Trans>
|
||||
</Button>
|
||||
|
||||
|
||||
@@ -12,6 +12,7 @@ import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from "
|
||||
import { Input } from "@/components/ui/input";
|
||||
import type { DialogProps } from "@/dialogs/store";
|
||||
import { useDialogStore } from "@/dialogs/store";
|
||||
import { useFormBlocker } from "@/hooks/use-form-blocker";
|
||||
import { volunteerItemSchema } from "@/schema/resume/data";
|
||||
import { generateId } from "@/utils/string";
|
||||
|
||||
@@ -48,8 +49,10 @@ export function CreateVolunteerDialog({ data }: DialogProps<"resume.sections.vol
|
||||
closeDialog();
|
||||
};
|
||||
|
||||
const { blockEvents, requestClose } = useFormBlocker(form);
|
||||
|
||||
return (
|
||||
<DialogContent>
|
||||
<DialogContent {...blockEvents}>
|
||||
<DialogHeader>
|
||||
<DialogTitle className="flex items-center gap-x-2">
|
||||
<PlusIcon />
|
||||
@@ -63,7 +66,7 @@ export function CreateVolunteerDialog({ data }: DialogProps<"resume.sections.vol
|
||||
<VolunteerForm />
|
||||
|
||||
<DialogFooter className="sm:col-span-full">
|
||||
<Button variant="ghost" onClick={closeDialog}>
|
||||
<Button variant="ghost" onClick={requestClose}>
|
||||
<Trans>Cancel</Trans>
|
||||
</Button>
|
||||
|
||||
@@ -109,8 +112,10 @@ export function UpdateVolunteerDialog({ data }: DialogProps<"resume.sections.vol
|
||||
closeDialog();
|
||||
};
|
||||
|
||||
const { blockEvents, requestClose } = useFormBlocker(form);
|
||||
|
||||
return (
|
||||
<DialogContent>
|
||||
<DialogContent {...blockEvents}>
|
||||
<DialogHeader>
|
||||
<DialogTitle className="flex items-center gap-x-2">
|
||||
<PencilSimpleLineIcon />
|
||||
@@ -124,7 +129,7 @@ export function UpdateVolunteerDialog({ data }: DialogProps<"resume.sections.vol
|
||||
<VolunteerForm />
|
||||
|
||||
<DialogFooter className="sm:col-span-full">
|
||||
<Button variant="ghost" onClick={closeDialog}>
|
||||
<Button variant="ghost" onClick={requestClose}>
|
||||
<Trans>Cancel</Trans>
|
||||
</Button>
|
||||
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
import { t } from "@lingui/core/macro";
|
||||
import { useCallback } from "react";
|
||||
import type { FieldValues, UseFormReturn } from "react-hook-form";
|
||||
import { useDialogStore } from "@/dialogs/store";
|
||||
import { useConfirm } from "@/hooks/use-confirm";
|
||||
|
||||
interface UseFormBlockerOptions {
|
||||
shouldBlock?: () => boolean;
|
||||
}
|
||||
|
||||
export function useFormBlocker<T extends FieldValues>(form: UseFormReturn<T>, options?: UseFormBlockerOptions) {
|
||||
const confirm = useConfirm();
|
||||
const closeDialog = useDialogStore((state) => state.closeDialog);
|
||||
|
||||
// Subscribe to formState changes by reading during render (react-hook-form requirement)
|
||||
const { isDirty, isSubmitting } = form.formState;
|
||||
|
||||
const shouldBlock = useCallback(() => {
|
||||
// Use custom shouldBlock if provided, otherwise use default form state check
|
||||
if (options?.shouldBlock) return options.shouldBlock();
|
||||
return isDirty && !isSubmitting;
|
||||
}, [options, isDirty, isSubmitting]);
|
||||
|
||||
const requestClose = useCallback(async () => {
|
||||
if (!shouldBlock()) {
|
||||
closeDialog();
|
||||
return;
|
||||
}
|
||||
|
||||
const confirmed = await confirm(t`Are you sure you want to close this dialog?`, {
|
||||
description: t`You have unsaved changes that will be lost.`,
|
||||
confirmText: t`Leave`,
|
||||
cancelText: t`Stay`,
|
||||
});
|
||||
|
||||
if (confirmed) closeDialog();
|
||||
}, [shouldBlock, closeDialog, confirm]);
|
||||
|
||||
const blockEvents = {
|
||||
onEscapeKeyDown: (event: KeyboardEvent) => {
|
||||
if (shouldBlock()) {
|
||||
event.preventDefault();
|
||||
requestClose();
|
||||
}
|
||||
},
|
||||
onPointerDownOutside: (event: Event) => {
|
||||
if (shouldBlock()) {
|
||||
event.preventDefault();
|
||||
requestClose();
|
||||
}
|
||||
},
|
||||
onInteractOutside: (event: Event) => {
|
||||
if (shouldBlock()) {
|
||||
event.preventDefault();
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
return { requestClose, blockEvents };
|
||||
}
|
||||
@@ -94,7 +94,7 @@ function BuilderLayout({ initialLayout, ...props }: BuilderLayoutProps) {
|
||||
<div className="flex h-svh flex-col" {...props}>
|
||||
<BuilderHeader />
|
||||
|
||||
<ResizableGroup orientation="horizontal" onLayoutChange={onLayoutChange} className="mt-14 flex-1">
|
||||
<ResizableGroup orientation="horizontal" className="mt-14 flex-1" onLayoutChange={onLayoutChange}>
|
||||
<ResizablePanel
|
||||
collapsible
|
||||
id="left"
|
||||
@@ -107,11 +107,11 @@ function BuilderLayout({ initialLayout, ...props }: BuilderLayoutProps) {
|
||||
>
|
||||
<BuilderSidebarLeft />
|
||||
</ResizablePanel>
|
||||
<ResizableSeparator withHandle className="z-20 border-r" />
|
||||
<ResizableSeparator withHandle className="z-20 border-s" />
|
||||
<ResizablePanel id="artboard" defaultSize={artboardSize} className="h-[calc(100svh-3.5rem)]">
|
||||
<Outlet />
|
||||
</ResizablePanel>
|
||||
<ResizableSeparator withHandle className="z-20 border-l" />
|
||||
<ResizableSeparator withHandle className="z-20 border-e" />
|
||||
<ResizablePanel
|
||||
collapsible
|
||||
id="right"
|
||||
|
||||
Reference in New Issue
Block a user