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,
|
||||
} 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>
|
||||
);
|
||||
};
|
||||
|
||||
@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@ -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,
|
||||
|
||||
Reference in New Issue
Block a user