use vite+

This commit is contained in:
Amruth Pillai
2026-03-18 22:03:24 +01:00
parent d1dac8aeca
commit 192880e416
427 changed files with 58915 additions and 58759 deletions
+156 -156
View File
@@ -21,194 +21,194 @@ import { authClient } from "@/integrations/auth/client";
import { type DialogProps, useDialogStore } from "../store";
const formSchema = z.object({
name: z.string().min(1).max(64),
expiresIn: z.number().min(1),
name: z.string().min(1).max(64),
expiresIn: z.number().min(1),
});
type FormValues = z.infer<typeof formSchema>;
export function CreateApiKeyDialog(_: DialogProps<"api-key.create">) {
const [apiKey, setApiKey] = useState<string | null>(null);
const [apiKey, setApiKey] = useState<string | null>(null);
if (apiKey) return <CopyApiKeyForm apiKey={apiKey} />;
if (apiKey) return <CopyApiKeyForm apiKey={apiKey} />;
return <CreateApiKeyForm setApiKey={setApiKey} />;
return <CreateApiKeyForm setApiKey={setApiKey} />;
}
type CreateApiKeyFormProps = {
setApiKey: (apiKey: string) => void;
setApiKey: (apiKey: string) => void;
};
const CreateApiKeyForm = ({ setApiKey }: CreateApiKeyFormProps) => {
const form = useForm<FormValues>({
resolver: zodResolver(formSchema),
defaultValues: {
name: "",
expiresIn: 3600 * 24 * 30,
},
});
const form = useForm<FormValues>({
resolver: zodResolver(formSchema),
defaultValues: {
name: "",
expiresIn: 3600 * 24 * 30,
},
});
const { blockEvents } = useFormBlocker(form);
const { blockEvents } = useFormBlocker(form);
const onSubmit = async (values: FormValues) => {
const toastId = toast.loading(t`Creating your API key...`);
const onSubmit = async (values: FormValues) => {
const toastId = toast.loading(t`Creating your API key...`);
const { data, error } = await authClient.apiKey.create({
name: values.name,
expiresIn: values.expiresIn,
});
const { data, error } = await authClient.apiKey.create({
name: values.name,
expiresIn: values.expiresIn,
});
if (error) {
toast.error(error.message, { id: toastId });
return;
}
if (error) {
toast.error(error.message, { id: toastId });
return;
}
setApiKey(data.key);
toast.dismiss(toastId);
};
setApiKey(data.key);
toast.dismiss(toastId);
};
return (
<DialogContent {...blockEvents}>
<DialogHeader>
<DialogTitle className="flex items-center gap-x-2">
<PlusIcon />
<Trans>Create a new API key</Trans>
</DialogTitle>
<DialogDescription>
<Trans>
This will generate a new API key to access the Reactive Resume API to allow machines to interact with your
resume data.
</Trans>
</DialogDescription>
</DialogHeader>
return (
<DialogContent {...blockEvents}>
<DialogHeader>
<DialogTitle className="flex items-center gap-x-2">
<PlusIcon />
<Trans>Create a new API key</Trans>
</DialogTitle>
<DialogDescription>
<Trans>
This will generate a new API key to access the Reactive Resume API to allow machines to interact with your
resume data.
</Trans>
</DialogDescription>
</DialogHeader>
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-6 py-2">
<FormField
control={form.control}
name="name"
render={({ field }) => (
<FormItem>
<FormLabel>
<Trans>Name</Trans>
</FormLabel>
<FormControl render={<Input min={1} max={64} {...field} />} />
<FormMessage />
<FormDescription>
<Trans>
Tip: Give your API key a name, corresponding to the purpose of the key, to help you identify it
later.
</Trans>
</FormDescription>
</FormItem>
)}
/>
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-6 py-2">
<FormField
control={form.control}
name="name"
render={({ field }) => (
<FormItem>
<FormLabel>
<Trans>Name</Trans>
</FormLabel>
<FormControl render={<Input min={1} max={64} {...field} />} />
<FormMessage />
<FormDescription>
<Trans>
Tip: Give your API key a name, corresponding to the purpose of the key, to help you identify it
later.
</Trans>
</FormDescription>
</FormItem>
)}
/>
<FormField
control={form.control}
name="expiresIn"
render={({ field }) => (
<FormItem>
<FormLabel>
<Trans>Expires in</Trans>
</FormLabel>
<FormControl
render={
<Combobox
value={field.value}
onValueChange={(value) => value && field.onChange(Number(value))}
options={[
{
// 1 month = 30 days
value: 3600 * 24 * 30,
label: t`1 month`,
},
{
// 3 months = 90 days
value: 3600 * 24 * 90,
label: t`3 months`,
},
{
// 6 months = 180 days
value: 3600 * 24 * 180,
label: t`6 months`,
},
{
// 1 year = 365 days
value: 3600 * 24 * 365,
label: t`1 year`,
},
]}
/>
}
/>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="expiresIn"
render={({ field }) => (
<FormItem>
<FormLabel>
<Trans>Expires in</Trans>
</FormLabel>
<FormControl
render={
<Combobox
value={field.value}
onValueChange={(value) => value && field.onChange(Number(value))}
options={[
{
// 1 month = 30 days
value: 3600 * 24 * 30,
label: t`1 month`,
},
{
// 3 months = 90 days
value: 3600 * 24 * 90,
label: t`3 months`,
},
{
// 6 months = 180 days
value: 3600 * 24 * 180,
label: t`6 months`,
},
{
// 1 year = 365 days
value: 3600 * 24 * 365,
label: t`1 year`,
},
]}
/>
}
/>
<FormMessage />
</FormItem>
)}
/>
<DialogFooter>
<Button type="submit">
<Trans>Create</Trans>
</Button>
</DialogFooter>
</form>
</Form>
</DialogContent>
);
<DialogFooter>
<Button type="submit">
<Trans>Create</Trans>
</Button>
</DialogFooter>
</form>
</Form>
</DialogContent>
);
};
type CopyApiKeyFormProps = {
apiKey: string;
apiKey: string;
};
const CopyApiKeyForm = ({ apiKey }: CopyApiKeyFormProps) => {
const queryClient = useQueryClient();
const [_, copyToClipboard] = useCopyToClipboard();
const closeDialog = useDialogStore((state) => state.closeDialog);
const queryClient = useQueryClient();
const [_, copyToClipboard] = useCopyToClipboard();
const closeDialog = useDialogStore((state) => state.closeDialog);
const onCopy = async () => {
await copyToClipboard(apiKey);
toast.success(t`Your API key has been copied to the clipboard.`);
};
const onCopy = async () => {
await copyToClipboard(apiKey);
toast.success(t`Your API key has been copied to the clipboard.`);
};
const onConfirm = () => {
closeDialog();
void queryClient.invalidateQueries({ queryKey: ["auth", "api-keys"] });
};
const onConfirm = () => {
closeDialog();
void queryClient.invalidateQueries({ queryKey: ["auth", "api-keys"] });
};
return (
<DialogContent>
<DialogHeader>
<DialogTitle className="flex items-center gap-x-2">
<CopyIcon />
<Trans>Here's your new API key</Trans>
</DialogTitle>
<DialogDescription>
<Trans>Copy this secret key and use it in your applications to access your data.</Trans>
</DialogDescription>
</DialogHeader>
return (
<DialogContent>
<DialogHeader>
<DialogTitle className="flex items-center gap-x-2">
<CopyIcon />
<Trans>Here's your new API key</Trans>
</DialogTitle>
<DialogDescription>
<Trans>Copy this secret key and use it in your applications to access your data.</Trans>
</DialogDescription>
</DialogHeader>
<div className="space-y-2 py-2">
<InputGroup>
<InputGroupInput value={apiKey} readOnly />
<InputGroupAddon align="inline-end">
<InputGroupButton size="icon-sm" onClick={onCopy}>
<CopyIcon />
</InputGroupButton>
</InputGroupAddon>
</InputGroup>
<div className="space-y-2 py-2">
<InputGroup>
<InputGroupInput value={apiKey} readOnly />
<InputGroupAddon align="inline-end">
<InputGroupButton size="icon-sm" onClick={onCopy}>
<CopyIcon />
</InputGroupButton>
</InputGroupAddon>
</InputGroup>
<span className="text-sm font-medium text-muted-foreground">
<Trans>For security reasons, this key will only be displayed once.</Trans>
</span>
</div>
<span className="text-sm font-medium text-muted-foreground">
<Trans>For security reasons, this key will only be displayed once.</Trans>
</span>
</div>
<DialogFooter>
<Button onClick={onConfirm}>
<Trans>Confirm</Trans>
</Button>
</DialogFooter>
</DialogContent>
);
<DialogFooter>
<Button onClick={onConfirm}>
<Trans>Confirm</Trans>
</Button>
</DialogFooter>
</DialogContent>
);
};
+112 -112
View File
@@ -18,133 +18,133 @@ import { authClient } from "@/integrations/auth/client";
import { type DialogProps, useDialogStore } from "../store";
const formSchema = z
.object({
currentPassword: z.string().min(6).max(64),
newPassword: z.string().min(6).max(64),
})
.refine((data) => data.newPassword !== data.currentPassword, {
message: "New password cannot be the same as the current password.",
path: ["newPassword"],
});
.object({
currentPassword: z.string().min(6).max(64),
newPassword: z.string().min(6).max(64),
})
.refine((data) => data.newPassword !== data.currentPassword, {
message: "New password cannot be the same as the current password.",
path: ["newPassword"],
});
type FormValues = z.infer<typeof formSchema>;
export function ChangePasswordDialog(_: DialogProps<"auth.change-password">) {
const queryClient = useQueryClient();
const closeDialog = useDialogStore((state) => state.closeDialog);
const queryClient = useQueryClient();
const closeDialog = useDialogStore((state) => state.closeDialog);
const [showCurrentPassword, toggleShowCurrentPassword] = useToggle(false);
const [showNewPassword, toggleShowNewPassword] = useToggle(false);
const [showCurrentPassword, toggleShowCurrentPassword] = useToggle(false);
const [showNewPassword, toggleShowNewPassword] = useToggle(false);
const form = useForm<FormValues>({
resolver: zodResolver(formSchema),
defaultValues: {
currentPassword: "",
newPassword: "",
},
});
const form = useForm<FormValues>({
resolver: zodResolver(formSchema),
defaultValues: {
currentPassword: "",
newPassword: "",
},
});
const { blockEvents } = useFormBlocker(form);
const { blockEvents } = useFormBlocker(form);
const onSubmit = async (data: FormValues) => {
const toastId = toast.loading(t`Updating your password...`);
const onSubmit = async (data: FormValues) => {
const toastId = toast.loading(t`Updating your password...`);
const { error } = await authClient.changePassword({
currentPassword: data.currentPassword,
newPassword: data.newPassword,
});
const { error } = await authClient.changePassword({
currentPassword: data.currentPassword,
newPassword: data.newPassword,
});
if (error) {
toast.error(error.message, { id: toastId });
return;
}
if (error) {
toast.error(error.message, { id: toastId });
return;
}
toast.success(t`Your password has been updated successfully.`, { id: toastId });
void queryClient.invalidateQueries({ queryKey: ["auth", "accounts"] });
closeDialog();
};
toast.success(t`Your password has been updated successfully.`, { id: toastId });
void queryClient.invalidateQueries({ queryKey: ["auth", "accounts"] });
closeDialog();
};
return (
<DialogContent {...blockEvents}>
<DialogHeader>
<DialogTitle className="flex items-center gap-x-2">
<PasswordIcon />
<Trans>Update your password</Trans>
</DialogTitle>
<DialogDescription>
<Trans>Enter your current password and a new password to update your account.</Trans>
</DialogDescription>
</DialogHeader>
return (
<DialogContent {...blockEvents}>
<DialogHeader>
<DialogTitle className="flex items-center gap-x-2">
<PasswordIcon />
<Trans>Update your password</Trans>
</DialogTitle>
<DialogDescription>
<Trans>Enter your current password and a new password to update your account.</Trans>
</DialogDescription>
</DialogHeader>
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
<FormField
control={form.control}
name="currentPassword"
render={({ field }) => (
<FormItem>
<FormLabel>
<Trans>Current Password</Trans>
</FormLabel>
<div className="flex items-center gap-x-1.5">
<FormControl
render={
<Input
min={6}
max={64}
type={showCurrentPassword ? "text" : "password"}
autoComplete="current-password"
{...field}
/>
}
/>
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
<FormField
control={form.control}
name="currentPassword"
render={({ field }) => (
<FormItem>
<FormLabel>
<Trans>Current Password</Trans>
</FormLabel>
<div className="flex items-center gap-x-1.5">
<FormControl
render={
<Input
min={6}
max={64}
type={showCurrentPassword ? "text" : "password"}
autoComplete="current-password"
{...field}
/>
}
/>
<Button size="icon" variant="ghost" type="button" onClick={toggleShowCurrentPassword}>
{showCurrentPassword ? <EyeIcon /> : <EyeSlashIcon />}
</Button>
</div>
<FormMessage />
</FormItem>
)}
/>
<Button size="icon" variant="ghost" type="button" onClick={toggleShowCurrentPassword}>
{showCurrentPassword ? <EyeIcon /> : <EyeSlashIcon />}
</Button>
</div>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="newPassword"
render={({ field }) => (
<FormItem>
<FormLabel>
<Trans>New Password</Trans>
</FormLabel>
<div className="flex items-center gap-x-1.5">
<FormControl
render={
<Input
min={6}
max={64}
type={showNewPassword ? "text" : "password"}
autoComplete="new-password"
{...field}
/>
}
/>
<FormField
control={form.control}
name="newPassword"
render={({ field }) => (
<FormItem>
<FormLabel>
<Trans>New Password</Trans>
</FormLabel>
<div className="flex items-center gap-x-1.5">
<FormControl
render={
<Input
min={6}
max={64}
type={showNewPassword ? "text" : "password"}
autoComplete="new-password"
{...field}
/>
}
/>
<Button size="icon" variant="ghost" type="button" onClick={toggleShowNewPassword}>
{showNewPassword ? <EyeIcon /> : <EyeSlashIcon />}
</Button>
</div>
<FormMessage />
</FormItem>
)}
/>
<Button size="icon" variant="ghost" type="button" onClick={toggleShowNewPassword}>
{showNewPassword ? <EyeIcon /> : <EyeSlashIcon />}
</Button>
</div>
<FormMessage />
</FormItem>
)}
/>
<DialogFooter>
<Button type="submit">
<Trans>Update Password</Trans>
</Button>
</DialogFooter>
</form>
</Form>
</DialogContent>
);
<DialogFooter>
<Button type="submit">
<Trans>Update Password</Trans>
</Button>
</DialogFooter>
</form>
</Form>
</DialogContent>
);
}
+76 -76
View File
@@ -18,95 +18,95 @@ import { authClient } from "@/integrations/auth/client";
import { type DialogProps, useDialogStore } from "../store";
const formSchema = z.object({
password: z.string().min(6).max(64),
password: z.string().min(6).max(64),
});
type FormValues = z.infer<typeof formSchema>;
export function DisableTwoFactorDialog(_: DialogProps<"auth.two-factor.disable">) {
const router = useRouter();
const [showPassword, toggleShowPassword] = useToggle(false);
const closeDialog = useDialogStore((state) => state.closeDialog);
const router = useRouter();
const [showPassword, toggleShowPassword] = useToggle(false);
const closeDialog = useDialogStore((state) => state.closeDialog);
const form = useForm<FormValues>({
resolver: zodResolver(formSchema),
defaultValues: {
password: "",
},
});
const form = useForm<FormValues>({
resolver: zodResolver(formSchema),
defaultValues: {
password: "",
},
});
const { blockEvents } = useFormBlocker(form);
const { blockEvents } = useFormBlocker(form);
const onSubmit = async (data: FormValues) => {
const toastId = toast.loading(t`Disabling two-factor authentication...`);
const onSubmit = async (data: FormValues) => {
const toastId = toast.loading(t`Disabling two-factor authentication...`);
const { error } = await authClient.twoFactor.disable({ password: data.password });
const { error } = await authClient.twoFactor.disable({ password: data.password });
if (error) {
toast.error(error.message, { id: toastId });
return;
}
if (error) {
toast.error(error.message, { id: toastId });
return;
}
toast.success(t`Two-factor authentication has been disabled successfully.`, { id: toastId });
void router.invalidate();
closeDialog();
form.reset();
};
toast.success(t`Two-factor authentication has been disabled successfully.`, { id: toastId });
void router.invalidate();
closeDialog();
form.reset();
};
return (
<DialogContent {...blockEvents}>
<DialogHeader>
<DialogTitle className="flex items-center gap-x-2">
<LockOpenIcon />
<Trans>Disable Two-Factor Authentication</Trans>
</DialogTitle>
<DialogDescription>
<Trans>
Enter your password to disable two-factor authentication. Your account will be less secure without 2FA
enabled.
</Trans>
</DialogDescription>
</DialogHeader>
return (
<DialogContent {...blockEvents}>
<DialogHeader>
<DialogTitle className="flex items-center gap-x-2">
<LockOpenIcon />
<Trans>Disable Two-Factor Authentication</Trans>
</DialogTitle>
<DialogDescription>
<Trans>
Enter your password to disable two-factor authentication. Your account will be less secure without 2FA
enabled.
</Trans>
</DialogDescription>
</DialogHeader>
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
<FormField
control={form.control}
name="password"
render={({ field }) => (
<FormItem>
<FormLabel>
<Trans>Password</Trans>
</FormLabel>
<div className="flex items-center gap-x-1.5">
<FormControl
render={
<Input
min={6}
max={64}
type={showPassword ? "text" : "password"}
autoComplete="current-password"
{...field}
/>
}
/>
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
<FormField
control={form.control}
name="password"
render={({ field }) => (
<FormItem>
<FormLabel>
<Trans>Password</Trans>
</FormLabel>
<div className="flex items-center gap-x-1.5">
<FormControl
render={
<Input
min={6}
max={64}
type={showPassword ? "text" : "password"}
autoComplete="current-password"
{...field}
/>
}
/>
<Button size="icon" variant="ghost" type="button" onClick={toggleShowPassword}>
{showPassword ? <EyeIcon /> : <EyeSlashIcon />}
</Button>
</div>
<FormMessage />
</FormItem>
)}
/>
<Button size="icon" variant="ghost" type="button" onClick={toggleShowPassword}>
{showPassword ? <EyeIcon /> : <EyeSlashIcon />}
</Button>
</div>
<FormMessage />
</FormItem>
)}
/>
<DialogFooter>
<Button type="submit" variant="destructive">
<Trans>Disable 2FA</Trans>
</Button>
</DialogFooter>
</form>
</Form>
</DialogContent>
);
<DialogFooter>
<Button type="submit" variant="destructive">
<Trans>Disable 2FA</Trans>
</Button>
</DialogFooter>
</form>
</Form>
</DialogContent>
);
}
+275 -275
View File
@@ -23,324 +23,324 @@ import { authClient } from "@/integrations/auth/client";
import { type DialogProps, useDialogStore } from "../store";
const enableFormSchema = z.object({
password: z.string().min(6).max(64),
password: z.string().min(6).max(64),
});
type EnableFormValues = z.infer<typeof enableFormSchema>;
const verifyFormSchema = z.object({
code: z.string().length(6, "Code must be 6 digits"),
code: z.string().length(6, "Code must be 6 digits"),
});
type VerifyFormValues = z.infer<typeof verifyFormSchema>;
export function EnableTwoFactorDialog(_: DialogProps<"auth.two-factor.enable">) {
const router = useRouter();
const router = useRouter();
const [totpUri, setTotpUri] = useState<string | null>(null);
const [backupCodes, setBackupCodes] = useState<string[] | null>(null);
const [step, setStep] = useState<"enable" | "verify" | "backup">("enable");
const [totpUri, setTotpUri] = useState<string | null>(null);
const [backupCodes, setBackupCodes] = useState<string[] | null>(null);
const [step, setStep] = useState<"enable" | "verify" | "backup">("enable");
const [showPassword, toggleShowPassword] = useToggle(false);
const closeDialog = useDialogStore((state) => state.closeDialog);
const [showPassword, toggleShowPassword] = useToggle(false);
const closeDialog = useDialogStore((state) => state.closeDialog);
const enableForm = useForm<EnableFormValues>({
resolver: zodResolver(enableFormSchema),
defaultValues: {
password: "",
},
});
const enableForm = useForm<EnableFormValues>({
resolver: zodResolver(enableFormSchema),
defaultValues: {
password: "",
},
});
const verifyForm = useForm<VerifyFormValues>({
resolver: zodResolver(verifyFormSchema),
defaultValues: {
code: "",
},
});
const verifyForm = useForm<VerifyFormValues>({
resolver: zodResolver(verifyFormSchema),
defaultValues: {
code: "",
},
});
const enableFormState = enableForm.formState;
const verifyFormState = verifyForm.formState;
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 { 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...`);
const onEnableSubmit = async (values: EnableFormValues) => {
const toastId = toast.loading(t`Enabling two-factor authentication...`);
const { data, error } = await authClient.twoFactor.enable({
password: values.password,
issuer: "Reactive Resume",
});
const { data, error } = await authClient.twoFactor.enable({
password: values.password,
issuer: "Reactive Resume",
});
if (error) {
toast.error(error.message, { id: toastId });
return;
}
if (error) {
toast.error(error.message, { id: toastId });
return;
}
if (data.totpURI && data.backupCodes) {
setTotpUri(data.totpURI);
setBackupCodes(data.backupCodes);
setStep("verify");
toast.dismiss(toastId);
} else {
toast.error(t`Failed to setup two-factor authentication.`, { id: toastId });
}
};
if (data.totpURI && data.backupCodes) {
setTotpUri(data.totpURI);
setBackupCodes(data.backupCodes);
setStep("verify");
toast.dismiss(toastId);
} else {
toast.error(t`Failed to setup two-factor authentication.`, { id: toastId });
}
};
const onVerifySubmit = async (data: VerifyFormValues) => {
const toastId = toast.loading(t`Verifying code...`);
const onVerifySubmit = async (data: VerifyFormValues) => {
const toastId = toast.loading(t`Verifying code...`);
const { error } = await authClient.twoFactor.verifyTotp({ code: data.code });
const { error } = await authClient.twoFactor.verifyTotp({ code: data.code });
if (error) {
toast.error(error.message, { id: toastId });
return;
}
if (error) {
toast.error(error.message, { id: toastId });
return;
}
toast.dismiss(toastId);
setStep("backup");
};
toast.dismiss(toastId);
setStep("backup");
};
const onConfirmBackup = () => {
toast.success(t`Two-factor authentication has been setup successfully.`);
void router.invalidate();
closeDialog();
onReset();
};
const onConfirmBackup = () => {
toast.success(t`Two-factor authentication has been setup successfully.`);
void router.invalidate();
closeDialog();
onReset();
};
const onReset = () => {
enableForm.reset();
verifyForm.reset();
setStep("enable");
setTotpUri(null);
setBackupCodes(null);
};
const onReset = () => {
enableForm.reset();
verifyForm.reset();
setStep("enable");
setTotpUri(null);
setBackupCodes(null);
};
const handleCopySecret = async () => {
if (!totpUri) return;
const secret = extractSecretFromTotpUri(totpUri);
if (!secret) return;
await navigator.clipboard.writeText(secret);
toast.success(t`Secret copied to clipboard.`);
};
const handleCopySecret = async () => {
if (!totpUri) return;
const secret = extractSecretFromTotpUri(totpUri);
if (!secret) return;
await navigator.clipboard.writeText(secret);
toast.success(t`Secret copied to clipboard.`);
};
const handleCopyBackupCodes = async () => {
if (!backupCodes) return;
await navigator.clipboard.writeText(backupCodes.join("\n"));
toast.success(t`Backup codes copied to clipboard.`);
};
const handleCopyBackupCodes = async () => {
if (!backupCodes) return;
await navigator.clipboard.writeText(backupCodes.join("\n"));
toast.success(t`Backup codes copied to clipboard.`);
};
const handleDownloadBackupCodes = () => {
if (!backupCodes) return;
const content = backupCodes.join("\n");
const blob = new Blob([content], { type: "text/plain" });
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download = "reactive-resume_backup-codes.txt";
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
};
const handleDownloadBackupCodes = () => {
if (!backupCodes) return;
const content = backupCodes.join("\n");
const blob = new Blob([content], { type: "text/plain" });
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download = "reactive-resume_backup-codes.txt";
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
};
return (
<DialogContent className="max-w-md" {...blockEvents}>
<DialogHeader>
<DialogTitle>
{match(step)
.with("enable", () => <Trans>Enable Two-Factor Authentication</Trans>)
.with("verify", () => <Trans>Setup Authenticator App</Trans>)
.with("backup", () => <Trans>Copy Backup Codes</Trans>)
.exhaustive()}
</DialogTitle>
<DialogDescription>
{match(step)
.with("enable", () => (
<Trans>
Enter your password to confirm setting up two-factor authentication. When enabled, you'll need to enter
a code from your authenticator app every time you log in.
</Trans>
))
.with("verify", () => (
<Trans>
Scan the QR code below with your preferred authenticator app. You can also copy the secret below and
paste it into your app.
</Trans>
))
.with("backup", () => <Trans>Copy and store these backup codes in case you lose your device.</Trans>)
.exhaustive()}
</DialogDescription>
</DialogHeader>
return (
<DialogContent className="max-w-md" {...blockEvents}>
<DialogHeader>
<DialogTitle>
{match(step)
.with("enable", () => <Trans>Enable Two-Factor Authentication</Trans>)
.with("verify", () => <Trans>Setup Authenticator App</Trans>)
.with("backup", () => <Trans>Copy Backup Codes</Trans>)
.exhaustive()}
</DialogTitle>
<DialogDescription>
{match(step)
.with("enable", () => (
<Trans>
Enter your password to confirm setting up two-factor authentication. When enabled, you'll need to enter
a code from your authenticator app every time you log in.
</Trans>
))
.with("verify", () => (
<Trans>
Scan the QR code below with your preferred authenticator app. You can also copy the secret below and
paste it into your app.
</Trans>
))
.with("backup", () => <Trans>Copy and store these backup codes in case you lose your device.</Trans>)
.exhaustive()}
</DialogDescription>
</DialogHeader>
{match(step)
.with("enable", () => (
<Form {...enableForm}>
<form onSubmit={enableForm.handleSubmit(onEnableSubmit)} className="space-y-4">
<FormField
control={enableForm.control}
name="password"
render={({ field }) => (
<FormItem>
<FormLabel>
<Trans>Password</Trans>
</FormLabel>
<div className="flex items-center gap-x-1.5">
<FormControl
render={
<Input
min={6}
max={64}
type={showPassword ? "text" : "password"}
autoComplete="current-password"
{...field}
/>
}
/>
{match(step)
.with("enable", () => (
<Form {...enableForm}>
<form onSubmit={enableForm.handleSubmit(onEnableSubmit)} className="space-y-4">
<FormField
control={enableForm.control}
name="password"
render={({ field }) => (
<FormItem>
<FormLabel>
<Trans>Password</Trans>
</FormLabel>
<div className="flex items-center gap-x-1.5">
<FormControl
render={
<Input
min={6}
max={64}
type={showPassword ? "text" : "password"}
autoComplete="current-password"
{...field}
/>
}
/>
<Button size="icon" variant="ghost" type="button" onClick={toggleShowPassword}>
{showPassword ? <EyeIcon /> : <EyeSlashIcon />}
</Button>
</div>
<FormMessage />
</FormItem>
)}
/>
<Button size="icon" variant="ghost" type="button" onClick={toggleShowPassword}>
{showPassword ? <EyeIcon /> : <EyeSlashIcon />}
</Button>
</div>
<FormMessage />
</FormItem>
)}
/>
<DialogFooter>
<Button type="submit">
<Trans>Continue</Trans>
</Button>
</DialogFooter>
</form>
</Form>
))
.with("verify", () => {
const secret = totpUri ? extractSecretFromTotpUri(totpUri) : null;
return (
<div className="space-y-4">
{totpUri && secret && (
<>
<div className="flex items-center gap-x-2">
<Input readOnly value={secret} className="font-mono text-sm" />
<Button size="icon" variant="ghost" type="button" onClick={handleCopySecret}>
<CopyIcon />
</Button>
</div>
<DialogFooter>
<Button type="submit">
<Trans>Continue</Trans>
</Button>
</DialogFooter>
</form>
</Form>
))
.with("verify", () => {
const secret = totpUri ? extractSecretFromTotpUri(totpUri) : null;
return (
<div className="space-y-4">
{totpUri && secret && (
<>
<div className="flex items-center gap-x-2">
<Input readOnly value={secret} className="font-mono text-sm" />
<Button size="icon" variant="ghost" type="button" onClick={handleCopySecret}>
<CopyIcon />
</Button>
</div>
<TwoFactorQRCode totpUri={totpUri} />
</>
)}
<TwoFactorQRCode totpUri={totpUri} />
</>
)}
<p>
<Trans>Then, enter the 6 digit code that the app provides to continue.</Trans>
</p>
<p>
<Trans>Then, enter the 6 digit code that the app provides to continue.</Trans>
</p>
<Form {...verifyForm}>
<form onSubmit={verifyForm.handleSubmit(onVerifySubmit)}>
<FormField
control={verifyForm.control}
name="code"
render={({ field }) => (
<FormItem>
<FormControl
render={
<InputOTP
maxLength={6}
value={field.value}
onChange={field.onChange}
pattern={REGEXP_ONLY_DIGITS}
onComplete={verifyForm.handleSubmit(onVerifySubmit)}
pasteTransformer={(pasted) => pasted.replaceAll("-", "")}
>
<InputOTPGroup>
<InputOTPSlot index={0} className="size-12" />
<InputOTPSlot index={1} className="size-12" />
<InputOTPSlot index={2} className="size-12" />
<InputOTPSlot index={3} className="size-12" />
<InputOTPSlot index={4} className="size-12" />
<InputOTPSlot index={5} className="size-12" />
</InputOTPGroup>
</InputOTP>
}
/>
<FormMessage />
</FormItem>
)}
/>
<Form {...verifyForm}>
<form onSubmit={verifyForm.handleSubmit(onVerifySubmit)}>
<FormField
control={verifyForm.control}
name="code"
render={({ field }) => (
<FormItem>
<FormControl
render={
<InputOTP
maxLength={6}
value={field.value}
onChange={field.onChange}
pattern={REGEXP_ONLY_DIGITS}
onComplete={verifyForm.handleSubmit(onVerifySubmit)}
pasteTransformer={(pasted) => pasted.replaceAll("-", "")}
>
<InputOTPGroup>
<InputOTPSlot index={0} className="size-12" />
<InputOTPSlot index={1} className="size-12" />
<InputOTPSlot index={2} className="size-12" />
<InputOTPSlot index={3} className="size-12" />
<InputOTPSlot index={4} className="size-12" />
<InputOTPSlot index={5} className="size-12" />
</InputOTPGroup>
</InputOTP>
}
/>
<FormMessage />
</FormItem>
)}
/>
<DialogFooter className="gap-x-2">
<Button type="button" variant="outline" onClick={requestClose}>
<Trans>Cancel</Trans>
</Button>
<Button type="submit">
<Trans>Continue</Trans>
</Button>
</DialogFooter>
</form>
</Form>
</div>
);
})
.with("backup", () => (
<div className="space-y-4">
{backupCodes && (
<div className="space-y-4">
<div className="grid grid-cols-2 gap-2">
{backupCodes.map((code, index) => (
<div key={index} className="rounded-md border border-border p-2 text-center font-mono text-sm">
{code}
</div>
))}
</div>
<DialogFooter className="gap-x-2">
<Button type="button" variant="outline" onClick={requestClose}>
<Trans>Cancel</Trans>
</Button>
<Button type="submit">
<Trans>Continue</Trans>
</Button>
</DialogFooter>
</form>
</Form>
</div>
);
})
.with("backup", () => (
<div className="space-y-4">
{backupCodes && (
<div className="space-y-4">
<div className="grid grid-cols-2 gap-2">
{backupCodes.map((code, index) => (
<div key={index} className="rounded-md border border-border p-2 text-center font-mono text-sm">
{code}
</div>
))}
</div>
<div className="flex items-center gap-x-2">
<Button type="button" variant="outline" onClick={handleDownloadBackupCodes} className="flex-1">
<ArrowDownIcon className="me-2 size-4" />
<Trans>Download</Trans>
</Button>
<Button type="button" variant="ghost" onClick={handleCopyBackupCodes} className="flex-1">
<CopyIcon className="me-2 size-4" />
<Trans>Copy</Trans>
</Button>
</div>
</div>
)}
<div className="flex items-center gap-x-2">
<Button type="button" variant="outline" onClick={handleDownloadBackupCodes} className="flex-1">
<ArrowDownIcon className="me-2 size-4" />
<Trans>Download</Trans>
</Button>
<Button type="button" variant="ghost" onClick={handleCopyBackupCodes} className="flex-1">
<CopyIcon className="me-2 size-4" />
<Trans>Copy</Trans>
</Button>
</div>
</div>
)}
<DialogFooter>
<Button type="button" onClick={onConfirmBackup}>
<Trans>Continue</Trans>
</Button>
</DialogFooter>
</div>
))
.exhaustive()}
</DialogContent>
);
<DialogFooter>
<Button type="button" onClick={onConfirmBackup}>
<Trans>Continue</Trans>
</Button>
</DialogFooter>
</div>
))
.exhaustive()}
</DialogContent>
);
}
function extractSecretFromTotpUri(totpUri: string): string | null {
try {
const url = new URL(totpUri);
return url.searchParams.get("secret");
} catch {
return null;
}
try {
const url = new URL(totpUri);
return url.searchParams.get("secret");
} catch {
return null;
}
}
function TwoFactorQRCode({ totpUri }: { totpUri: string }) {
return (
<QRCodeSVG
value={totpUri}
size={256}
marginSize={2}
className="rounded-md"
title="Two-Factor Authentication QR Code"
/>
);
return (
<QRCodeSVG
value={totpUri}
size={256}
marginSize={2}
className="rounded-md"
title="Two-Factor Authentication QR Code"
/>
);
}
+47 -47
View File
@@ -27,53 +27,53 @@ import { TemplateGalleryDialog } from "./resume/template/gallery";
import { useDialogStore } from "./store";
export function DialogManager() {
const { open, activeDialog, onOpenChange } = useDialogStore();
const { open, activeDialog, onOpenChange } = useDialogStore();
const DialogContent = match(activeDialog)
.with({ type: "auth.change-password" }, () => <ChangePasswordDialog />)
.with({ type: "auth.two-factor.enable" }, () => <EnableTwoFactorDialog />)
.with({ type: "auth.two-factor.disable" }, () => <DisableTwoFactorDialog />)
.with({ type: "api-key.create" }, () => <CreateApiKeyDialog />)
.with({ type: "resume.create" }, () => <CreateResumeDialog />)
.with({ type: "resume.update" }, ({ data }) => <UpdateResumeDialog data={data} />)
.with({ type: "resume.duplicate" }, ({ data }) => <DuplicateResumeDialog data={data} />)
.with({ type: "resume.import" }, () => <ImportResumeDialog />)
.with({ type: "resume.template.gallery" }, () => <TemplateGalleryDialog />)
.with({ type: "resume.sections.profiles.create" }, ({ data }) => <CreateProfileDialog data={data} />)
.with({ type: "resume.sections.profiles.update" }, ({ data }) => <UpdateProfileDialog data={data} />)
.with({ type: "resume.sections.experience.create" }, ({ data }) => <CreateExperienceDialog data={data} />)
.with({ type: "resume.sections.experience.update" }, ({ data }) => <UpdateExperienceDialog data={data} />)
.with({ type: "resume.sections.education.create" }, ({ data }) => <CreateEducationDialog data={data} />)
.with({ type: "resume.sections.education.update" }, ({ data }) => <UpdateEducationDialog data={data} />)
.with({ type: "resume.sections.skills.create" }, ({ data }) => <CreateSkillDialog data={data} />)
.with({ type: "resume.sections.skills.update" }, ({ data }) => <UpdateSkillDialog data={data} />)
.with({ type: "resume.sections.projects.create" }, ({ data }) => <CreateProjectDialog data={data} />)
.with({ type: "resume.sections.projects.update" }, ({ data }) => <UpdateProjectDialog data={data} />)
.with({ type: "resume.sections.certifications.create" }, ({ data }) => <CreateCertificationDialog data={data} />)
.with({ type: "resume.sections.certifications.update" }, ({ data }) => <UpdateCertificationDialog data={data} />)
.with({ type: "resume.sections.languages.create" }, ({ data }) => <CreateLanguageDialog data={data} />)
.with({ type: "resume.sections.languages.update" }, ({ data }) => <UpdateLanguageDialog data={data} />)
.with({ type: "resume.sections.publications.create" }, ({ data }) => <CreatePublicationDialog data={data} />)
.with({ type: "resume.sections.publications.update" }, ({ data }) => <UpdatePublicationDialog data={data} />)
.with({ type: "resume.sections.awards.create" }, ({ data }) => <CreateAwardDialog data={data} />)
.with({ type: "resume.sections.awards.update" }, ({ data }) => <UpdateAwardDialog data={data} />)
.with({ type: "resume.sections.interests.create" }, ({ data }) => <CreateInterestDialog data={data} />)
.with({ type: "resume.sections.interests.update" }, ({ data }) => <UpdateInterestDialog data={data} />)
.with({ type: "resume.sections.volunteer.create" }, ({ data }) => <CreateVolunteerDialog data={data} />)
.with({ type: "resume.sections.volunteer.update" }, ({ data }) => <UpdateVolunteerDialog data={data} />)
.with({ type: "resume.sections.references.create" }, ({ data }) => <CreateReferenceDialog data={data} />)
.with({ type: "resume.sections.references.update" }, ({ data }) => <UpdateReferenceDialog data={data} />)
.with({ type: "resume.sections.summary.create" }, ({ data }) => <CreateSummaryItemDialog data={data} />)
.with({ type: "resume.sections.summary.update" }, ({ data }) => <UpdateSummaryItemDialog data={data} />)
.with({ type: "resume.sections.cover-letter.create" }, ({ data }) => <CreateCoverLetterDialog data={data} />)
.with({ type: "resume.sections.cover-letter.update" }, ({ data }) => <UpdateCoverLetterDialog data={data} />)
.with({ type: "resume.sections.custom.create" }, ({ data }) => <CreateCustomSectionDialog data={data} />)
.with({ type: "resume.sections.custom.update" }, ({ data }) => <UpdateCustomSectionDialog data={data} />)
.otherwise(() => null);
const DialogContent = match(activeDialog)
.with({ type: "auth.change-password" }, () => <ChangePasswordDialog />)
.with({ type: "auth.two-factor.enable" }, () => <EnableTwoFactorDialog />)
.with({ type: "auth.two-factor.disable" }, () => <DisableTwoFactorDialog />)
.with({ type: "api-key.create" }, () => <CreateApiKeyDialog />)
.with({ type: "resume.create" }, () => <CreateResumeDialog />)
.with({ type: "resume.update" }, ({ data }) => <UpdateResumeDialog data={data} />)
.with({ type: "resume.duplicate" }, ({ data }) => <DuplicateResumeDialog data={data} />)
.with({ type: "resume.import" }, () => <ImportResumeDialog />)
.with({ type: "resume.template.gallery" }, () => <TemplateGalleryDialog />)
.with({ type: "resume.sections.profiles.create" }, ({ data }) => <CreateProfileDialog data={data} />)
.with({ type: "resume.sections.profiles.update" }, ({ data }) => <UpdateProfileDialog data={data} />)
.with({ type: "resume.sections.experience.create" }, ({ data }) => <CreateExperienceDialog data={data} />)
.with({ type: "resume.sections.experience.update" }, ({ data }) => <UpdateExperienceDialog data={data} />)
.with({ type: "resume.sections.education.create" }, ({ data }) => <CreateEducationDialog data={data} />)
.with({ type: "resume.sections.education.update" }, ({ data }) => <UpdateEducationDialog data={data} />)
.with({ type: "resume.sections.skills.create" }, ({ data }) => <CreateSkillDialog data={data} />)
.with({ type: "resume.sections.skills.update" }, ({ data }) => <UpdateSkillDialog data={data} />)
.with({ type: "resume.sections.projects.create" }, ({ data }) => <CreateProjectDialog data={data} />)
.with({ type: "resume.sections.projects.update" }, ({ data }) => <UpdateProjectDialog data={data} />)
.with({ type: "resume.sections.certifications.create" }, ({ data }) => <CreateCertificationDialog data={data} />)
.with({ type: "resume.sections.certifications.update" }, ({ data }) => <UpdateCertificationDialog data={data} />)
.with({ type: "resume.sections.languages.create" }, ({ data }) => <CreateLanguageDialog data={data} />)
.with({ type: "resume.sections.languages.update" }, ({ data }) => <UpdateLanguageDialog data={data} />)
.with({ type: "resume.sections.publications.create" }, ({ data }) => <CreatePublicationDialog data={data} />)
.with({ type: "resume.sections.publications.update" }, ({ data }) => <UpdatePublicationDialog data={data} />)
.with({ type: "resume.sections.awards.create" }, ({ data }) => <CreateAwardDialog data={data} />)
.with({ type: "resume.sections.awards.update" }, ({ data }) => <UpdateAwardDialog data={data} />)
.with({ type: "resume.sections.interests.create" }, ({ data }) => <CreateInterestDialog data={data} />)
.with({ type: "resume.sections.interests.update" }, ({ data }) => <UpdateInterestDialog data={data} />)
.with({ type: "resume.sections.volunteer.create" }, ({ data }) => <CreateVolunteerDialog data={data} />)
.with({ type: "resume.sections.volunteer.update" }, ({ data }) => <UpdateVolunteerDialog data={data} />)
.with({ type: "resume.sections.references.create" }, ({ data }) => <CreateReferenceDialog data={data} />)
.with({ type: "resume.sections.references.update" }, ({ data }) => <UpdateReferenceDialog data={data} />)
.with({ type: "resume.sections.summary.create" }, ({ data }) => <CreateSummaryItemDialog data={data} />)
.with({ type: "resume.sections.summary.update" }, ({ data }) => <UpdateSummaryItemDialog data={data} />)
.with({ type: "resume.sections.cover-letter.create" }, ({ data }) => <CreateCoverLetterDialog data={data} />)
.with({ type: "resume.sections.cover-letter.update" }, ({ data }) => <UpdateCoverLetterDialog data={data} />)
.with({ type: "resume.sections.custom.create" }, ({ data }) => <CreateCustomSectionDialog data={data} />)
.with({ type: "resume.sections.custom.update" }, ({ data }) => <UpdateCustomSectionDialog data={data} />)
.otherwise(() => null);
return (
<Dialog open={open} onOpenChange={onOpenChange}>
{DialogContent}
</Dialog>
);
return (
<Dialog open={open} onOpenChange={onOpenChange}>
{DialogContent}
</Dialog>
);
}
+240 -240
View File
@@ -29,281 +29,281 @@ import { cn } from "@/utils/style";
import { type DialogProps, useDialogStore } from "../store";
const formSchema = z.discriminatedUnion("type", [
z.object({ type: z.literal("") }),
z.object({
type: z.literal("pdf"),
file: z.instanceof(File).refine((file) => file.type === "application/pdf", { message: "File must be a PDF" }),
}),
z.object({
type: z.literal("docx"),
file: z
.instanceof(File)
.refine(
(file) =>
file.type === "application/msword" ||
file.type === "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
{ message: "File must be a Microsoft Word document" },
),
}),
z.object({
type: z.literal("reactive-resume-json"),
file: z
.instanceof(File)
.refine((file) => file.type === "application/json", { message: "File must be a JSON file" }),
}),
z.object({
type: z.literal("reactive-resume-v4-json"),
file: z
.instanceof(File)
.refine((file) => file.type === "application/json", { message: "File must be a JSON file" }),
}),
z.object({
type: z.literal("json-resume-json"),
file: z
.instanceof(File)
.refine((file) => file.type === "application/json", { message: "File must be a JSON file" }),
}),
z.object({ type: z.literal("") }),
z.object({
type: z.literal("pdf"),
file: z.instanceof(File).refine((file) => file.type === "application/pdf", { message: "File must be a PDF" }),
}),
z.object({
type: z.literal("docx"),
file: z
.instanceof(File)
.refine(
(file) =>
file.type === "application/msword" ||
file.type === "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
{ message: "File must be a Microsoft Word document" },
),
}),
z.object({
type: z.literal("reactive-resume-json"),
file: z
.instanceof(File)
.refine((file) => file.type === "application/json", { message: "File must be a JSON file" }),
}),
z.object({
type: z.literal("reactive-resume-v4-json"),
file: z
.instanceof(File)
.refine((file) => file.type === "application/json", { message: "File must be a JSON file" }),
}),
z.object({
type: z.literal("json-resume-json"),
file: z
.instanceof(File)
.refine((file) => file.type === "application/json", { message: "File must be a JSON file" }),
}),
]);
type FormValues = z.infer<typeof formSchema>;
function fileToBase64(file: File): Promise<string> {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onload = () => {
const result = reader.result as string;
// remove data URL prefix (e.g., "data:application/pdf;base64," or "data:application/vnd...;base64,")
resolve(result.split(",")[1]);
};
reader.onerror = reject;
reader.readAsDataURL(file);
});
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onload = () => {
const result = reader.result as string;
// remove data URL prefix (e.g., "data:application/pdf;base64," or "data:application/vnd...;base64,")
resolve(result.split(",")[1]);
};
reader.onerror = reject;
reader.readAsDataURL(file);
});
}
export function ImportResumeDialog(_: DialogProps<"resume.import">) {
const navigate = useNavigate();
const { enabled: isAIEnabled, provider, model, apiKey, baseURL } = useAIStore();
const closeDialog = useDialogStore((state) => state.closeDialog);
const navigate = useNavigate();
const { enabled: isAIEnabled, provider, model, apiKey, baseURL } = useAIStore();
const closeDialog = useDialogStore((state) => state.closeDialog);
const prevTypeRef = useRef<string>("");
const inputRef = useRef<HTMLInputElement>(null);
const [isLoading, setIsLoading] = useState<boolean>(false);
const prevTypeRef = useRef<string>("");
const inputRef = useRef<HTMLInputElement>(null);
const [isLoading, setIsLoading] = useState<boolean>(false);
const { mutateAsync: importResume } = useMutation(orpc.resume.import.mutationOptions());
const { mutateAsync: importResume } = useMutation(orpc.resume.import.mutationOptions());
const form = useForm<FormValues>({
resolver: zodResolver(formSchema),
defaultValues: {
type: "",
},
});
const form = useForm<FormValues>({
resolver: zodResolver(formSchema),
defaultValues: {
type: "",
},
});
const type = useWatch({ control: form.control, name: "type" });
const type = useWatch({ control: form.control, name: "type" });
useEffect(() => {
if (prevTypeRef.current === type) return;
prevTypeRef.current = type;
form.resetField("file");
}, [form.resetField, type]);
useEffect(() => {
if (prevTypeRef.current === type) return;
prevTypeRef.current = type;
form.resetField("file");
}, [form.resetField, type]);
const onSelectFile = () => {
if (!inputRef.current) return;
inputRef.current.click();
};
const onSelectFile = () => {
if (!inputRef.current) return;
inputRef.current.click();
};
const onUploadFile = (e: React.ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0];
if (!file) return;
form.setValue("file", file, { shouldDirty: true });
};
const onUploadFile = (e: React.ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0];
if (!file) return;
form.setValue("file", file, { shouldDirty: true });
};
const { blockEvents } = useFormBlocker(form);
const { blockEvents } = useFormBlocker(form);
const onSubmit = async (values: FormValues) => {
if (values.type === "") return;
const onSubmit = async (values: FormValues) => {
if (values.type === "") return;
setIsLoading(true);
setIsLoading(true);
const toastId = toast.loading(t`Importing your resume...`, {
description: t`This may take a few minutes, depending on the response of the AI provider. Please do not close the window or refresh the page.`,
});
const toastId = toast.loading(t`Importing your resume...`, {
description: t`This may take a few minutes, depending on the response of the AI provider. Please do not close the window or refresh the page.`,
});
try {
let data: ResumeData | undefined;
try {
let data: ResumeData | undefined;
if (values.type === "json-resume-json") {
const json = await values.file.text();
const importer = new JSONResumeImporter();
data = importer.parse(json);
}
if (values.type === "json-resume-json") {
const json = await values.file.text();
const importer = new JSONResumeImporter();
data = importer.parse(json);
}
if (values.type === "reactive-resume-json") {
const json = await values.file.text();
const importer = new ReactiveResumeJSONImporter();
data = importer.parse(json);
}
if (values.type === "reactive-resume-json") {
const json = await values.file.text();
const importer = new ReactiveResumeJSONImporter();
data = importer.parse(json);
}
if (values.type === "reactive-resume-v4-json") {
const json = await values.file.text();
const importer = new ReactiveResumeV4JSONImporter();
data = importer.parse(json);
}
if (values.type === "reactive-resume-v4-json") {
const json = await values.file.text();
const importer = new ReactiveResumeV4JSONImporter();
data = importer.parse(json);
}
if (values.type === "pdf") {
if (!isAIEnabled)
throw new Error(t`This feature requires AI Integration to be enabled. Please enable it in the settings.`);
if (values.type === "pdf") {
if (!isAIEnabled)
throw new Error(t`This feature requires AI Integration to be enabled. Please enable it in the settings.`);
// const arrayBuffer = await values.file.arrayBuffer();
const base64 = await fileToBase64(values.file);
// const arrayBuffer = await values.file.arrayBuffer();
const base64 = await fileToBase64(values.file);
data = await client.ai.parsePdf({
provider,
model,
apiKey,
baseURL,
file: { name: values.file.name, data: base64 },
});
}
data = await client.ai.parsePdf({
provider,
model,
apiKey,
baseURL,
file: { name: values.file.name, data: base64 },
});
}
if (values.type === "docx") {
if (!isAIEnabled)
throw new Error(t`This feature requires AI Integration to be enabled. Please enable it in the settings.`);
if (values.type === "docx") {
if (!isAIEnabled)
throw new Error(t`This feature requires AI Integration to be enabled. Please enable it in the settings.`);
// const arrayBuffer = await values.file.arrayBuffer();
const base64 = await fileToBase64(values.file);
// const arrayBuffer = await values.file.arrayBuffer();
const base64 = await fileToBase64(values.file);
const mediaType =
values.file.type === "application/msword"
? ("application/msword" as const)
: ("application/vnd.openxmlformats-officedocument.wordprocessingml.document" as const);
const mediaType =
values.file.type === "application/msword"
? ("application/msword" as const)
: ("application/vnd.openxmlformats-officedocument.wordprocessingml.document" as const);
data = await client.ai.parseDocx({
provider,
model,
apiKey,
baseURL,
mediaType,
file: { name: values.file.name, data: base64 },
});
}
data = await client.ai.parseDocx({
provider,
model,
apiKey,
baseURL,
mediaType,
file: { name: values.file.name, data: base64 },
});
}
if (!data) throw new Error("No data was returned from the AI provider.");
if (!data) throw new Error("No data was returned from the AI provider.");
const id = await importResume({ data });
toast.success(t`Your resume has been imported successfully.`, { id: toastId, description: null });
closeDialog();
void navigate({ to: `/builder/$resumeId`, params: { resumeId: id } });
} catch (error: unknown) {
if (error instanceof Error) {
toast.error(error.message, { id: toastId, description: null });
} else {
toast.error(t`An unknown error occurred while importing your resume.`, { id: toastId, description: null });
}
} finally {
setIsLoading(false);
}
};
const id = await importResume({ data });
toast.success(t`Your resume has been imported successfully.`, { id: toastId, description: null });
closeDialog();
void navigate({ to: `/builder/$resumeId`, params: { resumeId: id } });
} catch (error: unknown) {
if (error instanceof Error) {
toast.error(error.message, { id: toastId, description: null });
} else {
toast.error(t`An unknown error occurred while importing your resume.`, { id: toastId, description: null });
}
} finally {
setIsLoading(false);
}
};
return (
<DialogContent {...blockEvents}>
<DialogHeader>
<DialogTitle className="flex items-center gap-x-2">
<DownloadSimpleIcon />
<Trans>Import an existing resume</Trans>
</DialogTitle>
<DialogDescription>
<Trans>
Continue where you left off by importing an existing resume you created using Reactive Resume or any another
resume builder. Supported formats include PDF, Microsoft Word, as well as JSON files from Reactive Resume.
</Trans>
</DialogDescription>
</DialogHeader>
return (
<DialogContent {...blockEvents}>
<DialogHeader>
<DialogTitle className="flex items-center gap-x-2">
<DownloadSimpleIcon />
<Trans>Import an existing resume</Trans>
</DialogTitle>
<DialogDescription>
<Trans>
Continue where you left off by importing an existing resume you created using Reactive Resume or any another
resume builder. Supported formats include PDF, Microsoft Word, as well as JSON files from Reactive Resume.
</Trans>
</DialogDescription>
</DialogHeader>
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
<FormField
control={form.control}
name="type"
render={({ field }) => (
<FormItem>
<FormLabel>
<Trans>Type</Trans>
</FormLabel>
<FormControl
render={
<Combobox
showClear={false}
value={field.value}
onValueChange={field.onChange}
options={[
{ value: "reactive-resume-json", label: "Reactive Resume (JSON)" },
{ value: "reactive-resume-v4-json", label: "Reactive Resume v4 (JSON)" },
{ value: "json-resume-json", label: "JSON Resume" },
{
value: "pdf",
label: (
<div className="flex items-center gap-x-2">
PDF <Badge>{t`AI`}</Badge>
</div>
),
},
{
value: "docx",
label: (
<div className="flex items-center gap-x-2">
Microsoft Word <Badge>{t`AI`}</Badge>
</div>
),
},
]}
/>
}
/>
<FormMessage />
</FormItem>
)}
/>
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
<FormField
control={form.control}
name="type"
render={({ field }) => (
<FormItem>
<FormLabel>
<Trans>Type</Trans>
</FormLabel>
<FormControl
render={
<Combobox
showClear={false}
value={field.value}
onValueChange={field.onChange}
options={[
{ value: "reactive-resume-json", label: "Reactive Resume (JSON)" },
{ value: "reactive-resume-v4-json", label: "Reactive Resume v4 (JSON)" },
{ value: "json-resume-json", label: "JSON Resume" },
{
value: "pdf",
label: (
<div className="flex items-center gap-x-2">
PDF <Badge>{t`AI`}</Badge>
</div>
),
},
{
value: "docx",
label: (
<div className="flex items-center gap-x-2">
Microsoft Word <Badge>{t`AI`}</Badge>
</div>
),
},
]}
/>
}
/>
<FormMessage />
</FormItem>
)}
/>
<FormField
key={type}
control={form.control}
name="file"
render={({ field }) => (
<FormItem className={cn(!type && "hidden")}>
<FormControl>
<Input type="file" className="hidden" ref={inputRef} onChange={onUploadFile} />
<FormField
key={type}
control={form.control}
name="file"
render={({ field }) => (
<FormItem className={cn(!type && "hidden")}>
<FormControl>
<Input type="file" className="hidden" ref={inputRef} onChange={onUploadFile} />
<Button
variant="outline"
className="h-auto w-full flex-col border-dashed py-8 font-normal"
onClick={onSelectFile}
>
{field.value ? (
<>
<FileIcon weight="thin" size={32} />
<p>{field.value.name}</p>
</>
) : (
<>
<UploadSimpleIcon weight="thin" size={32} />
<Trans>Click here to select a file to import</Trans>
</>
)}
</Button>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<Button
variant="outline"
className="h-auto w-full flex-col border-dashed py-8 font-normal"
onClick={onSelectFile}
>
{field.value ? (
<>
<FileIcon weight="thin" size={32} />
<p>{field.value.name}</p>
</>
) : (
<>
<UploadSimpleIcon weight="thin" size={32} />
<Trans>Click here to select a file to import</Trans>
</>
)}
</Button>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<DialogFooter>
<Button type="submit" disabled={!type || isLoading}>
{isLoading ? <Spinner /> : null}
{isLoading ? t`Importing...` : t`Import`}
</Button>
</DialogFooter>
</form>
</Form>
</DialogContent>
);
<DialogFooter>
<Button type="submit" disabled={!type || isLoading}>
{isLoading ? <Spinner /> : null}
{isLoading ? t`Importing...` : t`Import`}
</Button>
</DialogFooter>
</form>
</Form>
</DialogContent>
);
}
+285 -285
View File
@@ -14,10 +14,10 @@ import { Button } from "@/components/ui/button";
import { ButtonGroup } from "@/components/ui/button-group";
import { DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from "@/components/ui/dialog";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { Form, FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage } from "@/components/ui/form";
import { Input } from "@/components/ui/input";
@@ -30,344 +30,344 @@ import { generateId, generateRandomName, slugify } from "@/utils/string";
import { type DialogProps, useDialogStore } from "../store";
const formSchema = z.object({
id: z.string(),
name: z.string().min(1).max(64),
slug: z.string().min(1).max(64).transform(slugify),
tags: z.array(z.string()),
id: z.string(),
name: z.string().min(1).max(64),
slug: z.string().min(1).max(64).transform(slugify),
tags: z.array(z.string()),
});
type FormValues = z.infer<typeof formSchema>;
export function CreateResumeDialog(_: DialogProps<"resume.create">) {
const closeDialog = useDialogStore((state) => state.closeDialog);
const closeDialog = useDialogStore((state) => state.closeDialog);
const { mutate: createResume, isPending } = useMutation(orpc.resume.create.mutationOptions());
const { mutate: createResume, isPending } = useMutation(orpc.resume.create.mutationOptions());
const form = useForm<FormValues>({
resolver: zodResolver(formSchema),
defaultValues: {
id: generateId(),
name: "",
slug: "",
tags: [],
},
});
const form = useForm<FormValues>({
resolver: zodResolver(formSchema),
defaultValues: {
id: generateId(),
name: "",
slug: "",
tags: [],
},
});
const name = useWatch({ control: form.control, name: "name" });
const name = useWatch({ control: form.control, name: "name" });
useEffect(() => {
form.setValue("slug", slugify(name), { shouldDirty: true });
}, [form, name]);
useEffect(() => {
form.setValue("slug", slugify(name), { shouldDirty: true });
}, [form, name]);
const { blockEvents } = useFormBlocker(form);
const { blockEvents } = useFormBlocker(form);
const onSubmit = (data: FormValues) => {
const toastId = toast.loading(t`Creating your resume...`);
const onSubmit = (data: FormValues) => {
const toastId = toast.loading(t`Creating your resume...`);
createResume(data, {
onSuccess: () => {
toast.success(t`Your resume has been created successfully.`, { id: toastId });
closeDialog();
},
onError: (error) => {
if (error.message === "RESUME_SLUG_ALREADY_EXISTS") {
toast.error(t`A resume with this slug already exists.`, { id: toastId });
return;
}
createResume(data, {
onSuccess: () => {
toast.success(t`Your resume has been created successfully.`, { id: toastId });
closeDialog();
},
onError: (error) => {
if (error.message === "RESUME_SLUG_ALREADY_EXISTS") {
toast.error(t`A resume with this slug already exists.`, { id: toastId });
return;
}
toast.error(error.message, { id: toastId });
},
});
};
toast.error(error.message, { id: toastId });
},
});
};
const onCreateSampleResume = () => {
const values = form.getValues();
const randomName = generateRandomName();
const onCreateSampleResume = () => {
const values = form.getValues();
const randomName = generateRandomName();
const data = {
name: values.name || randomName,
slug: values.slug || slugify(randomName),
tags: values.tags,
withSampleData: true,
} satisfies RouterInput["resume"]["create"];
const data = {
name: values.name || randomName,
slug: values.slug || slugify(randomName),
tags: values.tags,
withSampleData: true,
} satisfies RouterInput["resume"]["create"];
const toastId = toast.loading(t`Creating your resume...`);
const toastId = toast.loading(t`Creating your resume...`);
createResume(data, {
onSuccess: () => {
toast.success(t`Your resume has been created successfully.`, { id: toastId });
closeDialog();
},
onError: (error) => {
toast.error(error.message, { id: toastId });
},
});
};
createResume(data, {
onSuccess: () => {
toast.success(t`Your resume has been created successfully.`, { id: toastId });
closeDialog();
},
onError: (error) => {
toast.error(error.message, { id: toastId });
},
});
};
return (
<DialogContent {...blockEvents}>
<DialogHeader>
<DialogTitle className="flex items-center gap-x-2">
<PlusIcon />
<Trans>Create a new resume</Trans>
</DialogTitle>
<DialogDescription>
<Trans>Start building your resume by giving it a name.</Trans>
</DialogDescription>
</DialogHeader>
return (
<DialogContent {...blockEvents}>
<DialogHeader>
<DialogTitle className="flex items-center gap-x-2">
<PlusIcon />
<Trans>Create a new resume</Trans>
</DialogTitle>
<DialogDescription>
<Trans>Start building your resume by giving it a name.</Trans>
</DialogDescription>
</DialogHeader>
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
<ResumeForm />
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
<ResumeForm />
<DialogFooter>
<ButtonGroup aria-label="Create Resume with Options" className="gap-x-px rtl:flex-row-reverse">
<Button type="submit" disabled={isPending}>
<Trans>Create</Trans>
</Button>
<DialogFooter>
<ButtonGroup aria-label="Create Resume with Options" className="gap-x-px rtl:flex-row-reverse">
<Button type="submit" disabled={isPending}>
<Trans>Create</Trans>
</Button>
<DropdownMenu>
<DropdownMenuTrigger
render={
<Button size="icon" disabled={isPending}>
<CaretDownIcon />
</Button>
}
/>
<DropdownMenu>
<DropdownMenuTrigger
render={
<Button size="icon" disabled={isPending}>
<CaretDownIcon />
</Button>
}
/>
<DropdownMenuContent align="end" className="w-fit">
<DropdownMenuItem onClick={onCreateSampleResume}>
<TestTubeIcon />
<Trans>Create a Sample Resume</Trans>
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</ButtonGroup>
</DialogFooter>
</form>
</Form>
</DialogContent>
);
<DropdownMenuContent align="end" className="w-fit">
<DropdownMenuItem onClick={onCreateSampleResume}>
<TestTubeIcon />
<Trans>Create a Sample Resume</Trans>
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</ButtonGroup>
</DialogFooter>
</form>
</Form>
</DialogContent>
);
}
export function UpdateResumeDialog({ data }: DialogProps<"resume.update">) {
const closeDialog = useDialogStore((state) => state.closeDialog);
const closeDialog = useDialogStore((state) => state.closeDialog);
const { mutate: updateResume, isPending } = useMutation(orpc.resume.update.mutationOptions());
const { mutate: updateResume, isPending } = useMutation(orpc.resume.update.mutationOptions());
const form = useForm<FormValues>({
resolver: zodResolver(formSchema),
defaultValues: {
id: data.id,
name: data.name,
slug: data.slug,
tags: data.tags,
},
});
const form = useForm<FormValues>({
resolver: zodResolver(formSchema),
defaultValues: {
id: data.id,
name: data.name,
slug: data.slug,
tags: data.tags,
},
});
const name = useWatch({ control: form.control, name: "name" });
const name = useWatch({ control: form.control, name: "name" });
useEffect(() => {
if (!name) return;
form.setValue("slug", slugify(name), { shouldDirty: true });
}, [form, name]);
useEffect(() => {
if (!name) return;
form.setValue("slug", slugify(name), { shouldDirty: true });
}, [form, name]);
const { blockEvents } = useFormBlocker(form);
const { blockEvents } = useFormBlocker(form);
const onSubmit = (data: FormValues) => {
const toastId = toast.loading(t`Updating your resume...`);
const onSubmit = (data: FormValues) => {
const toastId = toast.loading(t`Updating your resume...`);
updateResume(data, {
onSuccess: () => {
toast.success(t`Your resume has been updated successfully.`, { id: toastId });
closeDialog();
},
onError: (error) => {
if (error.message === "RESUME_SLUG_ALREADY_EXISTS") {
toast.error(t`A resume with this slug already exists.`, { id: toastId });
return;
}
updateResume(data, {
onSuccess: () => {
toast.success(t`Your resume has been updated successfully.`, { id: toastId });
closeDialog();
},
onError: (error) => {
if (error.message === "RESUME_SLUG_ALREADY_EXISTS") {
toast.error(t`A resume with this slug already exists.`, { id: toastId });
return;
}
toast.error(error.message, { id: toastId });
},
});
};
toast.error(error.message, { id: toastId });
},
});
};
return (
<DialogContent {...blockEvents}>
<DialogHeader>
<DialogTitle className="flex items-center gap-x-2">
<PencilSimpleLineIcon />
<Trans>Update Resume</Trans>
</DialogTitle>
<DialogDescription>
<Trans>Changed your mind? Rename your resume to something more descriptive.</Trans>
</DialogDescription>
</DialogHeader>
return (
<DialogContent {...blockEvents}>
<DialogHeader>
<DialogTitle className="flex items-center gap-x-2">
<PencilSimpleLineIcon />
<Trans>Update Resume</Trans>
</DialogTitle>
<DialogDescription>
<Trans>Changed your mind? Rename your resume to something more descriptive.</Trans>
</DialogDescription>
</DialogHeader>
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
<ResumeForm />
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
<ResumeForm />
<DialogFooter>
<Button type="submit" disabled={isPending}>
<Trans>Save Changes</Trans>
</Button>
</DialogFooter>
</form>
</Form>
</DialogContent>
);
<DialogFooter>
<Button type="submit" disabled={isPending}>
<Trans>Save Changes</Trans>
</Button>
</DialogFooter>
</form>
</Form>
</DialogContent>
);
}
export function DuplicateResumeDialog({ data }: DialogProps<"resume.duplicate">) {
const navigate = useNavigate();
const closeDialog = useDialogStore((state) => state.closeDialog);
const navigate = useNavigate();
const closeDialog = useDialogStore((state) => state.closeDialog);
const { mutate: duplicateResume, isPending } = useMutation(orpc.resume.duplicate.mutationOptions());
const { mutate: duplicateResume, isPending } = useMutation(orpc.resume.duplicate.mutationOptions());
const form = useForm<FormValues>({
resolver: zodResolver(formSchema),
defaultValues: {
id: data.id,
name: `${data.name} (Copy)`,
slug: `${data.slug}-copy`,
tags: data.tags,
},
});
const form = useForm<FormValues>({
resolver: zodResolver(formSchema),
defaultValues: {
id: data.id,
name: `${data.name} (Copy)`,
slug: `${data.slug}-copy`,
tags: data.tags,
},
});
const name = useWatch({ control: form.control, name: "name" });
const name = useWatch({ control: form.control, name: "name" });
useEffect(() => {
if (!name) return;
form.setValue("slug", slugify(name), { shouldDirty: true });
}, [form, name]);
useEffect(() => {
if (!name) return;
form.setValue("slug", slugify(name), { shouldDirty: true });
}, [form, name]);
const { blockEvents } = useFormBlocker(form);
const { blockEvents } = useFormBlocker(form);
const onSubmit = (values: FormValues) => {
const toastId = toast.loading(t`Duplicating your resume...`);
const onSubmit = (values: FormValues) => {
const toastId = toast.loading(t`Duplicating your resume...`);
duplicateResume(values, {
onSuccess: async (id) => {
toast.success(t`Your resume has been duplicated successfully.`, { id: toastId });
closeDialog();
duplicateResume(values, {
onSuccess: async (id) => {
toast.success(t`Your resume has been duplicated successfully.`, { id: toastId });
closeDialog();
if (!data.shouldRedirect) return;
void navigate({ to: `/builder/$resumeId`, params: { resumeId: id } });
},
onError: (error) => {
toast.error(error.message, { id: toastId });
},
});
};
if (!data.shouldRedirect) return;
void navigate({ to: `/builder/$resumeId`, params: { resumeId: id } });
},
onError: (error) => {
toast.error(error.message, { id: toastId });
},
});
};
return (
<DialogContent {...blockEvents}>
<DialogHeader>
<DialogTitle className="flex items-center gap-x-2">
<PencilSimpleLineIcon />
<Trans>Duplicate Resume</Trans>
</DialogTitle>
<DialogDescription>
<Trans>Duplicate your resume to create a new one, just like the original.</Trans>
</DialogDescription>
</DialogHeader>
return (
<DialogContent {...blockEvents}>
<DialogHeader>
<DialogTitle className="flex items-center gap-x-2">
<PencilSimpleLineIcon />
<Trans>Duplicate Resume</Trans>
</DialogTitle>
<DialogDescription>
<Trans>Duplicate your resume to create a new one, just like the original.</Trans>
</DialogDescription>
</DialogHeader>
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
<ResumeForm />
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
<ResumeForm />
<DialogFooter>
<Button type="submit" disabled={isPending}>
<Trans>Duplicate</Trans>
</Button>
</DialogFooter>
</form>
</Form>
</DialogContent>
);
<DialogFooter>
<Button type="submit" disabled={isPending}>
<Trans>Duplicate</Trans>
</Button>
</DialogFooter>
</form>
</Form>
</DialogContent>
);
}
function ResumeForm() {
const form = useFormContext<FormValues>();
const { data: session } = authClient.useSession();
const form = useFormContext<FormValues>();
const { data: session } = authClient.useSession();
const slugPrefix = useMemo(() => {
return `${window.location.origin}/${session?.user.username ?? ""}/`;
}, [session]);
const slugPrefix = useMemo(() => {
return `${window.location.origin}/${session?.user.username ?? ""}/`;
}, [session]);
const onGenerateName = () => {
form.setValue("name", generateRandomName(), { shouldDirty: true });
};
const onGenerateName = () => {
form.setValue("name", generateRandomName(), { shouldDirty: true });
};
return (
<>
<FormField
control={form.control}
name="name"
render={({ field }) => (
<FormItem>
<FormLabel>
<Trans>Name</Trans>
</FormLabel>
<div className="flex items-center gap-x-2">
<FormControl render={<Input min={1} max={64} {...field} />} />
return (
<>
<FormField
control={form.control}
name="name"
render={({ field }) => (
<FormItem>
<FormLabel>
<Trans>Name</Trans>
</FormLabel>
<div className="flex items-center gap-x-2">
<FormControl render={<Input min={1} max={64} {...field} />} />
<Button size="icon" variant="outline" title={t`Generate a random name`} onClick={onGenerateName}>
<MagicWandIcon />
</Button>
</div>
<FormMessage />
<FormDescription>
<Trans>Tip: You can name the resume referring to the position you are applying for.</Trans>
</FormDescription>
</FormItem>
)}
/>
<Button size="icon" variant="outline" title={t`Generate a random name`} onClick={onGenerateName}>
<MagicWandIcon />
</Button>
</div>
<FormMessage />
<FormDescription>
<Trans>Tip: You can name the resume referring to the position you are applying for.</Trans>
</FormDescription>
</FormItem>
)}
/>
<FormField
control={form.control}
name="slug"
render={({ field }) => (
<FormItem>
<FormLabel>
<Trans>Slug</Trans>
</FormLabel>
<FormControl
render={
<InputGroup>
<InputGroupAddon align="inline-start" className="hidden sm:flex">
<InputGroupText>{slugPrefix}</InputGroupText>
</InputGroupAddon>
<InputGroupInput min={1} max={64} className="ps-0!" {...field} />
</InputGroup>
}
/>
<FormMessage />
<FormDescription>
<Trans>This is a URL-friendly name for your resume.</Trans>
</FormDescription>
</FormItem>
)}
/>
<FormField
control={form.control}
name="slug"
render={({ field }) => (
<FormItem>
<FormLabel>
<Trans>Slug</Trans>
</FormLabel>
<FormControl
render={
<InputGroup>
<InputGroupAddon align="inline-start" className="hidden sm:flex">
<InputGroupText>{slugPrefix}</InputGroupText>
</InputGroupAddon>
<InputGroupInput min={1} max={64} className="ps-0!" {...field} />
</InputGroup>
}
/>
<FormMessage />
<FormDescription>
<Trans>This is a URL-friendly name for your resume.</Trans>
</FormDescription>
</FormItem>
)}
/>
<FormField
control={form.control}
name="tags"
render={({ field }) => (
<FormItem>
<FormLabel>
<Trans>Tags</Trans>
</FormLabel>
<FormControl render={<ChipInput {...field} />} />
<FormMessage />
<FormDescription>
<Trans>Tags can be used to categorize your resume by keywords.</Trans>
</FormDescription>
</FormItem>
)}
/>
</>
);
<FormField
control={form.control}
name="tags"
render={({ field }) => (
<FormItem>
<FormLabel>
<Trans>Tags</Trans>
</FormLabel>
<FormControl render={<ChipInput {...field} />} />
<FormMessage />
<FormDescription>
<Trans>Tags can be used to categorize your resume by keywords.</Trans>
</FormDescription>
</FormItem>
)}
/>
</>
);
}
+192 -192
View File
@@ -25,222 +25,222 @@ const formSchema = awardItemSchema;
type FormValues = z.infer<typeof formSchema>;
export function CreateAwardDialog({ data }: DialogProps<"resume.sections.awards.create">) {
const closeDialog = useDialogStore((state) => state.closeDialog);
const updateResumeData = useResumeStore((state) => state.updateResumeData);
const closeDialog = useDialogStore((state) => state.closeDialog);
const updateResumeData = useResumeStore((state) => state.updateResumeData);
const form = useForm<FormValues>({
resolver: zodResolver(formSchema),
defaultValues: {
id: generateId(),
hidden: data?.item?.hidden ?? false,
options: data?.item?.options ?? { showLinkInTitle: false },
title: data?.item?.title ?? "",
awarder: data?.item?.awarder ?? "",
date: data?.item?.date ?? "",
website: data?.item?.website ?? { url: "", label: "" },
description: data?.item?.description ?? "",
},
});
const form = useForm<FormValues>({
resolver: zodResolver(formSchema),
defaultValues: {
id: generateId(),
hidden: data?.item?.hidden ?? false,
options: data?.item?.options ?? { showLinkInTitle: false },
title: data?.item?.title ?? "",
awarder: data?.item?.awarder ?? "",
date: data?.item?.date ?? "",
website: data?.item?.website ?? { url: "", label: "" },
description: data?.item?.description ?? "",
},
});
const onSubmit = (formData: FormValues) => {
updateResumeData((draft) => {
if (data?.customSectionId) {
const section = draft.customSections.find((s) => s.id === data.customSectionId);
if (section) section.items.push(formData);
} else {
draft.sections.awards.items.push(formData);
}
});
closeDialog();
};
const onSubmit = (formData: FormValues) => {
updateResumeData((draft) => {
if (data?.customSectionId) {
const section = draft.customSections.find((s) => s.id === data.customSectionId);
if (section) section.items.push(formData);
} else {
draft.sections.awards.items.push(formData);
}
});
closeDialog();
};
const { blockEvents, requestClose } = useFormBlocker(form);
const { blockEvents, requestClose } = useFormBlocker(form);
return (
<DialogContent {...blockEvents}>
<DialogHeader>
<DialogTitle className="flex items-center gap-x-2">
<PlusIcon />
<Trans>Create a new award</Trans>
</DialogTitle>
<DialogDescription />
</DialogHeader>
return (
<DialogContent {...blockEvents}>
<DialogHeader>
<DialogTitle className="flex items-center gap-x-2">
<PlusIcon />
<Trans>Create a new award</Trans>
</DialogTitle>
<DialogDescription />
</DialogHeader>
<Form {...form}>
<form className="grid gap-4 sm:grid-cols-2" onSubmit={form.handleSubmit(onSubmit)}>
<AwardForm />
<Form {...form}>
<form className="grid gap-4 sm:grid-cols-2" onSubmit={form.handleSubmit(onSubmit)}>
<AwardForm />
<DialogFooter className="sm:col-span-full">
<Button variant="ghost" onClick={requestClose}>
<Trans>Cancel</Trans>
</Button>
<DialogFooter className="sm:col-span-full">
<Button variant="ghost" onClick={requestClose}>
<Trans>Cancel</Trans>
</Button>
<Button type="submit" disabled={form.formState.isSubmitting}>
<Trans>Create</Trans>
</Button>
</DialogFooter>
</form>
</Form>
</DialogContent>
);
<Button type="submit" disabled={form.formState.isSubmitting}>
<Trans>Create</Trans>
</Button>
</DialogFooter>
</form>
</Form>
</DialogContent>
);
}
export function UpdateAwardDialog({ data }: DialogProps<"resume.sections.awards.update">) {
const closeDialog = useDialogStore((state) => state.closeDialog);
const updateResumeData = useResumeStore((state) => state.updateResumeData);
const closeDialog = useDialogStore((state) => state.closeDialog);
const updateResumeData = useResumeStore((state) => state.updateResumeData);
const form = useForm<FormValues>({
resolver: zodResolver(formSchema),
defaultValues: {
id: data.item.id,
hidden: data.item.hidden,
options: data.item.options ?? { showLinkInTitle: false },
title: data.item.title,
awarder: data.item.awarder,
date: data.item.date,
website: data.item.website,
description: data.item.description,
},
});
const form = useForm<FormValues>({
resolver: zodResolver(formSchema),
defaultValues: {
id: data.item.id,
hidden: data.item.hidden,
options: data.item.options ?? { showLinkInTitle: false },
title: data.item.title,
awarder: data.item.awarder,
date: data.item.date,
website: data.item.website,
description: data.item.description,
},
});
const onSubmit = (formData: FormValues) => {
updateResumeData((draft) => {
if (data?.customSectionId) {
const section = draft.customSections.find((s) => s.id === data.customSectionId);
if (!section) return;
const index = section.items.findIndex((item) => item.id === formData.id);
if (index !== -1) section.items[index] = formData;
} else {
const index = draft.sections.awards.items.findIndex((item) => item.id === formData.id);
if (index !== -1) draft.sections.awards.items[index] = formData;
}
});
closeDialog();
};
const onSubmit = (formData: FormValues) => {
updateResumeData((draft) => {
if (data?.customSectionId) {
const section = draft.customSections.find((s) => s.id === data.customSectionId);
if (!section) return;
const index = section.items.findIndex((item) => item.id === formData.id);
if (index !== -1) section.items[index] = formData;
} else {
const index = draft.sections.awards.items.findIndex((item) => item.id === formData.id);
if (index !== -1) draft.sections.awards.items[index] = formData;
}
});
closeDialog();
};
const { blockEvents, requestClose } = useFormBlocker(form);
const { blockEvents, requestClose } = useFormBlocker(form);
return (
<DialogContent {...blockEvents}>
<DialogHeader>
<DialogTitle className="flex items-center gap-x-2">
<PencilSimpleLineIcon />
<Trans>Update an existing award</Trans>
</DialogTitle>
<DialogDescription />
</DialogHeader>
return (
<DialogContent {...blockEvents}>
<DialogHeader>
<DialogTitle className="flex items-center gap-x-2">
<PencilSimpleLineIcon />
<Trans>Update an existing award</Trans>
</DialogTitle>
<DialogDescription />
</DialogHeader>
<Form {...form}>
<form className="grid gap-4 sm:grid-cols-2" onSubmit={form.handleSubmit(onSubmit)}>
<AwardForm />
<Form {...form}>
<form className="grid gap-4 sm:grid-cols-2" onSubmit={form.handleSubmit(onSubmit)}>
<AwardForm />
<DialogFooter className="sm:col-span-full">
<Button variant="ghost" onClick={requestClose}>
<Trans>Cancel</Trans>
</Button>
<DialogFooter className="sm:col-span-full">
<Button variant="ghost" onClick={requestClose}>
<Trans>Cancel</Trans>
</Button>
<Button type="submit" disabled={form.formState.isSubmitting}>
<Trans>Save Changes</Trans>
</Button>
</DialogFooter>
</form>
</Form>
</DialogContent>
);
<Button type="submit" disabled={form.formState.isSubmitting}>
<Trans>Save Changes</Trans>
</Button>
</DialogFooter>
</form>
</Form>
</DialogContent>
);
}
function AwardForm() {
const form = useFormContext<FormValues>();
const form = useFormContext<FormValues>();
return (
<>
<FormField
control={form.control}
name="title"
render={({ field }) => (
<FormItem>
<FormLabel>
<Trans>Title</Trans>
</FormLabel>
<FormControl render={<Input {...field} />} />
<FormMessage />
</FormItem>
)}
/>
return (
<>
<FormField
control={form.control}
name="title"
render={({ field }) => (
<FormItem>
<FormLabel>
<Trans>Title</Trans>
</FormLabel>
<FormControl render={<Input {...field} />} />
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="awarder"
render={({ field }) => (
<FormItem>
<FormLabel>
<Trans context="(noun) person, organization, or entity that gives an award">Awarder</Trans>
</FormLabel>
<FormControl render={<Input {...field} />} />
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="awarder"
render={({ field }) => (
<FormItem>
<FormLabel>
<Trans context="(noun) person, organization, or entity that gives an award">Awarder</Trans>
</FormLabel>
<FormControl render={<Input {...field} />} />
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="date"
render={({ field }) => (
<FormItem>
<FormLabel>
<Trans>Date</Trans>
</FormLabel>
<FormControl render={<Input {...field} />} />
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="date"
render={({ field }) => (
<FormItem>
<FormLabel>
<Trans>Date</Trans>
</FormLabel>
<FormControl render={<Input {...field} />} />
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="website"
render={({ field }) => (
<FormItem>
<FormLabel>
<Trans>Website</Trans>
</FormLabel>
<URLInput
{...field}
value={field.value}
onChange={field.onChange}
hideLabelButton={form.watch("options.showLinkInTitle")}
/>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="website"
render={({ field }) => (
<FormItem>
<FormLabel>
<Trans>Website</Trans>
</FormLabel>
<URLInput
{...field}
value={field.value}
onChange={field.onChange}
hideLabelButton={form.watch("options.showLinkInTitle")}
/>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="options.showLinkInTitle"
render={({ field }) => (
<FormItem className="flex items-center gap-x-2">
<FormControl render={<Switch checked={field.value} onCheckedChange={field.onChange} />} />
<FormLabel className="mt-0!">
<Trans>Show link in title</Trans>
</FormLabel>
</FormItem>
)}
/>
<FormField
control={form.control}
name="options.showLinkInTitle"
render={({ field }) => (
<FormItem className="flex items-center gap-x-2">
<FormControl render={<Switch checked={field.value} onCheckedChange={field.onChange} />} />
<FormLabel className="mt-0!">
<Trans>Show link in title</Trans>
</FormLabel>
</FormItem>
)}
/>
<FormField
control={form.control}
name="description"
render={({ field }) => (
<FormItem className="sm:col-span-full">
<FormLabel>
<Trans>Description</Trans>
</FormLabel>
<FormControl render={<RichInput {...field} value={field.value} onChange={field.onChange} />} />
<FormMessage />
</FormItem>
)}
/>
</>
);
<FormField
control={form.control}
name="description"
render={({ field }) => (
<FormItem className="sm:col-span-full">
<FormLabel>
<Trans>Description</Trans>
</FormLabel>
<FormControl render={<RichInput {...field} value={field.value} onChange={field.onChange} />} />
<FormMessage />
</FormItem>
)}
/>
</>
);
}
+192 -192
View File
@@ -25,222 +25,222 @@ const formSchema = certificationItemSchema;
type FormValues = z.infer<typeof formSchema>;
export function CreateCertificationDialog({ data }: DialogProps<"resume.sections.certifications.create">) {
const closeDialog = useDialogStore((state) => state.closeDialog);
const updateResumeData = useResumeStore((state) => state.updateResumeData);
const closeDialog = useDialogStore((state) => state.closeDialog);
const updateResumeData = useResumeStore((state) => state.updateResumeData);
const form = useForm<FormValues>({
resolver: zodResolver(formSchema),
defaultValues: {
id: generateId(),
hidden: data?.item?.hidden ?? false,
options: data?.item?.options ?? { showLinkInTitle: false },
title: data?.item?.title ?? "",
issuer: data?.item?.issuer ?? "",
date: data?.item?.date ?? "",
website: data?.item?.website ?? { url: "", label: "" },
description: data?.item?.description ?? "",
},
});
const form = useForm<FormValues>({
resolver: zodResolver(formSchema),
defaultValues: {
id: generateId(),
hidden: data?.item?.hidden ?? false,
options: data?.item?.options ?? { showLinkInTitle: false },
title: data?.item?.title ?? "",
issuer: data?.item?.issuer ?? "",
date: data?.item?.date ?? "",
website: data?.item?.website ?? { url: "", label: "" },
description: data?.item?.description ?? "",
},
});
const onSubmit = (formData: FormValues) => {
updateResumeData((draft) => {
if (data?.customSectionId) {
const section = draft.customSections.find((s) => s.id === data.customSectionId);
if (section) section.items.push(formData);
} else {
draft.sections.certifications.items.push(formData);
}
});
closeDialog();
};
const onSubmit = (formData: FormValues) => {
updateResumeData((draft) => {
if (data?.customSectionId) {
const section = draft.customSections.find((s) => s.id === data.customSectionId);
if (section) section.items.push(formData);
} else {
draft.sections.certifications.items.push(formData);
}
});
closeDialog();
};
const { blockEvents, requestClose } = useFormBlocker(form);
const { blockEvents, requestClose } = useFormBlocker(form);
return (
<DialogContent {...blockEvents}>
<DialogHeader>
<DialogTitle className="flex items-center gap-x-2">
<PlusIcon />
<Trans>Create a new certification</Trans>
</DialogTitle>
<DialogDescription />
</DialogHeader>
return (
<DialogContent {...blockEvents}>
<DialogHeader>
<DialogTitle className="flex items-center gap-x-2">
<PlusIcon />
<Trans>Create a new certification</Trans>
</DialogTitle>
<DialogDescription />
</DialogHeader>
<Form {...form}>
<form className="grid gap-4 sm:grid-cols-2" onSubmit={form.handleSubmit(onSubmit)}>
<CertificationForm />
<Form {...form}>
<form className="grid gap-4 sm:grid-cols-2" onSubmit={form.handleSubmit(onSubmit)}>
<CertificationForm />
<DialogFooter className="sm:col-span-full">
<Button variant="ghost" onClick={requestClose}>
<Trans>Cancel</Trans>
</Button>
<DialogFooter className="sm:col-span-full">
<Button variant="ghost" onClick={requestClose}>
<Trans>Cancel</Trans>
</Button>
<Button type="submit" disabled={form.formState.isSubmitting}>
<Trans>Create</Trans>
</Button>
</DialogFooter>
</form>
</Form>
</DialogContent>
);
<Button type="submit" disabled={form.formState.isSubmitting}>
<Trans>Create</Trans>
</Button>
</DialogFooter>
</form>
</Form>
</DialogContent>
);
}
export function UpdateCertificationDialog({ data }: DialogProps<"resume.sections.certifications.update">) {
const closeDialog = useDialogStore((state) => state.closeDialog);
const updateResumeData = useResumeStore((state) => state.updateResumeData);
const closeDialog = useDialogStore((state) => state.closeDialog);
const updateResumeData = useResumeStore((state) => state.updateResumeData);
const form = useForm<FormValues>({
resolver: zodResolver(formSchema),
defaultValues: {
id: data.item.id,
hidden: data.item.hidden,
options: data.item.options ?? { showLinkInTitle: false },
title: data.item.title,
issuer: data.item.issuer,
date: data.item.date,
website: data.item.website,
description: data.item.description,
},
});
const form = useForm<FormValues>({
resolver: zodResolver(formSchema),
defaultValues: {
id: data.item.id,
hidden: data.item.hidden,
options: data.item.options ?? { showLinkInTitle: false },
title: data.item.title,
issuer: data.item.issuer,
date: data.item.date,
website: data.item.website,
description: data.item.description,
},
});
const onSubmit = (formData: FormValues) => {
updateResumeData((draft) => {
if (data?.customSectionId) {
const section = draft.customSections.find((s) => s.id === data.customSectionId);
if (!section) return;
const index = section.items.findIndex((item) => item.id === formData.id);
if (index !== -1) section.items[index] = formData;
} else {
const index = draft.sections.certifications.items.findIndex((item) => item.id === formData.id);
if (index !== -1) draft.sections.certifications.items[index] = formData;
}
});
closeDialog();
};
const onSubmit = (formData: FormValues) => {
updateResumeData((draft) => {
if (data?.customSectionId) {
const section = draft.customSections.find((s) => s.id === data.customSectionId);
if (!section) return;
const index = section.items.findIndex((item) => item.id === formData.id);
if (index !== -1) section.items[index] = formData;
} else {
const index = draft.sections.certifications.items.findIndex((item) => item.id === formData.id);
if (index !== -1) draft.sections.certifications.items[index] = formData;
}
});
closeDialog();
};
const { blockEvents, requestClose } = useFormBlocker(form);
const { blockEvents, requestClose } = useFormBlocker(form);
return (
<DialogContent {...blockEvents}>
<DialogHeader>
<DialogTitle className="flex items-center gap-x-2">
<PencilSimpleLineIcon />
<Trans>Update an existing certification</Trans>
</DialogTitle>
<DialogDescription />
</DialogHeader>
return (
<DialogContent {...blockEvents}>
<DialogHeader>
<DialogTitle className="flex items-center gap-x-2">
<PencilSimpleLineIcon />
<Trans>Update an existing certification</Trans>
</DialogTitle>
<DialogDescription />
</DialogHeader>
<Form {...form}>
<form className="grid gap-4 sm:grid-cols-2" onSubmit={form.handleSubmit(onSubmit)}>
<CertificationForm />
<Form {...form}>
<form className="grid gap-4 sm:grid-cols-2" onSubmit={form.handleSubmit(onSubmit)}>
<CertificationForm />
<DialogFooter className="sm:col-span-full">
<Button variant="ghost" onClick={requestClose}>
<Trans>Cancel</Trans>
</Button>
<DialogFooter className="sm:col-span-full">
<Button variant="ghost" onClick={requestClose}>
<Trans>Cancel</Trans>
</Button>
<Button type="submit" disabled={form.formState.isSubmitting}>
<Trans>Save Changes</Trans>
</Button>
</DialogFooter>
</form>
</Form>
</DialogContent>
);
<Button type="submit" disabled={form.formState.isSubmitting}>
<Trans>Save Changes</Trans>
</Button>
</DialogFooter>
</form>
</Form>
</DialogContent>
);
}
function CertificationForm() {
const form = useFormContext<FormValues>();
const form = useFormContext<FormValues>();
return (
<>
<FormField
control={form.control}
name="title"
render={({ field }) => (
<FormItem>
<FormLabel>
<Trans>Title</Trans>
</FormLabel>
<FormControl render={<Input {...field} />} />
<FormMessage />
</FormItem>
)}
/>
return (
<>
<FormField
control={form.control}
name="title"
render={({ field }) => (
<FormItem>
<FormLabel>
<Trans>Title</Trans>
</FormLabel>
<FormControl render={<Input {...field} />} />
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="issuer"
render={({ field }) => (
<FormItem>
<FormLabel>
<Trans>Issuer</Trans>
</FormLabel>
<FormControl render={<Input {...field} />} />
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="issuer"
render={({ field }) => (
<FormItem>
<FormLabel>
<Trans>Issuer</Trans>
</FormLabel>
<FormControl render={<Input {...field} />} />
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="date"
render={({ field }) => (
<FormItem>
<FormLabel>
<Trans>Date</Trans>
</FormLabel>
<FormControl render={<Input {...field} />} />
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="date"
render={({ field }) => (
<FormItem>
<FormLabel>
<Trans>Date</Trans>
</FormLabel>
<FormControl render={<Input {...field} />} />
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="website"
render={({ field }) => (
<FormItem>
<FormLabel>
<Trans>Website</Trans>
</FormLabel>
<URLInput
{...field}
value={field.value}
onChange={field.onChange}
hideLabelButton={form.watch("options.showLinkInTitle")}
/>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="website"
render={({ field }) => (
<FormItem>
<FormLabel>
<Trans>Website</Trans>
</FormLabel>
<URLInput
{...field}
value={field.value}
onChange={field.onChange}
hideLabelButton={form.watch("options.showLinkInTitle")}
/>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="options.showLinkInTitle"
render={({ field }) => (
<FormItem className="flex items-center gap-x-2">
<FormControl render={<Switch checked={field.value} onCheckedChange={field.onChange} />} />
<FormLabel className="mt-0!">
<Trans>Show link in title</Trans>
</FormLabel>
</FormItem>
)}
/>
<FormField
control={form.control}
name="options.showLinkInTitle"
render={({ field }) => (
<FormItem className="flex items-center gap-x-2">
<FormControl render={<Switch checked={field.value} onCheckedChange={field.onChange} />} />
<FormLabel className="mt-0!">
<Trans>Show link in title</Trans>
</FormLabel>
</FormItem>
)}
/>
<FormField
control={form.control}
name="description"
render={({ field }) => (
<FormItem className="sm:col-span-full">
<FormLabel>
<Trans>Description</Trans>
</FormLabel>
<FormControl render={<RichInput {...field} value={field.value} onChange={field.onChange} />} />
<FormMessage />
</FormItem>
)}
/>
</>
);
<FormField
control={form.control}
name="description"
render={({ field }) => (
<FormItem className="sm:col-span-full">
<FormLabel>
<Trans>Description</Trans>
</FormLabel>
<FormControl render={<RichInput {...field} value={field.value} onChange={field.onChange} />} />
<FormMessage />
</FormItem>
)}
/>
</>
);
}
+123 -123
View File
@@ -22,149 +22,149 @@ const formSchema = coverLetterItemSchema;
type FormValues = z.infer<typeof formSchema>;
export function CreateCoverLetterDialog({ data }: DialogProps<"resume.sections.cover-letter.create">) {
const closeDialog = useDialogStore((state) => state.closeDialog);
const updateResumeData = useResumeStore((state) => state.updateResumeData);
const closeDialog = useDialogStore((state) => state.closeDialog);
const updateResumeData = useResumeStore((state) => state.updateResumeData);
const form = useForm<FormValues>({
resolver: zodResolver(formSchema),
defaultValues: {
id: generateId(),
hidden: data?.item?.hidden ?? false,
recipient: data?.item?.recipient ?? "",
content: data?.item?.content ?? "",
},
});
const form = useForm<FormValues>({
resolver: zodResolver(formSchema),
defaultValues: {
id: generateId(),
hidden: data?.item?.hidden ?? false,
recipient: data?.item?.recipient ?? "",
content: data?.item?.content ?? "",
},
});
const onSubmit = (formData: FormValues) => {
updateResumeData((draft) => {
if (data?.customSectionId) {
const section = draft.customSections.find((s) => s.id === data.customSectionId);
if (section) section.items.push(formData);
}
});
closeDialog();
};
const onSubmit = (formData: FormValues) => {
updateResumeData((draft) => {
if (data?.customSectionId) {
const section = draft.customSections.find((s) => s.id === data.customSectionId);
if (section) section.items.push(formData);
}
});
closeDialog();
};
const { blockEvents, requestClose } = useFormBlocker(form);
const { blockEvents, requestClose } = useFormBlocker(form);
return (
<DialogContent {...blockEvents}>
<DialogHeader>
<DialogTitle className="flex items-center gap-x-2">
<PlusIcon />
<Trans>Create a new cover letter</Trans>
</DialogTitle>
<DialogDescription />
</DialogHeader>
return (
<DialogContent {...blockEvents}>
<DialogHeader>
<DialogTitle className="flex items-center gap-x-2">
<PlusIcon />
<Trans>Create a new cover letter</Trans>
</DialogTitle>
<DialogDescription />
</DialogHeader>
<Form {...form}>
<form className="grid gap-4" onSubmit={form.handleSubmit(onSubmit)}>
<CoverLetterForm />
<Form {...form}>
<form className="grid gap-4" onSubmit={form.handleSubmit(onSubmit)}>
<CoverLetterForm />
<DialogFooter>
<Button variant="ghost" onClick={requestClose}>
<Trans>Cancel</Trans>
</Button>
<DialogFooter>
<Button variant="ghost" onClick={requestClose}>
<Trans>Cancel</Trans>
</Button>
<Button type="submit" disabled={form.formState.isSubmitting}>
<Trans>Create</Trans>
</Button>
</DialogFooter>
</form>
</Form>
</DialogContent>
);
<Button type="submit" disabled={form.formState.isSubmitting}>
<Trans>Create</Trans>
</Button>
</DialogFooter>
</form>
</Form>
</DialogContent>
);
}
export function UpdateCoverLetterDialog({ data }: DialogProps<"resume.sections.cover-letter.update">) {
const closeDialog = useDialogStore((state) => state.closeDialog);
const updateResumeData = useResumeStore((state) => state.updateResumeData);
const closeDialog = useDialogStore((state) => state.closeDialog);
const updateResumeData = useResumeStore((state) => state.updateResumeData);
const form = useForm<FormValues>({
resolver: zodResolver(formSchema),
defaultValues: {
id: data.item.id,
hidden: data.item.hidden,
recipient: data.item.recipient,
content: data.item.content,
},
});
const form = useForm<FormValues>({
resolver: zodResolver(formSchema),
defaultValues: {
id: data.item.id,
hidden: data.item.hidden,
recipient: data.item.recipient,
content: data.item.content,
},
});
const onSubmit = (formData: FormValues) => {
updateResumeData((draft) => {
if (data?.customSectionId) {
const section = draft.customSections.find((s) => s.id === data.customSectionId);
if (!section) return;
const index = section.items.findIndex((item) => item.id === formData.id);
if (index !== -1) section.items[index] = formData;
}
});
closeDialog();
};
const onSubmit = (formData: FormValues) => {
updateResumeData((draft) => {
if (data?.customSectionId) {
const section = draft.customSections.find((s) => s.id === data.customSectionId);
if (!section) return;
const index = section.items.findIndex((item) => item.id === formData.id);
if (index !== -1) section.items[index] = formData;
}
});
closeDialog();
};
const { blockEvents, requestClose } = useFormBlocker(form);
const { blockEvents, requestClose } = useFormBlocker(form);
return (
<DialogContent {...blockEvents}>
<DialogHeader>
<DialogTitle className="flex items-center gap-x-2">
<PencilSimpleLineIcon />
<Trans>Update an existing cover letter</Trans>
</DialogTitle>
<DialogDescription />
</DialogHeader>
return (
<DialogContent {...blockEvents}>
<DialogHeader>
<DialogTitle className="flex items-center gap-x-2">
<PencilSimpleLineIcon />
<Trans>Update an existing cover letter</Trans>
</DialogTitle>
<DialogDescription />
</DialogHeader>
<Form {...form}>
<form className="grid gap-4" onSubmit={form.handleSubmit(onSubmit)}>
<CoverLetterForm />
<Form {...form}>
<form className="grid gap-4" onSubmit={form.handleSubmit(onSubmit)}>
<CoverLetterForm />
<DialogFooter>
<Button variant="ghost" onClick={requestClose}>
<Trans>Cancel</Trans>
</Button>
<DialogFooter>
<Button variant="ghost" onClick={requestClose}>
<Trans>Cancel</Trans>
</Button>
<Button type="submit" disabled={form.formState.isSubmitting}>
<Trans>Save Changes</Trans>
</Button>
</DialogFooter>
</form>
</Form>
</DialogContent>
);
<Button type="submit" disabled={form.formState.isSubmitting}>
<Trans>Save Changes</Trans>
</Button>
</DialogFooter>
</form>
</Form>
</DialogContent>
);
}
function CoverLetterForm() {
const form = useFormContext<FormValues>();
const form = useFormContext<FormValues>();
return (
<>
<FormField
control={form.control}
name="recipient"
render={({ field }) => (
<FormItem>
<FormLabel>
<Trans>Recipient</Trans>
</FormLabel>
<FormControl render={<RichInput {...field} value={field.value} onChange={field.onChange} />} />
<FormMessage />
</FormItem>
)}
/>
return (
<>
<FormField
control={form.control}
name="recipient"
render={({ field }) => (
<FormItem>
<FormLabel>
<Trans>Recipient</Trans>
</FormLabel>
<FormControl render={<RichInput {...field} value={field.value} onChange={field.onChange} />} />
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="content"
render={({ field }) => (
<FormItem>
<FormLabel>
<Trans>Content</Trans>
</FormLabel>
<FormControl render={<RichInput {...field} value={field.value} onChange={field.onChange} />} />
<FormMessage />
</FormItem>
)}
/>
</>
);
<FormField
control={form.control}
name="content"
render={({ field }) => (
<FormItem>
<FormLabel>
<Trans>Content</Trans>
</FormLabel>
<FormControl render={<RichInput {...field} value={field.value} onChange={field.onChange} />} />
<FormMessage />
</FormItem>
)}
/>
</>
);
}
+152 -152
View File
@@ -26,182 +26,182 @@ const formSchema = customSectionSchema;
type FormValues = z.infer<typeof formSchema>;
const SECTION_TYPE_OPTIONS: { value: CustomSectionType; label: MessageDescriptor }[] = [
{ value: "summary", label: msg`Summary` },
{ value: "experience", label: msg`Experience` },
{ value: "education", label: msg`Education` },
{ value: "projects", label: msg`Projects` },
{ value: "profiles", label: msg`Profiles` },
{ value: "skills", label: msg`Skills` },
{ value: "languages", label: msg`Languages` },
{ value: "interests", label: msg`Interests` },
{ value: "awards", label: msg`Awards` },
{ value: "certifications", label: msg`Certifications` },
{ value: "publications", label: msg`Publications` },
{ value: "volunteer", label: msg`Volunteer` },
{ value: "references", label: msg`References` },
{ value: "cover-letter", label: msg`Cover Letter` },
{ value: "summary", label: msg`Summary` },
{ value: "experience", label: msg`Experience` },
{ value: "education", label: msg`Education` },
{ value: "projects", label: msg`Projects` },
{ value: "profiles", label: msg`Profiles` },
{ value: "skills", label: msg`Skills` },
{ value: "languages", label: msg`Languages` },
{ value: "interests", label: msg`Interests` },
{ value: "awards", label: msg`Awards` },
{ value: "certifications", label: msg`Certifications` },
{ value: "publications", label: msg`Publications` },
{ value: "volunteer", label: msg`Volunteer` },
{ value: "references", label: msg`References` },
{ value: "cover-letter", label: msg`Cover Letter` },
];
export function CreateCustomSectionDialog({ data }: DialogProps<"resume.sections.custom.create">) {
const closeDialog = useDialogStore((state) => state.closeDialog);
const updateResumeData = useResumeStore((state) => state.updateResumeData);
const closeDialog = useDialogStore((state) => state.closeDialog);
const updateResumeData = useResumeStore((state) => state.updateResumeData);
const form = useForm<FormValues>({
resolver: zodResolver(formSchema),
defaultValues: {
id: generateId(),
title: data?.title ?? "",
type: data?.type ?? "experience",
columns: data?.columns ?? 1,
hidden: data?.hidden ?? false,
items: data?.items ?? [],
},
});
const form = useForm<FormValues>({
resolver: zodResolver(formSchema),
defaultValues: {
id: generateId(),
title: data?.title ?? "",
type: data?.type ?? "experience",
columns: data?.columns ?? 1,
hidden: data?.hidden ?? false,
items: data?.items ?? [],
},
});
const onSubmit = (formData: FormValues) => {
updateResumeData((draft) => {
draft.customSections.push(formData);
const onSubmit = (formData: FormValues) => {
updateResumeData((draft) => {
draft.customSections.push(formData);
const lastPageIndex = draft.metadata.layout.pages.length - 1;
if (lastPageIndex < 0) return;
draft.metadata.layout.pages[lastPageIndex].main.push(formData.id);
});
closeDialog();
};
const lastPageIndex = draft.metadata.layout.pages.length - 1;
if (lastPageIndex < 0) return;
draft.metadata.layout.pages[lastPageIndex].main.push(formData.id);
});
closeDialog();
};
const { blockEvents, requestClose } = useFormBlocker(form);
const { blockEvents, requestClose } = useFormBlocker(form);
return (
<DialogContent {...blockEvents}>
<DialogHeader>
<DialogTitle className="flex items-center gap-x-2">
<PlusIcon />
<Trans>Create a new custom section</Trans>
</DialogTitle>
<DialogDescription />
</DialogHeader>
return (
<DialogContent {...blockEvents}>
<DialogHeader>
<DialogTitle className="flex items-center gap-x-2">
<PlusIcon />
<Trans>Create a new custom section</Trans>
</DialogTitle>
<DialogDescription />
</DialogHeader>
<Form {...form}>
<form className="grid gap-4 sm:grid-cols-2" onSubmit={form.handleSubmit(onSubmit)}>
<CustomSectionForm />
<Form {...form}>
<form className="grid gap-4 sm:grid-cols-2" onSubmit={form.handleSubmit(onSubmit)}>
<CustomSectionForm />
<DialogFooter className="sm:col-span-full">
<Button variant="ghost" onClick={requestClose}>
<Trans>Cancel</Trans>
</Button>
<DialogFooter className="sm:col-span-full">
<Button variant="ghost" onClick={requestClose}>
<Trans>Cancel</Trans>
</Button>
<Button type="submit" disabled={form.formState.isSubmitting}>
<Trans>Create</Trans>
</Button>
</DialogFooter>
</form>
</Form>
</DialogContent>
);
<Button type="submit" disabled={form.formState.isSubmitting}>
<Trans>Create</Trans>
</Button>
</DialogFooter>
</form>
</Form>
</DialogContent>
);
}
export function UpdateCustomSectionDialog({ data }: DialogProps<"resume.sections.custom.update">) {
const closeDialog = useDialogStore((state) => state.closeDialog);
const updateResumeData = useResumeStore((state) => state.updateResumeData);
const closeDialog = useDialogStore((state) => state.closeDialog);
const updateResumeData = useResumeStore((state) => state.updateResumeData);
const form = useForm<FormValues>({
resolver: zodResolver(formSchema),
defaultValues: {
id: data.id,
title: data.title,
type: data.type,
columns: data.columns,
hidden: data.hidden,
items: data.items,
},
});
const form = useForm<FormValues>({
resolver: zodResolver(formSchema),
defaultValues: {
id: data.id,
title: data.title,
type: data.type,
columns: data.columns,
hidden: data.hidden,
items: data.items,
},
});
const onSubmit = (formData: FormValues) => {
updateResumeData((draft) => {
const index = draft.customSections.findIndex((item) => item.id === formData.id);
if (index === -1) return;
draft.customSections[index] = formData;
});
closeDialog();
};
const onSubmit = (formData: FormValues) => {
updateResumeData((draft) => {
const index = draft.customSections.findIndex((item) => item.id === formData.id);
if (index === -1) return;
draft.customSections[index] = formData;
});
closeDialog();
};
const { blockEvents, requestClose } = useFormBlocker(form);
const { blockEvents, requestClose } = useFormBlocker(form);
return (
<DialogContent {...blockEvents}>
<DialogHeader>
<DialogTitle className="flex items-center gap-x-2">
<PencilSimpleLineIcon />
<Trans>Update an existing custom section</Trans>
</DialogTitle>
<DialogDescription />
</DialogHeader>
return (
<DialogContent {...blockEvents}>
<DialogHeader>
<DialogTitle className="flex items-center gap-x-2">
<PencilSimpleLineIcon />
<Trans>Update an existing custom section</Trans>
</DialogTitle>
<DialogDescription />
</DialogHeader>
<Form {...form}>
<form className="grid gap-4 sm:grid-cols-2" onSubmit={form.handleSubmit(onSubmit)}>
<CustomSectionForm isUpdate />
<Form {...form}>
<form className="grid gap-4 sm:grid-cols-2" onSubmit={form.handleSubmit(onSubmit)}>
<CustomSectionForm isUpdate />
<DialogFooter className="sm:col-span-full">
<Button variant="ghost" onClick={requestClose}>
<Trans>Cancel</Trans>
</Button>
<DialogFooter className="sm:col-span-full">
<Button variant="ghost" onClick={requestClose}>
<Trans>Cancel</Trans>
</Button>
<Button type="submit" disabled={form.formState.isSubmitting}>
<Trans>Save Changes</Trans>
</Button>
</DialogFooter>
</form>
</Form>
</DialogContent>
);
<Button type="submit" disabled={form.formState.isSubmitting}>
<Trans>Save Changes</Trans>
</Button>
</DialogFooter>
</form>
</Form>
</DialogContent>
);
}
function CustomSectionForm({ isUpdate = false }: { isUpdate?: boolean }) {
const { i18n } = useLingui();
const form = useFormContext<FormValues>();
const { i18n } = useLingui();
const form = useFormContext<FormValues>();
return (
<>
<FormField
control={form.control}
name="title"
render={({ field }) => (
<FormItem className="sm:col-span-full">
<FormLabel>
<Trans>Title</Trans>
</FormLabel>
<FormControl render={<Input {...field} />} />
<FormMessage />
</FormItem>
)}
/>
return (
<>
<FormField
control={form.control}
name="title"
render={({ field }) => (
<FormItem className="sm:col-span-full">
<FormLabel>
<Trans>Title</Trans>
</FormLabel>
<FormControl render={<Input {...field} />} />
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="type"
render={({ field }) => (
<FormItem className="sm:col-span-full">
<FormLabel>
<Trans>Section Type</Trans>
</FormLabel>
<FormControl
render={
<Combobox
{...field}
value={field.value}
disabled={isUpdate}
onValueChange={field.onChange}
options={SECTION_TYPE_OPTIONS.map((option) => ({
value: option.value,
label: i18n.t(option.label),
}))}
/>
}
/>
<FormMessage />
</FormItem>
)}
/>
</>
);
<FormField
control={form.control}
name="type"
render={({ field }) => (
<FormItem className="sm:col-span-full">
<FormLabel>
<Trans>Section Type</Trans>
</FormLabel>
<FormControl
render={
<Combobox
{...field}
value={field.value}
disabled={isUpdate}
onValueChange={field.onChange}
options={SECTION_TYPE_OPTIONS.map((option) => ({
value: option.value,
label: i18n.t(option.label),
}))}
/>
}
/>
<FormMessage />
</FormItem>
)}
/>
</>
);
}
+237 -237
View File
@@ -25,270 +25,270 @@ const formSchema = educationItemSchema;
type FormValues = z.infer<typeof formSchema>;
export function CreateEducationDialog({ data }: DialogProps<"resume.sections.education.create">) {
const closeDialog = useDialogStore((state) => state.closeDialog);
const updateResumeData = useResumeStore((state) => state.updateResumeData);
const closeDialog = useDialogStore((state) => state.closeDialog);
const updateResumeData = useResumeStore((state) => state.updateResumeData);
const form = useForm<FormValues>({
resolver: zodResolver(formSchema),
defaultValues: {
id: generateId(),
hidden: data?.item?.hidden ?? false,
options: data?.item?.options ?? { showLinkInTitle: false },
school: data?.item?.school ?? "",
degree: data?.item?.degree ?? "",
area: data?.item?.area ?? "",
grade: data?.item?.grade ?? "",
location: data?.item?.location ?? "",
period: data?.item?.period ?? "",
website: data?.item?.website ?? { url: "", label: "" },
description: data?.item?.description ?? "",
},
});
const form = useForm<FormValues>({
resolver: zodResolver(formSchema),
defaultValues: {
id: generateId(),
hidden: data?.item?.hidden ?? false,
options: data?.item?.options ?? { showLinkInTitle: false },
school: data?.item?.school ?? "",
degree: data?.item?.degree ?? "",
area: data?.item?.area ?? "",
grade: data?.item?.grade ?? "",
location: data?.item?.location ?? "",
period: data?.item?.period ?? "",
website: data?.item?.website ?? { url: "", label: "" },
description: data?.item?.description ?? "",
},
});
const onSubmit = (formData: FormValues) => {
updateResumeData((draft) => {
if (data?.customSectionId) {
const section = draft.customSections.find((s) => s.id === data.customSectionId);
if (section) section.items.push(formData);
} else {
draft.sections.education.items.push(formData);
}
});
closeDialog();
};
const onSubmit = (formData: FormValues) => {
updateResumeData((draft) => {
if (data?.customSectionId) {
const section = draft.customSections.find((s) => s.id === data.customSectionId);
if (section) section.items.push(formData);
} else {
draft.sections.education.items.push(formData);
}
});
closeDialog();
};
const { blockEvents, requestClose } = useFormBlocker(form);
const { blockEvents, requestClose } = useFormBlocker(form);
return (
<DialogContent {...blockEvents}>
<DialogHeader>
<DialogTitle className="flex items-center gap-x-2">
<PlusIcon />
<Trans>Create a new education</Trans>
</DialogTitle>
<DialogDescription />
</DialogHeader>
return (
<DialogContent {...blockEvents}>
<DialogHeader>
<DialogTitle className="flex items-center gap-x-2">
<PlusIcon />
<Trans>Create a new education</Trans>
</DialogTitle>
<DialogDescription />
</DialogHeader>
<Form {...form}>
<form className="grid gap-4 sm:grid-cols-2" onSubmit={form.handleSubmit(onSubmit)}>
<EducationForm />
<Form {...form}>
<form className="grid gap-4 sm:grid-cols-2" onSubmit={form.handleSubmit(onSubmit)}>
<EducationForm />
<DialogFooter className="sm:col-span-full">
<Button variant="ghost" onClick={requestClose}>
<Trans>Cancel</Trans>
</Button>
<DialogFooter className="sm:col-span-full">
<Button variant="ghost" onClick={requestClose}>
<Trans>Cancel</Trans>
</Button>
<Button type="submit" disabled={form.formState.isSubmitting}>
<Trans>Create</Trans>
</Button>
</DialogFooter>
</form>
</Form>
</DialogContent>
);
<Button type="submit" disabled={form.formState.isSubmitting}>
<Trans>Create</Trans>
</Button>
</DialogFooter>
</form>
</Form>
</DialogContent>
);
}
export function UpdateEducationDialog({ data }: DialogProps<"resume.sections.education.update">) {
const closeDialog = useDialogStore((state) => state.closeDialog);
const updateResumeData = useResumeStore((state) => state.updateResumeData);
const closeDialog = useDialogStore((state) => state.closeDialog);
const updateResumeData = useResumeStore((state) => state.updateResumeData);
const form = useForm<FormValues>({
resolver: zodResolver(formSchema),
defaultValues: {
id: data.item.id,
hidden: data.item.hidden,
options: data.item.options ?? { showLinkInTitle: false },
school: data.item.school,
degree: data.item.degree,
area: data.item.area,
grade: data.item.grade,
location: data.item.location,
period: data.item.period,
website: data.item.website,
description: data.item.description,
},
});
const form = useForm<FormValues>({
resolver: zodResolver(formSchema),
defaultValues: {
id: data.item.id,
hidden: data.item.hidden,
options: data.item.options ?? { showLinkInTitle: false },
school: data.item.school,
degree: data.item.degree,
area: data.item.area,
grade: data.item.grade,
location: data.item.location,
period: data.item.period,
website: data.item.website,
description: data.item.description,
},
});
const onSubmit = (formData: FormValues) => {
updateResumeData((draft) => {
if (data?.customSectionId) {
const section = draft.customSections.find((s) => s.id === data.customSectionId);
if (!section) return;
const index = section.items.findIndex((item) => item.id === formData.id);
if (index !== -1) section.items[index] = formData;
} else {
const index = draft.sections.education.items.findIndex((item) => item.id === formData.id);
if (index !== -1) draft.sections.education.items[index] = formData;
}
});
closeDialog();
};
const onSubmit = (formData: FormValues) => {
updateResumeData((draft) => {
if (data?.customSectionId) {
const section = draft.customSections.find((s) => s.id === data.customSectionId);
if (!section) return;
const index = section.items.findIndex((item) => item.id === formData.id);
if (index !== -1) section.items[index] = formData;
} else {
const index = draft.sections.education.items.findIndex((item) => item.id === formData.id);
if (index !== -1) draft.sections.education.items[index] = formData;
}
});
closeDialog();
};
const { blockEvents, requestClose } = useFormBlocker(form);
const { blockEvents, requestClose } = useFormBlocker(form);
return (
<DialogContent {...blockEvents}>
<DialogHeader>
<DialogTitle className="flex items-center gap-x-2">
<PencilSimpleLineIcon />
<Trans>Update an existing education</Trans>
</DialogTitle>
<DialogDescription />
</DialogHeader>
return (
<DialogContent {...blockEvents}>
<DialogHeader>
<DialogTitle className="flex items-center gap-x-2">
<PencilSimpleLineIcon />
<Trans>Update an existing education</Trans>
</DialogTitle>
<DialogDescription />
</DialogHeader>
<Form {...form}>
<form className="grid gap-4 sm:grid-cols-2" onSubmit={form.handleSubmit(onSubmit)}>
<EducationForm />
<Form {...form}>
<form className="grid gap-4 sm:grid-cols-2" onSubmit={form.handleSubmit(onSubmit)}>
<EducationForm />
<DialogFooter className="sm:col-span-full">
<Button variant="ghost" onClick={requestClose}>
<Trans>Cancel</Trans>
</Button>
<DialogFooter className="sm:col-span-full">
<Button variant="ghost" onClick={requestClose}>
<Trans>Cancel</Trans>
</Button>
<Button type="submit" disabled={form.formState.isSubmitting}>
<Trans>Save Changes</Trans>
</Button>
</DialogFooter>
</form>
</Form>
</DialogContent>
);
<Button type="submit" disabled={form.formState.isSubmitting}>
<Trans>Save Changes</Trans>
</Button>
</DialogFooter>
</form>
</Form>
</DialogContent>
);
}
function EducationForm() {
const form = useFormContext<FormValues>();
const form = useFormContext<FormValues>();
return (
<>
<FormField
control={form.control}
name="school"
render={({ field }) => (
<FormItem>
<FormLabel>
<Trans>School</Trans>
</FormLabel>
<FormControl render={<Input {...field} />} />
<FormMessage />
</FormItem>
)}
/>
return (
<>
<FormField
control={form.control}
name="school"
render={({ field }) => (
<FormItem>
<FormLabel>
<Trans>School</Trans>
</FormLabel>
<FormControl render={<Input {...field} />} />
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="degree"
render={({ field }) => (
<FormItem>
<FormLabel>
<Trans>Degree</Trans>
</FormLabel>
<FormControl render={<Input {...field} />} />
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="degree"
render={({ field }) => (
<FormItem>
<FormLabel>
<Trans>Degree</Trans>
</FormLabel>
<FormControl render={<Input {...field} />} />
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="area"
render={({ field }) => (
<FormItem>
<FormLabel>
<Trans>Area of Study</Trans>
</FormLabel>
<FormControl render={<Input {...field} />} />
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="area"
render={({ field }) => (
<FormItem>
<FormLabel>
<Trans>Area of Study</Trans>
</FormLabel>
<FormControl render={<Input {...field} />} />
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="grade"
render={({ field }) => (
<FormItem>
<FormLabel>
<Trans>Grade</Trans>
</FormLabel>
<FormControl render={<Input {...field} />} />
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="grade"
render={({ field }) => (
<FormItem>
<FormLabel>
<Trans>Grade</Trans>
</FormLabel>
<FormControl render={<Input {...field} />} />
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="location"
render={({ field }) => (
<FormItem>
<FormLabel>
<Trans>Location</Trans>
</FormLabel>
<FormControl render={<Input {...field} />} />
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="location"
render={({ field }) => (
<FormItem>
<FormLabel>
<Trans>Location</Trans>
</FormLabel>
<FormControl render={<Input {...field} />} />
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="period"
render={({ field }) => (
<FormItem>
<FormLabel>
<Trans>Period</Trans>
</FormLabel>
<FormControl render={<Input {...field} />} />
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="period"
render={({ field }) => (
<FormItem>
<FormLabel>
<Trans>Period</Trans>
</FormLabel>
<FormControl render={<Input {...field} />} />
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="website"
render={({ field }) => (
<FormItem className="sm:col-span-full">
<FormLabel>
<Trans>Website</Trans>
</FormLabel>
<URLInput
{...field}
value={field.value}
onChange={field.onChange}
hideLabelButton={form.watch("options.showLinkInTitle")}
/>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="website"
render={({ field }) => (
<FormItem className="sm:col-span-full">
<FormLabel>
<Trans>Website</Trans>
</FormLabel>
<URLInput
{...field}
value={field.value}
onChange={field.onChange}
hideLabelButton={form.watch("options.showLinkInTitle")}
/>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="options.showLinkInTitle"
render={({ field }) => (
<FormItem className="flex items-center gap-x-2 sm:col-span-full">
<FormControl render={<Switch checked={field.value} onCheckedChange={field.onChange} />} />
<FormLabel className="mt-0!">
<Trans>Show link in title</Trans>
</FormLabel>
</FormItem>
)}
/>
<FormField
control={form.control}
name="options.showLinkInTitle"
render={({ field }) => (
<FormItem className="flex items-center gap-x-2 sm:col-span-full">
<FormControl render={<Switch checked={field.value} onCheckedChange={field.onChange} />} />
<FormLabel className="mt-0!">
<Trans>Show link in title</Trans>
</FormLabel>
</FormItem>
)}
/>
<FormField
control={form.control}
name="description"
render={({ field }) => (
<FormItem className="sm:col-span-full">
<FormLabel>
<Trans>Description</Trans>
</FormLabel>
<FormControl render={<RichInput {...field} value={field.value} onChange={field.onChange} />} />
<FormMessage />
</FormItem>
)}
/>
</>
);
<FormField
control={form.control}
name="description"
render={({ field }) => (
<FormItem className="sm:col-span-full">
<FormLabel>
<Trans>Description</Trans>
</FormLabel>
<FormControl render={<RichInput {...field} value={field.value} onChange={field.onChange} />} />
<FormMessage />
</FormItem>
)}
/>
</>
);
}
+329 -329
View File
@@ -28,379 +28,379 @@ const formSchema = experienceItemSchema;
type FormValues = z.infer<typeof formSchema>;
export function CreateExperienceDialog({ data }: DialogProps<"resume.sections.experience.create">) {
const closeDialog = useDialogStore((state) => state.closeDialog);
const updateResumeData = useResumeStore((state) => state.updateResumeData);
const closeDialog = useDialogStore((state) => state.closeDialog);
const updateResumeData = useResumeStore((state) => state.updateResumeData);
const form = useForm<FormValues>({
resolver: zodResolver(formSchema),
defaultValues: {
id: generateId(),
hidden: data?.item?.hidden ?? false,
options: data?.item?.options ?? { showLinkInTitle: false },
company: data?.item?.company ?? "",
position: data?.item?.position ?? "",
location: data?.item?.location ?? "",
period: data?.item?.period ?? "",
website: data?.item?.website ?? { url: "", label: "" },
description: data?.item?.description ?? "",
roles: data?.item?.roles ?? [],
},
});
const form = useForm<FormValues>({
resolver: zodResolver(formSchema),
defaultValues: {
id: generateId(),
hidden: data?.item?.hidden ?? false,
options: data?.item?.options ?? { showLinkInTitle: false },
company: data?.item?.company ?? "",
position: data?.item?.position ?? "",
location: data?.item?.location ?? "",
period: data?.item?.period ?? "",
website: data?.item?.website ?? { url: "", label: "" },
description: data?.item?.description ?? "",
roles: data?.item?.roles ?? [],
},
});
const onSubmit = (formData: FormValues) => {
updateResumeData((draft) => {
if (data?.customSectionId) {
const section = draft.customSections.find((s) => s.id === data.customSectionId);
if (section) section.items.push(formData);
} else {
draft.sections.experience.items.push(formData);
}
});
const onSubmit = (formData: FormValues) => {
updateResumeData((draft) => {
if (data?.customSectionId) {
const section = draft.customSections.find((s) => s.id === data.customSectionId);
if (section) section.items.push(formData);
} else {
draft.sections.experience.items.push(formData);
}
});
closeDialog();
};
closeDialog();
};
const { blockEvents, requestClose } = useFormBlocker(form);
const { blockEvents, requestClose } = useFormBlocker(form);
return (
<DialogContent {...blockEvents}>
<DialogHeader>
<DialogTitle className="flex items-center gap-x-2">
<PlusIcon />
<Trans>Create a new experience</Trans>
</DialogTitle>
<DialogDescription />
</DialogHeader>
return (
<DialogContent {...blockEvents}>
<DialogHeader>
<DialogTitle className="flex items-center gap-x-2">
<PlusIcon />
<Trans>Create a new experience</Trans>
</DialogTitle>
<DialogDescription />
</DialogHeader>
<Form {...form}>
<form className="grid gap-4 sm:grid-cols-2" onSubmit={form.handleSubmit(onSubmit)}>
<ExperienceForm />
<Form {...form}>
<form className="grid gap-4 sm:grid-cols-2" onSubmit={form.handleSubmit(onSubmit)}>
<ExperienceForm />
<DialogFooter className="sm:col-span-full">
<Button variant="ghost" onClick={requestClose}>
<Trans>Cancel</Trans>
</Button>
<DialogFooter className="sm:col-span-full">
<Button variant="ghost" onClick={requestClose}>
<Trans>Cancel</Trans>
</Button>
<Button type="submit" disabled={form.formState.isSubmitting}>
<Trans>Create</Trans>
</Button>
</DialogFooter>
</form>
</Form>
</DialogContent>
);
<Button type="submit" disabled={form.formState.isSubmitting}>
<Trans>Create</Trans>
</Button>
</DialogFooter>
</form>
</Form>
</DialogContent>
);
}
export function UpdateExperienceDialog({ data }: DialogProps<"resume.sections.experience.update">) {
const closeDialog = useDialogStore((state) => state.closeDialog);
const updateResumeData = useResumeStore((state) => state.updateResumeData);
const closeDialog = useDialogStore((state) => state.closeDialog);
const updateResumeData = useResumeStore((state) => state.updateResumeData);
const form = useForm<FormValues>({
resolver: zodResolver(formSchema),
defaultValues: {
id: data.item.id,
hidden: data.item.hidden,
options: data.item.options ?? { showLinkInTitle: false },
company: data.item.company,
position: data.item.position,
location: data.item.location,
period: data.item.period,
website: data.item.website,
description: data.item.description,
roles: data.item.roles ?? [],
},
});
const form = useForm<FormValues>({
resolver: zodResolver(formSchema),
defaultValues: {
id: data.item.id,
hidden: data.item.hidden,
options: data.item.options ?? { showLinkInTitle: false },
company: data.item.company,
position: data.item.position,
location: data.item.location,
period: data.item.period,
website: data.item.website,
description: data.item.description,
roles: data.item.roles ?? [],
},
});
const onSubmit = (formData: FormValues) => {
updateResumeData((draft) => {
if (data?.customSectionId) {
const section = draft.customSections.find((s) => s.id === data.customSectionId);
if (!section) return;
const index = section.items.findIndex((item) => item.id === formData.id);
if (index !== -1) section.items[index] = formData;
} else {
const index = draft.sections.experience.items.findIndex((item) => item.id === formData.id);
if (index !== -1) draft.sections.experience.items[index] = formData;
}
});
const onSubmit = (formData: FormValues) => {
updateResumeData((draft) => {
if (data?.customSectionId) {
const section = draft.customSections.find((s) => s.id === data.customSectionId);
if (!section) return;
const index = section.items.findIndex((item) => item.id === formData.id);
if (index !== -1) section.items[index] = formData;
} else {
const index = draft.sections.experience.items.findIndex((item) => item.id === formData.id);
if (index !== -1) draft.sections.experience.items[index] = formData;
}
});
closeDialog();
};
closeDialog();
};
const { blockEvents, requestClose } = useFormBlocker(form);
const { blockEvents, requestClose } = useFormBlocker(form);
return (
<DialogContent {...blockEvents}>
<DialogHeader>
<DialogTitle className="flex items-center gap-x-2">
<PencilSimpleLineIcon />
<Trans>Update an existing experience</Trans>
</DialogTitle>
<DialogDescription />
</DialogHeader>
return (
<DialogContent {...blockEvents}>
<DialogHeader>
<DialogTitle className="flex items-center gap-x-2">
<PencilSimpleLineIcon />
<Trans>Update an existing experience</Trans>
</DialogTitle>
<DialogDescription />
</DialogHeader>
<Form {...form}>
<form className="grid gap-4 sm:grid-cols-2" onSubmit={form.handleSubmit(onSubmit)}>
<ExperienceForm />
<Form {...form}>
<form className="grid gap-4 sm:grid-cols-2" onSubmit={form.handleSubmit(onSubmit)}>
<ExperienceForm />
<DialogFooter className="sm:col-span-full">
<Button variant="ghost" onClick={requestClose}>
<Trans>Cancel</Trans>
</Button>
<DialogFooter className="sm:col-span-full">
<Button variant="ghost" onClick={requestClose}>
<Trans>Cancel</Trans>
</Button>
<Button type="submit" disabled={form.formState.isSubmitting}>
<Trans>Save Changes</Trans>
</Button>
</DialogFooter>
</form>
</Form>
</DialogContent>
);
<Button type="submit" disabled={form.formState.isSubmitting}>
<Trans>Save Changes</Trans>
</Button>
</DialogFooter>
</form>
</Form>
</DialogContent>
);
}
type RoleFieldsProps = {
role: RoleItem;
index: number;
onRemove: () => void;
role: RoleItem;
index: number;
onRemove: () => void;
};
function RoleFields({ role, index, onRemove }: RoleFieldsProps) {
const form = useFormContext<FormValues>();
const controls = useDragControls();
const form = useFormContext<FormValues>();
const controls = useDragControls();
return (
<Reorder.Item
value={role}
dragListener={false}
dragControls={controls}
initial={{ opacity: 1, y: -10 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -10 }}
className="relative grid rounded border sm:col-span-full sm:grid-cols-2"
>
<div className="col-span-full flex items-center justify-between rounded-t bg-border/30 px-2 py-1.5">
<Button
size="sm"
variant="ghost"
className="cursor-grab touch-none"
onPointerDown={(e) => {
e.preventDefault();
controls.start(e);
}}
>
<RowsIcon />
<Trans>Reorder</Trans>
</Button>
return (
<Reorder.Item
value={role}
dragListener={false}
dragControls={controls}
initial={{ opacity: 1, y: -10 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -10 }}
className="relative grid rounded border sm:col-span-full sm:grid-cols-2"
>
<div className="col-span-full flex items-center justify-between rounded-t bg-border/30 px-2 py-1.5">
<Button
size="sm"
variant="ghost"
className="cursor-grab touch-none"
onPointerDown={(e) => {
e.preventDefault();
controls.start(e);
}}
>
<RowsIcon />
<Trans>Reorder</Trans>
</Button>
<Button size="sm" variant="ghost" className="text-destructive hover:text-destructive" onClick={onRemove}>
<TrashSimpleIcon />
<Trans>Remove</Trans>
</Button>
</div>
<Button size="sm" variant="ghost" className="text-destructive hover:text-destructive" onClick={onRemove}>
<TrashSimpleIcon />
<Trans>Remove</Trans>
</Button>
</div>
<div className="grid gap-4 p-4 sm:col-span-full sm:grid-cols-2">
<FormField
control={form.control}
name={`roles.${index}.position`}
render={({ field }) => (
<FormItem>
<FormLabel>
<Trans>Position</Trans>
</FormLabel>
<FormControl render={<Input {...field} />} />
<FormMessage />
</FormItem>
)}
/>
<div className="grid gap-4 p-4 sm:col-span-full sm:grid-cols-2">
<FormField
control={form.control}
name={`roles.${index}.position`}
render={({ field }) => (
<FormItem>
<FormLabel>
<Trans>Position</Trans>
</FormLabel>
<FormControl render={<Input {...field} />} />
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name={`roles.${index}.period`}
render={({ field }) => (
<FormItem>
<FormLabel>
<Trans>Period</Trans>
</FormLabel>
<FormControl render={<Input {...field} />} />
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name={`roles.${index}.period`}
render={({ field }) => (
<FormItem>
<FormLabel>
<Trans>Period</Trans>
</FormLabel>
<FormControl render={<Input {...field} />} />
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name={`roles.${index}.description`}
render={({ field }) => (
<FormItem className="sm:col-span-full">
<FormLabel>
<Trans>Description</Trans>
</FormLabel>
<FormControl render={<RichInput {...field} value={field.value} onChange={field.onChange} />} />
<FormMessage />
</FormItem>
)}
/>
</div>
</Reorder.Item>
);
<FormField
control={form.control}
name={`roles.${index}.description`}
render={({ field }) => (
<FormItem className="sm:col-span-full">
<FormLabel>
<Trans>Description</Trans>
</FormLabel>
<FormControl render={<RichInput {...field} value={field.value} onChange={field.onChange} />} />
<FormMessage />
</FormItem>
)}
/>
</div>
</Reorder.Item>
);
}
function ExperienceForm() {
const form = useFormContext<FormValues>();
const form = useFormContext<FormValues>();
const { fields, append, remove } = useFieldArray({
name: "roles",
keyName: "fieldId",
control: form.control,
});
const { fields, append, remove } = useFieldArray({
name: "roles",
keyName: "fieldId",
control: form.control,
});
const hasRoles = useMemo(() => fields.length > 0, [fields]);
const hasRoles = useMemo(() => fields.length > 0, [fields]);
const handleReorderRoles = (newOrder: RoleItem[]) => {
form.setValue("roles", newOrder);
};
const handleReorderRoles = (newOrder: RoleItem[]) => {
form.setValue("roles", newOrder);
};
return (
<>
<FormField
control={form.control}
name="company"
render={({ field }) => (
<FormItem>
<FormLabel>
<Trans>Company</Trans>
</FormLabel>
<FormControl render={<Input {...field} />} />
<FormMessage />
</FormItem>
)}
/>
return (
<>
<FormField
control={form.control}
name="company"
render={({ field }) => (
<FormItem>
<FormLabel>
<Trans>Company</Trans>
</FormLabel>
<FormControl render={<Input {...field} />} />
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="position"
render={({ field }) => (
<FormItem>
<FormLabel>{hasRoles ? <Trans>Overall Title (optional)</Trans> : <Trans>Position</Trans>}</FormLabel>
<FormControl
render={<Input {...field} placeholder={hasRoles ? "e.g. Software Engineer → Senior Engineer" : ""} />}
/>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="position"
render={({ field }) => (
<FormItem>
<FormLabel>{hasRoles ? <Trans>Overall Title (optional)</Trans> : <Trans>Position</Trans>}</FormLabel>
<FormControl
render={<Input {...field} placeholder={hasRoles ? "e.g. Software Engineer → Senior Engineer" : ""} />}
/>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="location"
render={({ field }) => (
<FormItem>
<FormLabel>
<Trans>Location</Trans>
</FormLabel>
<FormControl render={<Input {...field} />} />
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="location"
render={({ field }) => (
<FormItem>
<FormLabel>
<Trans>Location</Trans>
</FormLabel>
<FormControl render={<Input {...field} />} />
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="period"
render={({ field }) => (
<FormItem>
<FormLabel>{hasRoles ? <Trans>Overall Period</Trans> : <Trans>Period</Trans>}</FormLabel>
<FormControl render={<Input {...field} placeholder={hasRoles ? "e.g. 2018 Present" : ""} />} />
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="period"
render={({ field }) => (
<FormItem>
<FormLabel>{hasRoles ? <Trans>Overall Period</Trans> : <Trans>Period</Trans>}</FormLabel>
<FormControl render={<Input {...field} placeholder={hasRoles ? "e.g. 2018 Present" : ""} />} />
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="website"
render={({ field }) => (
<FormItem className="sm:col-span-full">
<FormLabel>
<Trans>Website</Trans>
</FormLabel>
<URLInput
{...field}
value={field.value}
onChange={field.onChange}
hideLabelButton={form.watch("options.showLinkInTitle")}
/>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="website"
render={({ field }) => (
<FormItem className="sm:col-span-full">
<FormLabel>
<Trans>Website</Trans>
</FormLabel>
<URLInput
{...field}
value={field.value}
onChange={field.onChange}
hideLabelButton={form.watch("options.showLinkInTitle")}
/>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="options.showLinkInTitle"
render={({ field }) => (
<FormItem className="flex items-center gap-x-2 sm:col-span-full">
<FormControl render={<Switch checked={field.value} onCheckedChange={field.onChange} />} />
<FormLabel>
<Trans>Show link in title</Trans>
</FormLabel>
</FormItem>
)}
/>
<FormField
control={form.control}
name="options.showLinkInTitle"
render={({ field }) => (
<FormItem className="flex items-center gap-x-2 sm:col-span-full">
<FormControl render={<Switch checked={field.value} onCheckedChange={field.onChange} />} />
<FormLabel>
<Trans>Show link in title</Trans>
</FormLabel>
</FormItem>
)}
/>
{/* Role Progression */}
<div className="flex items-center justify-between sm:col-span-full">
<div className="space-y-1">
<p className="font-medium text-foreground">
<Trans>Role Progression</Trans>
</p>
<p className="text-xs text-muted-foreground">
<Trans>Add multiple roles to show career progression at the same company.</Trans>
</p>
</div>
{/* Role Progression */}
<div className="flex items-center justify-between sm:col-span-full">
<div className="space-y-1">
<p className="font-medium text-foreground">
<Trans>Role Progression</Trans>
</p>
<p className="text-xs text-muted-foreground">
<Trans>Add multiple roles to show career progression at the same company.</Trans>
</p>
</div>
<Button
size="sm"
variant="outline"
className="shrink-0"
onClick={() => append({ id: generateId(), position: "", period: "", description: "" })}
>
<PlusIcon />
<Trans>Add Role</Trans>
</Button>
</div>
<Button
size="sm"
variant="outline"
className="shrink-0"
onClick={() => append({ id: generateId(), position: "", period: "", description: "" })}
>
<PlusIcon />
<Trans>Add Role</Trans>
</Button>
</div>
{hasRoles && (
<Reorder.Group
axis="y"
values={fields}
onReorder={handleReorderRoles}
className="flex flex-col gap-4 sm:col-span-full"
>
<AnimatePresence>
{fields.map((field, index) => (
<RoleFields key={field.id} role={fields[index]} index={index} onRemove={() => remove(index)} />
))}
</AnimatePresence>
</Reorder.Group>
)}
{hasRoles && (
<Reorder.Group
axis="y"
values={fields}
onReorder={handleReorderRoles}
className="flex flex-col gap-4 sm:col-span-full"
>
<AnimatePresence>
{fields.map((field, index) => (
<RoleFields key={field.id} role={fields[index]} index={index} onRemove={() => remove(index)} />
))}
</AnimatePresence>
</Reorder.Group>
)}
{/* Single Role Description — only show when no roles are defined */}
{!hasRoles && (
<FormField
control={form.control}
name="description"
render={({ field }) => (
<FormItem className="sm:col-span-full">
<FormLabel>
<Trans>Description</Trans>
</FormLabel>
<FormControl render={<RichInput {...field} value={field.value} onChange={field.onChange} />} />
<FormMessage />
</FormItem>
)}
/>
)}
</>
);
{/* Single Role Description — only show when no roles are defined */}
{!hasRoles && (
<FormField
control={form.control}
name="description"
render={({ field }) => (
<FormItem className="sm:col-span-full">
<FormLabel>
<Trans>Description</Trans>
</FormLabel>
<FormControl render={<RichInput {...field} value={field.value} onChange={field.onChange} />} />
<FormMessage />
</FormItem>
)}
/>
)}
</>
);
}
+149 -149
View File
@@ -26,177 +26,177 @@ const formSchema = interestItemSchema;
type FormValues = z.infer<typeof formSchema>;
export function CreateInterestDialog({ data }: DialogProps<"resume.sections.interests.create">) {
const closeDialog = useDialogStore((state) => state.closeDialog);
const updateResumeData = useResumeStore((state) => state.updateResumeData);
const closeDialog = useDialogStore((state) => state.closeDialog);
const updateResumeData = useResumeStore((state) => state.updateResumeData);
const form = useForm<FormValues>({
resolver: zodResolver(formSchema),
defaultValues: {
id: generateId(),
hidden: data?.item?.hidden ?? false,
icon: data?.item?.icon ?? "acorn",
name: data?.item?.name ?? "",
keywords: data?.item?.keywords ?? [],
},
});
const form = useForm<FormValues>({
resolver: zodResolver(formSchema),
defaultValues: {
id: generateId(),
hidden: data?.item?.hidden ?? false,
icon: data?.item?.icon ?? "acorn",
name: data?.item?.name ?? "",
keywords: data?.item?.keywords ?? [],
},
});
const onSubmit = (formData: FormValues) => {
updateResumeData((draft) => {
if (data?.customSectionId) {
const section = draft.customSections.find((s) => s.id === data.customSectionId);
if (section) section.items.push(formData);
} else {
draft.sections.interests.items.push(formData);
}
});
closeDialog();
};
const onSubmit = (formData: FormValues) => {
updateResumeData((draft) => {
if (data?.customSectionId) {
const section = draft.customSections.find((s) => s.id === data.customSectionId);
if (section) section.items.push(formData);
} else {
draft.sections.interests.items.push(formData);
}
});
closeDialog();
};
const { blockEvents, requestClose } = useFormBlocker(form);
const { blockEvents, requestClose } = useFormBlocker(form);
return (
<DialogContent {...blockEvents}>
<DialogHeader>
<DialogTitle className="flex items-center gap-x-2">
<PlusIcon />
<Trans>Create a new interest</Trans>
</DialogTitle>
<DialogDescription />
</DialogHeader>
return (
<DialogContent {...blockEvents}>
<DialogHeader>
<DialogTitle className="flex items-center gap-x-2">
<PlusIcon />
<Trans>Create a new interest</Trans>
</DialogTitle>
<DialogDescription />
</DialogHeader>
<Form {...form}>
<form className="grid gap-4 sm:grid-cols-2" onSubmit={form.handleSubmit(onSubmit)}>
<InterestForm />
<Form {...form}>
<form className="grid gap-4 sm:grid-cols-2" onSubmit={form.handleSubmit(onSubmit)}>
<InterestForm />
<DialogFooter className="sm:col-span-full">
<Button variant="ghost" onClick={requestClose}>
<Trans>Cancel</Trans>
</Button>
<DialogFooter className="sm:col-span-full">
<Button variant="ghost" onClick={requestClose}>
<Trans>Cancel</Trans>
</Button>
<Button type="submit" disabled={form.formState.isSubmitting}>
<Trans>Create</Trans>
</Button>
</DialogFooter>
</form>
</Form>
</DialogContent>
);
<Button type="submit" disabled={form.formState.isSubmitting}>
<Trans>Create</Trans>
</Button>
</DialogFooter>
</form>
</Form>
</DialogContent>
);
}
export function UpdateInterestDialog({ data }: DialogProps<"resume.sections.interests.update">) {
const closeDialog = useDialogStore((state) => state.closeDialog);
const updateResumeData = useResumeStore((state) => state.updateResumeData);
const closeDialog = useDialogStore((state) => state.closeDialog);
const updateResumeData = useResumeStore((state) => state.updateResumeData);
const form = useForm<FormValues>({
resolver: zodResolver(formSchema),
defaultValues: {
id: data.item.id,
hidden: data.item.hidden,
icon: data.item.icon,
name: data.item.name,
keywords: data.item.keywords,
},
});
const form = useForm<FormValues>({
resolver: zodResolver(formSchema),
defaultValues: {
id: data.item.id,
hidden: data.item.hidden,
icon: data.item.icon,
name: data.item.name,
keywords: data.item.keywords,
},
});
const onSubmit = (formData: FormValues) => {
updateResumeData((draft) => {
if (data?.customSectionId) {
const section = draft.customSections.find((s) => s.id === data.customSectionId);
if (!section) return;
const index = section.items.findIndex((item) => item.id === formData.id);
if (index !== -1) section.items[index] = formData;
} else {
const index = draft.sections.interests.items.findIndex((item) => item.id === formData.id);
if (index !== -1) draft.sections.interests.items[index] = formData;
}
});
closeDialog();
};
const onSubmit = (formData: FormValues) => {
updateResumeData((draft) => {
if (data?.customSectionId) {
const section = draft.customSections.find((s) => s.id === data.customSectionId);
if (!section) return;
const index = section.items.findIndex((item) => item.id === formData.id);
if (index !== -1) section.items[index] = formData;
} else {
const index = draft.sections.interests.items.findIndex((item) => item.id === formData.id);
if (index !== -1) draft.sections.interests.items[index] = formData;
}
});
closeDialog();
};
const { blockEvents, requestClose } = useFormBlocker(form);
const { blockEvents, requestClose } = useFormBlocker(form);
return (
<DialogContent {...blockEvents}>
<DialogHeader>
<DialogTitle className="flex items-center gap-x-2">
<PencilSimpleLineIcon />
<Trans>Update an existing interest</Trans>
</DialogTitle>
<DialogDescription />
</DialogHeader>
return (
<DialogContent {...blockEvents}>
<DialogHeader>
<DialogTitle className="flex items-center gap-x-2">
<PencilSimpleLineIcon />
<Trans>Update an existing interest</Trans>
</DialogTitle>
<DialogDescription />
</DialogHeader>
<Form {...form}>
<form className="grid gap-4 sm:grid-cols-2" onSubmit={form.handleSubmit(onSubmit)}>
<InterestForm />
<Form {...form}>
<form className="grid gap-4 sm:grid-cols-2" onSubmit={form.handleSubmit(onSubmit)}>
<InterestForm />
<DialogFooter className="sm:col-span-full">
<Button variant="ghost" onClick={requestClose}>
<Trans>Cancel</Trans>
</Button>
<DialogFooter className="sm:col-span-full">
<Button variant="ghost" onClick={requestClose}>
<Trans>Cancel</Trans>
</Button>
<Button type="submit" disabled={form.formState.isSubmitting}>
<Trans>Save Changes</Trans>
</Button>
</DialogFooter>
</form>
</Form>
</DialogContent>
);
<Button type="submit" disabled={form.formState.isSubmitting}>
<Trans>Save Changes</Trans>
</Button>
</DialogFooter>
</form>
</Form>
</DialogContent>
);
}
function InterestForm() {
const form = useFormContext<FormValues>();
const nameState = useFormState({ control: form.control, name: "name" });
const form = useFormContext<FormValues>();
const nameState = useFormState({ control: form.control, name: "name" });
const isNameInvalid = useMemo(() => {
return nameState.errors && Object.keys(nameState.errors).length > 0;
}, [nameState]);
const isNameInvalid = useMemo(() => {
return nameState.errors && Object.keys(nameState.errors).length > 0;
}, [nameState]);
return (
<>
<div className={cn("col-span-full flex items-end", isNameInvalid && "items-center")}>
<FormField
control={form.control}
name={"icon"}
render={({ field }) => (
<FormItem className="shrink-0">
<FormControl
render={
<IconPicker {...field} popoverProps={{ modal: true }} className="rounded-r-none! border-e-0!" />
}
/>
</FormItem>
)}
/>
return (
<>
<div className={cn("col-span-full flex items-end", isNameInvalid && "items-center")}>
<FormField
control={form.control}
name={"icon"}
render={({ field }) => (
<FormItem className="shrink-0">
<FormControl
render={
<IconPicker {...field} popoverProps={{ modal: true }} className="rounded-r-none! border-e-0!" />
}
/>
</FormItem>
)}
/>
<FormField
control={form.control}
name="name"
render={({ field }) => (
<FormItem className="flex-1">
<FormLabel>
<Trans>Name</Trans>
</FormLabel>
<FormControl render={<Input className="rounded-l-none!" {...field} />} />
<FormMessage />
</FormItem>
)}
/>
</div>
<FormField
control={form.control}
name="name"
render={({ field }) => (
<FormItem className="flex-1">
<FormLabel>
<Trans>Name</Trans>
</FormLabel>
<FormControl render={<Input className="rounded-l-none!" {...field} />} />
<FormMessage />
</FormItem>
)}
/>
</div>
<FormField
control={form.control}
name="keywords"
render={({ field }) => (
<FormItem className="col-span-full">
<FormLabel>
<Trans>Keywords</Trans>
</FormLabel>
<FormControl render={<ChipInput {...field} />} />
<FormMessage />
</FormItem>
)}
/>
</>
);
<FormField
control={form.control}
name="keywords"
render={({ field }) => (
<FormItem className="col-span-full">
<FormLabel>
<Trans>Keywords</Trans>
</FormLabel>
<FormControl render={<ChipInput {...field} />} />
<FormMessage />
</FormItem>
)}
/>
</>
);
}
+154 -154
View File
@@ -24,181 +24,181 @@ const formSchema = languageItemSchema;
type FormValues = z.infer<typeof formSchema>;
export function CreateLanguageDialog({ data }: DialogProps<"resume.sections.languages.create">) {
const closeDialog = useDialogStore((state) => state.closeDialog);
const updateResumeData = useResumeStore((state) => state.updateResumeData);
const closeDialog = useDialogStore((state) => state.closeDialog);
const updateResumeData = useResumeStore((state) => state.updateResumeData);
const form = useForm<FormValues>({
resolver: zodResolver(formSchema),
defaultValues: {
id: generateId(),
hidden: data?.item?.hidden ?? false,
language: data?.item?.language ?? "",
fluency: data?.item?.fluency ?? "",
level: data?.item?.level ?? 0,
},
});
const form = useForm<FormValues>({
resolver: zodResolver(formSchema),
defaultValues: {
id: generateId(),
hidden: data?.item?.hidden ?? false,
language: data?.item?.language ?? "",
fluency: data?.item?.fluency ?? "",
level: data?.item?.level ?? 0,
},
});
const onSubmit = (formData: FormValues) => {
updateResumeData((draft) => {
if (data?.customSectionId) {
const section = draft.customSections.find((s) => s.id === data.customSectionId);
if (section) section.items.push(formData);
} else {
draft.sections.languages.items.push(formData);
}
});
closeDialog();
};
const onSubmit = (formData: FormValues) => {
updateResumeData((draft) => {
if (data?.customSectionId) {
const section = draft.customSections.find((s) => s.id === data.customSectionId);
if (section) section.items.push(formData);
} else {
draft.sections.languages.items.push(formData);
}
});
closeDialog();
};
const { blockEvents, requestClose } = useFormBlocker(form);
const { blockEvents, requestClose } = useFormBlocker(form);
return (
<DialogContent {...blockEvents}>
<DialogHeader>
<DialogTitle className="flex items-center gap-x-2">
<PlusIcon />
<Trans>Create a new language</Trans>
</DialogTitle>
<DialogDescription />
</DialogHeader>
return (
<DialogContent {...blockEvents}>
<DialogHeader>
<DialogTitle className="flex items-center gap-x-2">
<PlusIcon />
<Trans>Create a new language</Trans>
</DialogTitle>
<DialogDescription />
</DialogHeader>
<Form {...form}>
<form className="grid gap-4 sm:grid-cols-2" onSubmit={form.handleSubmit(onSubmit)}>
<LanguageForm />
<Form {...form}>
<form className="grid gap-4 sm:grid-cols-2" onSubmit={form.handleSubmit(onSubmit)}>
<LanguageForm />
<DialogFooter className="sm:col-span-full">
<Button variant="ghost" onClick={requestClose}>
<Trans>Cancel</Trans>
</Button>
<DialogFooter className="sm:col-span-full">
<Button variant="ghost" onClick={requestClose}>
<Trans>Cancel</Trans>
</Button>
<Button type="submit" disabled={form.formState.isSubmitting}>
<Trans>Create</Trans>
</Button>
</DialogFooter>
</form>
</Form>
</DialogContent>
);
<Button type="submit" disabled={form.formState.isSubmitting}>
<Trans>Create</Trans>
</Button>
</DialogFooter>
</form>
</Form>
</DialogContent>
);
}
export function UpdateLanguageDialog({ data }: DialogProps<"resume.sections.languages.update">) {
const closeDialog = useDialogStore((state) => state.closeDialog);
const updateResumeData = useResumeStore((state) => state.updateResumeData);
const closeDialog = useDialogStore((state) => state.closeDialog);
const updateResumeData = useResumeStore((state) => state.updateResumeData);
const form = useForm<FormValues>({
resolver: zodResolver(formSchema),
defaultValues: {
id: data.item.id,
hidden: data.item.hidden,
language: data.item.language,
fluency: data.item.fluency,
level: data.item.level,
},
});
const form = useForm<FormValues>({
resolver: zodResolver(formSchema),
defaultValues: {
id: data.item.id,
hidden: data.item.hidden,
language: data.item.language,
fluency: data.item.fluency,
level: data.item.level,
},
});
const onSubmit = (formData: FormValues) => {
updateResumeData((draft) => {
if (data?.customSectionId) {
const section = draft.customSections.find((s) => s.id === data.customSectionId);
if (!section) return;
const index = section.items.findIndex((item) => item.id === formData.id);
if (index !== -1) section.items[index] = formData;
} else {
const index = draft.sections.languages.items.findIndex((item) => item.id === formData.id);
if (index !== -1) draft.sections.languages.items[index] = formData;
}
});
closeDialog();
};
const onSubmit = (formData: FormValues) => {
updateResumeData((draft) => {
if (data?.customSectionId) {
const section = draft.customSections.find((s) => s.id === data.customSectionId);
if (!section) return;
const index = section.items.findIndex((item) => item.id === formData.id);
if (index !== -1) section.items[index] = formData;
} else {
const index = draft.sections.languages.items.findIndex((item) => item.id === formData.id);
if (index !== -1) draft.sections.languages.items[index] = formData;
}
});
closeDialog();
};
const { blockEvents, requestClose } = useFormBlocker(form);
const { blockEvents, requestClose } = useFormBlocker(form);
return (
<DialogContent {...blockEvents}>
<DialogHeader>
<DialogTitle className="flex items-center gap-x-2">
<PencilSimpleLineIcon />
<Trans>Update an existing language</Trans>
</DialogTitle>
<DialogDescription />
</DialogHeader>
return (
<DialogContent {...blockEvents}>
<DialogHeader>
<DialogTitle className="flex items-center gap-x-2">
<PencilSimpleLineIcon />
<Trans>Update an existing language</Trans>
</DialogTitle>
<DialogDescription />
</DialogHeader>
<Form {...form}>
<form className="grid gap-4 sm:grid-cols-2" onSubmit={form.handleSubmit(onSubmit)}>
<LanguageForm />
<Form {...form}>
<form className="grid gap-4 sm:grid-cols-2" onSubmit={form.handleSubmit(onSubmit)}>
<LanguageForm />
<DialogFooter className="sm:col-span-full">
<Button variant="ghost" onClick={requestClose}>
<Trans>Cancel</Trans>
</Button>
<DialogFooter className="sm:col-span-full">
<Button variant="ghost" onClick={requestClose}>
<Trans>Cancel</Trans>
</Button>
<Button type="submit" disabled={form.formState.isSubmitting}>
<Trans>Save Changes</Trans>
</Button>
</DialogFooter>
</form>
</Form>
</DialogContent>
);
<Button type="submit" disabled={form.formState.isSubmitting}>
<Trans>Save Changes</Trans>
</Button>
</DialogFooter>
</form>
</Form>
</DialogContent>
);
}
function LanguageForm() {
const form = useFormContext<FormValues>();
const form = useFormContext<FormValues>();
return (
<>
<FormField
control={form.control}
name="language"
render={({ field }) => (
<FormItem>
<FormLabel>
<Trans>Language</Trans>
</FormLabel>
<FormControl render={<Input {...field} />} />
<FormMessage />
</FormItem>
)}
/>
return (
<>
<FormField
control={form.control}
name="language"
render={({ field }) => (
<FormItem>
<FormLabel>
<Trans>Language</Trans>
</FormLabel>
<FormControl render={<Input {...field} />} />
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="fluency"
render={({ field }) => (
<FormItem>
<FormLabel>
<Trans>Fluency</Trans>
</FormLabel>
<FormControl render={<Input {...field} />} />
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="fluency"
render={({ field }) => (
<FormItem>
<FormLabel>
<Trans>Fluency</Trans>
</FormLabel>
<FormControl render={<Input {...field} />} />
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="level"
render={({ field }) => (
<FormItem className="gap-4 sm:col-span-full">
<FormLabel>
<Trans>Level</Trans>
</FormLabel>
<FormControl
render={
<Slider
min={0}
max={5}
step={1}
value={[field.value]}
onValueChange={(value) => field.onChange(Array.isArray(value) ? value[0] : value)}
/>
}
/>
<FormMessage />
<FormDescription>{Number(field.value) === 0 ? t`Hidden` : `${field.value} / 5`}</FormDescription>
</FormItem>
)}
/>
</>
);
<FormField
control={form.control}
name="level"
render={({ field }) => (
<FormItem className="gap-4 sm:col-span-full">
<FormLabel>
<Trans>Level</Trans>
</FormLabel>
<FormControl
render={
<Slider
min={0}
max={5}
step={1}
value={[field.value]}
onValueChange={(value) => field.onChange(Array.isArray(value) ? value[0] : value)}
/>
}
/>
<FormMessage />
<FormDescription>{Number(field.value) === 0 ? t`Hidden` : `${field.value} / 5`}</FormDescription>
</FormItem>
)}
/>
</>
);
}
+190 -190
View File
@@ -26,221 +26,221 @@ const formSchema = profileItemSchema;
type FormValues = z.infer<typeof formSchema>;
export function CreateProfileDialog({ data }: DialogProps<"resume.sections.profiles.create">) {
const closeDialog = useDialogStore((state) => state.closeDialog);
const updateResumeData = useResumeStore((state) => state.updateResumeData);
const closeDialog = useDialogStore((state) => state.closeDialog);
const updateResumeData = useResumeStore((state) => state.updateResumeData);
const form = useForm<FormValues>({
resolver: zodResolver(formSchema),
defaultValues: {
id: generateId(),
hidden: data?.item?.hidden ?? false,
options: data?.item?.options ?? { showLinkInTitle: false },
icon: data?.item?.icon ?? "acorn",
network: data?.item?.network ?? "",
username: data?.item?.username ?? "",
website: data?.item?.website ?? { url: "", label: "" },
},
});
const form = useForm<FormValues>({
resolver: zodResolver(formSchema),
defaultValues: {
id: generateId(),
hidden: data?.item?.hidden ?? false,
options: data?.item?.options ?? { showLinkInTitle: false },
icon: data?.item?.icon ?? "acorn",
network: data?.item?.network ?? "",
username: data?.item?.username ?? "",
website: data?.item?.website ?? { url: "", label: "" },
},
});
const onSubmit = (formData: FormValues) => {
updateResumeData((draft) => {
if (data?.customSectionId) {
const section = draft.customSections.find((s) => s.id === data.customSectionId);
if (section) section.items.push(formData);
} else {
draft.sections.profiles.items.push(formData);
}
});
closeDialog();
};
const onSubmit = (formData: FormValues) => {
updateResumeData((draft) => {
if (data?.customSectionId) {
const section = draft.customSections.find((s) => s.id === data.customSectionId);
if (section) section.items.push(formData);
} else {
draft.sections.profiles.items.push(formData);
}
});
closeDialog();
};
const { blockEvents, requestClose } = useFormBlocker(form);
const { blockEvents, requestClose } = useFormBlocker(form);
return (
<DialogContent {...blockEvents}>
<DialogHeader>
<DialogTitle className="flex items-center gap-x-2">
<PlusIcon />
<Trans>Create a new profile</Trans>
</DialogTitle>
<DialogDescription />
</DialogHeader>
return (
<DialogContent {...blockEvents}>
<DialogHeader>
<DialogTitle className="flex items-center gap-x-2">
<PlusIcon />
<Trans>Create a new profile</Trans>
</DialogTitle>
<DialogDescription />
</DialogHeader>
<Form {...form}>
<form className="grid gap-4 sm:grid-cols-2" onSubmit={form.handleSubmit(onSubmit)}>
<ProfileForm />
<Form {...form}>
<form className="grid gap-4 sm:grid-cols-2" onSubmit={form.handleSubmit(onSubmit)}>
<ProfileForm />
<DialogFooter className="sm:col-span-full">
<Button variant="ghost" onClick={requestClose}>
<Trans>Cancel</Trans>
</Button>
<DialogFooter className="sm:col-span-full">
<Button variant="ghost" onClick={requestClose}>
<Trans>Cancel</Trans>
</Button>
<Button type="submit" disabled={form.formState.isSubmitting}>
<Trans>Create</Trans>
</Button>
</DialogFooter>
</form>
</Form>
</DialogContent>
);
<Button type="submit" disabled={form.formState.isSubmitting}>
<Trans>Create</Trans>
</Button>
</DialogFooter>
</form>
</Form>
</DialogContent>
);
}
export function UpdateProfileDialog({ data }: DialogProps<"resume.sections.profiles.update">) {
const closeDialog = useDialogStore((state) => state.closeDialog);
const updateResumeData = useResumeStore((state) => state.updateResumeData);
const closeDialog = useDialogStore((state) => state.closeDialog);
const updateResumeData = useResumeStore((state) => state.updateResumeData);
const form = useForm<FormValues>({
resolver: zodResolver(formSchema),
defaultValues: {
id: data.item.id,
hidden: data.item.hidden,
options: data.item.options ?? { showLinkInTitle: false },
icon: data.item.icon,
network: data.item.network,
username: data.item.username,
website: data.item.website,
},
});
const form = useForm<FormValues>({
resolver: zodResolver(formSchema),
defaultValues: {
id: data.item.id,
hidden: data.item.hidden,
options: data.item.options ?? { showLinkInTitle: false },
icon: data.item.icon,
network: data.item.network,
username: data.item.username,
website: data.item.website,
},
});
const onSubmit = (formData: FormValues) => {
updateResumeData((draft) => {
if (data?.customSectionId) {
const section = draft.customSections.find((s) => s.id === data.customSectionId);
if (!section) return;
const index = section.items.findIndex((item) => item.id === formData.id);
if (index !== -1) section.items[index] = formData;
} else {
const index = draft.sections.profiles.items.findIndex((item) => item.id === formData.id);
if (index !== -1) draft.sections.profiles.items[index] = formData;
}
});
closeDialog();
};
const onSubmit = (formData: FormValues) => {
updateResumeData((draft) => {
if (data?.customSectionId) {
const section = draft.customSections.find((s) => s.id === data.customSectionId);
if (!section) return;
const index = section.items.findIndex((item) => item.id === formData.id);
if (index !== -1) section.items[index] = formData;
} else {
const index = draft.sections.profiles.items.findIndex((item) => item.id === formData.id);
if (index !== -1) draft.sections.profiles.items[index] = formData;
}
});
closeDialog();
};
const { blockEvents, requestClose } = useFormBlocker(form);
const { blockEvents, requestClose } = useFormBlocker(form);
return (
<DialogContent {...blockEvents}>
<DialogHeader>
<DialogTitle className="flex items-center gap-x-2">
<PencilSimpleLineIcon />
<Trans>Update an existing profile</Trans>
</DialogTitle>
<DialogDescription />
</DialogHeader>
return (
<DialogContent {...blockEvents}>
<DialogHeader>
<DialogTitle className="flex items-center gap-x-2">
<PencilSimpleLineIcon />
<Trans>Update an existing profile</Trans>
</DialogTitle>
<DialogDescription />
</DialogHeader>
<Form {...form}>
<form className="grid gap-4 sm:grid-cols-2" onSubmit={form.handleSubmit(onSubmit)}>
<ProfileForm />
<Form {...form}>
<form className="grid gap-4 sm:grid-cols-2" onSubmit={form.handleSubmit(onSubmit)}>
<ProfileForm />
<DialogFooter className="sm:col-span-full">
<Button variant="ghost" onClick={requestClose}>
<Trans>Cancel</Trans>
</Button>
<DialogFooter className="sm:col-span-full">
<Button variant="ghost" onClick={requestClose}>
<Trans>Cancel</Trans>
</Button>
<Button type="submit" disabled={form.formState.isSubmitting}>
<Trans>Save Changes</Trans>
</Button>
</DialogFooter>
</form>
</Form>
</DialogContent>
);
<Button type="submit" disabled={form.formState.isSubmitting}>
<Trans>Save Changes</Trans>
</Button>
</DialogFooter>
</form>
</Form>
</DialogContent>
);
}
function ProfileForm() {
const form = useFormContext<FormValues>();
const networkState = useFormState({ control: form.control, name: "network" });
const form = useFormContext<FormValues>();
const networkState = useFormState({ control: form.control, name: "network" });
const isNetworkInvalid = useMemo(() => {
return networkState.errors && Object.keys(networkState.errors).length > 0;
}, [networkState]);
const isNetworkInvalid = useMemo(() => {
return networkState.errors && Object.keys(networkState.errors).length > 0;
}, [networkState]);
return (
<>
<div className={cn("flex items-end", isNetworkInvalid && "items-center")}>
<FormField
control={form.control}
name={"icon"}
render={({ field }) => (
<FormItem className="shrink-0">
<FormControl
render={
<IconPicker {...field} popoverProps={{ modal: true }} className="rounded-r-none! border-e-0!" />
}
/>
</FormItem>
)}
/>
return (
<>
<div className={cn("flex items-end", isNetworkInvalid && "items-center")}>
<FormField
control={form.control}
name={"icon"}
render={({ field }) => (
<FormItem className="shrink-0">
<FormControl
render={
<IconPicker {...field} popoverProps={{ modal: true }} className="rounded-r-none! border-e-0!" />
}
/>
</FormItem>
)}
/>
<FormField
control={form.control}
name="network"
render={({ field }) => (
<FormItem className="flex-1">
<FormLabel>
<Trans>Network</Trans>
</FormLabel>
<FormControl render={<Input className="rounded-l-none!" {...field} />} />
<FormMessage />
</FormItem>
)}
/>
</div>
<FormField
control={form.control}
name="network"
render={({ field }) => (
<FormItem className="flex-1">
<FormLabel>
<Trans>Network</Trans>
</FormLabel>
<FormControl render={<Input className="rounded-l-none!" {...field} />} />
<FormMessage />
</FormItem>
)}
/>
</div>
<FormField
control={form.control}
name="username"
render={({ field }) => (
<FormItem>
<FormLabel>
<Trans>Username</Trans>
</FormLabel>
<InputGroup>
<InputGroupAddon align="inline-start">
<InputGroupText>
<AtIcon />
</InputGroupText>
</InputGroupAddon>
<FormField
control={form.control}
name="username"
render={({ field }) => (
<FormItem>
<FormLabel>
<Trans>Username</Trans>
</FormLabel>
<InputGroup>
<InputGroupAddon align="inline-start">
<InputGroupText>
<AtIcon />
</InputGroupText>
</InputGroupAddon>
<FormControl render={<InputGroupInput {...field} />} />
</InputGroup>
<FormMessage />
</FormItem>
)}
/>
<FormControl render={<InputGroupInput {...field} />} />
</InputGroup>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="website"
render={({ field }) => (
<FormItem className="sm:col-span-full">
<FormLabel>
<Trans>Website</Trans>
</FormLabel>
<URLInput
{...field}
value={field.value}
onChange={field.onChange}
hideLabelButton={form.watch("options.showLinkInTitle")}
/>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="website"
render={({ field }) => (
<FormItem className="sm:col-span-full">
<FormLabel>
<Trans>Website</Trans>
</FormLabel>
<URLInput
{...field}
value={field.value}
onChange={field.onChange}
hideLabelButton={form.watch("options.showLinkInTitle")}
/>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="options.showLinkInTitle"
render={({ field }) => (
<FormItem className="flex items-center gap-x-2 sm:col-span-full">
<FormControl render={<Switch checked={field.value} onCheckedChange={field.onChange} />} />
<FormLabel className="mt-0!">
<Trans>Show link in title</Trans>
</FormLabel>
</FormItem>
)}
/>
</>
);
<FormField
control={form.control}
name="options.showLinkInTitle"
render={({ field }) => (
<FormItem className="flex items-center gap-x-2 sm:col-span-full">
<FormControl render={<Switch checked={field.value} onCheckedChange={field.onChange} />} />
<FormLabel className="mt-0!">
<Trans>Show link in title</Trans>
</FormLabel>
</FormItem>
)}
/>
</>
);
}
+177 -177
View File
@@ -25,206 +25,206 @@ const formSchema = projectItemSchema;
type FormValues = z.infer<typeof formSchema>;
export function CreateProjectDialog({ data }: DialogProps<"resume.sections.projects.create">) {
const closeDialog = useDialogStore((state) => state.closeDialog);
const updateResumeData = useResumeStore((state) => state.updateResumeData);
const closeDialog = useDialogStore((state) => state.closeDialog);
const updateResumeData = useResumeStore((state) => state.updateResumeData);
const form = useForm<FormValues>({
resolver: zodResolver(formSchema),
defaultValues: {
id: generateId(),
hidden: data?.item?.hidden ?? false,
options: data?.item?.options ?? { showLinkInTitle: false },
name: data?.item?.name ?? "",
period: data?.item?.period ?? "",
website: data?.item?.website ?? { url: "", label: "" },
description: data?.item?.description ?? "",
},
});
const form = useForm<FormValues>({
resolver: zodResolver(formSchema),
defaultValues: {
id: generateId(),
hidden: data?.item?.hidden ?? false,
options: data?.item?.options ?? { showLinkInTitle: false },
name: data?.item?.name ?? "",
period: data?.item?.period ?? "",
website: data?.item?.website ?? { url: "", label: "" },
description: data?.item?.description ?? "",
},
});
const onSubmit = (formData: FormValues) => {
updateResumeData((draft) => {
if (data?.customSectionId) {
const section = draft.customSections.find((s) => s.id === data.customSectionId);
if (section) section.items.push(formData);
} else {
draft.sections.projects.items.push(formData);
}
});
closeDialog();
};
const onSubmit = (formData: FormValues) => {
updateResumeData((draft) => {
if (data?.customSectionId) {
const section = draft.customSections.find((s) => s.id === data.customSectionId);
if (section) section.items.push(formData);
} else {
draft.sections.projects.items.push(formData);
}
});
closeDialog();
};
const { blockEvents, requestClose } = useFormBlocker(form);
const { blockEvents, requestClose } = useFormBlocker(form);
return (
<DialogContent {...blockEvents}>
<DialogHeader>
<DialogTitle className="flex items-center gap-x-2">
<PlusIcon />
<Trans>Create a new project</Trans>
</DialogTitle>
<DialogDescription />
</DialogHeader>
return (
<DialogContent {...blockEvents}>
<DialogHeader>
<DialogTitle className="flex items-center gap-x-2">
<PlusIcon />
<Trans>Create a new project</Trans>
</DialogTitle>
<DialogDescription />
</DialogHeader>
<Form {...form}>
<form className="grid gap-4 sm:grid-cols-2" onSubmit={form.handleSubmit(onSubmit)}>
<ProjectForm />
<Form {...form}>
<form className="grid gap-4 sm:grid-cols-2" onSubmit={form.handleSubmit(onSubmit)}>
<ProjectForm />
<DialogFooter className="sm:col-span-full">
<Button variant="ghost" onClick={requestClose}>
<Trans>Cancel</Trans>
</Button>
<DialogFooter className="sm:col-span-full">
<Button variant="ghost" onClick={requestClose}>
<Trans>Cancel</Trans>
</Button>
<Button type="submit" disabled={form.formState.isSubmitting}>
<Trans>Create</Trans>
</Button>
</DialogFooter>
</form>
</Form>
</DialogContent>
);
<Button type="submit" disabled={form.formState.isSubmitting}>
<Trans>Create</Trans>
</Button>
</DialogFooter>
</form>
</Form>
</DialogContent>
);
}
export function UpdateProjectDialog({ data }: DialogProps<"resume.sections.projects.update">) {
const closeDialog = useDialogStore((state) => state.closeDialog);
const updateResumeData = useResumeStore((state) => state.updateResumeData);
const closeDialog = useDialogStore((state) => state.closeDialog);
const updateResumeData = useResumeStore((state) => state.updateResumeData);
const form = useForm<FormValues>({
resolver: zodResolver(formSchema),
defaultValues: {
id: data.item.id,
hidden: data.item.hidden,
options: data.item.options ?? { showLinkInTitle: false },
name: data.item.name,
period: data.item.period,
website: data.item.website,
description: data.item.description,
},
});
const form = useForm<FormValues>({
resolver: zodResolver(formSchema),
defaultValues: {
id: data.item.id,
hidden: data.item.hidden,
options: data.item.options ?? { showLinkInTitle: false },
name: data.item.name,
period: data.item.period,
website: data.item.website,
description: data.item.description,
},
});
const onSubmit = (formData: FormValues) => {
updateResumeData((draft) => {
if (data?.customSectionId) {
const section = draft.customSections.find((s) => s.id === data.customSectionId);
if (!section) return;
const index = section.items.findIndex((item) => item.id === formData.id);
if (index !== -1) section.items[index] = formData;
} else {
const index = draft.sections.projects.items.findIndex((item) => item.id === formData.id);
if (index !== -1) draft.sections.projects.items[index] = formData;
}
});
closeDialog();
};
const onSubmit = (formData: FormValues) => {
updateResumeData((draft) => {
if (data?.customSectionId) {
const section = draft.customSections.find((s) => s.id === data.customSectionId);
if (!section) return;
const index = section.items.findIndex((item) => item.id === formData.id);
if (index !== -1) section.items[index] = formData;
} else {
const index = draft.sections.projects.items.findIndex((item) => item.id === formData.id);
if (index !== -1) draft.sections.projects.items[index] = formData;
}
});
closeDialog();
};
const { blockEvents, requestClose } = useFormBlocker(form);
const { blockEvents, requestClose } = useFormBlocker(form);
return (
<DialogContent {...blockEvents}>
<DialogHeader>
<DialogTitle className="flex items-center gap-x-2">
<PencilSimpleLineIcon />
<Trans>Update an existing project</Trans>
</DialogTitle>
<DialogDescription />
</DialogHeader>
return (
<DialogContent {...blockEvents}>
<DialogHeader>
<DialogTitle className="flex items-center gap-x-2">
<PencilSimpleLineIcon />
<Trans>Update an existing project</Trans>
</DialogTitle>
<DialogDescription />
</DialogHeader>
<Form {...form}>
<form className="grid gap-4 sm:grid-cols-2" onSubmit={form.handleSubmit(onSubmit)}>
<ProjectForm />
<Form {...form}>
<form className="grid gap-4 sm:grid-cols-2" onSubmit={form.handleSubmit(onSubmit)}>
<ProjectForm />
<DialogFooter className="sm:col-span-full">
<Button variant="ghost" onClick={requestClose}>
<Trans>Cancel</Trans>
</Button>
<DialogFooter className="sm:col-span-full">
<Button variant="ghost" onClick={requestClose}>
<Trans>Cancel</Trans>
</Button>
<Button type="submit" disabled={form.formState.isSubmitting}>
<Trans>Save Changes</Trans>
</Button>
</DialogFooter>
</form>
</Form>
</DialogContent>
);
<Button type="submit" disabled={form.formState.isSubmitting}>
<Trans>Save Changes</Trans>
</Button>
</DialogFooter>
</form>
</Form>
</DialogContent>
);
}
function ProjectForm() {
const form = useFormContext<FormValues>();
const form = useFormContext<FormValues>();
return (
<>
<FormField
control={form.control}
name="name"
render={({ field }) => (
<FormItem>
<FormLabel>
<Trans>Name</Trans>
</FormLabel>
<FormControl render={<Input {...field} />} />
<FormMessage />
</FormItem>
)}
/>
return (
<>
<FormField
control={form.control}
name="name"
render={({ field }) => (
<FormItem>
<FormLabel>
<Trans>Name</Trans>
</FormLabel>
<FormControl render={<Input {...field} />} />
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="period"
render={({ field }) => (
<FormItem>
<FormLabel>
<Trans>Period</Trans>
</FormLabel>
<FormControl render={<Input {...field} />} />
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="period"
render={({ field }) => (
<FormItem>
<FormLabel>
<Trans>Period</Trans>
</FormLabel>
<FormControl render={<Input {...field} />} />
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="website"
render={({ field }) => (
<FormItem className="sm:col-span-full">
<FormLabel>
<Trans>Website</Trans>
</FormLabel>
<URLInput
{...field}
value={field.value}
onChange={field.onChange}
hideLabelButton={form.watch("options.showLinkInTitle")}
/>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="website"
render={({ field }) => (
<FormItem className="sm:col-span-full">
<FormLabel>
<Trans>Website</Trans>
</FormLabel>
<URLInput
{...field}
value={field.value}
onChange={field.onChange}
hideLabelButton={form.watch("options.showLinkInTitle")}
/>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="options.showLinkInTitle"
render={({ field }) => (
<FormItem className="flex items-center gap-x-2 sm:col-span-full">
<FormControl render={<Switch checked={field.value} onCheckedChange={field.onChange} />} />
<FormLabel className="mt-0!">
<Trans>Show link in title</Trans>
</FormLabel>
</FormItem>
)}
/>
<FormField
control={form.control}
name="options.showLinkInTitle"
render={({ field }) => (
<FormItem className="flex items-center gap-x-2 sm:col-span-full">
<FormControl render={<Switch checked={field.value} onCheckedChange={field.onChange} />} />
<FormLabel className="mt-0!">
<Trans>Show link in title</Trans>
</FormLabel>
</FormItem>
)}
/>
<FormField
control={form.control}
name="description"
render={({ field }) => (
<FormItem className="sm:col-span-full">
<FormLabel>
<Trans>Description</Trans>
</FormLabel>
<FormControl render={<RichInput {...field} value={field.value} onChange={field.onChange} />} />
<FormMessage />
</FormItem>
)}
/>
</>
);
<FormField
control={form.control}
name="description"
render={({ field }) => (
<FormItem className="sm:col-span-full">
<FormLabel>
<Trans>Description</Trans>
</FormLabel>
<FormControl render={<RichInput {...field} value={field.value} onChange={field.onChange} />} />
<FormMessage />
</FormItem>
)}
/>
</>
);
}
+192 -192
View File
@@ -25,222 +25,222 @@ const formSchema = publicationItemSchema;
type FormValues = z.infer<typeof formSchema>;
export function CreatePublicationDialog({ data }: DialogProps<"resume.sections.publications.create">) {
const closeDialog = useDialogStore((state) => state.closeDialog);
const updateResumeData = useResumeStore((state) => state.updateResumeData);
const closeDialog = useDialogStore((state) => state.closeDialog);
const updateResumeData = useResumeStore((state) => state.updateResumeData);
const form = useForm<FormValues>({
resolver: zodResolver(formSchema),
defaultValues: {
id: generateId(),
hidden: data?.item?.hidden ?? false,
options: data?.item?.options ?? { showLinkInTitle: false },
title: data?.item?.title ?? "",
publisher: data?.item?.publisher ?? "",
date: data?.item?.date ?? "",
website: data?.item?.website ?? { url: "", label: "" },
description: data?.item?.description ?? "",
},
});
const form = useForm<FormValues>({
resolver: zodResolver(formSchema),
defaultValues: {
id: generateId(),
hidden: data?.item?.hidden ?? false,
options: data?.item?.options ?? { showLinkInTitle: false },
title: data?.item?.title ?? "",
publisher: data?.item?.publisher ?? "",
date: data?.item?.date ?? "",
website: data?.item?.website ?? { url: "", label: "" },
description: data?.item?.description ?? "",
},
});
const onSubmit = (formData: FormValues) => {
updateResumeData((draft) => {
if (data?.customSectionId) {
const section = draft.customSections.find((s) => s.id === data.customSectionId);
if (section) section.items.push(formData);
} else {
draft.sections.publications.items.push(formData);
}
});
closeDialog();
};
const onSubmit = (formData: FormValues) => {
updateResumeData((draft) => {
if (data?.customSectionId) {
const section = draft.customSections.find((s) => s.id === data.customSectionId);
if (section) section.items.push(formData);
} else {
draft.sections.publications.items.push(formData);
}
});
closeDialog();
};
const { blockEvents, requestClose } = useFormBlocker(form);
const { blockEvents, requestClose } = useFormBlocker(form);
return (
<DialogContent {...blockEvents}>
<DialogHeader>
<DialogTitle className="flex items-center gap-x-2">
<PlusIcon />
<Trans>Create a new publication</Trans>
</DialogTitle>
<DialogDescription />
</DialogHeader>
return (
<DialogContent {...blockEvents}>
<DialogHeader>
<DialogTitle className="flex items-center gap-x-2">
<PlusIcon />
<Trans>Create a new publication</Trans>
</DialogTitle>
<DialogDescription />
</DialogHeader>
<Form {...form}>
<form className="grid gap-4 sm:grid-cols-2" onSubmit={form.handleSubmit(onSubmit)}>
<PublicationForm />
<Form {...form}>
<form className="grid gap-4 sm:grid-cols-2" onSubmit={form.handleSubmit(onSubmit)}>
<PublicationForm />
<DialogFooter className="sm:col-span-full">
<Button variant="ghost" onClick={requestClose}>
<Trans>Cancel</Trans>
</Button>
<DialogFooter className="sm:col-span-full">
<Button variant="ghost" onClick={requestClose}>
<Trans>Cancel</Trans>
</Button>
<Button type="submit" disabled={form.formState.isSubmitting}>
<Trans>Create</Trans>
</Button>
</DialogFooter>
</form>
</Form>
</DialogContent>
);
<Button type="submit" disabled={form.formState.isSubmitting}>
<Trans>Create</Trans>
</Button>
</DialogFooter>
</form>
</Form>
</DialogContent>
);
}
export function UpdatePublicationDialog({ data }: DialogProps<"resume.sections.publications.update">) {
const closeDialog = useDialogStore((state) => state.closeDialog);
const updateResumeData = useResumeStore((state) => state.updateResumeData);
const closeDialog = useDialogStore((state) => state.closeDialog);
const updateResumeData = useResumeStore((state) => state.updateResumeData);
const form = useForm<FormValues>({
resolver: zodResolver(formSchema),
defaultValues: {
id: data.item.id,
hidden: data.item.hidden,
options: data.item.options ?? { showLinkInTitle: false },
title: data.item.title,
publisher: data.item.publisher,
date: data.item.date,
website: data.item.website,
description: data.item.description,
},
});
const form = useForm<FormValues>({
resolver: zodResolver(formSchema),
defaultValues: {
id: data.item.id,
hidden: data.item.hidden,
options: data.item.options ?? { showLinkInTitle: false },
title: data.item.title,
publisher: data.item.publisher,
date: data.item.date,
website: data.item.website,
description: data.item.description,
},
});
const onSubmit = (formData: FormValues) => {
updateResumeData((draft) => {
if (data?.customSectionId) {
const section = draft.customSections.find((s) => s.id === data.customSectionId);
if (!section) return;
const index = section.items.findIndex((item) => item.id === formData.id);
if (index !== -1) section.items[index] = formData;
} else {
const index = draft.sections.publications.items.findIndex((item) => item.id === formData.id);
if (index !== -1) draft.sections.publications.items[index] = formData;
}
});
closeDialog();
};
const onSubmit = (formData: FormValues) => {
updateResumeData((draft) => {
if (data?.customSectionId) {
const section = draft.customSections.find((s) => s.id === data.customSectionId);
if (!section) return;
const index = section.items.findIndex((item) => item.id === formData.id);
if (index !== -1) section.items[index] = formData;
} else {
const index = draft.sections.publications.items.findIndex((item) => item.id === formData.id);
if (index !== -1) draft.sections.publications.items[index] = formData;
}
});
closeDialog();
};
const { blockEvents, requestClose } = useFormBlocker(form);
const { blockEvents, requestClose } = useFormBlocker(form);
return (
<DialogContent {...blockEvents}>
<DialogHeader>
<DialogTitle className="flex items-center gap-x-2">
<PencilSimpleLineIcon />
<Trans>Update an existing publication</Trans>
</DialogTitle>
<DialogDescription />
</DialogHeader>
return (
<DialogContent {...blockEvents}>
<DialogHeader>
<DialogTitle className="flex items-center gap-x-2">
<PencilSimpleLineIcon />
<Trans>Update an existing publication</Trans>
</DialogTitle>
<DialogDescription />
</DialogHeader>
<Form {...form}>
<form className="grid gap-4 sm:grid-cols-2" onSubmit={form.handleSubmit(onSubmit)}>
<PublicationForm />
<Form {...form}>
<form className="grid gap-4 sm:grid-cols-2" onSubmit={form.handleSubmit(onSubmit)}>
<PublicationForm />
<DialogFooter className="sm:col-span-full">
<Button variant="ghost" onClick={requestClose}>
<Trans>Cancel</Trans>
</Button>
<DialogFooter className="sm:col-span-full">
<Button variant="ghost" onClick={requestClose}>
<Trans>Cancel</Trans>
</Button>
<Button type="submit" disabled={form.formState.isSubmitting}>
<Trans>Save Changes</Trans>
</Button>
</DialogFooter>
</form>
</Form>
</DialogContent>
);
<Button type="submit" disabled={form.formState.isSubmitting}>
<Trans>Save Changes</Trans>
</Button>
</DialogFooter>
</form>
</Form>
</DialogContent>
);
}
function PublicationForm() {
const form = useFormContext<FormValues>();
const form = useFormContext<FormValues>();
return (
<>
<FormField
control={form.control}
name="title"
render={({ field }) => (
<FormItem>
<FormLabel>
<Trans>Title</Trans>
</FormLabel>
<FormControl render={<Input {...field} />} />
<FormMessage />
</FormItem>
)}
/>
return (
<>
<FormField
control={form.control}
name="title"
render={({ field }) => (
<FormItem>
<FormLabel>
<Trans>Title</Trans>
</FormLabel>
<FormControl render={<Input {...field} />} />
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="publisher"
render={({ field }) => (
<FormItem>
<FormLabel>
<Trans>Publisher</Trans>
</FormLabel>
<FormControl render={<Input {...field} />} />
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="publisher"
render={({ field }) => (
<FormItem>
<FormLabel>
<Trans>Publisher</Trans>
</FormLabel>
<FormControl render={<Input {...field} />} />
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="date"
render={({ field }) => (
<FormItem>
<FormLabel>
<Trans>Date</Trans>
</FormLabel>
<FormControl render={<Input {...field} />} />
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="date"
render={({ field }) => (
<FormItem>
<FormLabel>
<Trans>Date</Trans>
</FormLabel>
<FormControl render={<Input {...field} />} />
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="website"
render={({ field }) => (
<FormItem>
<FormLabel>
<Trans>Website</Trans>
</FormLabel>
<URLInput
{...field}
value={field.value}
onChange={field.onChange}
hideLabelButton={form.watch("options.showLinkInTitle")}
/>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="website"
render={({ field }) => (
<FormItem>
<FormLabel>
<Trans>Website</Trans>
</FormLabel>
<URLInput
{...field}
value={field.value}
onChange={field.onChange}
hideLabelButton={form.watch("options.showLinkInTitle")}
/>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="options.showLinkInTitle"
render={({ field }) => (
<FormItem className="flex items-center gap-x-2">
<FormControl render={<Switch checked={field.value} onCheckedChange={field.onChange} />} />
<FormLabel className="mt-0!">
<Trans>Show link in title</Trans>
</FormLabel>
</FormItem>
)}
/>
<FormField
control={form.control}
name="options.showLinkInTitle"
render={({ field }) => (
<FormItem className="flex items-center gap-x-2">
<FormControl render={<Switch checked={field.value} onCheckedChange={field.onChange} />} />
<FormLabel className="mt-0!">
<Trans>Show link in title</Trans>
</FormLabel>
</FormItem>
)}
/>
<FormField
control={form.control}
name="description"
render={({ field }) => (
<FormItem className="sm:col-span-full">
<FormLabel>
<Trans>Description</Trans>
</FormLabel>
<FormControl render={<RichInput {...field} value={field.value} onChange={field.onChange} />} />
<FormMessage />
</FormItem>
)}
/>
</>
);
<FormField
control={form.control}
name="description"
render={({ field }) => (
<FormItem className="sm:col-span-full">
<FormLabel>
<Trans>Description</Trans>
</FormLabel>
<FormControl render={<RichInput {...field} value={field.value} onChange={field.onChange} />} />
<FormMessage />
</FormItem>
)}
/>
</>
);
}
+192 -192
View File
@@ -25,222 +25,222 @@ const formSchema = referenceItemSchema;
type FormValues = z.infer<typeof formSchema>;
export function CreateReferenceDialog({ data }: DialogProps<"resume.sections.references.create">) {
const closeDialog = useDialogStore((state) => state.closeDialog);
const updateResumeData = useResumeStore((state) => state.updateResumeData);
const closeDialog = useDialogStore((state) => state.closeDialog);
const updateResumeData = useResumeStore((state) => state.updateResumeData);
const form = useForm<FormValues>({
resolver: zodResolver(formSchema),
defaultValues: {
id: generateId(),
hidden: data?.item?.hidden ?? false,
options: data?.item?.options ?? { showLinkInTitle: false },
name: data?.item?.name ?? "",
position: data?.item?.position ?? "",
website: data?.item?.website ?? { url: "", label: "" },
phone: data?.item?.phone ?? "",
description: data?.item?.description ?? "",
},
});
const form = useForm<FormValues>({
resolver: zodResolver(formSchema),
defaultValues: {
id: generateId(),
hidden: data?.item?.hidden ?? false,
options: data?.item?.options ?? { showLinkInTitle: false },
name: data?.item?.name ?? "",
position: data?.item?.position ?? "",
website: data?.item?.website ?? { url: "", label: "" },
phone: data?.item?.phone ?? "",
description: data?.item?.description ?? "",
},
});
const onSubmit = (formData: FormValues) => {
updateResumeData((draft) => {
if (data?.customSectionId) {
const section = draft.customSections.find((s) => s.id === data.customSectionId);
if (section) section.items.push(formData);
} else {
draft.sections.references.items.push(formData);
}
});
closeDialog();
};
const onSubmit = (formData: FormValues) => {
updateResumeData((draft) => {
if (data?.customSectionId) {
const section = draft.customSections.find((s) => s.id === data.customSectionId);
if (section) section.items.push(formData);
} else {
draft.sections.references.items.push(formData);
}
});
closeDialog();
};
const { blockEvents, requestClose } = useFormBlocker(form);
const { blockEvents, requestClose } = useFormBlocker(form);
return (
<DialogContent {...blockEvents}>
<DialogHeader>
<DialogTitle className="flex items-center gap-x-2">
<PlusIcon />
<Trans>Create a new reference</Trans>
</DialogTitle>
<DialogDescription />
</DialogHeader>
return (
<DialogContent {...blockEvents}>
<DialogHeader>
<DialogTitle className="flex items-center gap-x-2">
<PlusIcon />
<Trans>Create a new reference</Trans>
</DialogTitle>
<DialogDescription />
</DialogHeader>
<Form {...form}>
<form className="grid gap-4 sm:grid-cols-2" onSubmit={form.handleSubmit(onSubmit)}>
<ReferenceForm />
<Form {...form}>
<form className="grid gap-4 sm:grid-cols-2" onSubmit={form.handleSubmit(onSubmit)}>
<ReferenceForm />
<DialogFooter className="sm:col-span-full">
<Button variant="ghost" onClick={requestClose}>
<Trans>Cancel</Trans>
</Button>
<DialogFooter className="sm:col-span-full">
<Button variant="ghost" onClick={requestClose}>
<Trans>Cancel</Trans>
</Button>
<Button type="submit" disabled={form.formState.isSubmitting}>
<Trans>Create</Trans>
</Button>
</DialogFooter>
</form>
</Form>
</DialogContent>
);
<Button type="submit" disabled={form.formState.isSubmitting}>
<Trans>Create</Trans>
</Button>
</DialogFooter>
</form>
</Form>
</DialogContent>
);
}
export function UpdateReferenceDialog({ data }: DialogProps<"resume.sections.references.update">) {
const closeDialog = useDialogStore((state) => state.closeDialog);
const updateResumeData = useResumeStore((state) => state.updateResumeData);
const closeDialog = useDialogStore((state) => state.closeDialog);
const updateResumeData = useResumeStore((state) => state.updateResumeData);
const form = useForm<FormValues>({
resolver: zodResolver(formSchema),
defaultValues: {
id: data.item.id,
hidden: data.item.hidden,
options: data.item.options ?? { showLinkInTitle: false },
name: data.item.name,
position: data.item.position,
website: data.item.website,
phone: data.item.phone,
description: data.item.description,
},
});
const form = useForm<FormValues>({
resolver: zodResolver(formSchema),
defaultValues: {
id: data.item.id,
hidden: data.item.hidden,
options: data.item.options ?? { showLinkInTitle: false },
name: data.item.name,
position: data.item.position,
website: data.item.website,
phone: data.item.phone,
description: data.item.description,
},
});
const onSubmit = (formData: FormValues) => {
updateResumeData((draft) => {
if (data?.customSectionId) {
const section = draft.customSections.find((s) => s.id === data.customSectionId);
if (!section) return;
const index = section.items.findIndex((item) => item.id === formData.id);
if (index !== -1) section.items[index] = formData;
} else {
const index = draft.sections.references.items.findIndex((item) => item.id === formData.id);
if (index !== -1) draft.sections.references.items[index] = formData;
}
});
closeDialog();
};
const onSubmit = (formData: FormValues) => {
updateResumeData((draft) => {
if (data?.customSectionId) {
const section = draft.customSections.find((s) => s.id === data.customSectionId);
if (!section) return;
const index = section.items.findIndex((item) => item.id === formData.id);
if (index !== -1) section.items[index] = formData;
} else {
const index = draft.sections.references.items.findIndex((item) => item.id === formData.id);
if (index !== -1) draft.sections.references.items[index] = formData;
}
});
closeDialog();
};
const { blockEvents, requestClose } = useFormBlocker(form);
const { blockEvents, requestClose } = useFormBlocker(form);
return (
<DialogContent {...blockEvents}>
<DialogHeader>
<DialogTitle className="flex items-center gap-x-2">
<PencilSimpleLineIcon />
<Trans>Update an existing reference</Trans>
</DialogTitle>
<DialogDescription />
</DialogHeader>
return (
<DialogContent {...blockEvents}>
<DialogHeader>
<DialogTitle className="flex items-center gap-x-2">
<PencilSimpleLineIcon />
<Trans>Update an existing reference</Trans>
</DialogTitle>
<DialogDescription />
</DialogHeader>
<Form {...form}>
<form className="grid gap-4 sm:grid-cols-2" onSubmit={form.handleSubmit(onSubmit)}>
<ReferenceForm />
<Form {...form}>
<form className="grid gap-4 sm:grid-cols-2" onSubmit={form.handleSubmit(onSubmit)}>
<ReferenceForm />
<DialogFooter className="sm:col-span-full">
<Button variant="ghost" onClick={requestClose}>
<Trans>Cancel</Trans>
</Button>
<DialogFooter className="sm:col-span-full">
<Button variant="ghost" onClick={requestClose}>
<Trans>Cancel</Trans>
</Button>
<Button type="submit" disabled={form.formState.isSubmitting}>
<Trans>Save Changes</Trans>
</Button>
</DialogFooter>
</form>
</Form>
</DialogContent>
);
<Button type="submit" disabled={form.formState.isSubmitting}>
<Trans>Save Changes</Trans>
</Button>
</DialogFooter>
</form>
</Form>
</DialogContent>
);
}
function ReferenceForm() {
const form = useFormContext<FormValues>();
const form = useFormContext<FormValues>();
return (
<>
<FormField
control={form.control}
name="name"
render={({ field }) => (
<FormItem>
<FormLabel>
<Trans>Name</Trans>
</FormLabel>
<FormControl render={<Input {...field} />} />
<FormMessage />
</FormItem>
)}
/>
return (
<>
<FormField
control={form.control}
name="name"
render={({ field }) => (
<FormItem>
<FormLabel>
<Trans>Name</Trans>
</FormLabel>
<FormControl render={<Input {...field} />} />
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="position"
render={({ field }) => (
<FormItem>
<FormLabel>
<Trans>Position</Trans>
</FormLabel>
<FormControl render={<Input {...field} />} />
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="position"
render={({ field }) => (
<FormItem>
<FormLabel>
<Trans>Position</Trans>
</FormLabel>
<FormControl render={<Input {...field} />} />
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="phone"
render={({ field }) => (
<FormItem>
<FormLabel>
<Trans>Phone</Trans>
</FormLabel>
<FormControl render={<Input {...field} />} />
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="phone"
render={({ field }) => (
<FormItem>
<FormLabel>
<Trans>Phone</Trans>
</FormLabel>
<FormControl render={<Input {...field} />} />
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="website"
render={({ field }) => (
<FormItem>
<FormLabel>
<Trans>Website</Trans>
</FormLabel>
<URLInput
{...field}
value={field.value}
onChange={field.onChange}
hideLabelButton={form.watch("options.showLinkInTitle")}
/>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="website"
render={({ field }) => (
<FormItem>
<FormLabel>
<Trans>Website</Trans>
</FormLabel>
<URLInput
{...field}
value={field.value}
onChange={field.onChange}
hideLabelButton={form.watch("options.showLinkInTitle")}
/>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="options.showLinkInTitle"
render={({ field }) => (
<FormItem className="flex items-center gap-x-2">
<FormControl render={<Switch checked={field.value} onCheckedChange={field.onChange} />} />
<FormLabel className="mt-0!">
<Trans>Show link in title</Trans>
</FormLabel>
</FormItem>
)}
/>
<FormField
control={form.control}
name="options.showLinkInTitle"
render={({ field }) => (
<FormItem className="flex items-center gap-x-2">
<FormControl render={<Switch checked={field.value} onCheckedChange={field.onChange} />} />
<FormLabel className="mt-0!">
<Trans>Show link in title</Trans>
</FormLabel>
</FormItem>
)}
/>
<FormField
control={form.control}
name="description"
render={({ field }) => (
<FormItem className="sm:col-span-full">
<FormLabel>
<Trans>Description</Trans>
</FormLabel>
<FormControl render={<RichInput {...field} value={field.value} onChange={field.onChange} />} />
<FormMessage />
</FormItem>
)}
/>
</>
);
<FormField
control={form.control}
name="description"
render={({ field }) => (
<FormItem className="sm:col-span-full">
<FormLabel>
<Trans>Description</Trans>
</FormLabel>
<FormControl render={<RichInput {...field} value={field.value} onChange={field.onChange} />} />
<FormMessage />
</FormItem>
)}
/>
</>
);
}
+190 -190
View File
@@ -28,220 +28,220 @@ const formSchema = skillItemSchema;
type FormValues = z.infer<typeof formSchema>;
export function CreateSkillDialog({ data }: DialogProps<"resume.sections.skills.create">) {
const closeDialog = useDialogStore((state) => state.closeDialog);
const updateResumeData = useResumeStore((state) => state.updateResumeData);
const closeDialog = useDialogStore((state) => state.closeDialog);
const updateResumeData = useResumeStore((state) => state.updateResumeData);
const form = useForm<FormValues>({
resolver: zodResolver(formSchema),
defaultValues: {
id: generateId(),
hidden: data?.item?.hidden ?? false,
icon: data?.item?.icon ?? "acorn",
name: data?.item?.name ?? "",
proficiency: data?.item?.proficiency ?? "",
level: data?.item?.level ?? 0,
keywords: data?.item?.keywords ?? [],
},
});
const form = useForm<FormValues>({
resolver: zodResolver(formSchema),
defaultValues: {
id: generateId(),
hidden: data?.item?.hidden ?? false,
icon: data?.item?.icon ?? "acorn",
name: data?.item?.name ?? "",
proficiency: data?.item?.proficiency ?? "",
level: data?.item?.level ?? 0,
keywords: data?.item?.keywords ?? [],
},
});
const onSubmit = (formData: FormValues) => {
updateResumeData((draft) => {
if (data?.customSectionId) {
const section = draft.customSections.find((s) => s.id === data.customSectionId);
if (section) section.items.push(formData);
} else {
draft.sections.skills.items.push(formData);
}
});
closeDialog();
};
const onSubmit = (formData: FormValues) => {
updateResumeData((draft) => {
if (data?.customSectionId) {
const section = draft.customSections.find((s) => s.id === data.customSectionId);
if (section) section.items.push(formData);
} else {
draft.sections.skills.items.push(formData);
}
});
closeDialog();
};
const { blockEvents, requestClose } = useFormBlocker(form);
const { blockEvents, requestClose } = useFormBlocker(form);
return (
<DialogContent {...blockEvents}>
<DialogHeader>
<DialogTitle className="flex items-center gap-x-2">
<PlusIcon />
<Trans>Create a new skill</Trans>
</DialogTitle>
<DialogDescription />
</DialogHeader>
return (
<DialogContent {...blockEvents}>
<DialogHeader>
<DialogTitle className="flex items-center gap-x-2">
<PlusIcon />
<Trans>Create a new skill</Trans>
</DialogTitle>
<DialogDescription />
</DialogHeader>
<Form {...form}>
<form className="grid gap-4 sm:grid-cols-2" onSubmit={form.handleSubmit(onSubmit)}>
<SkillForm />
<Form {...form}>
<form className="grid gap-4 sm:grid-cols-2" onSubmit={form.handleSubmit(onSubmit)}>
<SkillForm />
<DialogFooter className="sm:col-span-full">
<Button variant="ghost" onClick={requestClose}>
<Trans>Cancel</Trans>
</Button>
<DialogFooter className="sm:col-span-full">
<Button variant="ghost" onClick={requestClose}>
<Trans>Cancel</Trans>
</Button>
<Button type="submit" disabled={form.formState.isSubmitting}>
<Trans>Create</Trans>
</Button>
</DialogFooter>
</form>
</Form>
</DialogContent>
);
<Button type="submit" disabled={form.formState.isSubmitting}>
<Trans>Create</Trans>
</Button>
</DialogFooter>
</form>
</Form>
</DialogContent>
);
}
export function UpdateSkillDialog({ data }: DialogProps<"resume.sections.skills.update">) {
const closeDialog = useDialogStore((state) => state.closeDialog);
const updateResumeData = useResumeStore((state) => state.updateResumeData);
const closeDialog = useDialogStore((state) => state.closeDialog);
const updateResumeData = useResumeStore((state) => state.updateResumeData);
const form = useForm<FormValues>({
resolver: zodResolver(formSchema),
defaultValues: {
id: data.item.id,
hidden: data.item.hidden,
icon: data.item.icon,
name: data.item.name,
proficiency: data.item.proficiency,
level: data.item.level,
keywords: data.item.keywords,
},
});
const form = useForm<FormValues>({
resolver: zodResolver(formSchema),
defaultValues: {
id: data.item.id,
hidden: data.item.hidden,
icon: data.item.icon,
name: data.item.name,
proficiency: data.item.proficiency,
level: data.item.level,
keywords: data.item.keywords,
},
});
const onSubmit = (formData: FormValues) => {
updateResumeData((draft) => {
if (data?.customSectionId) {
const section = draft.customSections.find((s) => s.id === data.customSectionId);
if (!section) return;
const index = section.items.findIndex((item) => item.id === formData.id);
if (index !== -1) section.items[index] = formData;
} else {
const index = draft.sections.skills.items.findIndex((item) => item.id === formData.id);
if (index !== -1) draft.sections.skills.items[index] = formData;
}
});
closeDialog();
};
const onSubmit = (formData: FormValues) => {
updateResumeData((draft) => {
if (data?.customSectionId) {
const section = draft.customSections.find((s) => s.id === data.customSectionId);
if (!section) return;
const index = section.items.findIndex((item) => item.id === formData.id);
if (index !== -1) section.items[index] = formData;
} else {
const index = draft.sections.skills.items.findIndex((item) => item.id === formData.id);
if (index !== -1) draft.sections.skills.items[index] = formData;
}
});
closeDialog();
};
const { blockEvents, requestClose } = useFormBlocker(form);
const { blockEvents, requestClose } = useFormBlocker(form);
return (
<DialogContent {...blockEvents}>
<DialogHeader>
<DialogTitle className="flex items-center gap-x-2">
<PencilSimpleLineIcon />
<Trans>Update an existing skill</Trans>
</DialogTitle>
<DialogDescription />
</DialogHeader>
return (
<DialogContent {...blockEvents}>
<DialogHeader>
<DialogTitle className="flex items-center gap-x-2">
<PencilSimpleLineIcon />
<Trans>Update an existing skill</Trans>
</DialogTitle>
<DialogDescription />
</DialogHeader>
<Form {...form}>
<form className="grid gap-4 sm:grid-cols-2" onSubmit={form.handleSubmit(onSubmit)}>
<SkillForm />
<Form {...form}>
<form className="grid gap-4 sm:grid-cols-2" onSubmit={form.handleSubmit(onSubmit)}>
<SkillForm />
<DialogFooter className="sm:col-span-full">
<Button variant="ghost" onClick={requestClose}>
<Trans>Cancel</Trans>
</Button>
<DialogFooter className="sm:col-span-full">
<Button variant="ghost" onClick={requestClose}>
<Trans>Cancel</Trans>
</Button>
<Button type="submit" disabled={form.formState.isSubmitting}>
<Trans>Save Changes</Trans>
</Button>
</DialogFooter>
</form>
</Form>
</DialogContent>
);
<Button type="submit" disabled={form.formState.isSubmitting}>
<Trans>Save Changes</Trans>
</Button>
</DialogFooter>
</form>
</Form>
</DialogContent>
);
}
function SkillForm() {
const form = useFormContext<FormValues>();
const nameState = useFormState({ control: form.control, name: "name" });
const form = useFormContext<FormValues>();
const nameState = useFormState({ control: form.control, name: "name" });
const isNameInvalid = useMemo(() => {
return nameState.errors && Object.keys(nameState.errors).length > 0;
}, [nameState]);
const isNameInvalid = useMemo(() => {
return nameState.errors && Object.keys(nameState.errors).length > 0;
}, [nameState]);
return (
<>
<div className={cn("flex items-end", isNameInvalid && "items-center")}>
<FormField
control={form.control}
name={"icon"}
render={({ field }) => (
<FormItem className="shrink-0">
<FormControl
render={
<IconPicker {...field} popoverProps={{ modal: true }} className="rounded-r-none! border-e-0!" />
}
/>
</FormItem>
)}
/>
return (
<>
<div className={cn("flex items-end", isNameInvalid && "items-center")}>
<FormField
control={form.control}
name={"icon"}
render={({ field }) => (
<FormItem className="shrink-0">
<FormControl
render={
<IconPicker {...field} popoverProps={{ modal: true }} className="rounded-r-none! border-e-0!" />
}
/>
</FormItem>
)}
/>
<FormField
control={form.control}
name="name"
render={({ field }) => (
<FormItem className="flex-1">
<FormLabel>
<Trans>Name</Trans>
</FormLabel>
<FormControl render={<Input className="rounded-l-none!" {...field} />} />
<FormMessage />
</FormItem>
)}
/>
</div>
<FormField
control={form.control}
name="name"
render={({ field }) => (
<FormItem className="flex-1">
<FormLabel>
<Trans>Name</Trans>
</FormLabel>
<FormControl render={<Input className="rounded-l-none!" {...field} />} />
<FormMessage />
</FormItem>
)}
/>
</div>
<FormField
control={form.control}
name="proficiency"
render={({ field }) => (
<FormItem>
<FormLabel>
<Trans>Proficiency</Trans>
</FormLabel>
<FormControl render={<Input {...field} />} />
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="proficiency"
render={({ field }) => (
<FormItem>
<FormLabel>
<Trans>Proficiency</Trans>
</FormLabel>
<FormControl render={<Input {...field} />} />
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="level"
render={({ field }) => (
<FormItem className="gap-4 sm:col-span-full">
<FormLabel>
<Trans>Level</Trans>
</FormLabel>
<FormControl
render={
<Slider
min={0}
max={5}
step={1}
value={[field.value]}
onValueChange={(value) => field.onChange(Array.isArray(value) ? value[0] : value)}
/>
}
/>
<FormMessage />
<FormDescription>{Number(field.value) === 0 ? t`Hidden` : `${field.value} / 5`}</FormDescription>
</FormItem>
)}
/>
<FormField
control={form.control}
name="level"
render={({ field }) => (
<FormItem className="gap-4 sm:col-span-full">
<FormLabel>
<Trans>Level</Trans>
</FormLabel>
<FormControl
render={
<Slider
min={0}
max={5}
step={1}
value={[field.value]}
onValueChange={(value) => field.onChange(Array.isArray(value) ? value[0] : value)}
/>
}
/>
<FormMessage />
<FormDescription>{Number(field.value) === 0 ? t`Hidden` : `${field.value} / 5`}</FormDescription>
</FormItem>
)}
/>
<FormField
control={form.control}
name="keywords"
render={({ field }) => (
<FormItem className="sm:col-span-full">
<FormLabel>
<Trans>Keywords</Trans>
</FormLabel>
<FormControl render={<ChipInput {...field} />} />
<FormMessage />
</FormItem>
)}
/>
</>
);
<FormField
control={form.control}
name="keywords"
render={({ field }) => (
<FormItem className="sm:col-span-full">
<FormLabel>
<Trans>Keywords</Trans>
</FormLabel>
<FormControl render={<ChipInput {...field} />} />
<FormMessage />
</FormItem>
)}
/>
</>
);
}
+106 -106
View File
@@ -22,131 +22,131 @@ const formSchema = summaryItemSchema;
type FormValues = z.infer<typeof formSchema>;
export function CreateSummaryItemDialog({ data }: DialogProps<"resume.sections.summary.create">) {
const closeDialog = useDialogStore((state) => state.closeDialog);
const updateResumeData = useResumeStore((state) => state.updateResumeData);
const closeDialog = useDialogStore((state) => state.closeDialog);
const updateResumeData = useResumeStore((state) => state.updateResumeData);
const form = useForm<FormValues>({
resolver: zodResolver(formSchema),
defaultValues: {
id: generateId(),
hidden: data?.item?.hidden ?? false,
content: data?.item?.content ?? "",
},
});
const form = useForm<FormValues>({
resolver: zodResolver(formSchema),
defaultValues: {
id: generateId(),
hidden: data?.item?.hidden ?? false,
content: data?.item?.content ?? "",
},
});
const onSubmit = (formData: FormValues) => {
updateResumeData((draft) => {
if (data?.customSectionId) {
const section = draft.customSections.find((s) => s.id === data.customSectionId);
if (section) section.items.push(formData);
}
});
closeDialog();
};
const onSubmit = (formData: FormValues) => {
updateResumeData((draft) => {
if (data?.customSectionId) {
const section = draft.customSections.find((s) => s.id === data.customSectionId);
if (section) section.items.push(formData);
}
});
closeDialog();
};
const { blockEvents, requestClose } = useFormBlocker(form);
const { blockEvents, requestClose } = useFormBlocker(form);
return (
<DialogContent {...blockEvents}>
<DialogHeader>
<DialogTitle className="flex items-center gap-x-2">
<PlusIcon />
<Trans>Create a new summary item</Trans>
</DialogTitle>
<DialogDescription />
</DialogHeader>
return (
<DialogContent {...blockEvents}>
<DialogHeader>
<DialogTitle className="flex items-center gap-x-2">
<PlusIcon />
<Trans>Create a new summary item</Trans>
</DialogTitle>
<DialogDescription />
</DialogHeader>
<Form {...form}>
<form className="grid gap-4" onSubmit={form.handleSubmit(onSubmit)}>
<SummaryItemForm />
<Form {...form}>
<form className="grid gap-4" onSubmit={form.handleSubmit(onSubmit)}>
<SummaryItemForm />
<DialogFooter>
<Button variant="ghost" onClick={requestClose}>
<Trans>Cancel</Trans>
</Button>
<DialogFooter>
<Button variant="ghost" onClick={requestClose}>
<Trans>Cancel</Trans>
</Button>
<Button type="submit" disabled={form.formState.isSubmitting}>
<Trans>Create</Trans>
</Button>
</DialogFooter>
</form>
</Form>
</DialogContent>
);
<Button type="submit" disabled={form.formState.isSubmitting}>
<Trans>Create</Trans>
</Button>
</DialogFooter>
</form>
</Form>
</DialogContent>
);
}
export function UpdateSummaryItemDialog({ data }: DialogProps<"resume.sections.summary.update">) {
const closeDialog = useDialogStore((state) => state.closeDialog);
const updateResumeStore = useResumeStore((state) => state.updateResumeData);
const closeDialog = useDialogStore((state) => state.closeDialog);
const updateResumeStore = useResumeStore((state) => state.updateResumeData);
const form = useForm<FormValues>({
resolver: zodResolver(formSchema),
defaultValues: {
id: data.item.id,
hidden: data.item.hidden,
content: data.item.content,
},
});
const form = useForm<FormValues>({
resolver: zodResolver(formSchema),
defaultValues: {
id: data.item.id,
hidden: data.item.hidden,
content: data.item.content,
},
});
const onSubmit = (formData: FormValues) => {
updateResumeStore((draft) => {
if (data?.customSectionId) {
const section = draft.customSections.find((s) => s.id === data.customSectionId);
if (!section) return;
const index = section.items.findIndex((item) => item.id === formData.id);
if (index !== -1) section.items[index] = formData;
}
});
closeDialog();
};
const onSubmit = (formData: FormValues) => {
updateResumeStore((draft) => {
if (data?.customSectionId) {
const section = draft.customSections.find((s) => s.id === data.customSectionId);
if (!section) return;
const index = section.items.findIndex((item) => item.id === formData.id);
if (index !== -1) section.items[index] = formData;
}
});
closeDialog();
};
const { blockEvents, requestClose } = useFormBlocker(form);
const { blockEvents, requestClose } = useFormBlocker(form);
return (
<DialogContent {...blockEvents}>
<DialogHeader>
<DialogTitle className="flex items-center gap-x-2">
<PencilSimpleLineIcon />
<Trans>Update an existing summary item</Trans>
</DialogTitle>
<DialogDescription />
</DialogHeader>
return (
<DialogContent {...blockEvents}>
<DialogHeader>
<DialogTitle className="flex items-center gap-x-2">
<PencilSimpleLineIcon />
<Trans>Update an existing summary item</Trans>
</DialogTitle>
<DialogDescription />
</DialogHeader>
<Form {...form}>
<form className="grid gap-4" onSubmit={form.handleSubmit(onSubmit)}>
<SummaryItemForm />
<Form {...form}>
<form className="grid gap-4" onSubmit={form.handleSubmit(onSubmit)}>
<SummaryItemForm />
<DialogFooter>
<Button variant="ghost" onClick={requestClose}>
<Trans>Cancel</Trans>
</Button>
<DialogFooter>
<Button variant="ghost" onClick={requestClose}>
<Trans>Cancel</Trans>
</Button>
<Button type="submit" disabled={form.formState.isSubmitting}>
<Trans>Save Changes</Trans>
</Button>
</DialogFooter>
</form>
</Form>
</DialogContent>
);
<Button type="submit" disabled={form.formState.isSubmitting}>
<Trans>Save Changes</Trans>
</Button>
</DialogFooter>
</form>
</Form>
</DialogContent>
);
}
function SummaryItemForm() {
const form = useFormContext<FormValues>();
const form = useFormContext<FormValues>();
return (
<FormField
control={form.control}
name="content"
render={({ field }) => (
<FormItem>
<FormLabel>
<Trans>Content</Trans>
</FormLabel>
<FormControl render={<RichInput {...field} value={field.value} onChange={field.onChange} />} />
<FormMessage />
</FormItem>
)}
/>
);
return (
<FormField
control={form.control}
name="content"
render={({ field }) => (
<FormItem>
<FormLabel>
<Trans>Content</Trans>
</FormLabel>
<FormControl render={<RichInput {...field} value={field.value} onChange={field.onChange} />} />
<FormMessage />
</FormItem>
)}
/>
);
}
+192 -192
View File
@@ -25,222 +25,222 @@ const formSchema = volunteerItemSchema;
type FormValues = z.infer<typeof formSchema>;
export function CreateVolunteerDialog({ data }: DialogProps<"resume.sections.volunteer.create">) {
const closeDialog = useDialogStore((state) => state.closeDialog);
const updateResumeData = useResumeStore((state) => state.updateResumeData);
const closeDialog = useDialogStore((state) => state.closeDialog);
const updateResumeData = useResumeStore((state) => state.updateResumeData);
const form = useForm<FormValues>({
resolver: zodResolver(formSchema),
defaultValues: {
id: generateId(),
hidden: data?.item?.hidden ?? false,
options: data?.item?.options ?? { showLinkInTitle: false },
organization: data?.item?.organization ?? "",
location: data?.item?.location ?? "",
period: data?.item?.period ?? "",
website: data?.item?.website ?? { url: "", label: "" },
description: data?.item?.description ?? "",
},
});
const form = useForm<FormValues>({
resolver: zodResolver(formSchema),
defaultValues: {
id: generateId(),
hidden: data?.item?.hidden ?? false,
options: data?.item?.options ?? { showLinkInTitle: false },
organization: data?.item?.organization ?? "",
location: data?.item?.location ?? "",
period: data?.item?.period ?? "",
website: data?.item?.website ?? { url: "", label: "" },
description: data?.item?.description ?? "",
},
});
const onSubmit = (formData: FormValues) => {
updateResumeData((draft) => {
if (data?.customSectionId) {
const section = draft.customSections.find((s) => s.id === data.customSectionId);
if (section) section.items.push(formData);
} else {
draft.sections.volunteer.items.push(formData);
}
});
closeDialog();
};
const onSubmit = (formData: FormValues) => {
updateResumeData((draft) => {
if (data?.customSectionId) {
const section = draft.customSections.find((s) => s.id === data.customSectionId);
if (section) section.items.push(formData);
} else {
draft.sections.volunteer.items.push(formData);
}
});
closeDialog();
};
const { blockEvents, requestClose } = useFormBlocker(form);
const { blockEvents, requestClose } = useFormBlocker(form);
return (
<DialogContent {...blockEvents}>
<DialogHeader>
<DialogTitle className="flex items-center gap-x-2">
<PlusIcon />
<Trans>Create a new volunteer experience</Trans>
</DialogTitle>
<DialogDescription />
</DialogHeader>
return (
<DialogContent {...blockEvents}>
<DialogHeader>
<DialogTitle className="flex items-center gap-x-2">
<PlusIcon />
<Trans>Create a new volunteer experience</Trans>
</DialogTitle>
<DialogDescription />
</DialogHeader>
<Form {...form}>
<form className="grid gap-4 sm:grid-cols-2" onSubmit={form.handleSubmit(onSubmit)}>
<VolunteerForm />
<Form {...form}>
<form className="grid gap-4 sm:grid-cols-2" onSubmit={form.handleSubmit(onSubmit)}>
<VolunteerForm />
<DialogFooter className="sm:col-span-full">
<Button variant="ghost" onClick={requestClose}>
<Trans>Cancel</Trans>
</Button>
<DialogFooter className="sm:col-span-full">
<Button variant="ghost" onClick={requestClose}>
<Trans>Cancel</Trans>
</Button>
<Button type="submit" disabled={form.formState.isSubmitting}>
<Trans>Create</Trans>
</Button>
</DialogFooter>
</form>
</Form>
</DialogContent>
);
<Button type="submit" disabled={form.formState.isSubmitting}>
<Trans>Create</Trans>
</Button>
</DialogFooter>
</form>
</Form>
</DialogContent>
);
}
export function UpdateVolunteerDialog({ data }: DialogProps<"resume.sections.volunteer.update">) {
const closeDialog = useDialogStore((state) => state.closeDialog);
const updateResumeData = useResumeStore((state) => state.updateResumeData);
const closeDialog = useDialogStore((state) => state.closeDialog);
const updateResumeData = useResumeStore((state) => state.updateResumeData);
const form = useForm<FormValues>({
resolver: zodResolver(formSchema),
defaultValues: {
id: data.item.id,
hidden: data.item.hidden,
options: data.item.options ?? { showLinkInTitle: false },
organization: data.item.organization,
location: data.item.location,
period: data.item.period,
website: data.item.website,
description: data.item.description,
},
});
const form = useForm<FormValues>({
resolver: zodResolver(formSchema),
defaultValues: {
id: data.item.id,
hidden: data.item.hidden,
options: data.item.options ?? { showLinkInTitle: false },
organization: data.item.organization,
location: data.item.location,
period: data.item.period,
website: data.item.website,
description: data.item.description,
},
});
const onSubmit = (formData: FormValues) => {
updateResumeData((draft) => {
if (data?.customSectionId) {
const section = draft.customSections.find((s) => s.id === data.customSectionId);
if (!section) return;
const index = section.items.findIndex((item) => item.id === formData.id);
if (index !== -1) section.items[index] = formData;
} else {
const index = draft.sections.volunteer.items.findIndex((item) => item.id === formData.id);
if (index !== -1) draft.sections.volunteer.items[index] = formData;
}
});
closeDialog();
};
const onSubmit = (formData: FormValues) => {
updateResumeData((draft) => {
if (data?.customSectionId) {
const section = draft.customSections.find((s) => s.id === data.customSectionId);
if (!section) return;
const index = section.items.findIndex((item) => item.id === formData.id);
if (index !== -1) section.items[index] = formData;
} else {
const index = draft.sections.volunteer.items.findIndex((item) => item.id === formData.id);
if (index !== -1) draft.sections.volunteer.items[index] = formData;
}
});
closeDialog();
};
const { blockEvents, requestClose } = useFormBlocker(form);
const { blockEvents, requestClose } = useFormBlocker(form);
return (
<DialogContent {...blockEvents}>
<DialogHeader>
<DialogTitle className="flex items-center gap-x-2">
<PencilSimpleLineIcon />
<Trans>Update an existing volunteer experience</Trans>
</DialogTitle>
<DialogDescription />
</DialogHeader>
return (
<DialogContent {...blockEvents}>
<DialogHeader>
<DialogTitle className="flex items-center gap-x-2">
<PencilSimpleLineIcon />
<Trans>Update an existing volunteer experience</Trans>
</DialogTitle>
<DialogDescription />
</DialogHeader>
<Form {...form}>
<form className="grid gap-4 sm:grid-cols-2" onSubmit={form.handleSubmit(onSubmit)}>
<VolunteerForm />
<Form {...form}>
<form className="grid gap-4 sm:grid-cols-2" onSubmit={form.handleSubmit(onSubmit)}>
<VolunteerForm />
<DialogFooter className="sm:col-span-full">
<Button variant="ghost" onClick={requestClose}>
<Trans>Cancel</Trans>
</Button>
<DialogFooter className="sm:col-span-full">
<Button variant="ghost" onClick={requestClose}>
<Trans>Cancel</Trans>
</Button>
<Button type="submit" disabled={form.formState.isSubmitting}>
<Trans>Save Changes</Trans>
</Button>
</DialogFooter>
</form>
</Form>
</DialogContent>
);
<Button type="submit" disabled={form.formState.isSubmitting}>
<Trans>Save Changes</Trans>
</Button>
</DialogFooter>
</form>
</Form>
</DialogContent>
);
}
function VolunteerForm() {
const form = useFormContext<FormValues>();
const form = useFormContext<FormValues>();
return (
<>
<FormField
control={form.control}
name="organization"
render={({ field }) => (
<FormItem>
<FormLabel>
<Trans>Organization</Trans>
</FormLabel>
<FormControl render={<Input {...field} />} />
<FormMessage />
</FormItem>
)}
/>
return (
<>
<FormField
control={form.control}
name="organization"
render={({ field }) => (
<FormItem>
<FormLabel>
<Trans>Organization</Trans>
</FormLabel>
<FormControl render={<Input {...field} />} />
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="location"
render={({ field }) => (
<FormItem>
<FormLabel>
<Trans>Location</Trans>
</FormLabel>
<FormControl render={<Input {...field} />} />
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="location"
render={({ field }) => (
<FormItem>
<FormLabel>
<Trans>Location</Trans>
</FormLabel>
<FormControl render={<Input {...field} />} />
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="period"
render={({ field }) => (
<FormItem>
<FormLabel>
<Trans>Period</Trans>
</FormLabel>
<FormControl render={<Input {...field} />} />
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="period"
render={({ field }) => (
<FormItem>
<FormLabel>
<Trans>Period</Trans>
</FormLabel>
<FormControl render={<Input {...field} />} />
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="website"
render={({ field }) => (
<FormItem>
<FormLabel>
<Trans>Website</Trans>
</FormLabel>
<URLInput
{...field}
value={field.value}
onChange={field.onChange}
hideLabelButton={form.watch("options.showLinkInTitle")}
/>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="website"
render={({ field }) => (
<FormItem>
<FormLabel>
<Trans>Website</Trans>
</FormLabel>
<URLInput
{...field}
value={field.value}
onChange={field.onChange}
hideLabelButton={form.watch("options.showLinkInTitle")}
/>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="options.showLinkInTitle"
render={({ field }) => (
<FormItem className="flex items-center gap-x-2">
<FormControl render={<Switch checked={field.value} onCheckedChange={field.onChange} />} />
<FormLabel className="mt-0!">
<Trans>Show link in title</Trans>
</FormLabel>
</FormItem>
)}
/>
<FormField
control={form.control}
name="options.showLinkInTitle"
render={({ field }) => (
<FormItem className="flex items-center gap-x-2">
<FormControl render={<Switch checked={field.value} onCheckedChange={field.onChange} />} />
<FormLabel className="mt-0!">
<Trans>Show link in title</Trans>
</FormLabel>
</FormItem>
)}
/>
<FormField
control={form.control}
name="description"
render={({ field }) => (
<FormItem className="sm:col-span-full">
<FormLabel>
<Trans>Description</Trans>
</FormLabel>
<FormControl render={<RichInput {...field} value={field.value} onChange={field.onChange} />} />
<FormMessage />
</FormItem>
)}
/>
</>
);
<FormField
control={form.control}
name="description"
render={({ field }) => (
<FormItem className="sm:col-span-full">
<FormLabel>
<Trans>Description</Trans>
</FormLabel>
<FormControl render={<RichInput {...field} value={field.value} onChange={field.onChange} />} />
<FormMessage />
</FormItem>
)}
/>
</>
);
}
+96 -96
View File
@@ -5,103 +5,103 @@ import { msg } from "@lingui/core/macro";
import type { Template } from "@/schema/templates";
export type TemplateMetadata = {
name: string;
description: MessageDescriptor;
imageUrl: string;
tags: string[];
sidebarPosition: "left" | "right" | "none";
name: string;
description: MessageDescriptor;
imageUrl: string;
tags: string[];
sidebarPosition: "left" | "right" | "none";
};
export const templates = {
azurill: {
name: "Azurill",
description: msg`Two-column with a bold colored sidebar and skill bars; great for creative or tech roles where visual flair is welcome.`,
imageUrl: "/templates/jpg/azurill.jpg",
tags: ["Two-column", "Creative", "Tech", "Visual flair"],
sidebarPosition: "left",
},
bronzor: {
name: "Bronzor",
description: msg`Two-column, clean and professional with subtle section dividers; suits corporate, finance, or consulting positions.`,
imageUrl: "/templates/jpg/bronzor.jpg",
tags: ["Two-column", "Clean", "Professional", "Corporate", "Finance", "Consulting"],
sidebarPosition: "none",
},
chikorita: {
name: "Chikorita",
description: msg`Two-column with a soft header accent and circular profile photo; ideal for marketing, HR, or client-facing roles.`,
imageUrl: "/templates/jpg/chikorita.jpg",
tags: ["Two-column", "Soft accent", "Marketing", "HR", "Client-facing"],
sidebarPosition: "right",
},
ditgar: {
name: "Ditgar",
description: msg`Two-column with a dark teal sidebar and skills grid; modern feel for developers, data scientists, or technical PMs.`,
imageUrl: "/templates/jpg/ditgar.jpg",
tags: ["Two-column", "Modern", "Developer", "Data science", "Technical PM", "Dark sidebar"],
sidebarPosition: "left",
},
ditto: {
name: "Ditto",
description: msg`Two-column, minimal and text-dense with no decorative elements; perfect for traditional industries or ATS-heavy applications.`,
imageUrl: "/templates/jpg/ditto.jpg",
tags: ["Two-column", "ATS friendly", "Minimal", "Text-dense", "Traditional", "No decoration"],
sidebarPosition: "left",
},
gengar: {
name: "Gengar",
description: msg`Two-column with accent colors and clean typography; balanced choice for business analysts or operations roles.`,
imageUrl: "/templates/jpg/gengar.jpg",
tags: ["Two-column", "Accent colors", "Clean typography", "Business analyst", "Operations"],
sidebarPosition: "left",
},
glalie: {
name: "Glalie",
description: msg`Two-column, minimal with light gray sidebar and subtle icons; professional and understated for legal, finance, or executive roles.`,
imageUrl: "/templates/jpg/glalie.jpg",
tags: ["Two-column", "Minimal", "Professional", "Legal", "Finance", "Executive", "Understated"],
sidebarPosition: "left",
},
kakuna: {
name: "Kakuna",
description: msg`Single-column with a magenta left border accent; compact and efficient for entry-level or internship applications.`,
imageUrl: "/templates/jpg/kakuna.jpg",
tags: ["Single-column", "ATS friendly", "Compact", "Efficient", "Entry level", "Internship", "Magenta accent"],
sidebarPosition: "none",
},
lapras: {
name: "Lapras",
description: msg`Single-column; polished and serious for senior or enterprise-level positions.`,
imageUrl: "/templates/jpg/lapras.jpg",
tags: ["Single-column", "ATS friendly", "Polished", "Senior", "Enterprise"],
sidebarPosition: "none",
},
leafish: {
name: "Leafish",
description: msg`Two-column with a muted color sidebar; earthy and calm, suits sustainability, healthcare, or nonprofit sectors.`,
imageUrl: "/templates/jpg/leafish.jpg",
tags: ["Two-column", "Muted sidebar", "Earthy", "Calm", "Sustainability", "Healthcare", "Nonprofit"],
sidebarPosition: "right",
},
onyx: {
name: "Onyx",
description: msg`Single-column with a sidebar and clean grid layout; versatile for any professional or technical role.`,
imageUrl: "/templates/jpg/onyx.jpg",
tags: ["Single-column", "ATS friendly", "Sidebar", "Grid layout", "Versatile", "Professional", "Technical"],
sidebarPosition: "none",
},
pikachu: {
name: "Pikachu",
description: msg`Two-column with a left margin color; simple and approachable for creative, editorial, or junior roles.`,
imageUrl: "/templates/jpg/pikachu.jpg",
tags: ["Two-column", "Simple", "Creative", "Editorial", "Junior", "Accent colors"],
sidebarPosition: "left",
},
rhyhorn: {
name: "Rhyhorn",
description: msg`Single-column with a minimal top header and lots of whitespace; clean and modern for designers or content creators.`,
imageUrl: "/templates/jpg/rhyhorn.jpg",
tags: ["Single-column", "ATS friendly", "Minimal", "Clean", "Modern", "Designer", "Content creator", "Whitespace"],
sidebarPosition: "none",
},
azurill: {
name: "Azurill",
description: msg`Two-column with a bold colored sidebar and skill bars; great for creative or tech roles where visual flair is welcome.`,
imageUrl: "/templates/jpg/azurill.jpg",
tags: ["Two-column", "Creative", "Tech", "Visual flair"],
sidebarPosition: "left",
},
bronzor: {
name: "Bronzor",
description: msg`Two-column, clean and professional with subtle section dividers; suits corporate, finance, or consulting positions.`,
imageUrl: "/templates/jpg/bronzor.jpg",
tags: ["Two-column", "Clean", "Professional", "Corporate", "Finance", "Consulting"],
sidebarPosition: "none",
},
chikorita: {
name: "Chikorita",
description: msg`Two-column with a soft header accent and circular profile photo; ideal for marketing, HR, or client-facing roles.`,
imageUrl: "/templates/jpg/chikorita.jpg",
tags: ["Two-column", "Soft accent", "Marketing", "HR", "Client-facing"],
sidebarPosition: "right",
},
ditgar: {
name: "Ditgar",
description: msg`Two-column with a dark teal sidebar and skills grid; modern feel for developers, data scientists, or technical PMs.`,
imageUrl: "/templates/jpg/ditgar.jpg",
tags: ["Two-column", "Modern", "Developer", "Data science", "Technical PM", "Dark sidebar"],
sidebarPosition: "left",
},
ditto: {
name: "Ditto",
description: msg`Two-column, minimal and text-dense with no decorative elements; perfect for traditional industries or ATS-heavy applications.`,
imageUrl: "/templates/jpg/ditto.jpg",
tags: ["Two-column", "ATS friendly", "Minimal", "Text-dense", "Traditional", "No decoration"],
sidebarPosition: "left",
},
gengar: {
name: "Gengar",
description: msg`Two-column with accent colors and clean typography; balanced choice for business analysts or operations roles.`,
imageUrl: "/templates/jpg/gengar.jpg",
tags: ["Two-column", "Accent colors", "Clean typography", "Business analyst", "Operations"],
sidebarPosition: "left",
},
glalie: {
name: "Glalie",
description: msg`Two-column, minimal with light gray sidebar and subtle icons; professional and understated for legal, finance, or executive roles.`,
imageUrl: "/templates/jpg/glalie.jpg",
tags: ["Two-column", "Minimal", "Professional", "Legal", "Finance", "Executive", "Understated"],
sidebarPosition: "left",
},
kakuna: {
name: "Kakuna",
description: msg`Single-column with a magenta left border accent; compact and efficient for entry-level or internship applications.`,
imageUrl: "/templates/jpg/kakuna.jpg",
tags: ["Single-column", "ATS friendly", "Compact", "Efficient", "Entry level", "Internship", "Magenta accent"],
sidebarPosition: "none",
},
lapras: {
name: "Lapras",
description: msg`Single-column; polished and serious for senior or enterprise-level positions.`,
imageUrl: "/templates/jpg/lapras.jpg",
tags: ["Single-column", "ATS friendly", "Polished", "Senior", "Enterprise"],
sidebarPosition: "none",
},
leafish: {
name: "Leafish",
description: msg`Two-column with a muted color sidebar; earthy and calm, suits sustainability, healthcare, or nonprofit sectors.`,
imageUrl: "/templates/jpg/leafish.jpg",
tags: ["Two-column", "Muted sidebar", "Earthy", "Calm", "Sustainability", "Healthcare", "Nonprofit"],
sidebarPosition: "right",
},
onyx: {
name: "Onyx",
description: msg`Single-column with a sidebar and clean grid layout; versatile for any professional or technical role.`,
imageUrl: "/templates/jpg/onyx.jpg",
tags: ["Single-column", "ATS friendly", "Sidebar", "Grid layout", "Versatile", "Professional", "Technical"],
sidebarPosition: "none",
},
pikachu: {
name: "Pikachu",
description: msg`Two-column with a left margin color; simple and approachable for creative, editorial, or junior roles.`,
imageUrl: "/templates/jpg/pikachu.jpg",
tags: ["Two-column", "Simple", "Creative", "Editorial", "Junior", "Accent colors"],
sidebarPosition: "left",
},
rhyhorn: {
name: "Rhyhorn",
description: msg`Single-column with a minimal top header and lots of whitespace; clean and modern for designers or content creators.`,
imageUrl: "/templates/jpg/rhyhorn.jpg",
tags: ["Single-column", "ATS friendly", "Minimal", "Clean", "Modern", "Designer", "Content creator", "Whitespace"],
sidebarPosition: "none",
},
} as const satisfies Record<Template, TemplateMetadata>;
+90 -90
View File
@@ -16,108 +16,108 @@ import { cn } from "@/utils/style";
import { type TemplateMetadata, templates } from "./data";
export function TemplateGalleryDialog(_: DialogProps<"resume.template.gallery">) {
const closeDialog = useDialogStore((state) => state.closeDialog);
const selectedTemplate = useResumeStore((state) => state.resume.data.metadata.template);
const updateResumeData = useResumeStore((state) => state.updateResumeData);
const closeDialog = useDialogStore((state) => state.closeDialog);
const selectedTemplate = useResumeStore((state) => state.resume.data.metadata.template);
const updateResumeData = useResumeStore((state) => state.updateResumeData);
function onSelectTemplate(template: Template) {
updateResumeData((draft) => {
draft.metadata.template = template;
});
function onSelectTemplate(template: Template) {
updateResumeData((draft) => {
draft.metadata.template = template;
});
closeDialog();
}
closeDialog();
}
return (
<DialogContent className="lg:max-w-5xl">
<DialogHeader className="gap-2">
<DialogTitle className="flex items-center gap-3 text-xl">
<SlideshowIcon size={20} />
<Trans>Template Gallery</Trans>
</DialogTitle>
<DialogDescription className="leading-relaxed">
<Trans>
Here's a range of resume templates for different professions and personalities. Whether you prefer modern or
classic, bold or simple, there is a design to match you. Look through the options below and choose a
template that fits your style.
</Trans>
</DialogDescription>
</DialogHeader>
return (
<DialogContent className="lg:max-w-5xl">
<DialogHeader className="gap-2">
<DialogTitle className="flex items-center gap-3 text-xl">
<SlideshowIcon size={20} />
<Trans>Template Gallery</Trans>
</DialogTitle>
<DialogDescription className="leading-relaxed">
<Trans>
Here's a range of resume templates for different professions and personalities. Whether you prefer modern or
classic, bold or simple, there is a design to match you. Look through the options below and choose a
template that fits your style.
</Trans>
</DialogDescription>
</DialogHeader>
<ScrollArea className="max-h-[80svh] pb-8">
<div className="grid grid-cols-2 gap-6 p-4 md:grid-cols-3 lg:grid-cols-4">
{Object.entries(templates).map(([template, metadata]) => (
<TemplateCard
key={template}
metadata={metadata}
id={template as Template}
isActive={template === selectedTemplate}
onSelect={onSelectTemplate}
/>
))}
</div>
</ScrollArea>
</DialogContent>
);
<ScrollArea className="max-h-[80svh] pb-8">
<div className="grid grid-cols-2 gap-6 p-4 md:grid-cols-3 lg:grid-cols-4">
{Object.entries(templates).map(([template, metadata]) => (
<TemplateCard
key={template}
metadata={metadata}
id={template as Template}
isActive={template === selectedTemplate}
onSelect={onSelectTemplate}
/>
))}
</div>
</ScrollArea>
</DialogContent>
);
}
type TemplateCardProps = {
id: Template;
isActive?: boolean;
metadata: TemplateMetadata;
onSelect: (template: Template) => void;
id: Template;
isActive?: boolean;
metadata: TemplateMetadata;
onSelect: (template: Template) => void;
};
function TemplateCard({ id, metadata, isActive, onSelect }: TemplateCardProps) {
const { i18n } = useLingui();
const { i18n } = useLingui();
return (
<HoverCard>
<CometCard translateDepth={3} rotateDepth={6} glareOpacity={0}>
<HoverCardTrigger
render={
<button
tabIndex={-1}
onClick={() => onSelect(id)}
className={cn(
"relative block aspect-page size-full cursor-pointer overflow-hidden rounded-md bg-popover outline-none",
isActive && "ring-2 ring-ring ring-offset-4 ring-offset-background",
)}
>
<img src={metadata.imageUrl} alt={metadata.name} className="size-full object-cover" />
</button>
}
/>
return (
<HoverCard>
<CometCard translateDepth={3} rotateDepth={6} glareOpacity={0}>
<HoverCardTrigger
render={
<button
tabIndex={-1}
onClick={() => onSelect(id)}
className={cn(
"relative block aspect-page size-full cursor-pointer overflow-hidden rounded-md bg-popover outline-none",
isActive && "ring-2 ring-ring ring-offset-4 ring-offset-background",
)}
>
<img src={metadata.imageUrl} alt={metadata.name} className="size-full object-cover" />
</button>
}
/>
<div className="flex items-center justify-center">
<span className="leading-loose font-bold tracking-tight">{metadata.name}</span>
</div>
<div className="flex items-center justify-center">
<span className="leading-loose font-bold tracking-tight">{metadata.name}</span>
</div>
<HoverCardContent
side="right"
sideOffset={-32}
align="start"
alignOffset={32}
className="pointer-events-none! flex w-80 flex-col justify-between space-y-6 rounded-md bg-background/80 p-4 pb-6"
>
<div className="space-y-1">
<h3 className="text-lg font-semibold">{metadata.name}</h3>
<p className="text-muted-foreground">{i18n.t(metadata.description)}</p>
</div>
<HoverCardContent
side="right"
sideOffset={-32}
align="start"
alignOffset={32}
className="pointer-events-none! flex w-80 flex-col justify-between space-y-6 rounded-md bg-background/80 p-4 pb-6"
>
<div className="space-y-1">
<h3 className="text-lg font-semibold">{metadata.name}</h3>
<p className="text-muted-foreground">{i18n.t(metadata.description)}</p>
</div>
{metadata.tags.length > 0 && (
<div className="flex flex-wrap gap-2">
{metadata.tags
.sort((a, b) => a.localeCompare(b))
.map((tag) => (
<Badge key={tag} variant="default">
{tag}
</Badge>
))}
</div>
)}
</HoverCardContent>
</CometCard>
</HoverCard>
);
{metadata.tags.length > 0 && (
<div className="flex flex-wrap gap-2">
{metadata.tags
.sort((a, b) => a.localeCompare(b))
.map((tag) => (
<Badge key={tag} variant="default">
{tag}
</Badge>
))}
</div>
)}
</HoverCardContent>
</CometCard>
</HoverCard>
);
}
+176 -176
View File
@@ -2,159 +2,159 @@ import z from "zod";
import { create } from "zustand/react";
import {
awardItemSchema,
certificationItemSchema,
coverLetterItemSchema,
customSectionSchema,
educationItemSchema,
experienceItemSchema,
interestItemSchema,
languageItemSchema,
profileItemSchema,
projectItemSchema,
publicationItemSchema,
referenceItemSchema,
skillItemSchema,
summaryItemSchema,
volunteerItemSchema,
awardItemSchema,
certificationItemSchema,
coverLetterItemSchema,
customSectionSchema,
educationItemSchema,
experienceItemSchema,
interestItemSchema,
languageItemSchema,
profileItemSchema,
projectItemSchema,
publicationItemSchema,
referenceItemSchema,
skillItemSchema,
summaryItemSchema,
volunteerItemSchema,
} from "@/schema/resume/data";
const dialogTypeSchema = z.discriminatedUnion("type", [
z.object({ type: z.literal("auth.change-password"), data: z.undefined() }),
z.object({ type: z.literal("auth.two-factor.enable"), data: z.undefined() }),
z.object({ type: z.literal("auth.two-factor.disable"), data: z.undefined() }),
z.object({ type: z.literal("api-key.create"), data: z.undefined() }),
z.object({ type: z.literal("resume.create"), data: z.undefined() }),
z.object({
type: z.literal("resume.update"),
data: z.object({ id: z.string(), name: z.string(), slug: z.string(), tags: z.array(z.string()) }),
}),
z.object({ type: z.literal("resume.import"), data: z.undefined() }),
z.object({
type: z.literal("resume.duplicate"),
data: z.object({
id: z.string(),
name: z.string(),
slug: z.string(),
tags: z.array(z.string()),
shouldRedirect: z.boolean().optional(),
}),
}),
z.object({ type: z.literal("resume.template.gallery"), data: z.undefined() }),
z.object({
type: z.literal("resume.sections.profiles.create"),
data: z.object({ item: profileItemSchema.optional(), customSectionId: z.string().optional() }).optional(),
}),
z.object({
type: z.literal("resume.sections.profiles.update"),
data: z.object({ item: profileItemSchema, customSectionId: z.string().optional() }),
}),
z.object({
type: z.literal("resume.sections.experience.create"),
data: z.object({ item: experienceItemSchema.optional(), customSectionId: z.string().optional() }).optional(),
}),
z.object({
type: z.literal("resume.sections.experience.update"),
data: z.object({ item: experienceItemSchema, customSectionId: z.string().optional() }),
}),
z.object({
type: z.literal("resume.sections.education.create"),
data: z.object({ item: educationItemSchema.optional(), customSectionId: z.string().optional() }).optional(),
}),
z.object({
type: z.literal("resume.sections.education.update"),
data: z.object({ item: educationItemSchema, customSectionId: z.string().optional() }),
}),
z.object({
type: z.literal("resume.sections.projects.create"),
data: z.object({ item: projectItemSchema.optional(), customSectionId: z.string().optional() }).optional(),
}),
z.object({
type: z.literal("resume.sections.projects.update"),
data: z.object({ item: projectItemSchema, customSectionId: z.string().optional() }),
}),
z.object({
type: z.literal("resume.sections.skills.create"),
data: z.object({ item: skillItemSchema.optional(), customSectionId: z.string().optional() }).optional(),
}),
z.object({
type: z.literal("resume.sections.skills.update"),
data: z.object({ item: skillItemSchema, customSectionId: z.string().optional() }),
}),
z.object({
type: z.literal("resume.sections.languages.create"),
data: z.object({ item: languageItemSchema.optional(), customSectionId: z.string().optional() }).optional(),
}),
z.object({
type: z.literal("resume.sections.languages.update"),
data: z.object({ item: languageItemSchema, customSectionId: z.string().optional() }),
}),
z.object({
type: z.literal("resume.sections.awards.create"),
data: z.object({ item: awardItemSchema.optional(), customSectionId: z.string().optional() }).optional(),
}),
z.object({
type: z.literal("resume.sections.awards.update"),
data: z.object({ item: awardItemSchema, customSectionId: z.string().optional() }),
}),
z.object({
type: z.literal("resume.sections.certifications.create"),
data: z.object({ item: certificationItemSchema.optional(), customSectionId: z.string().optional() }).optional(),
}),
z.object({
type: z.literal("resume.sections.certifications.update"),
data: z.object({ item: certificationItemSchema, customSectionId: z.string().optional() }),
}),
z.object({
type: z.literal("resume.sections.publications.create"),
data: z.object({ item: publicationItemSchema.optional(), customSectionId: z.string().optional() }).optional(),
}),
z.object({
type: z.literal("resume.sections.publications.update"),
data: z.object({ item: publicationItemSchema, customSectionId: z.string().optional() }),
}),
z.object({
type: z.literal("resume.sections.interests.create"),
data: z.object({ item: interestItemSchema.optional(), customSectionId: z.string().optional() }).optional(),
}),
z.object({
type: z.literal("resume.sections.interests.update"),
data: z.object({ item: interestItemSchema, customSectionId: z.string().optional() }),
}),
z.object({
type: z.literal("resume.sections.volunteer.create"),
data: z.object({ item: volunteerItemSchema.optional(), customSectionId: z.string().optional() }).optional(),
}),
z.object({
type: z.literal("resume.sections.volunteer.update"),
data: z.object({ item: volunteerItemSchema, customSectionId: z.string().optional() }),
}),
z.object({
type: z.literal("resume.sections.references.create"),
data: z.object({ item: referenceItemSchema.optional(), customSectionId: z.string().optional() }).optional(),
}),
z.object({
type: z.literal("resume.sections.references.update"),
data: z.object({ item: referenceItemSchema, customSectionId: z.string().optional() }),
}),
z.object({
type: z.literal("resume.sections.summary.create"),
data: z.object({ item: summaryItemSchema.optional(), customSectionId: z.string().optional() }).optional(),
}),
z.object({
type: z.literal("resume.sections.summary.update"),
data: z.object({ item: summaryItemSchema, customSectionId: z.string().optional() }),
}),
z.object({
type: z.literal("resume.sections.cover-letter.create"),
data: z.object({ item: coverLetterItemSchema.optional(), customSectionId: z.string().optional() }).optional(),
}),
z.object({
type: z.literal("resume.sections.cover-letter.update"),
data: z.object({ item: coverLetterItemSchema, customSectionId: z.string().optional() }),
}),
z.object({ type: z.literal("resume.sections.custom.create"), data: customSectionSchema.optional() }),
z.object({ type: z.literal("resume.sections.custom.update"), data: customSectionSchema }),
z.object({ type: z.literal("auth.change-password"), data: z.undefined() }),
z.object({ type: z.literal("auth.two-factor.enable"), data: z.undefined() }),
z.object({ type: z.literal("auth.two-factor.disable"), data: z.undefined() }),
z.object({ type: z.literal("api-key.create"), data: z.undefined() }),
z.object({ type: z.literal("resume.create"), data: z.undefined() }),
z.object({
type: z.literal("resume.update"),
data: z.object({ id: z.string(), name: z.string(), slug: z.string(), tags: z.array(z.string()) }),
}),
z.object({ type: z.literal("resume.import"), data: z.undefined() }),
z.object({
type: z.literal("resume.duplicate"),
data: z.object({
id: z.string(),
name: z.string(),
slug: z.string(),
tags: z.array(z.string()),
shouldRedirect: z.boolean().optional(),
}),
}),
z.object({ type: z.literal("resume.template.gallery"), data: z.undefined() }),
z.object({
type: z.literal("resume.sections.profiles.create"),
data: z.object({ item: profileItemSchema.optional(), customSectionId: z.string().optional() }).optional(),
}),
z.object({
type: z.literal("resume.sections.profiles.update"),
data: z.object({ item: profileItemSchema, customSectionId: z.string().optional() }),
}),
z.object({
type: z.literal("resume.sections.experience.create"),
data: z.object({ item: experienceItemSchema.optional(), customSectionId: z.string().optional() }).optional(),
}),
z.object({
type: z.literal("resume.sections.experience.update"),
data: z.object({ item: experienceItemSchema, customSectionId: z.string().optional() }),
}),
z.object({
type: z.literal("resume.sections.education.create"),
data: z.object({ item: educationItemSchema.optional(), customSectionId: z.string().optional() }).optional(),
}),
z.object({
type: z.literal("resume.sections.education.update"),
data: z.object({ item: educationItemSchema, customSectionId: z.string().optional() }),
}),
z.object({
type: z.literal("resume.sections.projects.create"),
data: z.object({ item: projectItemSchema.optional(), customSectionId: z.string().optional() }).optional(),
}),
z.object({
type: z.literal("resume.sections.projects.update"),
data: z.object({ item: projectItemSchema, customSectionId: z.string().optional() }),
}),
z.object({
type: z.literal("resume.sections.skills.create"),
data: z.object({ item: skillItemSchema.optional(), customSectionId: z.string().optional() }).optional(),
}),
z.object({
type: z.literal("resume.sections.skills.update"),
data: z.object({ item: skillItemSchema, customSectionId: z.string().optional() }),
}),
z.object({
type: z.literal("resume.sections.languages.create"),
data: z.object({ item: languageItemSchema.optional(), customSectionId: z.string().optional() }).optional(),
}),
z.object({
type: z.literal("resume.sections.languages.update"),
data: z.object({ item: languageItemSchema, customSectionId: z.string().optional() }),
}),
z.object({
type: z.literal("resume.sections.awards.create"),
data: z.object({ item: awardItemSchema.optional(), customSectionId: z.string().optional() }).optional(),
}),
z.object({
type: z.literal("resume.sections.awards.update"),
data: z.object({ item: awardItemSchema, customSectionId: z.string().optional() }),
}),
z.object({
type: z.literal("resume.sections.certifications.create"),
data: z.object({ item: certificationItemSchema.optional(), customSectionId: z.string().optional() }).optional(),
}),
z.object({
type: z.literal("resume.sections.certifications.update"),
data: z.object({ item: certificationItemSchema, customSectionId: z.string().optional() }),
}),
z.object({
type: z.literal("resume.sections.publications.create"),
data: z.object({ item: publicationItemSchema.optional(), customSectionId: z.string().optional() }).optional(),
}),
z.object({
type: z.literal("resume.sections.publications.update"),
data: z.object({ item: publicationItemSchema, customSectionId: z.string().optional() }),
}),
z.object({
type: z.literal("resume.sections.interests.create"),
data: z.object({ item: interestItemSchema.optional(), customSectionId: z.string().optional() }).optional(),
}),
z.object({
type: z.literal("resume.sections.interests.update"),
data: z.object({ item: interestItemSchema, customSectionId: z.string().optional() }),
}),
z.object({
type: z.literal("resume.sections.volunteer.create"),
data: z.object({ item: volunteerItemSchema.optional(), customSectionId: z.string().optional() }).optional(),
}),
z.object({
type: z.literal("resume.sections.volunteer.update"),
data: z.object({ item: volunteerItemSchema, customSectionId: z.string().optional() }),
}),
z.object({
type: z.literal("resume.sections.references.create"),
data: z.object({ item: referenceItemSchema.optional(), customSectionId: z.string().optional() }).optional(),
}),
z.object({
type: z.literal("resume.sections.references.update"),
data: z.object({ item: referenceItemSchema, customSectionId: z.string().optional() }),
}),
z.object({
type: z.literal("resume.sections.summary.create"),
data: z.object({ item: summaryItemSchema.optional(), customSectionId: z.string().optional() }).optional(),
}),
z.object({
type: z.literal("resume.sections.summary.update"),
data: z.object({ item: summaryItemSchema, customSectionId: z.string().optional() }),
}),
z.object({
type: z.literal("resume.sections.cover-letter.create"),
data: z.object({ item: coverLetterItemSchema.optional(), customSectionId: z.string().optional() }).optional(),
}),
z.object({
type: z.literal("resume.sections.cover-letter.update"),
data: z.object({ item: coverLetterItemSchema, customSectionId: z.string().optional() }),
}),
z.object({ type: z.literal("resume.sections.custom.create"), data: customSectionSchema.optional() }),
z.object({ type: z.literal("resume.sections.custom.update"), data: customSectionSchema }),
]);
type DialogSchema = z.infer<typeof dialogTypeSchema>;
@@ -167,39 +167,39 @@ type DialogPropsData<T extends DialogType> = DialogData<T> extends undefined ? {
export type DialogProps<T extends DialogType> = DialogPropsData<T>;
interface DialogStoreState {
open: boolean;
activeDialog: DialogSchema | null;
open: boolean;
activeDialog: DialogSchema | null;
}
interface DialogStoreActions {
onOpenChange: (open: boolean) => void;
openDialog: <T extends DialogType>(type: T, data: DialogData<T>) => void;
closeDialog: () => void;
onOpenChange: (open: boolean) => void;
openDialog: <T extends DialogType>(type: T, data: DialogData<T>) => void;
closeDialog: () => void;
}
type DialogStore = DialogStoreState & DialogStoreActions;
export const useDialogStore = create<DialogStore>((set) => ({
open: false,
activeDialog: null,
onOpenChange: (open) => {
set({ open });
open: false,
activeDialog: null,
onOpenChange: (open) => {
set({ open });
if (!open) {
setTimeout(() => {
set({ activeDialog: null });
}, 300);
}
},
openDialog: (type, data) =>
set({
open: true,
activeDialog: { type, data } as DialogSchema,
}),
closeDialog: () => {
set({ open: false });
setTimeout(() => {
set({ activeDialog: null });
}, 300);
},
if (!open) {
setTimeout(() => {
set({ activeDialog: null });
}, 300);
}
},
openDialog: (type, data) =>
set({
open: true,
activeDialog: { type, data } as DialogSchema,
}),
closeDialog: () => {
set({ open: false });
setTimeout(() => {
set({ activeDialog: null });
}, 300);
},
}));