mirror of
https://github.com/AmruthPillai/Reactive-Resume.git
synced 2025-11-10 04:22:27 +10:00
updated(builder-page): added collapsible feature for each section
This commit is contained in:
@ -15,7 +15,7 @@ import {
|
|||||||
verticalListSortingStrategy,
|
verticalListSortingStrategy,
|
||||||
} from "@dnd-kit/sortable";
|
} from "@dnd-kit/sortable";
|
||||||
import { t } from "@lingui/macro";
|
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 type { SectionItem, SectionKey, SectionWithItem } from "@reactive-resume/schema";
|
||||||
import { Button } from "@reactive-resume/ui";
|
import { Button } from "@reactive-resume/ui";
|
||||||
import { cn } from "@reactive-resume/utils";
|
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>) => {
|
export const SectionBase = <T extends SectionItem>({ id, title, description }: Props<T>) => {
|
||||||
const { open } = useDialog(id);
|
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 setValue = useResumeStore((state) => state.setValue);
|
||||||
const section = useResumeStore((state) =>
|
const section = useResumeStore(
|
||||||
get(state.resume.data.sections, id),
|
(state) => get(state.resume.data.sections, id) as SectionWithItem<T>,
|
||||||
) as SectionWithItem<T>;
|
);
|
||||||
|
|
||||||
const sensors = useSensors(
|
const sensors = useSensors(
|
||||||
useSensor(PointerSensor),
|
useSensor(PointerSensor),
|
||||||
@ -97,7 +100,17 @@ export const SectionBase = <T extends SectionItem>({ id, title, description }: P
|
|||||||
className="grid gap-y-6"
|
className="grid gap-y-6"
|
||||||
>
|
>
|
||||||
<header className="flex items-center justify-between">
|
<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} />
|
<SectionIcon id={id} size={18} />
|
||||||
<h2 className="line-clamp-1 text-2xl font-bold lg:text-3xl">{section.name}</h2>
|
<h2 className="line-clamp-1 text-2xl font-bold lg:text-3xl">{section.name}</h2>
|
||||||
</div>
|
</div>
|
||||||
@ -107,74 +120,91 @@ export const SectionBase = <T extends SectionItem>({ id, title, description }: P
|
|||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<main className={cn("grid transition-opacity", !section.visible && "opacity-50")}>
|
<AnimatePresence initial={false}>
|
||||||
{section.items.length === 0 && (
|
{!collapsed && (
|
||||||
<Button
|
<motion.div
|
||||||
variant="outline"
|
key="content"
|
||||||
className="gap-x-2 border-dashed py-6 leading-relaxed hover:bg-secondary-accent"
|
initial={{ height: 0, opacity: 0 }}
|
||||||
onClick={onCreate}
|
animate={{ height: "auto", opacity: 1 }}
|
||||||
|
exit={{ height: 0, opacity: 0 }}
|
||||||
|
transition={{ duration: 0.25, ease: "easeInOut" }}
|
||||||
|
className="overflow-hidden"
|
||||||
>
|
>
|
||||||
<PlusIcon size={14} />
|
<main className={cn("grid transition-opacity", !section.visible && "opacity-50")}>
|
||||||
<span className="font-medium">
|
{section.items.length === 0 && (
|
||||||
{t({
|
<Button
|
||||||
message: "Add a new item",
|
className="gap-x-2 border-dashed py-6 leading-relaxed hover:bg-secondary-accent"
|
||||||
context: "For example, add a new work experience, or add a new profile.",
|
variant="outline"
|
||||||
})}
|
onClick={() => {
|
||||||
</span>
|
onCreate();
|
||||||
</Button>
|
}}
|
||||||
|
>
|
||||||
|
<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>
|
||||||
)}
|
)}
|
||||||
|
</AnimatePresence>
|
||||||
<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>
|
|
||||||
)}
|
|
||||||
</motion.section>
|
</motion.section>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1,6 +1,9 @@
|
|||||||
|
import { t } from "@lingui/macro";
|
||||||
|
import { CaretDownIcon, CaretRightIcon } from "@phosphor-icons/react";
|
||||||
import { defaultSections } from "@reactive-resume/schema";
|
import { defaultSections } from "@reactive-resume/schema";
|
||||||
import { RichInput } from "@reactive-resume/ui";
|
import { RichInput } from "@reactive-resume/ui";
|
||||||
import { cn } from "@reactive-resume/utils";
|
import { cn } from "@reactive-resume/utils";
|
||||||
|
import { AnimatePresence, motion } from "framer-motion";
|
||||||
|
|
||||||
import { AiActions } from "@/client/components/ai-actions";
|
import { AiActions } from "@/client/components/ai-actions";
|
||||||
import { useResumeStore } from "@/client/stores/resume";
|
import { useResumeStore } from "@/client/stores/resume";
|
||||||
@ -14,11 +17,23 @@ export const SummarySection = () => {
|
|||||||
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
||||||
(state) => state.resume.data.sections.summary ?? defaultSections.summary,
|
(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 (
|
return (
|
||||||
<section id="summary" className="grid gap-y-6">
|
<section id="summary" className="grid gap-y-6">
|
||||||
<header className="flex items-center justify-between">
|
<header className="flex items-center justify-between">
|
||||||
<div className="flex items-center gap-x-4">
|
<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} />
|
<SectionIcon id="summary" size={18} />
|
||||||
<h2 className="line-clamp-1 text-2xl font-bold lg:text-3xl">{section.name}</h2>
|
<h2 className="line-clamp-1 text-2xl font-bold lg:text-3xl">{section.name}</h2>
|
||||||
</div>
|
</div>
|
||||||
@ -28,23 +43,35 @@ export const SummarySection = () => {
|
|||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<main className={cn(!section.visible && "opacity-50")}>
|
<AnimatePresence>
|
||||||
<RichInput
|
{!collapsed && (
|
||||||
content={section.content}
|
<motion.div
|
||||||
footer={(editor) => (
|
key="summary-content"
|
||||||
<AiActions
|
initial={{ opacity: 0, height: 0 }}
|
||||||
value={editor.getText()}
|
animate={{ opacity: 1, height: "auto" }}
|
||||||
onChange={(value) => {
|
exit={{ opacity: 0, height: 0 }}
|
||||||
editor.commands.setContent(value, true);
|
transition={{ duration: 0.2 }}
|
||||||
setValue("sections.summary.content", value);
|
>
|
||||||
}}
|
<main className={cn(!section.visible && "opacity-50")}>
|
||||||
/>
|
<RichInput
|
||||||
)}
|
content={section.content}
|
||||||
onChange={(value) => {
|
footer={(editor) => (
|
||||||
setValue("sections.summary.content", value);
|
<AiActions
|
||||||
}}
|
value={editor.getText()}
|
||||||
/>
|
onChange={(value) => {
|
||||||
</main>
|
editor.commands.setContent(value, true);
|
||||||
|
setValue("sections.summary.content", value);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
onChange={(value) => {
|
||||||
|
setValue("sections.summary.content", value);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</main>
|
||||||
|
</motion.div>
|
||||||
|
)}
|
||||||
|
</AnimatePresence>
|
||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -23,6 +23,10 @@ type ResumeStore = {
|
|||||||
// Custom Section Actions
|
// Custom Section Actions
|
||||||
addSection: () => void;
|
addSection: () => void;
|
||||||
removeSection: (sectionId: SectionKey) => void;
|
removeSection: (sectionId: SectionKey) => void;
|
||||||
|
|
||||||
|
collapsedSections: Record<string, boolean>;
|
||||||
|
toggleSectionCollapsed: (id: string) => void;
|
||||||
|
setSectionCollapsed: (id: string, collapsed: boolean) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const useResumeStore = create<ResumeStore>()(
|
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,
|
limit: 100,
|
||||||
|
|||||||
Reference in New Issue
Block a user