updated(builder-page): added collapsible feature for each section

This commit is contained in:
abhas20
2025-10-24 18:38:52 +05:30
parent 6fcb7a4845
commit 4c0cc947a2
3 changed files with 162 additions and 88 deletions

View File

@ -15,7 +15,7 @@ import {
verticalListSortingStrategy,
} from "@dnd-kit/sortable";
import { t } from "@lingui/macro";
import { PlusIcon } from "@phosphor-icons/react";
import { CaretDownIcon, CaretRightIcon, PlusIcon } from "@phosphor-icons/react";
import type { SectionItem, SectionKey, SectionWithItem } from "@reactive-resume/schema";
import { Button } from "@reactive-resume/ui";
import { cn } from "@reactive-resume/utils";
@ -38,10 +38,13 @@ type Props<T extends SectionItem> = {
export const SectionBase = <T extends SectionItem>({ id, title, description }: Props<T>) => {
const { open } = useDialog(id);
const collapsed = useResumeStore((state) => state.collapsedSections[id] ?? false);
const toggleSectionCollapse = useResumeStore((state) => state.toggleSectionCollapsed);
const setValue = useResumeStore((state) => state.setValue);
const section = useResumeStore((state) =>
get(state.resume.data.sections, id),
) as SectionWithItem<T>;
const section = useResumeStore(
(state) => get(state.resume.data.sections, id) as SectionWithItem<T>,
);
const sensors = useSensors(
useSensor(PointerSensor),
@ -97,7 +100,17 @@ export const SectionBase = <T extends SectionItem>({ id, title, description }: P
className="grid gap-y-6"
>
<header className="flex items-center justify-between">
<div className="flex items-center gap-x-4">
<div className="flex items-center gap-x-2">
<button
className="text-gray-500 transition-colors hover:text-gray-700"
aria-label={collapsed ? t`Expand section` : t`Collapse section`}
onClick={() => {
toggleSectionCollapse(id);
}}
>
{collapsed ? <CaretRightIcon size={18} /> : <CaretDownIcon size={18} />}
</button>
<SectionIcon id={id} size={18} />
<h2 className="line-clamp-1 text-2xl font-bold lg:text-3xl">{section.name}</h2>
</div>
@ -107,74 +120,91 @@ export const SectionBase = <T extends SectionItem>({ id, title, description }: P
</div>
</header>
<main className={cn("grid transition-opacity", !section.visible && "opacity-50")}>
{section.items.length === 0 && (
<Button
variant="outline"
className="gap-x-2 border-dashed py-6 leading-relaxed hover:bg-secondary-accent"
onClick={onCreate}
<AnimatePresence initial={false}>
{!collapsed && (
<motion.div
key="content"
initial={{ height: 0, opacity: 0 }}
animate={{ height: "auto", opacity: 1 }}
exit={{ height: 0, opacity: 0 }}
transition={{ duration: 0.25, ease: "easeInOut" }}
className="overflow-hidden"
>
<PlusIcon size={14} />
<span className="font-medium">
{t({
message: "Add a new item",
context: "For example, add a new work experience, or add a new profile.",
})}
</span>
</Button>
<main className={cn("grid transition-opacity", !section.visible && "opacity-50")}>
{section.items.length === 0 && (
<Button
className="gap-x-2 border-dashed py-6 leading-relaxed hover:bg-secondary-accent"
variant="outline"
onClick={() => {
onCreate();
}}
>
<PlusIcon size={14} />
<span className="font-medium">
{t({
message: "Add a new item",
context: "For example, add a new work experience, or add a new profile.",
})}
</span>
</Button>
)}
<DndContext
sensors={sensors}
collisionDetection={closestCenter}
modifiers={[restrictToParentElement]}
onDragEnd={onDragEnd}
>
<SortableContext items={section.items} strategy={verticalListSortingStrategy}>
<AnimatePresence>
{section.items.map((item, index) => (
<SectionListItem
key={item.id}
id={item.id}
visible={item.visible}
title={title(item as T)}
description={description?.(item as T)}
onUpdate={() => {
onUpdate(item as T);
}}
onDelete={() => {
onDelete(item as T);
}}
onDuplicate={() => {
onDuplicate(item as T);
}}
onToggleVisibility={() => {
onToggleVisibility(index);
}}
/>
))}
</AnimatePresence>
</SortableContext>
</DndContext>
</main>
{section.items.length > 0 && (
<footer className="flex items-center justify-end">
<Button
className="ml-auto gap-x-2 text-xs lg:text-sm"
variant="outline"
onClick={() => {
onCreate();
}}
>
<PlusIcon />
<span>
{t({
message: "Add a new item",
context: "For example, add a new work experience, or add a new profile.",
})}
</span>
</Button>
</footer>
)}
</motion.div>
)}
<DndContext
sensors={sensors}
collisionDetection={closestCenter}
modifiers={[restrictToParentElement]}
onDragEnd={onDragEnd}
>
<SortableContext items={section.items} strategy={verticalListSortingStrategy}>
<AnimatePresence>
{section.items.map((item, index) => (
<SectionListItem
key={item.id}
id={item.id}
visible={item.visible}
title={title(item as T)}
description={description?.(item as T)}
onUpdate={() => {
onUpdate(item as T);
}}
onDelete={() => {
onDelete(item as T);
}}
onDuplicate={() => {
onDuplicate(item as T);
}}
onToggleVisibility={() => {
onToggleVisibility(index);
}}
/>
))}
</AnimatePresence>
</SortableContext>
</DndContext>
</main>
{section.items.length > 0 && (
<footer className="flex items-center justify-end">
<Button
variant="outline"
className="ml-auto gap-x-2 text-xs lg:text-sm"
onClick={onCreate}
>
<PlusIcon />
<span>
{t({
message: "Add a new item",
context: "For example, add a new work experience, or add a new profile.",
})}
</span>
</Button>
</footer>
)}
</AnimatePresence>
</motion.section>
);
};

View File

@ -1,6 +1,9 @@
import { t } from "@lingui/macro";
import { CaretDownIcon, CaretRightIcon } from "@phosphor-icons/react";
import { defaultSections } from "@reactive-resume/schema";
import { RichInput } from "@reactive-resume/ui";
import { cn } from "@reactive-resume/utils";
import { AnimatePresence, motion } from "framer-motion";
import { AiActions } from "@/client/components/ai-actions";
import { useResumeStore } from "@/client/stores/resume";
@ -14,11 +17,23 @@ export const SummarySection = () => {
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
(state) => state.resume.data.sections.summary ?? defaultSections.summary,
);
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
const collapsed = useResumeStore((state) => state.collapsedSections.summary ?? false);
const toogleSectionCollapse = useResumeStore((state) => state.toggleSectionCollapsed);
return (
<section id="summary" className="grid gap-y-6">
<header className="flex items-center justify-between">
<div className="flex items-center gap-x-4">
<button
className="text-gray-500 transition-colors hover:text-gray-700"
aria-label={collapsed ? t`Expand section` : t`Collapse section`}
onClick={() => {
toogleSectionCollapse("summary");
}}
>
{collapsed ? <CaretRightIcon size={18} /> : <CaretDownIcon size={18} />}
</button>
<SectionIcon id="summary" size={18} />
<h2 className="line-clamp-1 text-2xl font-bold lg:text-3xl">{section.name}</h2>
</div>
@ -28,23 +43,35 @@ export const SummarySection = () => {
</div>
</header>
<main className={cn(!section.visible && "opacity-50")}>
<RichInput
content={section.content}
footer={(editor) => (
<AiActions
value={editor.getText()}
onChange={(value) => {
editor.commands.setContent(value, true);
setValue("sections.summary.content", value);
}}
/>
)}
onChange={(value) => {
setValue("sections.summary.content", value);
}}
/>
</main>
<AnimatePresence>
{!collapsed && (
<motion.div
key="summary-content"
initial={{ opacity: 0, height: 0 }}
animate={{ opacity: 1, height: "auto" }}
exit={{ opacity: 0, height: 0 }}
transition={{ duration: 0.2 }}
>
<main className={cn(!section.visible && "opacity-50")}>
<RichInput
content={section.content}
footer={(editor) => (
<AiActions
value={editor.getText()}
onChange={(value) => {
editor.commands.setContent(value, true);
setValue("sections.summary.content", value);
}}
/>
)}
onChange={(value) => {
setValue("sections.summary.content", value);
}}
/>
</main>
</motion.div>
)}
</AnimatePresence>
</section>
);
};

View File

@ -23,6 +23,10 @@ type ResumeStore = {
// Custom Section Actions
addSection: () => void;
removeSection: (sectionId: SectionKey) => void;
collapsedSections: Record<string, boolean>;
toggleSectionCollapsed: (id: string) => void;
setSectionCollapsed: (id: string, collapsed: boolean) => void;
};
export const useResumeStore = create<ResumeStore>()(
@ -69,6 +73,19 @@ export const useResumeStore = create<ResumeStore>()(
});
}
},
collapsedSections: {},
toggleSectionCollapsed: (id) => {
set((state) => {
state.collapsedSections[id] = !state.collapsedSections[id];
});
},
setSectionCollapsed: (id, collapsed) => {
set((state) => {
state.collapsedSections[id] = collapsed;
});
},
})),
{
limit: 100,