mirror of
https://github.com/AmruthPillai/Reactive-Resume.git
synced 2026-07-12 05:55:02 +10:00
Feature: Implement a new custom section type: summary (#2657)
* feat: add summaryItemSchema for custom summary section type * feat: add CreateSummaryItemDialog and UpdateSummaryItemDialog * feat: register summary item dialog types in store * feat: route summary item dialogs in manager * feat: add SummaryItem render component * feat: handle summary type in renderItemByType and hide title for summary sections * feat: handle summary type in sidebar helpers * feat: add summary to custom section type options * fix: update type definitions to support CustomSectionType for summary sections * chore: extract new i18n strings for summary section * style: apply biome formatting fixes * chore: remove TODO.md file containing outdated feature specifications
This commit is contained in:
@@ -18,6 +18,7 @@ import { CreateProjectDialog, UpdateProjectDialog } from "./resume/sections/proj
|
||||
import { CreatePublicationDialog, UpdatePublicationDialog } from "./resume/sections/publication";
|
||||
import { CreateReferenceDialog, UpdateReferenceDialog } from "./resume/sections/reference";
|
||||
import { CreateSkillDialog, UpdateSkillDialog } from "./resume/sections/skill";
|
||||
import { CreateSummaryItemDialog, UpdateSummaryItemDialog } from "./resume/sections/summary-item";
|
||||
import { CreateVolunteerDialog, UpdateVolunteerDialog } from "./resume/sections/volunteer";
|
||||
import { TemplateGalleryDialog } from "./resume/template/gallery";
|
||||
import { useDialogStore } from "./store";
|
||||
@@ -59,6 +60,8 @@ export function DialogManager() {
|
||||
.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.custom.create" }, ({ data }) => <CreateCustomSectionDialog data={data} />)
|
||||
.with({ type: "resume.sections.custom.update" }, ({ data }) => <UpdateCustomSectionDialog data={data} />)
|
||||
.otherwise(() => null);
|
||||
|
||||
@@ -11,14 +11,15 @@ import { Input } from "@/components/ui/input";
|
||||
import type { DialogProps } from "@/dialogs/store";
|
||||
import { useDialogStore } from "@/dialogs/store";
|
||||
import { useFormBlocker } from "@/hooks/use-form-blocker";
|
||||
import { customSectionSchema, type SectionType } from "@/schema/resume/data";
|
||||
import { type CustomSectionType, customSectionSchema } from "@/schema/resume/data";
|
||||
import { generateId } from "@/utils/string";
|
||||
|
||||
const formSchema = customSectionSchema;
|
||||
|
||||
type FormValues = z.infer<typeof formSchema>;
|
||||
|
||||
const SECTION_TYPE_OPTIONS: { value: SectionType; label: string }[] = [
|
||||
const SECTION_TYPE_OPTIONS: { value: CustomSectionType; label: string }[] = [
|
||||
{ value: "summary", label: "Summary" },
|
||||
{ value: "experience", label: "Experience" },
|
||||
{ value: "education", label: "Education" },
|
||||
{ value: "projects", label: "Projects" },
|
||||
|
||||
@@ -0,0 +1,151 @@
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { Trans } from "@lingui/react/macro";
|
||||
import { PencilSimpleLineIcon, PlusIcon } from "@phosphor-icons/react";
|
||||
import { useForm, useFormContext } from "react-hook-form";
|
||||
import type z from "zod";
|
||||
import { RichInput } from "@/components/input/rich-input";
|
||||
import { useResumeStore } from "@/components/resume/store/resume";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from "@/components/ui/dialog";
|
||||
import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from "@/components/ui/form";
|
||||
import type { DialogProps } from "@/dialogs/store";
|
||||
import { useDialogStore } from "@/dialogs/store";
|
||||
import { useFormBlocker } from "@/hooks/use-form-blocker";
|
||||
import { summaryItemSchema } from "@/schema/resume/data";
|
||||
import { generateId } from "@/utils/string";
|
||||
|
||||
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 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 { 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>
|
||||
|
||||
<Form {...form}>
|
||||
<form className="grid gap-4" onSubmit={form.handleSubmit(onSubmit)}>
|
||||
<SummaryItemForm />
|
||||
|
||||
<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>
|
||||
);
|
||||
}
|
||||
|
||||
export function UpdateSummaryItemDialog({ data }: DialogProps<"resume.sections.summary.update">) {
|
||||
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 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);
|
||||
|
||||
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 />
|
||||
|
||||
<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>
|
||||
);
|
||||
}
|
||||
|
||||
function SummaryItemForm() {
|
||||
const form = useFormContext<FormValues>();
|
||||
|
||||
return (
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="content"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>
|
||||
<Trans>Content</Trans>
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<RichInput {...field} value={field.value} onChange={field.onChange} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
publicationItemSchema,
|
||||
referenceItemSchema,
|
||||
skillItemSchema,
|
||||
summaryItemSchema,
|
||||
volunteerItemSchema,
|
||||
} from "@/schema/resume/data";
|
||||
|
||||
@@ -134,6 +135,14 @@ const dialogTypeSchema = z.discriminatedUnion("type", [
|
||||
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.custom.create"), data: customSectionSchema.optional() }),
|
||||
z.object({ type: z.literal("resume.sections.custom.update"), data: customSectionSchema }),
|
||||
]);
|
||||
|
||||
Reference in New Issue
Block a user