Structuring and fixes

This commit is contained in:
2026-02-27 22:35:31 +01:00
parent f856edc916
commit 162e2b3d3f
7 changed files with 1231 additions and 39 deletions
+916
View File
File diff suppressed because it is too large Load Diff
+1
View File
@@ -39,6 +39,7 @@
"js-cookie": "^3.0.5",
"lucide-react": "^0.553.0",
"next": "16.0.1",
"radix-ui": "^1.4.3",
"react": "19.2.0",
"react-dom": "19.2.0",
"tailwind-merge": "^3.4.0"
+9 -3
View File
@@ -16,7 +16,8 @@ import {
export default function Workspace() {
const [currentWorkspace, setCurrentWorkspace] = useState<string>();
const { workspace, loadWorkspace } = useWorkspace();
const { workspace, loadWorkspace, updateEntity, setWorkspace } =
useWorkspace();
const [route, setRoute] = useState<Route>({});
@@ -31,7 +32,12 @@ export default function Workspace() {
}, [currentWorkspace]);
return workspace ? (
<AppSidebar workspace={workspace} setRoute={setRoute}>
<AppSidebar
workspace={workspace}
setRoute={setRoute}
updateEntity={updateEntity}
setWorkspace={setWorkspace}
>
<SidebarTrigger className="w-full px-4">
<div className="px-4">
<header className="w-full border-b-2 pb-3 pt-2 hover:bg-accent/20 flex items-center text-foreground/90">
@@ -42,7 +48,7 @@ export default function Workspace() {
</div>
</SidebarTrigger>
<Router workspace={workspace} route={route} />
<Router workspace={workspace} route={route} updateEntity={updateEntity} />
</AppSidebar>
) : (
<main
+21 -24
View File
@@ -1,6 +1,14 @@
"use client";
import { Route, Task, Workspace } from "@/lib/types";
import {
Entity,
EntityId,
Route,
Task,
TaskId,
TodoEntity,
Workspace,
} from "@/lib/types";
import TaskView from "./view/TaskView";
import DocView from "./view/DocView";
import { OutputData } from "@editorjs/editorjs";
@@ -11,32 +19,17 @@ const id = (prefix = "t") =>
export default function Router({
route,
workspace,
setWorkspace,
updateEntity,
}: {
route: Route;
workspace: Workspace;
setWorkspace: React.Dispatch<React.SetStateAction<Workspace>>;
updateEntity: (entityId: EntityId, updater: (e: Entity) => Entity) => void;
}) {
const entityId = route.entity;
const entity = entityId ? workspace.entities[entityId] : null;
if (!entity || !entityId) return null;
const updateEntity = (updater: (e: any) => any) => {
setWorkspace((prev) => {
const updated = updater(prev.entities[entityId]);
return {
...prev,
updatedAt: Date.now(),
entities: {
...prev.entities,
[entityId]: updated,
},
};
});
};
switch (entity.type) {
case "todo": {
const createTask = (statusId: string) => {
@@ -49,16 +42,20 @@ export default function Router({
updatedAt: new Date().getTime(),
};
updateEntity((e) => ({
updateEntity(entityId, (e) => ({
...e,
tasks: [...e.tasks, task],
tasks: e.type === "todo" ? { ...e.tasks, task } : {},
}));
};
const setTasks = (tasks: Task[]) => {
updateEntity((e) => ({
const tasksRecord = tasks.reduce(
(acc, task) => ({ ...acc, [task.id]: task }),
{} as Record<TaskId, Task>,
);
updateEntity(entityId, (e) => ({
...e,
tasks,
tasks: e.type === "todo" ? tasksRecord : {},
}));
};
@@ -75,9 +72,9 @@ export default function Router({
case "doc": {
const setDoc = (doc: OutputData) => {
updateEntity((e) => ({
updateEntity(entityId, (e) => ({
...e,
doc,
content: doc,
}));
};
+31 -11
View File
@@ -12,6 +12,8 @@ import {
} from "@/components/ui/sidebar";
import EntitySwitch from "./Entity";
const id = (prefix = "t") =>
`${prefix}_${Math.random().toString(36).slice(2, 9)}`;
import {
DropdownMenu,
@@ -21,18 +23,28 @@ import {
} from "../ui/dropdown-menu";
import { ChevronDown } from "lucide-react";
import { Route, SetState, Workspace } from "@/lib/types";
import { Entity, EntityId, Route, SetState, Workspace } from "@/lib/types";
import {
ContextMenu,
ContextMenuContent,
ContextMenuItem,
ContextMenuTrigger,
} from "../ui/context-menu";
type AppSidebarProps = {
children: React.ReactNode;
workspace: Workspace;
setRoute: SetState<Route>;
updateEntity?: (entityId: EntityId, updater: (e: Entity) => Entity) => void;
setWorkspace: SetState<Workspace | undefined>;
};
export default function AppSidebar({
children,
workspace,
setRoute,
updateEntity,
setWorkspace,
}: AppSidebarProps) {
return (
<SidebarProvider>
@@ -62,16 +74,24 @@ export default function AppSidebar({
</SidebarHeader>
<SidebarContent>
<SidebarMenu>
{workspace.layout.map((entity) => (
<EntitySwitch
key={entity.id}
entity={entity}
workspace={workspace}
setRoute={setRoute}
/>
))}
</SidebarMenu>
<ContextMenu>
<ContextMenuTrigger asChild>
<div className="h-full">
<SidebarMenu>
{workspace.layout.map((entity) => (
<EntitySwitch
key={entity.id}
entity={entity}
workspace={workspace}
setRoute={setRoute}
/>
))}
</SidebarMenu>
</div>
</ContextMenuTrigger>
<ContextMenuContent></ContextMenuContent>
</ContextMenu>
</SidebarContent>
<SidebarFooter />
+252
View File
@@ -0,0 +1,252 @@
"use client"
import * as React from "react"
import { CheckIcon, ChevronRightIcon, CircleIcon } from "lucide-react"
import { ContextMenu as ContextMenuPrimitive } from "radix-ui"
import { cn } from "@/lib/utils"
function ContextMenu({
...props
}: React.ComponentProps<typeof ContextMenuPrimitive.Root>) {
return <ContextMenuPrimitive.Root data-slot="context-menu" {...props} />
}
function ContextMenuTrigger({
...props
}: React.ComponentProps<typeof ContextMenuPrimitive.Trigger>) {
return (
<ContextMenuPrimitive.Trigger data-slot="context-menu-trigger" {...props} />
)
}
function ContextMenuGroup({
...props
}: React.ComponentProps<typeof ContextMenuPrimitive.Group>) {
return (
<ContextMenuPrimitive.Group data-slot="context-menu-group" {...props} />
)
}
function ContextMenuPortal({
...props
}: React.ComponentProps<typeof ContextMenuPrimitive.Portal>) {
return (
<ContextMenuPrimitive.Portal data-slot="context-menu-portal" {...props} />
)
}
function ContextMenuSub({
...props
}: React.ComponentProps<typeof ContextMenuPrimitive.Sub>) {
return <ContextMenuPrimitive.Sub data-slot="context-menu-sub" {...props} />
}
function ContextMenuRadioGroup({
...props
}: React.ComponentProps<typeof ContextMenuPrimitive.RadioGroup>) {
return (
<ContextMenuPrimitive.RadioGroup
data-slot="context-menu-radio-group"
{...props}
/>
)
}
function ContextMenuSubTrigger({
className,
inset,
children,
...props
}: React.ComponentProps<typeof ContextMenuPrimitive.SubTrigger> & {
inset?: boolean
}) {
return (
<ContextMenuPrimitive.SubTrigger
data-slot="context-menu-sub-trigger"
data-inset={inset}
className={cn(
"focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground [&_svg:not([class*='text-'])]:text-muted-foreground flex cursor-default items-center rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className
)}
{...props}
>
{children}
<ChevronRightIcon className="ml-auto" />
</ContextMenuPrimitive.SubTrigger>
)
}
function ContextMenuSubContent({
className,
...props
}: React.ComponentProps<typeof ContextMenuPrimitive.SubContent>) {
return (
<ContextMenuPrimitive.SubContent
data-slot="context-menu-sub-content"
className={cn(
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 min-w-[8rem] origin-(--radix-context-menu-content-transform-origin) overflow-hidden rounded-md border p-1 shadow-lg",
className
)}
{...props}
/>
)
}
function ContextMenuContent({
className,
...props
}: React.ComponentProps<typeof ContextMenuPrimitive.Content>) {
return (
<ContextMenuPrimitive.Portal>
<ContextMenuPrimitive.Content
data-slot="context-menu-content"
className={cn(
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 max-h-(--radix-context-menu-content-available-height) min-w-[8rem] origin-(--radix-context-menu-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border p-1 shadow-md",
className
)}
{...props}
/>
</ContextMenuPrimitive.Portal>
)
}
function ContextMenuItem({
className,
inset,
variant = "default",
...props
}: React.ComponentProps<typeof ContextMenuPrimitive.Item> & {
inset?: boolean
variant?: "default" | "destructive"
}) {
return (
<ContextMenuPrimitive.Item
data-slot="context-menu-item"
data-inset={inset}
data-variant={variant}
className={cn(
"focus:bg-accent focus:text-accent-foreground data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 dark:data-[variant=destructive]:focus:bg-destructive/20 data-[variant=destructive]:focus:text-destructive data-[variant=destructive]:*:[svg]:!text-destructive [&_svg:not([class*='text-'])]:text-muted-foreground relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className
)}
{...props}
/>
)
}
function ContextMenuCheckboxItem({
className,
children,
checked,
...props
}: React.ComponentProps<typeof ContextMenuPrimitive.CheckboxItem>) {
return (
<ContextMenuPrimitive.CheckboxItem
data-slot="context-menu-checkbox-item"
className={cn(
"focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className
)}
checked={checked}
{...props}
>
<span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
<ContextMenuPrimitive.ItemIndicator>
<CheckIcon className="size-4" />
</ContextMenuPrimitive.ItemIndicator>
</span>
{children}
</ContextMenuPrimitive.CheckboxItem>
)
}
function ContextMenuRadioItem({
className,
children,
...props
}: React.ComponentProps<typeof ContextMenuPrimitive.RadioItem>) {
return (
<ContextMenuPrimitive.RadioItem
data-slot="context-menu-radio-item"
className={cn(
"focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className
)}
{...props}
>
<span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
<ContextMenuPrimitive.ItemIndicator>
<CircleIcon className="size-2 fill-current" />
</ContextMenuPrimitive.ItemIndicator>
</span>
{children}
</ContextMenuPrimitive.RadioItem>
)
}
function ContextMenuLabel({
className,
inset,
...props
}: React.ComponentProps<typeof ContextMenuPrimitive.Label> & {
inset?: boolean
}) {
return (
<ContextMenuPrimitive.Label
data-slot="context-menu-label"
data-inset={inset}
className={cn(
"text-foreground px-2 py-1.5 text-sm font-medium data-[inset]:pl-8",
className
)}
{...props}
/>
)
}
function ContextMenuSeparator({
className,
...props
}: React.ComponentProps<typeof ContextMenuPrimitive.Separator>) {
return (
<ContextMenuPrimitive.Separator
data-slot="context-menu-separator"
className={cn("bg-border -mx-1 my-1 h-px", className)}
{...props}
/>
)
}
function ContextMenuShortcut({
className,
...props
}: React.ComponentProps<"span">) {
return (
<span
data-slot="context-menu-shortcut"
className={cn(
"text-muted-foreground ml-auto text-xs tracking-widest",
className
)}
{...props}
/>
)
}
export {
ContextMenu,
ContextMenuTrigger,
ContextMenuContent,
ContextMenuItem,
ContextMenuCheckboxItem,
ContextMenuRadioItem,
ContextMenuLabel,
ContextMenuSeparator,
ContextMenuShortcut,
ContextMenuGroup,
ContextMenuPortal,
ContextMenuSub,
ContextMenuSubContent,
ContextMenuSubTrigger,
ContextMenuRadioGroup,
}
+1 -1
View File
@@ -37,5 +37,5 @@ export function useWorkspace() {
if (workspace) saveWorkspaceToLocalStorage(workspace);
}, [workspace]);
return { workspace, loadWorkspace, updateEntity };
return { workspace, loadWorkspace, updateEntity, setWorkspace };
}