From fcf67786ea4298213babb6a0c6db08dc5d1d6d03 Mon Sep 17 00:00:00 2001 From: SLcode777 Date: Wed, 31 Dec 2025 15:19:27 +0100 Subject: [PATCH] feat: added drag-and-drop feature. Closes #7 --- app/dashboard/page.tsx | 97 +++--- components/dashboard/droppable-column.tsx | 36 +++ components/dashboard/module-grid.tsx | 2 +- components/dashboard/module.tsx | 2 +- components/dashboard/multi-column-grid.tsx | 294 ++++++++++++++++++ components/dashboard/sortable-module-grid.tsx | 83 +++++ components/dashboard/sortable-module-item.tsx | 50 +++ package.json | 3 + pnpm-lock.yaml | 56 ++++ prisma/generated/prisma/internal/class.ts | 4 +- .../prisma/internal/prismaNamespace.ts | 2 +- .../prisma/internal/prismaNamespaceBrowser.ts | 2 +- prisma/generated/prisma/models/Workspace.ts | 54 ++-- .../migration.sql | 9 + .../migration.sql | 9 + .../migration.sql | 9 + prisma/schema.prisma | 2 +- src/contexts/modules-context.tsx | 78 ++++- src/hooks/useBreakpoint.tsx | 41 +++ src/hooks/useModule.tsx | 48 ++- src/server/routers/module.ts | 103 +++++- src/types/module-order.ts | 172 ++++++++++ 22 files changed, 1077 insertions(+), 79 deletions(-) create mode 100644 components/dashboard/droppable-column.tsx create mode 100644 components/dashboard/multi-column-grid.tsx create mode 100644 components/dashboard/sortable-module-grid.tsx create mode 100644 components/dashboard/sortable-module-item.tsx create mode 100644 prisma/migrations/20251231121111_add_module_order_for_drag_and_drop_feature/migration.sql create mode 100644 prisma/migrations/20251231131356_change_module_order_to_json/migration.sql create mode 100644 prisma/migrations/20251231133936_change_to_module_layouts/migration.sql create mode 100644 src/hooks/useBreakpoint.tsx create mode 100644 src/types/module-order.ts diff --git a/app/dashboard/page.tsx b/app/dashboard/page.tsx index ed050f8..72daee1 100644 --- a/app/dashboard/page.tsx +++ b/app/dashboard/page.tsx @@ -1,11 +1,13 @@ "use client"; -import { ModuleGrid } from "@/components/dashboard/module-grid"; +import { MultiColumnGrid } from "@/components/dashboard/multi-column-grid"; +import { SortableModuleItem } from "@/components/dashboard/sortable-module-item"; import { ModulesSidebar } from "@/components/layout/modules-sidebar"; import { MarketingSections } from "@/components/marketing/marketing-sections"; import { getModuleById } from "@/src/config/modules"; import { useModuleContext } from "@/src/contexts/modules-context"; import { useSession } from "@/src/lib/auth-client"; +import { useMemo } from "react"; export default function DashboardPage() { const { data: session, isPending } = useSession(); @@ -17,8 +19,20 @@ export default function DashboardPage() { sidebarCollapsed, isAuthenticated, onAuthRequired, + moduleOrder, + handleReorderModules, + visibleColumns, } = useModuleContext(); + // Get all modules that should be displayed + const allModules = useMemo( + () => + isAuthenticated + ? Array.from(new Set([...pinnedModules, ...tempOpenModules])) + : tempOpenModules, + [isAuthenticated, pinnedModules, tempOpenModules] + ); + if (isPending) { return (
@@ -29,12 +43,40 @@ export default function DashboardPage() { ); } - //modules to display : - //Visitors = tempOpenModules (3 default modules + the ones the visitor chooses to open) - //Auth users : pinnedModules + tempOpenModules - const allModules = isAuthenticated - ? Array.from(new Set([...pinnedModules, ...tempOpenModules])) - : tempOpenModules; + // Render a single module + const renderModule = (moduleId: string) => { + const moduleConfig = getModuleById(moduleId); + if (!moduleConfig) return null; + const ModuleComponent = moduleConfig.component; + const isPinned = pinnedModules.includes(moduleId); + const isTemp = tempOpenModules.includes(moduleId); + + return ( + + { + //visitors : close module + if (!isAuthenticated) { + toggleTempOpen(moduleId); + + //auth user + } else if (isPinned) { + //pinned module : unpin + handleTogglePin(moduleId); + + //unpinned module : can pin then can close + } else if (isTemp) { + handleTogglePin(moduleId); + toggleTempOpen(moduleId); + } + }} + isAuthenticated={isAuthenticated} + onAuthRequired={onAuthRequired} + /> + + ); + }; return (
@@ -48,40 +90,13 @@ export default function DashboardPage() { }`} > {allModules.length > 0 ? ( - - {allModules.map((moduleId) => { - const moduleConfig = getModuleById(moduleId); - if (!moduleConfig) return null; - const ModuleComponent = moduleConfig.component; - const isPinned = pinnedModules.includes(moduleId); - const isTemp = tempOpenModules.includes(moduleId); - - return ( - { - //visitors : close module - if (!isAuthenticated) { - toggleTempOpen(moduleId); - - //auth user - } else if (isPinned) { - //pinned module : unpin - handleTogglePin(moduleId); - - //unpinned module : can pin then can close - } else if (isTemp) { - handleTogglePin(moduleId); - toggleTempOpen(moduleId); - } - }} - isAuthenticated={isAuthenticated} - onAuthRequired={onAuthRequired} - /> - ); - })} - + ) : (

No pinned modules

diff --git a/components/dashboard/droppable-column.tsx b/components/dashboard/droppable-column.tsx new file mode 100644 index 0000000..7130e21 --- /dev/null +++ b/components/dashboard/droppable-column.tsx @@ -0,0 +1,36 @@ +"use client"; + +import { useDroppable } from "@dnd-kit/core"; +import { + SortableContext, + verticalListSortingStrategy, +} from "@dnd-kit/sortable"; +import { ReactNode } from "react"; +import { type ColumnId } from "@/src/types/module-order"; + +interface DroppableColumnProps { + columnId: ColumnId; + moduleIds: string[]; + children: ReactNode; +} + +export function DroppableColumn({ + columnId, + moduleIds, + children, +}: DroppableColumnProps) { + const { setNodeRef } = useDroppable({ + id: columnId, + }); + + return ( +
+ + {children} + +
+ ); +} diff --git a/components/dashboard/module-grid.tsx b/components/dashboard/module-grid.tsx index d64b623..751674f 100644 --- a/components/dashboard/module-grid.tsx +++ b/components/dashboard/module-grid.tsx @@ -6,7 +6,7 @@ interface ModuleGridProps { export function ModuleGrid({ children }: ModuleGridProps) { return ( -
+
{children}
); diff --git a/components/dashboard/module.tsx b/components/dashboard/module.tsx index 26eaf69..fb78aea 100644 --- a/components/dashboard/module.tsx +++ b/components/dashboard/module.tsx @@ -29,7 +29,7 @@ export function Module({ children, }: ModuleProps) { return ( - +
diff --git a/components/dashboard/multi-column-grid.tsx b/components/dashboard/multi-column-grid.tsx new file mode 100644 index 0000000..6bb8ff1 --- /dev/null +++ b/components/dashboard/multi-column-grid.tsx @@ -0,0 +1,294 @@ +"use client"; + +import { + type ColumnId, + type ModuleOrder, + COLUMN_IDS, +} from "@/src/types/module-order"; +import { + DndContext, + DragEndEvent, + DragOverEvent, + DragOverlay, + DragStartEvent, + KeyboardSensor, + PointerSensor, + closestCorners, + useSensor, + useSensors, +} from "@dnd-kit/core"; +import { sortableKeyboardCoordinates } from "@dnd-kit/sortable"; +import { ReactNode, useEffect, useRef, useState } from "react"; +import { DroppableColumn } from "./droppable-column"; + +interface MultiColumnGridProps { + moduleOrder: ModuleOrder; + visibleColumns: ColumnId[]; + onReorder: (newOrder: ModuleOrder) => void; + renderModule: (moduleId: string) => ReactNode; + allModules: string[]; +} + +export function MultiColumnGrid({ + moduleOrder, + visibleColumns, + onReorder, + renderModule, + allModules, +}: MultiColumnGridProps) { + const [activeId, setActiveId] = useState(null); + + const sensors = useSensors( + useSensor(PointerSensor, { + activationConstraint: { + distance: 8, + }, + }), + useSensor(KeyboardSensor, { + coordinateGetter: sortableKeyboardCoordinates, + }) + ); + + // Distribute unordered modules across visible columns + const getModulesForColumn = (columnId: ColumnId): string[] => { + const orderedInColumn = moduleOrder[columnId] || []; + + // Filter to only include modules that exist in allModules + return orderedInColumn.filter((id) => allModules.includes(id)); + }; + + // Add any modules not in any column + const ensureAllModulesPlaced = (order: ModuleOrder): ModuleOrder => { + const placedModules = new Set(COLUMN_IDS.flatMap((colId) => order[colId])); + + const unplacedModules = allModules.filter((id) => !placedModules.has(id)); + + if (unplacedModules.length === 0) { + return order; + } + + // Check if layout is completely empty (first time at this breakpoint) + const isLayoutEmpty = visibleColumns.every( + (colId) => order[colId].length === 0 + ); + + const newOrder = { ...order }; + + if (isLayoutEmpty && visibleColumns.length > 0) { + // Distribute modules evenly across all visible columns + unplacedModules.forEach((moduleId, index) => { + const columnIndex = index % visibleColumns.length; + const columnId = visibleColumns[columnIndex]; + if (columnId) { + newOrder[columnId].push(moduleId); + } + }); + } else if (visibleColumns.length > 0) { + // Add new modules to the first column (for newly pinned modules) + newOrder[visibleColumns[0]] = [ + ...unplacedModules, + ...order[visibleColumns[0]], + ]; + } + + return newOrder; + }; + + // Auto-save initial layout distribution + const hasInitialized = useRef(false); + useEffect(() => { + // Only run once on mount + if (hasInitialized.current) return; + + const placedModules = new Set( + COLUMN_IDS.flatMap((colId) => moduleOrder[colId]) + ); + const unplacedModules = allModules.filter((id) => !placedModules.has(id)); + + // Check if layout is empty and has unplaced modules + const isLayoutEmpty = visibleColumns.every( + (colId) => moduleOrder[colId].length === 0 + ); + + if ( + isLayoutEmpty && + unplacedModules.length > 0 && + visibleColumns.length > 0 + ) { + // Distribute and save + const distributedOrder = ensureAllModulesPlaced(moduleOrder); + onReorder(distributedOrder); + hasInitialized.current = true; + } + }, [allModules, moduleOrder, visibleColumns, onReorder]); + + const handleDragStart = (event: DragStartEvent) => { + console.log("🎯 Drag started:", event.active.id); + setActiveId(event.active.id as string); + }; + + const handleDragOver = (event: DragOverEvent) => { + const { active, over } = event; + console.log("🔄 Drag over:", { activeId: active.id, overId: over?.id }); + + if (!over) return; + + const activeId = active.id as string; + const overId = over.id as string; + + // Use finalOrder which has all modules placed + const currentOrder = ensureAllModulesPlaced(moduleOrder); + + // Find source column + let sourceColumn: ColumnId | null = null; + for (const colId of COLUMN_IDS) { + if (currentOrder[colId].includes(activeId)) { + sourceColumn = colId; + break; + } + } + + if (!sourceColumn) return; + + // Determine target column + let targetColumn: ColumnId | null = null; + if (COLUMN_IDS.includes(overId as ColumnId)) { + // Dropped on column itself + targetColumn = overId as ColumnId; + } else { + // Dropped on a module, find its column + for (const colId of COLUMN_IDS) { + if (currentOrder[colId].includes(overId)) { + targetColumn = colId; + break; + } + } + } + + if (!targetColumn) return; + + if (sourceColumn === targetColumn) return; + + // Move module to target column + const newOrder = { ...currentOrder }; + newOrder[sourceColumn] = newOrder[sourceColumn].filter( + (id) => id !== activeId + ); + + // Add to target column at the position of overId if it's a module + if (COLUMN_IDS.includes(overId as ColumnId)) { + // Dropped on empty column + newOrder[targetColumn] = [...newOrder[targetColumn], activeId]; + } else { + // Dropped on a module + const overIndex = newOrder[targetColumn].indexOf(overId); + newOrder[targetColumn].splice(overIndex, 0, activeId); + } + + onReorder(newOrder); + }; + + const handleDragEnd = (event: DragEndEvent) => { + const { active, over } = event; + console.log("✅ Drag ended:", { activeId: active.id, overId: over?.id }); + + setActiveId(null); + + if (!over) return; + + const activeId = active.id as string; + const overId = over.id as string; + + // Use finalOrder which has all modules placed + const currentOrder = ensureAllModulesPlaced(moduleOrder); + + // Find active column + let activeColumn: ColumnId | null = null; + for (const colId of COLUMN_IDS) { + if (currentOrder[colId].includes(activeId)) { + activeColumn = colId; + break; + } + } + + if (!activeColumn) return; + + // If dropped on a column container + if (COLUMN_IDS.includes(overId as ColumnId)) { + return; // Already handled in dragOver + } + + // Reorder within same column + const overColumn = COLUMN_IDS.find((colId) => + currentOrder[colId].includes(overId) + ); + + if (overColumn === activeColumn) { + const columnItems = [...currentOrder[activeColumn]]; + const activeIndex = columnItems.indexOf(activeId); + const overIndex = columnItems.indexOf(overId); + + if (activeIndex !== overIndex) { + columnItems.splice(activeIndex, 1); + columnItems.splice(overIndex, 0, activeId); + + const newOrder = { + ...currentOrder, + [activeColumn]: columnItems, + }; + + console.log("💾 Saving new order:", newOrder); + onReorder(newOrder); + } + } + }; + + const finalOrder = ensureAllModulesPlaced(moduleOrder); + + // Filter modules to only include those that exist in allModules + const filteredOrder: ModuleOrder = { + col1: finalOrder.col1.filter((id) => allModules.includes(id)), + col2: finalOrder.col2.filter((id) => allModules.includes(id)), + col3: finalOrder.col3.filter((id) => allModules.includes(id)), + col4: finalOrder.col4.filter((id) => allModules.includes(id)), + col5: finalOrder.col5.filter((id) => allModules.includes(id)), + }; + + return ( + +
+ {visibleColumns.map((columnId) => { + return ( + + {filteredOrder[columnId].map((moduleId) => + renderModule(moduleId) + )} + + ); + })} +
+ + {activeId ? ( +
+ {renderModule(activeId)} +
+ ) : null} +
+
+ ); +} diff --git a/components/dashboard/sortable-module-grid.tsx b/components/dashboard/sortable-module-grid.tsx new file mode 100644 index 0000000..1de5564 --- /dev/null +++ b/components/dashboard/sortable-module-grid.tsx @@ -0,0 +1,83 @@ +"use client"; + +import { + DndContext, + closestCenter, + KeyboardSensor, + PointerSensor, + useSensor, + useSensors, + DragEndEvent, + DragStartEvent, +} from "@dnd-kit/core"; +import { + arrayMove, + SortableContext, + sortableKeyboardCoordinates, + rectSortingStrategy, +} from "@dnd-kit/sortable"; +import { ReactNode, useState } from "react"; + +interface SortableModuleGridProps { + children: ReactNode; + moduleIds: string[]; + onReorder: (newOrder: string[]) => void; +} + +export function SortableModuleGrid({ + children, + moduleIds, + onReorder, +}: SortableModuleGridProps) { + const [activeId, setActiveId] = useState(null); + + // Sensors for drag interaction + const sensors = useSensors( + useSensor(PointerSensor, { + activationConstraint: { + distance: 8, // 8px movement before drag starts (prevents accidental drags) + }, + }), + useSensor(KeyboardSensor, { + coordinateGetter: sortableKeyboardCoordinates, + }) + ); + + const handleDragStart = (event: DragStartEvent) => { + setActiveId(event.active.id as string); + }; + + const handleDragEnd = (event: DragEndEvent) => { + const { active, over } = event; + + if (over && active.id !== over.id) { + const oldIndex = moduleIds.indexOf(active.id as string); + const newIndex = moduleIds.indexOf(over.id as string); + + const newOrder = arrayMove(moduleIds, oldIndex, newIndex); + onReorder(newOrder); + } + + setActiveId(null); + }; + + const handleDragCancel = () => { + setActiveId(null); + }; + + return ( + + +
+ {children} +
+
+
+ ); +} diff --git a/components/dashboard/sortable-module-item.tsx b/components/dashboard/sortable-module-item.tsx new file mode 100644 index 0000000..6f39fb7 --- /dev/null +++ b/components/dashboard/sortable-module-item.tsx @@ -0,0 +1,50 @@ +"use client"; + +import { useSortable } from "@dnd-kit/sortable"; +import { CSS } from "@dnd-kit/utilities"; +import { GripHorizontal } from "lucide-react"; +import { ReactNode } from "react"; + +interface SortableModuleItemProps { + id: string; + children: ReactNode; +} + +export function SortableModuleItem({ id, children }: SortableModuleItemProps) { + const { + attributes, + listeners, + setNodeRef, + setActivatorNodeRef, + transform, + transition, + isDragging, + } = useSortable({ id }); + + const style = { + transform: CSS.Transform.toString(transform), + transition: transition || 'transform 200ms cubic-bezier(0.18, 0.67, 0.6, 1.22)', + }; + + return ( +
+ {/* Drag handle */} + + {children} +
+ ); +} diff --git a/package.json b/package.json index 09d0327..501c97d 100644 --- a/package.json +++ b/package.json @@ -19,6 +19,9 @@ }, "dependencies": { "@base-ui/react": "^1.0.0", + "@dnd-kit/core": "^6.3.1", + "@dnd-kit/sortable": "^10.0.0", + "@dnd-kit/utilities": "^3.2.2", "@prisma/adapter-pg": "^7.2.0", "@prisma/client": "^7.2.0", "@radix-ui/react-switch": "^1.2.6", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index db4820c..0c40383 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -11,6 +11,15 @@ importers: '@base-ui/react': specifier: ^1.0.0 version: 1.0.0(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@dnd-kit/core': + specifier: ^6.3.1 + version: 6.3.1(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@dnd-kit/sortable': + specifier: ^10.0.0 + version: 10.0.0(@dnd-kit/core@6.3.1(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react@19.2.3) + '@dnd-kit/utilities': + specifier: ^3.2.2 + version: 3.2.2(react@19.2.3) '@prisma/adapter-pg': specifier: ^7.2.0 version: 7.2.0 @@ -357,6 +366,28 @@ packages: '@chevrotain/utils@10.5.0': resolution: {integrity: sha512-hBzuU5+JjB2cqNZyszkDHZgOSrUUT8V3dhgRl8Q9Gp6dAj/H5+KILGjbhDpc3Iy9qmqlm/akuOI2ut9VUtzJxQ==} + '@dnd-kit/accessibility@3.1.1': + resolution: {integrity: sha512-2P+YgaXF+gRsIihwwY1gCsQSYnu9Zyj2py8kY5fFvUM1qm2WA2u639R6YNVfU4GWr+ZM5mqEsfHZZLoRONbemw==} + peerDependencies: + react: '>=16.8.0' + + '@dnd-kit/core@6.3.1': + resolution: {integrity: sha512-xkGBRQQab4RLwgXxoqETICr6S5JlogafbhNsidmrkVv2YRs5MLwpjoF2qpiGjQt8S9AoxtIV603s0GIUpY5eYQ==} + peerDependencies: + react: '>=16.8.0' + react-dom: '>=16.8.0' + + '@dnd-kit/sortable@10.0.0': + resolution: {integrity: sha512-+xqhmIIzvAYMGfBYYnbKuNicfSsk4RksY2XdmJhT+HAC01nix6fHCztU68jooFiMUB01Ky3F0FyOvhG/BZrWkg==} + peerDependencies: + '@dnd-kit/core': ^6.3.0 + react: '>=16.8.0' + + '@dnd-kit/utilities@3.2.2': + resolution: {integrity: sha512-+MKAJEOfaBe5SmV6t34p80MMKhjvUz0vRrvVJbPT0WElzaOJ/1xs+D+KDv+tD/NE5ujfrChEcshd4fLn0wpiqg==} + peerDependencies: + react: '>=16.8.0' + '@dotenvx/dotenvx@1.51.2': resolution: {integrity: sha512-+693mNflujDZxudSEqSNGpn92QgFhJlBn9q2mDQ9yGWyHuz3hZ8B5g3EXCwdAz4DMJAI+OFCIbfEFZS+YRdrEA==} hasBin: true @@ -5792,6 +5823,31 @@ snapshots: '@chevrotain/utils@10.5.0': {} + '@dnd-kit/accessibility@3.1.1(react@19.2.3)': + dependencies: + react: 19.2.3 + tslib: 2.8.1 + + '@dnd-kit/core@6.3.1(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': + dependencies: + '@dnd-kit/accessibility': 3.1.1(react@19.2.3) + '@dnd-kit/utilities': 3.2.2(react@19.2.3) + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) + tslib: 2.8.1 + + '@dnd-kit/sortable@10.0.0(@dnd-kit/core@6.3.1(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react@19.2.3)': + dependencies: + '@dnd-kit/core': 6.3.1(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@dnd-kit/utilities': 3.2.2(react@19.2.3) + react: 19.2.3 + tslib: 2.8.1 + + '@dnd-kit/utilities@3.2.2(react@19.2.3)': + dependencies: + react: 19.2.3 + tslib: 2.8.1 + '@dotenvx/dotenvx@1.51.2': dependencies: commander: 11.1.0 diff --git a/prisma/generated/prisma/internal/class.ts b/prisma/generated/prisma/internal/class.ts index 69d5b09..f49c6a6 100644 --- a/prisma/generated/prisma/internal/class.ts +++ b/prisma/generated/prisma/internal/class.ts @@ -20,7 +20,7 @@ const config: runtime.GetPrismaClientConfig = { "clientVersion": "7.2.0", "engineVersion": "0c8ef2ce45c83248ab3df073180d5eda9e8be7a3", "activeProvider": "postgresql", - "inlineSchema": "generator client {\n provider = \"prisma-client\"\n output = \"../prisma/generated/prisma\"\n}\n\ndatasource db {\n provider = \"postgresql\"\n}\n\nmodel Workspace {\n id String @id\n userId String @unique\n layout Json?\n pinnedModules String[] @default([])\n createdAt DateTime @default(now())\n updatedAt DateTime @updatedAt\n User User @relation(fields: [userId], references: [id], onDelete: Cascade)\n}\n\nmodel User {\n id String @id\n name String\n email String\n emailVerified Boolean @default(false)\n image String?\n removeBgApiKey String?\n deeplApiKey String?\n\n createdAt DateTime @default(now())\n updatedAt DateTime @updatedAt\n\n sessions Session[]\n accounts Account[]\n\n //relations\n workspace Workspace?\n colorPalettes ColorPalette[]\n domainNames DomainName[]\n snippets Snippet[]\n youtubeVideos YouTubeVideo[]\n pomodoroSettings PomodoroSettings?\n stickyNote StickyNote?\n\n @@unique([email])\n @@map(\"user\")\n}\n\nmodel Session {\n id String @id\n expiresAt DateTime\n token String\n createdAt DateTime @default(now())\n updatedAt DateTime @updatedAt\n ipAddress String?\n userAgent String?\n userId String\n user User @relation(fields: [userId], references: [id], onDelete: Cascade)\n\n @@unique([token])\n @@index([userId])\n @@map(\"session\")\n}\n\nmodel Account {\n id String @id\n accountId String\n providerId String\n userId String\n user User @relation(fields: [userId], references: [id], onDelete: Cascade)\n accessToken String?\n refreshToken String?\n idToken String?\n accessTokenExpiresAt DateTime?\n refreshTokenExpiresAt DateTime?\n scope String?\n password String?\n createdAt DateTime @default(now())\n updatedAt DateTime @updatedAt\n\n @@index([userId])\n @@map(\"account\")\n}\n\nmodel Verification {\n id String @id\n identifier String\n value String\n expiresAt DateTime\n createdAt DateTime @default(now())\n updatedAt DateTime @updatedAt\n\n @@index([identifier])\n @@map(\"verification\")\n}\n\nmodel ColorPalette {\n id String @id @default(cuid())\n userId String\n name String\n colors Json\n isDefault Boolean @default(false)\n createdAt DateTime @default(now())\n updatedAt DateTime @updatedAt\n user User @relation(fields: [userId], references: [id], onDelete: Cascade)\n\n @@index([userId])\n @@map(\"color_palette\")\n}\n\nmodel DomainName {\n id String @id @default(cuid())\n userId String\n domain String\n registrar String\n registrarUrl String?\n expiresAt DateTime\n autoRenew Boolean @default(false)\n reminderOneMonth Boolean @default(true)\n reminderOneWeek Boolean @default(true)\n lastReminderSent DateTime?\n createdAt DateTime @default(now())\n updatedAt DateTime @updatedAt\n user User @relation(fields: [userId], references: [id], onDelete: Cascade)\n\n @@index([userId])\n @@index([expiresAt])\n @@map(\"domain_name\")\n}\n\nmodel Snippet {\n id String @id @default(cuid())\n userId String\n title String\n description String?\n language String\n code String\n tags String[] @default([])\n isFavorite Boolean @default(false)\n lastUsedAt DateTime?\n createdAt DateTime @default(now())\n updatedAt DateTime @updatedAt\n user User @relation(fields: [userId], references: [id], onDelete: Cascade)\n\n @@index([userId])\n @@index([isFavorite])\n @@index([lastUsedAt])\n @@map(\"snippet\")\n}\n\nmodel YouTubeVideo {\n id String @id @default(cuid())\n userId String\n url String\n title String\n videoId String\n playlistId String?\n isPlaylist Boolean @default(false)\n createdAt DateTime @default(now())\n updatedAt DateTime @updatedAt\n user User @relation(fields: [userId], references: [id], onDelete: Cascade)\n\n @@index([userId])\n @@index([createdAt])\n @@map(\"youtube_video\")\n}\n\nmodel PomodoroSettings {\n id String @id @default(cuid())\n userId String @unique\n\n //lengths in minutes\n workDuration Int @default(50)\n breakDuration Int @default(10)\n cycles Int @default(3)\n\n //predefined sound IDs\n sessionStartSound String @default(\"rythm-bell\")\n breakStartSound String @default(\"completed\")\n breakEndSound String @default(\"triangle\")\n sessionEndSound String @default(\"victory-dance\")\n\n //background customization\n backgroundImage String? @default(\"cyan\") // Base64 ou ID image galerie\n backgroundType String @default(\"gallery\") // \"gallery\" | \"custom\" | \"none\"\n textColor String @default(\"#FFFFFF\")\n\n //options\n autoStartBreaks Boolean @default(false)\n autoStartPomodoros Boolean @default(false)\n soundEnabled Boolean @default(true)\n\n createdAt DateTime @default(now())\n updatedAt DateTime @updatedAt\n\n user User @relation(fields: [userId], references: [id], onDelete: Cascade)\n\n @@index([userId])\n @@map(\"pomodoro_settings\")\n}\n\nmodel StickyNote {\n id String @id @default(cuid())\n userId String @unique\n content String @db.Text\n createdAt DateTime @default(now())\n updatedAt DateTime @updatedAt\n user User @relation(fields: [userId], references: [id], onDelete: Cascade)\n\n @@index([userId])\n @@map(\"sticky_note\")\n}\n", + "inlineSchema": "generator client {\n provider = \"prisma-client\"\n output = \"../prisma/generated/prisma\"\n}\n\ndatasource db {\n provider = \"postgresql\"\n}\n\nmodel Workspace {\n id String @id\n userId String @unique\n moduleLayouts Json?\n pinnedModules String[] @default([])\n createdAt DateTime @default(now())\n updatedAt DateTime @updatedAt\n User User @relation(fields: [userId], references: [id], onDelete: Cascade)\n}\n\nmodel User {\n id String @id\n name String\n email String\n emailVerified Boolean @default(false)\n image String?\n removeBgApiKey String?\n deeplApiKey String?\n\n createdAt DateTime @default(now())\n updatedAt DateTime @updatedAt\n\n sessions Session[]\n accounts Account[]\n\n //relations\n workspace Workspace?\n colorPalettes ColorPalette[]\n domainNames DomainName[]\n snippets Snippet[]\n youtubeVideos YouTubeVideo[]\n pomodoroSettings PomodoroSettings?\n stickyNote StickyNote?\n\n @@unique([email])\n @@map(\"user\")\n}\n\nmodel Session {\n id String @id\n expiresAt DateTime\n token String\n createdAt DateTime @default(now())\n updatedAt DateTime @updatedAt\n ipAddress String?\n userAgent String?\n userId String\n user User @relation(fields: [userId], references: [id], onDelete: Cascade)\n\n @@unique([token])\n @@index([userId])\n @@map(\"session\")\n}\n\nmodel Account {\n id String @id\n accountId String\n providerId String\n userId String\n user User @relation(fields: [userId], references: [id], onDelete: Cascade)\n accessToken String?\n refreshToken String?\n idToken String?\n accessTokenExpiresAt DateTime?\n refreshTokenExpiresAt DateTime?\n scope String?\n password String?\n createdAt DateTime @default(now())\n updatedAt DateTime @updatedAt\n\n @@index([userId])\n @@map(\"account\")\n}\n\nmodel Verification {\n id String @id\n identifier String\n value String\n expiresAt DateTime\n createdAt DateTime @default(now())\n updatedAt DateTime @updatedAt\n\n @@index([identifier])\n @@map(\"verification\")\n}\n\nmodel ColorPalette {\n id String @id @default(cuid())\n userId String\n name String\n colors Json\n isDefault Boolean @default(false)\n createdAt DateTime @default(now())\n updatedAt DateTime @updatedAt\n user User @relation(fields: [userId], references: [id], onDelete: Cascade)\n\n @@index([userId])\n @@map(\"color_palette\")\n}\n\nmodel DomainName {\n id String @id @default(cuid())\n userId String\n domain String\n registrar String\n registrarUrl String?\n expiresAt DateTime\n autoRenew Boolean @default(false)\n reminderOneMonth Boolean @default(true)\n reminderOneWeek Boolean @default(true)\n lastReminderSent DateTime?\n createdAt DateTime @default(now())\n updatedAt DateTime @updatedAt\n user User @relation(fields: [userId], references: [id], onDelete: Cascade)\n\n @@index([userId])\n @@index([expiresAt])\n @@map(\"domain_name\")\n}\n\nmodel Snippet {\n id String @id @default(cuid())\n userId String\n title String\n description String?\n language String\n code String\n tags String[] @default([])\n isFavorite Boolean @default(false)\n lastUsedAt DateTime?\n createdAt DateTime @default(now())\n updatedAt DateTime @updatedAt\n user User @relation(fields: [userId], references: [id], onDelete: Cascade)\n\n @@index([userId])\n @@index([isFavorite])\n @@index([lastUsedAt])\n @@map(\"snippet\")\n}\n\nmodel YouTubeVideo {\n id String @id @default(cuid())\n userId String\n url String\n title String\n videoId String\n playlistId String?\n isPlaylist Boolean @default(false)\n createdAt DateTime @default(now())\n updatedAt DateTime @updatedAt\n user User @relation(fields: [userId], references: [id], onDelete: Cascade)\n\n @@index([userId])\n @@index([createdAt])\n @@map(\"youtube_video\")\n}\n\nmodel PomodoroSettings {\n id String @id @default(cuid())\n userId String @unique\n\n //lengths in minutes\n workDuration Int @default(50)\n breakDuration Int @default(10)\n cycles Int @default(3)\n\n //predefined sound IDs\n sessionStartSound String @default(\"rythm-bell\")\n breakStartSound String @default(\"completed\")\n breakEndSound String @default(\"triangle\")\n sessionEndSound String @default(\"victory-dance\")\n\n //background customization\n backgroundImage String? @default(\"cyan\") // Base64 ou ID image galerie\n backgroundType String @default(\"gallery\") // \"gallery\" | \"custom\" | \"none\"\n textColor String @default(\"#FFFFFF\")\n\n //options\n autoStartBreaks Boolean @default(false)\n autoStartPomodoros Boolean @default(false)\n soundEnabled Boolean @default(true)\n\n createdAt DateTime @default(now())\n updatedAt DateTime @updatedAt\n\n user User @relation(fields: [userId], references: [id], onDelete: Cascade)\n\n @@index([userId])\n @@map(\"pomodoro_settings\")\n}\n\nmodel StickyNote {\n id String @id @default(cuid())\n userId String @unique\n content String @db.Text\n createdAt DateTime @default(now())\n updatedAt DateTime @updatedAt\n user User @relation(fields: [userId], references: [id], onDelete: Cascade)\n\n @@index([userId])\n @@map(\"sticky_note\")\n}\n", "runtimeDataModel": { "models": {}, "enums": {}, @@ -28,7 +28,7 @@ const config: runtime.GetPrismaClientConfig = { } } -config.runtimeDataModel = JSON.parse("{\"models\":{\"Workspace\":{\"fields\":[{\"name\":\"id\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"userId\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"layout\",\"kind\":\"scalar\",\"type\":\"Json\"},{\"name\":\"pinnedModules\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"createdAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"updatedAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"User\",\"kind\":\"object\",\"type\":\"User\",\"relationName\":\"UserToWorkspace\"}],\"dbName\":null},\"User\":{\"fields\":[{\"name\":\"id\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"name\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"email\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"emailVerified\",\"kind\":\"scalar\",\"type\":\"Boolean\"},{\"name\":\"image\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"removeBgApiKey\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"deeplApiKey\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"createdAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"updatedAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"sessions\",\"kind\":\"object\",\"type\":\"Session\",\"relationName\":\"SessionToUser\"},{\"name\":\"accounts\",\"kind\":\"object\",\"type\":\"Account\",\"relationName\":\"AccountToUser\"},{\"name\":\"workspace\",\"kind\":\"object\",\"type\":\"Workspace\",\"relationName\":\"UserToWorkspace\"},{\"name\":\"colorPalettes\",\"kind\":\"object\",\"type\":\"ColorPalette\",\"relationName\":\"ColorPaletteToUser\"},{\"name\":\"domainNames\",\"kind\":\"object\",\"type\":\"DomainName\",\"relationName\":\"DomainNameToUser\"},{\"name\":\"snippets\",\"kind\":\"object\",\"type\":\"Snippet\",\"relationName\":\"SnippetToUser\"},{\"name\":\"youtubeVideos\",\"kind\":\"object\",\"type\":\"YouTubeVideo\",\"relationName\":\"UserToYouTubeVideo\"},{\"name\":\"pomodoroSettings\",\"kind\":\"object\",\"type\":\"PomodoroSettings\",\"relationName\":\"PomodoroSettingsToUser\"},{\"name\":\"stickyNote\",\"kind\":\"object\",\"type\":\"StickyNote\",\"relationName\":\"StickyNoteToUser\"}],\"dbName\":\"user\"},\"Session\":{\"fields\":[{\"name\":\"id\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"expiresAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"token\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"createdAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"updatedAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"ipAddress\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"userAgent\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"userId\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"user\",\"kind\":\"object\",\"type\":\"User\",\"relationName\":\"SessionToUser\"}],\"dbName\":\"session\"},\"Account\":{\"fields\":[{\"name\":\"id\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"accountId\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"providerId\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"userId\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"user\",\"kind\":\"object\",\"type\":\"User\",\"relationName\":\"AccountToUser\"},{\"name\":\"accessToken\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"refreshToken\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"idToken\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"accessTokenExpiresAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"refreshTokenExpiresAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"scope\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"password\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"createdAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"updatedAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"}],\"dbName\":\"account\"},\"Verification\":{\"fields\":[{\"name\":\"id\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"identifier\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"value\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"expiresAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"createdAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"updatedAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"}],\"dbName\":\"verification\"},\"ColorPalette\":{\"fields\":[{\"name\":\"id\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"userId\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"name\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"colors\",\"kind\":\"scalar\",\"type\":\"Json\"},{\"name\":\"isDefault\",\"kind\":\"scalar\",\"type\":\"Boolean\"},{\"name\":\"createdAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"updatedAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"user\",\"kind\":\"object\",\"type\":\"User\",\"relationName\":\"ColorPaletteToUser\"}],\"dbName\":\"color_palette\"},\"DomainName\":{\"fields\":[{\"name\":\"id\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"userId\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"domain\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"registrar\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"registrarUrl\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"expiresAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"autoRenew\",\"kind\":\"scalar\",\"type\":\"Boolean\"},{\"name\":\"reminderOneMonth\",\"kind\":\"scalar\",\"type\":\"Boolean\"},{\"name\":\"reminderOneWeek\",\"kind\":\"scalar\",\"type\":\"Boolean\"},{\"name\":\"lastReminderSent\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"createdAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"updatedAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"user\",\"kind\":\"object\",\"type\":\"User\",\"relationName\":\"DomainNameToUser\"}],\"dbName\":\"domain_name\"},\"Snippet\":{\"fields\":[{\"name\":\"id\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"userId\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"title\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"description\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"language\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"code\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"tags\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"isFavorite\",\"kind\":\"scalar\",\"type\":\"Boolean\"},{\"name\":\"lastUsedAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"createdAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"updatedAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"user\",\"kind\":\"object\",\"type\":\"User\",\"relationName\":\"SnippetToUser\"}],\"dbName\":\"snippet\"},\"YouTubeVideo\":{\"fields\":[{\"name\":\"id\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"userId\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"url\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"title\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"videoId\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"playlistId\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"isPlaylist\",\"kind\":\"scalar\",\"type\":\"Boolean\"},{\"name\":\"createdAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"updatedAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"user\",\"kind\":\"object\",\"type\":\"User\",\"relationName\":\"UserToYouTubeVideo\"}],\"dbName\":\"youtube_video\"},\"PomodoroSettings\":{\"fields\":[{\"name\":\"id\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"userId\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"workDuration\",\"kind\":\"scalar\",\"type\":\"Int\"},{\"name\":\"breakDuration\",\"kind\":\"scalar\",\"type\":\"Int\"},{\"name\":\"cycles\",\"kind\":\"scalar\",\"type\":\"Int\"},{\"name\":\"sessionStartSound\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"breakStartSound\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"breakEndSound\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"sessionEndSound\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"backgroundImage\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"backgroundType\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"textColor\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"autoStartBreaks\",\"kind\":\"scalar\",\"type\":\"Boolean\"},{\"name\":\"autoStartPomodoros\",\"kind\":\"scalar\",\"type\":\"Boolean\"},{\"name\":\"soundEnabled\",\"kind\":\"scalar\",\"type\":\"Boolean\"},{\"name\":\"createdAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"updatedAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"user\",\"kind\":\"object\",\"type\":\"User\",\"relationName\":\"PomodoroSettingsToUser\"}],\"dbName\":\"pomodoro_settings\"},\"StickyNote\":{\"fields\":[{\"name\":\"id\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"userId\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"content\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"createdAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"updatedAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"user\",\"kind\":\"object\",\"type\":\"User\",\"relationName\":\"StickyNoteToUser\"}],\"dbName\":\"sticky_note\"}},\"enums\":{},\"types\":{}}") +config.runtimeDataModel = JSON.parse("{\"models\":{\"Workspace\":{\"fields\":[{\"name\":\"id\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"userId\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"moduleLayouts\",\"kind\":\"scalar\",\"type\":\"Json\"},{\"name\":\"pinnedModules\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"createdAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"updatedAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"User\",\"kind\":\"object\",\"type\":\"User\",\"relationName\":\"UserToWorkspace\"}],\"dbName\":null},\"User\":{\"fields\":[{\"name\":\"id\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"name\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"email\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"emailVerified\",\"kind\":\"scalar\",\"type\":\"Boolean\"},{\"name\":\"image\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"removeBgApiKey\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"deeplApiKey\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"createdAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"updatedAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"sessions\",\"kind\":\"object\",\"type\":\"Session\",\"relationName\":\"SessionToUser\"},{\"name\":\"accounts\",\"kind\":\"object\",\"type\":\"Account\",\"relationName\":\"AccountToUser\"},{\"name\":\"workspace\",\"kind\":\"object\",\"type\":\"Workspace\",\"relationName\":\"UserToWorkspace\"},{\"name\":\"colorPalettes\",\"kind\":\"object\",\"type\":\"ColorPalette\",\"relationName\":\"ColorPaletteToUser\"},{\"name\":\"domainNames\",\"kind\":\"object\",\"type\":\"DomainName\",\"relationName\":\"DomainNameToUser\"},{\"name\":\"snippets\",\"kind\":\"object\",\"type\":\"Snippet\",\"relationName\":\"SnippetToUser\"},{\"name\":\"youtubeVideos\",\"kind\":\"object\",\"type\":\"YouTubeVideo\",\"relationName\":\"UserToYouTubeVideo\"},{\"name\":\"pomodoroSettings\",\"kind\":\"object\",\"type\":\"PomodoroSettings\",\"relationName\":\"PomodoroSettingsToUser\"},{\"name\":\"stickyNote\",\"kind\":\"object\",\"type\":\"StickyNote\",\"relationName\":\"StickyNoteToUser\"}],\"dbName\":\"user\"},\"Session\":{\"fields\":[{\"name\":\"id\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"expiresAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"token\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"createdAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"updatedAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"ipAddress\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"userAgent\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"userId\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"user\",\"kind\":\"object\",\"type\":\"User\",\"relationName\":\"SessionToUser\"}],\"dbName\":\"session\"},\"Account\":{\"fields\":[{\"name\":\"id\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"accountId\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"providerId\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"userId\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"user\",\"kind\":\"object\",\"type\":\"User\",\"relationName\":\"AccountToUser\"},{\"name\":\"accessToken\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"refreshToken\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"idToken\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"accessTokenExpiresAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"refreshTokenExpiresAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"scope\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"password\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"createdAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"updatedAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"}],\"dbName\":\"account\"},\"Verification\":{\"fields\":[{\"name\":\"id\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"identifier\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"value\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"expiresAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"createdAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"updatedAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"}],\"dbName\":\"verification\"},\"ColorPalette\":{\"fields\":[{\"name\":\"id\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"userId\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"name\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"colors\",\"kind\":\"scalar\",\"type\":\"Json\"},{\"name\":\"isDefault\",\"kind\":\"scalar\",\"type\":\"Boolean\"},{\"name\":\"createdAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"updatedAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"user\",\"kind\":\"object\",\"type\":\"User\",\"relationName\":\"ColorPaletteToUser\"}],\"dbName\":\"color_palette\"},\"DomainName\":{\"fields\":[{\"name\":\"id\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"userId\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"domain\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"registrar\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"registrarUrl\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"expiresAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"autoRenew\",\"kind\":\"scalar\",\"type\":\"Boolean\"},{\"name\":\"reminderOneMonth\",\"kind\":\"scalar\",\"type\":\"Boolean\"},{\"name\":\"reminderOneWeek\",\"kind\":\"scalar\",\"type\":\"Boolean\"},{\"name\":\"lastReminderSent\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"createdAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"updatedAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"user\",\"kind\":\"object\",\"type\":\"User\",\"relationName\":\"DomainNameToUser\"}],\"dbName\":\"domain_name\"},\"Snippet\":{\"fields\":[{\"name\":\"id\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"userId\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"title\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"description\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"language\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"code\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"tags\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"isFavorite\",\"kind\":\"scalar\",\"type\":\"Boolean\"},{\"name\":\"lastUsedAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"createdAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"updatedAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"user\",\"kind\":\"object\",\"type\":\"User\",\"relationName\":\"SnippetToUser\"}],\"dbName\":\"snippet\"},\"YouTubeVideo\":{\"fields\":[{\"name\":\"id\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"userId\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"url\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"title\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"videoId\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"playlistId\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"isPlaylist\",\"kind\":\"scalar\",\"type\":\"Boolean\"},{\"name\":\"createdAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"updatedAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"user\",\"kind\":\"object\",\"type\":\"User\",\"relationName\":\"UserToYouTubeVideo\"}],\"dbName\":\"youtube_video\"},\"PomodoroSettings\":{\"fields\":[{\"name\":\"id\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"userId\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"workDuration\",\"kind\":\"scalar\",\"type\":\"Int\"},{\"name\":\"breakDuration\",\"kind\":\"scalar\",\"type\":\"Int\"},{\"name\":\"cycles\",\"kind\":\"scalar\",\"type\":\"Int\"},{\"name\":\"sessionStartSound\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"breakStartSound\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"breakEndSound\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"sessionEndSound\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"backgroundImage\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"backgroundType\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"textColor\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"autoStartBreaks\",\"kind\":\"scalar\",\"type\":\"Boolean\"},{\"name\":\"autoStartPomodoros\",\"kind\":\"scalar\",\"type\":\"Boolean\"},{\"name\":\"soundEnabled\",\"kind\":\"scalar\",\"type\":\"Boolean\"},{\"name\":\"createdAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"updatedAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"user\",\"kind\":\"object\",\"type\":\"User\",\"relationName\":\"PomodoroSettingsToUser\"}],\"dbName\":\"pomodoro_settings\"},\"StickyNote\":{\"fields\":[{\"name\":\"id\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"userId\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"content\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"createdAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"updatedAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"user\",\"kind\":\"object\",\"type\":\"User\",\"relationName\":\"StickyNoteToUser\"}],\"dbName\":\"sticky_note\"}},\"enums\":{},\"types\":{}}") async function decodeBase64AsWasm(wasmBase64: string): Promise { const { Buffer } = await import('node:buffer') diff --git a/prisma/generated/prisma/internal/prismaNamespace.ts b/prisma/generated/prisma/internal/prismaNamespace.ts index 8ea8764..0730d3b 100644 --- a/prisma/generated/prisma/internal/prismaNamespace.ts +++ b/prisma/generated/prisma/internal/prismaNamespace.ts @@ -1270,7 +1270,7 @@ export type TransactionIsolationLevel = (typeof TransactionIsolationLevel)[keyof export const WorkspaceScalarFieldEnum = { id: 'id', userId: 'userId', - layout: 'layout', + moduleLayouts: 'moduleLayouts', pinnedModules: 'pinnedModules', createdAt: 'createdAt', updatedAt: 'updatedAt' diff --git a/prisma/generated/prisma/internal/prismaNamespaceBrowser.ts b/prisma/generated/prisma/internal/prismaNamespaceBrowser.ts index b753eb5..63b4c9f 100644 --- a/prisma/generated/prisma/internal/prismaNamespaceBrowser.ts +++ b/prisma/generated/prisma/internal/prismaNamespaceBrowser.ts @@ -83,7 +83,7 @@ export type TransactionIsolationLevel = (typeof TransactionIsolationLevel)[keyof export const WorkspaceScalarFieldEnum = { id: 'id', userId: 'userId', - layout: 'layout', + moduleLayouts: 'moduleLayouts', pinnedModules: 'pinnedModules', createdAt: 'createdAt', updatedAt: 'updatedAt' diff --git a/prisma/generated/prisma/models/Workspace.ts b/prisma/generated/prisma/models/Workspace.ts index 6b93c86..92621a1 100644 --- a/prisma/generated/prisma/models/Workspace.ts +++ b/prisma/generated/prisma/models/Workspace.ts @@ -41,7 +41,7 @@ export type WorkspaceMaxAggregateOutputType = { export type WorkspaceCountAggregateOutputType = { id: number userId: number - layout: number + moduleLayouts: number pinnedModules: number createdAt: number updatedAt: number @@ -66,7 +66,7 @@ export type WorkspaceMaxAggregateInputType = { export type WorkspaceCountAggregateInputType = { id?: true userId?: true - layout?: true + moduleLayouts?: true pinnedModules?: true createdAt?: true updatedAt?: true @@ -148,7 +148,7 @@ export type WorkspaceGroupByArgs | string userId?: Prisma.StringFilter<"Workspace"> | string - layout?: Prisma.JsonNullableFilter<"Workspace"> + moduleLayouts?: Prisma.JsonNullableFilter<"Workspace"> pinnedModules?: Prisma.StringNullableListFilter<"Workspace"> createdAt?: Prisma.DateTimeFilter<"Workspace"> | Date | string updatedAt?: Prisma.DateTimeFilter<"Workspace"> | Date | string @@ -188,7 +188,7 @@ export type WorkspaceWhereInput = { export type WorkspaceOrderByWithRelationInput = { id?: Prisma.SortOrder userId?: Prisma.SortOrder - layout?: Prisma.SortOrderInput | Prisma.SortOrder + moduleLayouts?: Prisma.SortOrderInput | Prisma.SortOrder pinnedModules?: Prisma.SortOrder createdAt?: Prisma.SortOrder updatedAt?: Prisma.SortOrder @@ -201,7 +201,7 @@ export type WorkspaceWhereUniqueInput = Prisma.AtLeast<{ AND?: Prisma.WorkspaceWhereInput | Prisma.WorkspaceWhereInput[] OR?: Prisma.WorkspaceWhereInput[] NOT?: Prisma.WorkspaceWhereInput | Prisma.WorkspaceWhereInput[] - layout?: Prisma.JsonNullableFilter<"Workspace"> + moduleLayouts?: Prisma.JsonNullableFilter<"Workspace"> pinnedModules?: Prisma.StringNullableListFilter<"Workspace"> createdAt?: Prisma.DateTimeFilter<"Workspace"> | Date | string updatedAt?: Prisma.DateTimeFilter<"Workspace"> | Date | string @@ -211,7 +211,7 @@ export type WorkspaceWhereUniqueInput = Prisma.AtLeast<{ export type WorkspaceOrderByWithAggregationInput = { id?: Prisma.SortOrder userId?: Prisma.SortOrder - layout?: Prisma.SortOrderInput | Prisma.SortOrder + moduleLayouts?: Prisma.SortOrderInput | Prisma.SortOrder pinnedModules?: Prisma.SortOrder createdAt?: Prisma.SortOrder updatedAt?: Prisma.SortOrder @@ -226,7 +226,7 @@ export type WorkspaceScalarWhereWithAggregatesInput = { NOT?: Prisma.WorkspaceScalarWhereWithAggregatesInput | Prisma.WorkspaceScalarWhereWithAggregatesInput[] id?: Prisma.StringWithAggregatesFilter<"Workspace"> | string userId?: Prisma.StringWithAggregatesFilter<"Workspace"> | string - layout?: Prisma.JsonNullableWithAggregatesFilter<"Workspace"> + moduleLayouts?: Prisma.JsonNullableWithAggregatesFilter<"Workspace"> pinnedModules?: Prisma.StringNullableListFilter<"Workspace"> createdAt?: Prisma.DateTimeWithAggregatesFilter<"Workspace"> | Date | string updatedAt?: Prisma.DateTimeWithAggregatesFilter<"Workspace"> | Date | string @@ -234,7 +234,7 @@ export type WorkspaceScalarWhereWithAggregatesInput = { export type WorkspaceCreateInput = { id: string - layout?: Prisma.NullableJsonNullValueInput | runtime.InputJsonValue + moduleLayouts?: Prisma.NullableJsonNullValueInput | runtime.InputJsonValue pinnedModules?: Prisma.WorkspaceCreatepinnedModulesInput | string[] createdAt?: Date | string updatedAt?: Date | string @@ -244,7 +244,7 @@ export type WorkspaceCreateInput = { export type WorkspaceUncheckedCreateInput = { id: string userId: string - layout?: Prisma.NullableJsonNullValueInput | runtime.InputJsonValue + moduleLayouts?: Prisma.NullableJsonNullValueInput | runtime.InputJsonValue pinnedModules?: Prisma.WorkspaceCreatepinnedModulesInput | string[] createdAt?: Date | string updatedAt?: Date | string @@ -252,7 +252,7 @@ export type WorkspaceUncheckedCreateInput = { export type WorkspaceUpdateInput = { id?: Prisma.StringFieldUpdateOperationsInput | string - layout?: Prisma.NullableJsonNullValueInput | runtime.InputJsonValue + moduleLayouts?: Prisma.NullableJsonNullValueInput | runtime.InputJsonValue pinnedModules?: Prisma.WorkspaceUpdatepinnedModulesInput | string[] createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string @@ -262,7 +262,7 @@ export type WorkspaceUpdateInput = { export type WorkspaceUncheckedUpdateInput = { id?: Prisma.StringFieldUpdateOperationsInput | string userId?: Prisma.StringFieldUpdateOperationsInput | string - layout?: Prisma.NullableJsonNullValueInput | runtime.InputJsonValue + moduleLayouts?: Prisma.NullableJsonNullValueInput | runtime.InputJsonValue pinnedModules?: Prisma.WorkspaceUpdatepinnedModulesInput | string[] createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string @@ -271,7 +271,7 @@ export type WorkspaceUncheckedUpdateInput = { export type WorkspaceCreateManyInput = { id: string userId: string - layout?: Prisma.NullableJsonNullValueInput | runtime.InputJsonValue + moduleLayouts?: Prisma.NullableJsonNullValueInput | runtime.InputJsonValue pinnedModules?: Prisma.WorkspaceCreatepinnedModulesInput | string[] createdAt?: Date | string updatedAt?: Date | string @@ -279,7 +279,7 @@ export type WorkspaceCreateManyInput = { export type WorkspaceUpdateManyMutationInput = { id?: Prisma.StringFieldUpdateOperationsInput | string - layout?: Prisma.NullableJsonNullValueInput | runtime.InputJsonValue + moduleLayouts?: Prisma.NullableJsonNullValueInput | runtime.InputJsonValue pinnedModules?: Prisma.WorkspaceUpdatepinnedModulesInput | string[] createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string @@ -288,7 +288,7 @@ export type WorkspaceUpdateManyMutationInput = { export type WorkspaceUncheckedUpdateManyInput = { id?: Prisma.StringFieldUpdateOperationsInput | string userId?: Prisma.StringFieldUpdateOperationsInput | string - layout?: Prisma.NullableJsonNullValueInput | runtime.InputJsonValue + moduleLayouts?: Prisma.NullableJsonNullValueInput | runtime.InputJsonValue pinnedModules?: Prisma.WorkspaceUpdatepinnedModulesInput | string[] createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string @@ -305,7 +305,7 @@ export type StringNullableListFilter<$PrismaModel = never> = { export type WorkspaceCountOrderByAggregateInput = { id?: Prisma.SortOrder userId?: Prisma.SortOrder - layout?: Prisma.SortOrder + moduleLayouts?: Prisma.SortOrder pinnedModules?: Prisma.SortOrder createdAt?: Prisma.SortOrder updatedAt?: Prisma.SortOrder @@ -381,7 +381,7 @@ export type WorkspaceUncheckedUpdateOneWithoutUserNestedInput = { export type WorkspaceCreateWithoutUserInput = { id: string - layout?: Prisma.NullableJsonNullValueInput | runtime.InputJsonValue + moduleLayouts?: Prisma.NullableJsonNullValueInput | runtime.InputJsonValue pinnedModules?: Prisma.WorkspaceCreatepinnedModulesInput | string[] createdAt?: Date | string updatedAt?: Date | string @@ -389,7 +389,7 @@ export type WorkspaceCreateWithoutUserInput = { export type WorkspaceUncheckedCreateWithoutUserInput = { id: string - layout?: Prisma.NullableJsonNullValueInput | runtime.InputJsonValue + moduleLayouts?: Prisma.NullableJsonNullValueInput | runtime.InputJsonValue pinnedModules?: Prisma.WorkspaceCreatepinnedModulesInput | string[] createdAt?: Date | string updatedAt?: Date | string @@ -413,7 +413,7 @@ export type WorkspaceUpdateToOneWithWhereWithoutUserInput = { export type WorkspaceUpdateWithoutUserInput = { id?: Prisma.StringFieldUpdateOperationsInput | string - layout?: Prisma.NullableJsonNullValueInput | runtime.InputJsonValue + moduleLayouts?: Prisma.NullableJsonNullValueInput | runtime.InputJsonValue pinnedModules?: Prisma.WorkspaceUpdatepinnedModulesInput | string[] createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string @@ -421,7 +421,7 @@ export type WorkspaceUpdateWithoutUserInput = { export type WorkspaceUncheckedUpdateWithoutUserInput = { id?: Prisma.StringFieldUpdateOperationsInput | string - layout?: Prisma.NullableJsonNullValueInput | runtime.InputJsonValue + moduleLayouts?: Prisma.NullableJsonNullValueInput | runtime.InputJsonValue pinnedModules?: Prisma.WorkspaceUpdatepinnedModulesInput | string[] createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string @@ -432,7 +432,7 @@ export type WorkspaceUncheckedUpdateWithoutUserInput = { export type WorkspaceSelect = runtime.Types.Extensions.GetSelect<{ id?: boolean userId?: boolean - layout?: boolean + moduleLayouts?: boolean pinnedModules?: boolean createdAt?: boolean updatedAt?: boolean @@ -442,7 +442,7 @@ export type WorkspaceSelect = runtime.Types.Extensions.GetSelect<{ id?: boolean userId?: boolean - layout?: boolean + moduleLayouts?: boolean pinnedModules?: boolean createdAt?: boolean updatedAt?: boolean @@ -452,7 +452,7 @@ export type WorkspaceSelectCreateManyAndReturn = runtime.Types.Extensions.GetSelect<{ id?: boolean userId?: boolean - layout?: boolean + moduleLayouts?: boolean pinnedModules?: boolean createdAt?: boolean updatedAt?: boolean @@ -462,13 +462,13 @@ export type WorkspaceSelectUpdateManyAndReturn = runtime.Types.Extensions.GetOmit<"id" | "userId" | "layout" | "pinnedModules" | "createdAt" | "updatedAt", ExtArgs["result"]["workspace"]> +export type WorkspaceOmit = runtime.Types.Extensions.GetOmit<"id" | "userId" | "moduleLayouts" | "pinnedModules" | "createdAt" | "updatedAt", ExtArgs["result"]["workspace"]> export type WorkspaceInclude = { User?: boolean | Prisma.UserDefaultArgs } @@ -487,7 +487,7 @@ export type $WorkspacePayload readonly userId: Prisma.FieldRef<"Workspace", 'String'> - readonly layout: Prisma.FieldRef<"Workspace", 'Json'> + readonly moduleLayouts: Prisma.FieldRef<"Workspace", 'Json'> readonly pinnedModules: Prisma.FieldRef<"Workspace", 'String[]'> readonly createdAt: Prisma.FieldRef<"Workspace", 'DateTime'> readonly updatedAt: Prisma.FieldRef<"Workspace", 'DateTime'> diff --git a/prisma/migrations/20251231121111_add_module_order_for_drag_and_drop_feature/migration.sql b/prisma/migrations/20251231121111_add_module_order_for_drag_and_drop_feature/migration.sql new file mode 100644 index 0000000..d8dd005 --- /dev/null +++ b/prisma/migrations/20251231121111_add_module_order_for_drag_and_drop_feature/migration.sql @@ -0,0 +1,9 @@ +/* + Warnings: + + - You are about to drop the column `layout` on the `Workspace` table. All the data in the column will be lost. + +*/ +-- AlterTable +ALTER TABLE "Workspace" DROP COLUMN "layout", +ADD COLUMN "moduleOrder" TEXT[] DEFAULT ARRAY[]::TEXT[]; diff --git a/prisma/migrations/20251231131356_change_module_order_to_json/migration.sql b/prisma/migrations/20251231131356_change_module_order_to_json/migration.sql new file mode 100644 index 0000000..70f48dd --- /dev/null +++ b/prisma/migrations/20251231131356_change_module_order_to_json/migration.sql @@ -0,0 +1,9 @@ +/* + Warnings: + + - The `moduleOrder` column on the `Workspace` table would be dropped and recreated. This will lead to data loss if there is data in the column. + +*/ +-- AlterTable +ALTER TABLE "Workspace" DROP COLUMN "moduleOrder", +ADD COLUMN "moduleOrder" JSONB; diff --git a/prisma/migrations/20251231133936_change_to_module_layouts/migration.sql b/prisma/migrations/20251231133936_change_to_module_layouts/migration.sql new file mode 100644 index 0000000..e507e5c --- /dev/null +++ b/prisma/migrations/20251231133936_change_to_module_layouts/migration.sql @@ -0,0 +1,9 @@ +/* + Warnings: + + - You are about to drop the column `moduleOrder` on the `Workspace` table. All the data in the column will be lost. + +*/ +-- AlterTable +ALTER TABLE "Workspace" DROP COLUMN "moduleOrder", +ADD COLUMN "moduleLayouts" JSONB; diff --git a/prisma/schema.prisma b/prisma/schema.prisma index dbbe81e..6348a23 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -10,7 +10,7 @@ datasource db { model Workspace { id String @id userId String @unique - layout Json? + moduleLayouts Json? pinnedModules String[] @default([]) createdAt DateTime @default(now()) updatedAt DateTime @updatedAt diff --git a/src/contexts/modules-context.tsx b/src/contexts/modules-context.tsx index bb1ae49..5e9b8dd 100644 --- a/src/contexts/modules-context.tsx +++ b/src/contexts/modules-context.tsx @@ -4,12 +4,26 @@ import { AuthRequiredModal } from "@/components/auth/auth-required-modal"; import { createContext, ReactNode, + useCallback, useContext, useEffect, + useMemo, useRef, useState, } from "react"; import { useModule } from "../hooks/useModule"; +import { useBreakpoint } from "../hooks/useBreakpoint"; +import { + type ModuleOrder, + type ColumnId, + type ColumnCount, + type ModuleLayouts, + DEFAULT_MODULE_ORDER, + DEFAULT_MODULE_LAYOUTS, + getVisibleColumnIds, + getLayoutForColumnCount, + updateLayoutInLayouts, +} from "../types/module-order"; interface ModulesContextType { pinnedModules: string[]; @@ -24,6 +38,10 @@ interface ModulesContextType { setSidebarCollapsed: (collapsed: boolean) => void; isAuthenticated: boolean; onAuthRequired: () => void; + moduleOrder: ModuleOrder; + handleReorderModules: (newOrder: ModuleOrder) => void; + columnCount: number; + visibleColumns: ColumnId[]; } const ModulesContext = createContext(undefined); @@ -36,8 +54,22 @@ export function ModulesProvider({ session: any; }) { const isAuthenticated = !!session; - const { getPinnedModules, togglePin, isToggling, isLoading } = - useModule(isAuthenticated); + + // Detect current breakpoint/column count + const columnCount = useBreakpoint(); + const visibleColumns = useMemo( + () => getVisibleColumnIds(columnCount), + [columnCount] + ); + + const { + getPinnedModules, + togglePin, + isToggling, + isLoading, + getModuleOrder, + updateModuleOrder, + } = useModule(isAuthenticated, columnCount as ColumnCount); // tempOpenModules for visitors only const [tempOpenModules, setTempOpenModules] = useState([ @@ -47,6 +79,15 @@ export function ModulesProvider({ "pomodoro-timer", ]); + // moduleLayouts for visitors (localStorage) - multi-breakpoint storage + const [visitorModuleLayouts, setVisitorModuleLayouts] = useState( + () => { + if (typeof window === "undefined") return DEFAULT_MODULE_LAYOUTS; + const saved = localStorage.getItem("module-layouts"); + return saved ? JSON.parse(saved) : DEFAULT_MODULE_LAYOUTS; + } + ); + //load collapsed sidebar state from localStorage const [sidebarCollapsed, setSidebarCollapsedState] = useState(() => { if (typeof window === "undefined") return false; @@ -103,6 +144,35 @@ export function ModulesProvider({ const isTempOpen = (moduleId: string) => tempOpenModules.includes(moduleId); + // Unified module order state - extract layout for current columnCount + const moduleOrder = useMemo(() => { + if (isAuthenticated) { + return getModuleOrder ?? DEFAULT_MODULE_ORDER; + } else { + return getLayoutForColumnCount(visitorModuleLayouts, columnCount as ColumnCount); + } + }, [isAuthenticated, getModuleOrder, visitorModuleLayouts, columnCount]); + + // Handle reordering modules + const handleReorderModules = useCallback( + (newOrder: ModuleOrder) => { + if (isAuthenticated) { + // Save to database via tRPC mutation with columnCount + updateModuleOrder({ moduleOrder: newOrder, columnCount }); + } else { + // Update only the current columnCount's layout for visitors + const updatedLayouts = updateLayoutInLayouts( + visitorModuleLayouts, + columnCount as ColumnCount, + newOrder + ); + setVisitorModuleLayouts(updatedLayouts); + localStorage.setItem("module-layouts", JSON.stringify(updatedLayouts)); + } + }, + [isAuthenticated, updateModuleOrder, columnCount, visitorModuleLayouts] + ); + return ( {children} diff --git a/src/hooks/useBreakpoint.tsx b/src/hooks/useBreakpoint.tsx new file mode 100644 index 0000000..b413b99 --- /dev/null +++ b/src/hooks/useBreakpoint.tsx @@ -0,0 +1,41 @@ +"use client"; + +import { useEffect, useState } from "react"; + +/** + * Hook to detect the current number of columns based on screen width + * Matches Tailwind breakpoints: + * - < 768px (mobile): 1 column + * - 768px-1023px (md/tablet): 2 columns + * - 1024px-1919px (lg/desktop): 3 columns + * - 1920px-2399px (3xl/ultra-wide): 4 columns + * - >= 2400px (4xl/extreme): 5 columns + */ +export function useBreakpoint(): number { + const [columnCount, setColumnCount] = useState(3); // Default to 3 columns + + useEffect(() => { + const getColumnCount = () => { + const width = window.innerWidth; + + if (width < 768) return 1; // mobile + if (width < 1024) return 2; // tablet + if (width < 1920) return 3; // desktop + if (width < 2400) return 4; // ultra-wide + return 5; // extreme + }; + + // Set initial value + setColumnCount(getColumnCount()); + + // Update on resize + const handleResize = () => { + setColumnCount(getColumnCount()); + }; + + window.addEventListener("resize", handleResize); + return () => window.removeEventListener("resize", handleResize); + }, []); + + return columnCount; +} diff --git a/src/hooks/useModule.tsx b/src/hooks/useModule.tsx index 5402f9f..fb4eab1 100644 --- a/src/hooks/useModule.tsx +++ b/src/hooks/useModule.tsx @@ -1,7 +1,8 @@ import { toast } from "sonner"; import { api } from "../lib/trpc/client"; +import type { ColumnCount } from "../types/module-order"; -export function useModule(isAuthenticated: boolean) { +export function useModule(isAuthenticated: boolean, columnCount: ColumnCount) { const utils = api.useUtils(); //list pinned modules - only query if authenticated @@ -20,15 +21,60 @@ export function useModule(isAuthenticated: boolean) { }, }); + //get module order - only query if authenticated + const getModuleOrder = api.module.getModuleOrder.useQuery( + { columnCount }, + { + enabled: isAuthenticated, + } + ); + + //update module order + const updateModuleOrderMutation = api.module.updateModuleOrder.useMutation({ + onMutate: async (variables) => { + // Cancel outgoing refetches + await utils.module.getModuleOrder.cancel({ columnCount: variables.columnCount }); + + // Snapshot previous value + const previousOrder = utils.module.getModuleOrder.getData({ columnCount: variables.columnCount }); + + // Optimistically update + utils.module.getModuleOrder.setData( + { columnCount: variables.columnCount }, + variables.moduleOrder + ); + + return { previousOrder, columnCount: variables.columnCount }; + }, + onError: (err, variables, context) => { + // Rollback on error + if (context?.previousOrder && context?.columnCount) { + utils.module.getModuleOrder.setData( + { columnCount: context.columnCount }, + context.previousOrder + ); + } + toast.error("Failed to save module order"); + console.error("Error updating module order: ", err); + }, + onSettled: (data, error, variables) => { + utils.module.getModuleOrder.invalidate({ columnCount: variables.columnCount }); + }, + }); + return { //queries getPinnedModules: getPinnedModules.data, + getModuleOrder: getModuleOrder.data, //mutations togglePin: togglePinMutation.mutateAsync, + updateModuleOrder: updateModuleOrderMutation.mutateAsync, //states isToggling: togglePinMutation.isPending, isLoading: getPinnedModules.isLoading, + isOrderLoading: getModuleOrder.isLoading, + isOrderUpdating: updateModuleOrderMutation.isPending, }; } diff --git a/src/server/routers/module.ts b/src/server/routers/module.ts index b5f8b40..3c4de3e 100644 --- a/src/server/routers/module.ts +++ b/src/server/routers/module.ts @@ -1,4 +1,12 @@ import { db } from "@/src/lib/db"; +import { + DEFAULT_MODULE_LAYOUTS, + type ModuleLayouts, + type ModuleOrder, + type ColumnCount, + getLayoutForColumnCount, + updateLayoutInLayouts, +} from "@/src/types/module-order"; import z from "zod"; import { createTRPCRouter, protectedProcedure } from "../trpc"; @@ -20,9 +28,10 @@ export const moduleRouter = createTRPCRouter({ .mutation(async ({ ctx, input }) => { const userId = ctx.session.user.id; - // Get or create workspace + // Get or create workspace (only select pinnedModules to avoid moduleOrder parsing issues) let workspace = await db.workspace.findUnique({ where: { userId }, + select: { pinnedModules: true, id: true }, }); if (!workspace) { @@ -32,6 +41,7 @@ export const moduleRouter = createTRPCRouter({ userId, pinnedModules: [], }, + select: { pinnedModules: true, id: true }, }); } @@ -53,4 +63,95 @@ export const moduleRouter = createTRPCRouter({ return updated.pinnedModules; }), + + // Get all module layouts + getModuleLayouts: protectedProcedure.query(async ({ ctx }) => { + const workspace = await db.workspace.findUnique({ + where: { userId: ctx.session.user.id }, + select: { moduleLayouts: true }, + }); + + if (!workspace?.moduleLayouts) { + return DEFAULT_MODULE_LAYOUTS; + } + + // Parse JSON and validate structure + const layouts = workspace.moduleLayouts as unknown as ModuleLayouts; + return layouts; + }), + + // Get module order for specific column count + getModuleOrder: protectedProcedure + .input(z.object({ columnCount: z.number().min(1).max(5) })) + .query(async ({ ctx, input }) => { + const workspace = await db.workspace.findUnique({ + where: { userId: ctx.session.user.id }, + select: { moduleLayouts: true }, + }); + + if (!workspace?.moduleLayouts) { + return getLayoutForColumnCount( + DEFAULT_MODULE_LAYOUTS, + input.columnCount as ColumnCount + ); + } + + const layouts = workspace.moduleLayouts as unknown as ModuleLayouts; + return getLayoutForColumnCount(layouts, input.columnCount as ColumnCount); + }), + + // Update module order for specific column count + updateModuleOrder: protectedProcedure + .input( + z.object({ + columnCount: z.number().min(1).max(5), + moduleOrder: z.object({ + col1: z.array(z.string()), + col2: z.array(z.string()), + col3: z.array(z.string()), + col4: z.array(z.string()), + col5: z.array(z.string()), + }), + }) + ) + .mutation(async ({ ctx, input }) => { + const userId = ctx.session.user.id; + + // Get or create workspace + let workspace = await db.workspace.findUnique({ + where: { userId }, + select: { moduleLayouts: true, id: true }, + }); + + const currentLayouts = workspace?.moduleLayouts + ? (workspace.moduleLayouts as unknown as ModuleLayouts) + : DEFAULT_MODULE_LAYOUTS; + + // Update only the layout for this column count + const updatedLayouts = updateLayoutInLayouts( + currentLayouts, + input.columnCount as ColumnCount, + input.moduleOrder + ); + + if (!workspace) { + workspace = await db.workspace.create({ + data: { + id: crypto.randomUUID(), + userId, + pinnedModules: [], + moduleLayouts: updatedLayouts as any, + }, + select: { moduleLayouts: true, id: true }, + }); + } else { + workspace = await db.workspace.update({ + where: { userId }, + data: { moduleLayouts: updatedLayouts as any }, + select: { moduleLayouts: true, id: true }, + }); + } + + return workspace.moduleLayouts as unknown as ModuleLayouts; + }), }); diff --git a/src/types/module-order.ts b/src/types/module-order.ts new file mode 100644 index 0000000..b349578 --- /dev/null +++ b/src/types/module-order.ts @@ -0,0 +1,172 @@ +/** + * Type definitions for column-based module ordering with multi-breakpoint support + */ + +export interface ModuleOrder { + col1: string[]; + col2: string[]; + col3: string[]; + col4: string[]; + col5: string[]; +} + +export const DEFAULT_MODULE_ORDER: ModuleOrder = { + col1: [], + col2: [], + col3: [], + col4: [], + col5: [], +}; + +/** + * Multi-breakpoint layouts: one layout per column count (1-5) + */ +export interface ModuleLayouts { + 1: { col1: string[] }; + 2: { col1: string[]; col2: string[] }; + 3: { col1: string[]; col2: string[]; col3: string[] }; + 4: { col1: string[]; col2: string[]; col3: string[]; col4: string[] }; + 5: { col1: string[]; col2: string[]; col3: string[]; col4: string[]; col5: string[] }; +} + +export const DEFAULT_MODULE_LAYOUTS: ModuleLayouts = { + 1: { col1: [] }, + 2: { col1: [], col2: [] }, + 3: { col1: [], col2: [], col3: [] }, + 4: { col1: [], col2: [], col3: [], col4: [] }, + 5: { col1: [], col2: [], col3: [], col4: [], col5: [] }, +}; + +export type ColumnCount = 1 | 2 | 3 | 4 | 5; + +export type ColumnId = "col1" | "col2" | "col3" | "col4" | "col5"; + +export const COLUMN_IDS: ColumnId[] = ["col1", "col2", "col3", "col4", "col5"]; + +/** + * Get the number of visible columns based on breakpoint + */ +export function getVisibleColumnIds(breakpoint: number): ColumnId[] { + return COLUMN_IDS.slice(0, breakpoint) as ColumnId[]; +} + +/** + * Distribute modules across columns evenly + */ +export function distributeModulesAcrossColumns( + modules: string[], + columnCount: number +): ModuleOrder { + const order: ModuleOrder = { ...DEFAULT_MODULE_ORDER }; + const visibleColumns = getVisibleColumnIds(columnCount); + + modules.forEach((moduleId, index) => { + const columnIndex = index % columnCount; + const columnId = visibleColumns[columnIndex]; + if (columnId) { + order[columnId].push(moduleId); + } + }); + + return order; +} + +/** + * Flatten column order to get all module IDs in order + */ +export function flattenModuleOrder( + order: ModuleOrder, + columnCount: number +): string[] { + const visibleColumns = getVisibleColumnIds(columnCount); + const result: string[] = []; + + // Get max length to iterate through rows + const maxLength = Math.max( + ...visibleColumns.map((colId) => order[colId].length) + ); + + // Iterate row by row across columns (masonry order) + for (let i = 0; i < maxLength; i++) { + visibleColumns.forEach((colId) => { + const moduleId = order[colId][i]; + if (moduleId) { + result.push(moduleId); + } + }); + } + + return result; +} + +/** + * Get layout for specific column count from ModuleLayouts + */ +export function getLayoutForColumnCount( + layouts: ModuleLayouts, + columnCount: ColumnCount +): ModuleOrder { + const layout = layouts[columnCount]; + + // Extend layout to full ModuleOrder structure + return { + col1: layout.col1 || [], + col2: (layout as any).col2 || [], + col3: (layout as any).col3 || [], + col4: (layout as any).col4 || [], + col5: (layout as any).col5 || [], + }; +} + +/** + * Initialize a layout with modules distributed across available columns + */ +export function initializeLayoutForColumnCount( + modules: string[], + columnCount: ColumnCount +): ModuleOrder { + const layout: ModuleOrder = { ...DEFAULT_MODULE_ORDER }; + const visibleColumns = getVisibleColumnIds(columnCount); + + modules.forEach((moduleId, index) => { + const columnIndex = index % columnCount; + const columnId = visibleColumns[columnIndex]; + if (columnId) { + layout[columnId].push(moduleId); + } + }); + + return layout; +} + +/** + * Update specific layout in ModuleLayouts + */ +export function updateLayoutInLayouts( + layouts: ModuleLayouts, + columnCount: ColumnCount, + newLayout: ModuleOrder +): ModuleLayouts { + const updated = { ...layouts }; + + // Only store the columns relevant to this breakpoint + switch (columnCount) { + case 1: + updated[1] = { col1: newLayout.col1 }; + break; + case 2: + updated[2] = { col1: newLayout.col1, col2: newLayout.col2 }; + break; + case 3: + updated[3] = { col1: newLayout.col1, col2: newLayout.col2, col3: newLayout.col3 }; + break; + case 4: + updated[4] = { col1: newLayout.col1, col2: newLayout.col2, col3: newLayout.col3, col4: newLayout.col4 }; + break; + case 5: + updated[5] = newLayout; + break; + } + + return updated; +}