mirror of
https://github.com/AmruthPillai/Reactive-Resume.git
synced 2025-11-26 14:33:59 +10:00
refactor(v4.0.0-alpha): beginning of a new era
This commit is contained in:
269
apps/client/src/pages/builder/sidebars/right/sections/layout.tsx
Normal file
269
apps/client/src/pages/builder/sidebars/right/sections/layout.tsx
Normal file
@@ -0,0 +1,269 @@
|
||||
import {
|
||||
closestCenter,
|
||||
DndContext,
|
||||
DragEndEvent,
|
||||
DragOverEvent,
|
||||
DragOverlay,
|
||||
DragStartEvent,
|
||||
KeyboardSensor,
|
||||
PointerSensor,
|
||||
useDroppable,
|
||||
useSensor,
|
||||
useSensors,
|
||||
} from "@dnd-kit/core";
|
||||
import {
|
||||
SortableContext,
|
||||
sortableKeyboardCoordinates,
|
||||
useSortable,
|
||||
verticalListSortingStrategy,
|
||||
} from "@dnd-kit/sortable";
|
||||
import { CSS } from "@dnd-kit/utilities";
|
||||
import { ArrowCounterClockwise, DotsSixVertical, Plus, TrashSimple } from "@phosphor-icons/react";
|
||||
import { defaultMetadata } from "@reactive-resume/schema";
|
||||
import { Button, Portal, Tooltip } from "@reactive-resume/ui";
|
||||
import {
|
||||
cn,
|
||||
LayoutLocator,
|
||||
moveItemInLayout,
|
||||
parseLayoutLocator,
|
||||
SortablePayload,
|
||||
} from "@reactive-resume/utils";
|
||||
import get from "lodash.get";
|
||||
import { useState } from "react";
|
||||
|
||||
import { useResumeStore } from "@/client/stores/resume";
|
||||
|
||||
import { getSectionIcon } from "../shared/section-icon";
|
||||
|
||||
type ColumnProps = {
|
||||
id: string;
|
||||
name: string;
|
||||
items: string[];
|
||||
};
|
||||
|
||||
const Column = ({ id, name, items }: ColumnProps) => {
|
||||
const { setNodeRef } = useDroppable({ id });
|
||||
|
||||
return (
|
||||
<SortableContext id={id} items={items} strategy={verticalListSortingStrategy}>
|
||||
<div className="relative">
|
||||
<div className="absolute inset-0 w-3/4 rounded bg-secondary/50" />
|
||||
|
||||
<div className="relative z-10 p-3 pb-8">
|
||||
<p className="mb-3 text-xs font-bold">{name}</p>
|
||||
|
||||
<div ref={setNodeRef} className="space-y-3">
|
||||
{items.map((section) => (
|
||||
<SortableSection key={section} id={section} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</SortableContext>
|
||||
);
|
||||
};
|
||||
|
||||
type SortableSectionProps = {
|
||||
id: string;
|
||||
};
|
||||
|
||||
const SortableSection = ({ id }: SortableSectionProps) => {
|
||||
const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({
|
||||
id,
|
||||
});
|
||||
|
||||
const style = {
|
||||
transition,
|
||||
opacity: isDragging ? 0.5 : 1,
|
||||
transform: CSS.Translate.toString(transform),
|
||||
};
|
||||
|
||||
return (
|
||||
<div ref={setNodeRef} style={style} {...listeners} {...attributes}>
|
||||
<Section id={id} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
type SectionProps = {
|
||||
id: string;
|
||||
isDragging?: boolean;
|
||||
};
|
||||
|
||||
const Section = ({ id, isDragging = false }: SectionProps) => {
|
||||
const name = useResumeStore((state) =>
|
||||
get(state.resume.data.sections, `${id}.name`, id),
|
||||
) as string;
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"cursor-grab rounded bg-primary p-2 text-primary-foreground transition-colors hover:bg-primary-accent",
|
||||
isDragging && "cursor-grabbing",
|
||||
)}
|
||||
>
|
||||
<div className="flex items-center gap-x-2">
|
||||
<DotsSixVertical size={12} weight="bold" />
|
||||
<p className="flex-1 truncate text-xs font-medium">{name}</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const LayoutSection = () => {
|
||||
const setValue = useResumeStore((state) => state.setValue);
|
||||
const layout = useResumeStore((state) => state.resume.data.metadata.layout);
|
||||
|
||||
const [activeId, setActiveId] = useState<string | null>(null);
|
||||
|
||||
const sensors = useSensors(
|
||||
useSensor(PointerSensor),
|
||||
useSensor(KeyboardSensor, {
|
||||
coordinateGetter: sortableKeyboardCoordinates,
|
||||
}),
|
||||
);
|
||||
|
||||
const onDragStart = ({ active }: DragStartEvent) => {
|
||||
setActiveId(active.id as string);
|
||||
};
|
||||
|
||||
const onDragCancel = () => {
|
||||
setActiveId(null);
|
||||
};
|
||||
|
||||
const onDragEvent = ({ active, over }: DragOverEvent | DragEndEvent) => {
|
||||
if (!over || !active.data.current) return;
|
||||
|
||||
const currentPayload = active.data.current.sortable as SortablePayload | null;
|
||||
const current = parseLayoutLocator(currentPayload);
|
||||
|
||||
if (active.id === over.id) return;
|
||||
|
||||
if (!over.data.current) {
|
||||
const [page, column] = (over.id as string).split(".").map(Number);
|
||||
const target = { page, column, section: 0 } as LayoutLocator;
|
||||
|
||||
const newLayout = moveItemInLayout(current, target, layout);
|
||||
setValue("metadata.layout", newLayout);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const targetPayload = over.data.current.sortable as SortablePayload | null;
|
||||
const target = parseLayoutLocator(targetPayload);
|
||||
|
||||
const newLayout = moveItemInLayout(current, target, layout);
|
||||
setValue("metadata.layout", newLayout);
|
||||
};
|
||||
|
||||
const onDragEnd = (event: DragEndEvent) => {
|
||||
onDragEvent(event);
|
||||
setActiveId(null);
|
||||
};
|
||||
|
||||
const onAddPage = () => {
|
||||
const layoutCopy = JSON.parse(JSON.stringify(layout)) as string[][][];
|
||||
|
||||
layoutCopy.push([[], []]);
|
||||
|
||||
setValue("metadata.layout", layoutCopy);
|
||||
};
|
||||
|
||||
const onRemovePage = (page: number) => {
|
||||
const layoutCopy = JSON.parse(JSON.stringify(layout)) as string[][][];
|
||||
|
||||
layoutCopy[0][0].push(...layoutCopy[page][0]); // Main
|
||||
layoutCopy[0][1].push(...layoutCopy[page][1]); // Sidebar
|
||||
|
||||
layoutCopy.splice(page, 1);
|
||||
|
||||
setValue("metadata.layout", layoutCopy);
|
||||
};
|
||||
|
||||
const onResetLayout = () => {
|
||||
const layoutCopy = JSON.parse(JSON.stringify(defaultMetadata.layout)) as string[][][];
|
||||
|
||||
// Loop through all pages and columns, and get any sections that start with "custom."
|
||||
// These should be appended to the first page of the new layout.
|
||||
const customSections: string[] = [];
|
||||
|
||||
layout.forEach((page) => {
|
||||
page.forEach((column) => {
|
||||
customSections.push(...column.filter((section) => section.startsWith("custom.")));
|
||||
});
|
||||
});
|
||||
|
||||
if (customSections.length > 0) layoutCopy[0][0].push(...customSections);
|
||||
|
||||
setValue("metadata.layout", layoutCopy);
|
||||
};
|
||||
|
||||
return (
|
||||
<section id="layout" className="grid gap-y-6">
|
||||
<header className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-x-4">
|
||||
{getSectionIcon("layout")}
|
||||
<h2 className="line-clamp-1 text-3xl font-bold">Layout</h2>
|
||||
</div>
|
||||
|
||||
<Tooltip content="Reset Layout">
|
||||
<Button size="icon" variant="ghost" onClick={onResetLayout}>
|
||||
<ArrowCounterClockwise />
|
||||
</Button>
|
||||
</Tooltip>
|
||||
</header>
|
||||
|
||||
<main className="grid gap-y-4">
|
||||
{/* Pages */}
|
||||
<DndContext
|
||||
sensors={sensors}
|
||||
onDragEnd={onDragEnd}
|
||||
onDragStart={onDragStart}
|
||||
onDragCancel={onDragCancel}
|
||||
collisionDetection={closestCenter}
|
||||
>
|
||||
{layout.map((page, pageIndex) => {
|
||||
const mainIndex = `${pageIndex}.0`;
|
||||
const sidebarIndex = `${pageIndex}.1`;
|
||||
|
||||
const main = page[0];
|
||||
const sidebar = page[1];
|
||||
|
||||
return (
|
||||
<div key={pageIndex} className="rounded border p-3 pb-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<p className="mb-3 text-xs font-bold">Page {pageIndex + 1}</p>
|
||||
|
||||
{pageIndex !== 0 && (
|
||||
<Button
|
||||
size="icon"
|
||||
variant="ghost"
|
||||
className="h-8 w-8"
|
||||
onClick={() => onRemovePage(pageIndex)}
|
||||
>
|
||||
<TrashSimple size={12} />
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 items-start gap-x-4">
|
||||
<Column id={mainIndex} name="Main" items={main} />
|
||||
<Column id={sidebarIndex} name="Sidebar" items={sidebar} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
|
||||
<Portal>
|
||||
<DragOverlay>{activeId && <Section id={activeId} isDragging />}</DragOverlay>
|
||||
</Portal>
|
||||
</DndContext>
|
||||
|
||||
<Button variant="outline" className="ml-auto" onClick={onAddPage}>
|
||||
<Plus />
|
||||
<span className="ml-2">Add New Page</span>
|
||||
</Button>
|
||||
</main>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user