diff --git a/src/App.tsx b/src/App.tsx index 0e9230e..f92ed60 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,7 +1,10 @@ import { BrowserRouter as Router, Routes, Route } from 'react-router-dom' import { motion } from 'framer-motion' import { Code } from '@phosphor-icons/react' -import { Navigation, NavigationProvider, NavigationSidebar, useNavigation } from '@/components/layout/Navigation' +import { Navigation } from '@/components/layout/navigation/Navigation' +import { NavigationProvider } from '@/components/layout/navigation/NavigationProvider' +import { NavigationSidebar } from '@/components/layout/navigation/NavigationSidebar' +import { useNavigation } from '@/components/layout/navigation/useNavigation' import { BackendIndicator } from '@/components/layout/BackendIndicator' import { HomePage } from '@/pages/HomePage' import { DemoPage } from '@/pages/DemoPage' diff --git a/src/components/layout/navigation/Navigation.tsx b/src/components/layout/navigation/Navigation.tsx new file mode 100644 index 0000000..92c1493 --- /dev/null +++ b/src/components/layout/navigation/Navigation.tsx @@ -0,0 +1,16 @@ +import { List } from '@phosphor-icons/react' +import { Button } from '@/components/ui/button' +import { useNavigation } from './useNavigation' + +export function Navigation() { + const { menuOpen, setMenuOpen } = useNavigation() + return ( + + ) +} diff --git a/src/components/layout/navigation/NavigationProvider.tsx b/src/components/layout/navigation/NavigationProvider.tsx new file mode 100644 index 0000000..533610c --- /dev/null +++ b/src/components/layout/navigation/NavigationProvider.tsx @@ -0,0 +1,12 @@ +import { useState, type ReactNode } from 'react' +import { NavigationContext } from './navigation-context' + +export function NavigationProvider({ children }: { children: ReactNode }) { + const [menuOpen, setMenuOpen] = useState(false) + + return ( + + {children} + + ) +} diff --git a/src/components/layout/Navigation.tsx b/src/components/layout/navigation/NavigationSidebar.tsx similarity index 57% rename from src/components/layout/Navigation.tsx rename to src/components/layout/navigation/NavigationSidebar.tsx index e913825..099c131 100644 --- a/src/components/layout/Navigation.tsx +++ b/src/components/layout/navigation/NavigationSidebar.tsx @@ -1,66 +1,10 @@ -import { createContext, useContext, useState } from 'react' import { motion } from 'framer-motion' -import { Button } from '@/components/ui/button' -import { - List, - House, - Atom, - FlowArrow, - Layout, - X, - Sparkle, - Gear, -} from '@phosphor-icons/react' -import { cn } from '@/lib/utils' import { Link, useLocation } from 'react-router-dom' - -const navigationItems = [ - { path: '/', label: 'Home', icon: House }, - { path: '/demo', label: 'Split-Screen Demo', icon: Sparkle }, - { path: '/atoms', label: 'Atoms', icon: Atom }, - { path: '/molecules', label: 'Molecules', icon: FlowArrow }, - { path: '/organisms', label: 'Organisms', icon: Layout }, - { path: '/templates', label: 'Templates', icon: Layout }, - { path: '/settings', label: 'Settings', icon: Gear }, -] - -type NavigationContextType = { - menuOpen: boolean - setMenuOpen: (open: boolean) => void -} - -const NavigationContext = createContext(undefined) - -export function NavigationProvider({ children }: { children: React.ReactNode }) { - const [menuOpen, setMenuOpen] = useState(false) - - return ( - - {children} - - ) -} - -export function useNavigation() { - const context = useContext(NavigationContext) - if (!context) { - throw new Error('useNavigation must be used within NavigationProvider') - } - return context -} - -export function Navigation() { - const { menuOpen, setMenuOpen } = useNavigation() - return ( - - ) -} +import { X } from '@phosphor-icons/react' +import { Button } from '@/components/ui/button' +import { cn } from '@/lib/utils' +import { navigationItems } from './navigation-items' +import { useNavigation } from './useNavigation' export function NavigationSidebar() { const { menuOpen, setMenuOpen } = useNavigation() diff --git a/src/components/layout/navigation/navigation-context.tsx b/src/components/layout/navigation/navigation-context.tsx new file mode 100644 index 0000000..d04dd48 --- /dev/null +++ b/src/components/layout/navigation/navigation-context.tsx @@ -0,0 +1,10 @@ +import { createContext } from 'react' + +type NavigationContextType = { + menuOpen: boolean + setMenuOpen: (open: boolean) => void +} + +export const NavigationContext = createContext( + undefined +) diff --git a/src/components/layout/navigation/navigation-items.ts b/src/components/layout/navigation/navigation-items.ts new file mode 100644 index 0000000..bdcf6cb --- /dev/null +++ b/src/components/layout/navigation/navigation-items.ts @@ -0,0 +1,18 @@ +import { + Atom, + FlowArrow, + Gear, + House, + Layout, + Sparkle, +} from '@phosphor-icons/react' + +export const navigationItems = [ + { path: '/', label: 'Home', icon: House }, + { path: '/demo', label: 'Split-Screen Demo', icon: Sparkle }, + { path: '/atoms', label: 'Atoms', icon: Atom }, + { path: '/molecules', label: 'Molecules', icon: FlowArrow }, + { path: '/organisms', label: 'Organisms', icon: Layout }, + { path: '/templates', label: 'Templates', icon: Layout }, + { path: '/settings', label: 'Settings', icon: Gear }, +] diff --git a/src/components/layout/navigation/useNavigation.ts b/src/components/layout/navigation/useNavigation.ts new file mode 100644 index 0000000..47f69a8 --- /dev/null +++ b/src/components/layout/navigation/useNavigation.ts @@ -0,0 +1,10 @@ +import { useContext } from 'react' +import { NavigationContext } from './navigation-context' + +export function useNavigation() { + const context = useContext(NavigationContext) + if (!context) { + throw new Error('useNavigation must be used within NavigationProvider') + } + return context +} diff --git a/src/components/ui/sidebar-menu.tsx b/src/components/ui/sidebar-menu.tsx deleted file mode 100644 index a1153c4..0000000 --- a/src/components/ui/sidebar-menu.tsx +++ /dev/null @@ -1,318 +0,0 @@ -"use client" - -import { CSSProperties, ComponentProps, useMemo } from "react" -import { Slot } from "@radix-ui/react-slot" -import { VariantProps, cva } from "class-variance-authority" -import { cn } from "@/lib/utils" -import { Skeleton } from "@/components/ui/skeleton" -import { - Tooltip, - TooltipContent, - TooltipTrigger, -} from "@/components/ui/tooltip" -import { useSidebar } from "./sidebar-context" - -export function SidebarGroupLabel({ - className, - asChild = false, - ...props -}: ComponentProps<"div"> & { asChild?: boolean }) { - const Comp = asChild ? Slot : "div" - - return ( - svg]:size-4 [&>svg]:shrink-0", - "group-data-[collapsible=icon]:-mt-8 group-data-[collapsible=icon]:opacity-0", - className - )} - {...props} - /> - ) -} - -export function SidebarGroupAction({ - className, - asChild = false, - ...props -}: ComponentProps<"button"> & { asChild?: boolean }) { - const Comp = asChild ? Slot : "button" - - return ( - svg]:size-4 [&>svg]:shrink-0", - // Increases the hit area of the button on mobile. - "after:absolute after:-inset-2 md:after:hidden", - "group-data-[collapsible=icon]:hidden", - className - )} - {...props} - /> - ) -} - -export function SidebarGroupContent({ - className, - ...props -}: ComponentProps<"div">) { - return ( -
- ) -} - -export function SidebarMenu({ className, ...props }: ComponentProps<"ul">) { - return ( -
    - ) -} - -export function SidebarMenuItem({ className, ...props }: ComponentProps<"li">) { - return ( -
  • - ) -} - -const sidebarMenuButtonVariants = cva( - "peer/menu-button flex w-full items-center gap-2 overflow-hidden rounded-md p-2 text-left text-sm outline-hidden ring-sidebar-ring transition-[width,height,padding] hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 active:bg-sidebar-accent active:text-sidebar-accent-foreground disabled:pointer-events-none disabled:opacity-50 group-has-data-[sidebar=menu-action]/menu-item:pr-8 aria-disabled:pointer-events-none aria-disabled:opacity-50 data-[active=true]:bg-sidebar-accent data-[active=true]:font-medium data-[active=true]:text-sidebar-accent-foreground data-[state=open]:hover:bg-sidebar-accent data-[state=open]:hover:text-sidebar-accent-foreground group-data-[collapsible=icon]:size-8! group-data-[collapsible=icon]:p-2! [&>span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0", - { - variants: { - variant: { - default: "hover:bg-sidebar-accent hover:text-sidebar-accent-foreground", - outline: - "bg-background shadow-[0_0_0_1px_hsl(var(--sidebar-border))] hover:bg-sidebar-accent hover:text-sidebar-accent-foreground hover:shadow-[0_0_0_1px_hsl(var(--sidebar-accent))]", - }, - size: { - default: "h-8 text-sm", - sm: "h-7 text-xs", - lg: "h-12 text-sm group-data-[collapsible=icon]:p-0!", - }, - }, - defaultVariants: { - variant: "default", - size: "default", - }, - } -) - -export function SidebarMenuButton({ - asChild = false, - isActive = false, - variant = "default", - size = "default", - tooltip, - className, - ...props -}: ComponentProps<"button"> & { - asChild?: boolean - isActive?: boolean - tooltip?: string | ComponentProps -} & VariantProps) { - const Comp = asChild ? Slot : "button" - const { isMobile, state } = useSidebar() - - const button = ( - - ) - - if (!tooltip) { - return button - } - - if (typeof tooltip === "string") { - tooltip = { - children: tooltip, - } - } - - return ( - - {button} - - ) -} - -export function SidebarMenuAction({ - className, - asChild = false, - showOnHover = false, - ...props -}: ComponentProps<"button"> & { - asChild?: boolean - showOnHover?: boolean -}) { - const Comp = asChild ? Slot : "button" - - return ( - svg]:size-4 [&>svg]:shrink-0", - // Increases the hit area of the button on mobile. - "after:absolute after:-inset-2 md:after:hidden", - "peer-data-[size=sm]/menu-button:top-1", - "peer-data-[size=default]/menu-button:top-1.5", - "peer-data-[size=lg]/menu-button:top-2.5", - "group-data-[collapsible=icon]:hidden", - showOnHover && - "peer-data-[active=true]/menu-button:text-sidebar-accent-foreground group-focus-within/menu-item:opacity-100 group-hover/menu-item:opacity-100 data-[state=open]:opacity-100 md:opacity-0", - className - )} - {...props} - /> - ) -} - -export function SidebarMenuBadge({ - className, - ...props -}: ComponentProps<"div">) { - return ( -
    - ) -} - -export function SidebarMenuSkeleton({ - className, - showIcon = false, - ...props -}: ComponentProps<"div"> & { - showIcon?: boolean -}) { - // Random width between 50 to 90%. - const width = useMemo(() => { - return `${Math.floor(Math.random() * 40) + 50}%` - }, []) - - return ( -
    - {showIcon && ( - - )} - -
    - ) -} - -export function SidebarMenuSub({ className, ...props }: ComponentProps<"ul">) { - return ( -
      - ) -} - -export function SidebarMenuSubItem({ - className, - ...props -}: ComponentProps<"li">) { - return ( -
    • - ) -} - -export function SidebarMenuSubButton({ - asChild = false, - size = "md", - isActive = false, - className, - ...props -}: ComponentProps<"a"> & { - asChild?: boolean - size?: "sm" | "md" - isActive?: boolean -}) { - const Comp = asChild ? Slot : "a" - - return ( - svg]:text-sidebar-accent-foreground flex h-7 min-w-0 -translate-x-px items-center gap-2 overflow-hidden rounded-md px-2 outline-hidden focus-visible:ring-2 disabled:pointer-events-none disabled:opacity-50 aria-disabled:pointer-events-none aria-disabled:opacity-50 [&>span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0", - "data-[active=true]:bg-sidebar-accent data-[active=true]:text-sidebar-accent-foreground", - size === "sm" && "text-xs", - size === "md" && "text-sm", - "group-data-[collapsible=icon]:hidden", - className - )} - {...props} - /> - ) -} diff --git a/src/components/ui/sidebar-menu/SidebarGroupAction.tsx b/src/components/ui/sidebar-menu/SidebarGroupAction.tsx new file mode 100644 index 0000000..81fc12a --- /dev/null +++ b/src/components/ui/sidebar-menu/SidebarGroupAction.tsx @@ -0,0 +1,28 @@ +"use client" + +import { ComponentProps } from "react" +import { Slot } from "@radix-ui/react-slot" +import { cn } from "@/lib/utils" + +export function SidebarGroupAction({ + className, + asChild = false, + ...props +}: ComponentProps<"button"> & { asChild?: boolean }) { + const Comp = asChild ? Slot : "button" + + return ( + svg]:size-4 [&>svg]:shrink-0", + // Increases the hit area of the button on mobile. + "after:absolute after:-inset-2 md:after:hidden", + "group-data-[collapsible=icon]:hidden", + className + )} + {...props} + /> + ) +} diff --git a/src/components/ui/sidebar-menu/SidebarGroupContent.tsx b/src/components/ui/sidebar-menu/SidebarGroupContent.tsx new file mode 100644 index 0000000..738c767 --- /dev/null +++ b/src/components/ui/sidebar-menu/SidebarGroupContent.tsx @@ -0,0 +1,18 @@ +"use client" + +import { ComponentProps } from "react" +import { cn } from "@/lib/utils" + +export function SidebarGroupContent({ + className, + ...props +}: ComponentProps<"div">) { + return ( +
      + ) +} diff --git a/src/components/ui/sidebar-menu/SidebarGroupLabel.tsx b/src/components/ui/sidebar-menu/SidebarGroupLabel.tsx new file mode 100644 index 0000000..b9121c0 --- /dev/null +++ b/src/components/ui/sidebar-menu/SidebarGroupLabel.tsx @@ -0,0 +1,26 @@ +"use client" + +import { ComponentProps } from "react" +import { Slot } from "@radix-ui/react-slot" +import { cn } from "@/lib/utils" + +export function SidebarGroupLabel({ + className, + asChild = false, + ...props +}: ComponentProps<"div"> & { asChild?: boolean }) { + const Comp = asChild ? Slot : "div" + + return ( + svg]:size-4 [&>svg]:shrink-0", + "group-data-[collapsible=icon]:-mt-8 group-data-[collapsible=icon]:opacity-0", + className + )} + {...props} + /> + ) +} diff --git a/src/components/ui/sidebar-menu/SidebarMenu.tsx b/src/components/ui/sidebar-menu/SidebarMenu.tsx new file mode 100644 index 0000000..e535d5a --- /dev/null +++ b/src/components/ui/sidebar-menu/SidebarMenu.tsx @@ -0,0 +1,15 @@ +"use client" + +import { ComponentProps } from "react" +import { cn } from "@/lib/utils" + +export function SidebarMenu({ className, ...props }: ComponentProps<"ul">) { + return ( +
        + ) +} diff --git a/src/components/ui/sidebar-menu/SidebarMenuAction.tsx b/src/components/ui/sidebar-menu/SidebarMenuAction.tsx new file mode 100644 index 0000000..2259710 --- /dev/null +++ b/src/components/ui/sidebar-menu/SidebarMenuAction.tsx @@ -0,0 +1,37 @@ +"use client" + +import { ComponentProps } from "react" +import { Slot } from "@radix-ui/react-slot" +import { cn } from "@/lib/utils" + +export function SidebarMenuAction({ + className, + asChild = false, + showOnHover = false, + ...props +}: ComponentProps<"button"> & { + asChild?: boolean + showOnHover?: boolean +}) { + const Comp = asChild ? Slot : "button" + + return ( + svg]:size-4 [&>svg]:shrink-0", + // Increases the hit area of the button on mobile. + "after:absolute after:-inset-2 md:after:hidden", + "peer-data-[size=sm]/menu-button:top-1", + "peer-data-[size=default]/menu-button:top-1.5", + "peer-data-[size=lg]/menu-button:top-2.5", + "group-data-[collapsible=icon]:hidden", + showOnHover && + "peer-data-[active=true]/menu-button:text-sidebar-accent-foreground group-focus-within/menu-item:opacity-100 group-hover/menu-item:opacity-100 data-[state=open]:opacity-100 md:opacity-0", + className + )} + {...props} + /> + ) +} diff --git a/src/components/ui/sidebar-menu/SidebarMenuBadge.tsx b/src/components/ui/sidebar-menu/SidebarMenuBadge.tsx new file mode 100644 index 0000000..1e8979d --- /dev/null +++ b/src/components/ui/sidebar-menu/SidebarMenuBadge.tsx @@ -0,0 +1,26 @@ +"use client" + +import { ComponentProps } from "react" +import { cn } from "@/lib/utils" + +export function SidebarMenuBadge({ + className, + ...props +}: ComponentProps<"div">) { + return ( +
        + ) +} diff --git a/src/components/ui/sidebar-menu/SidebarMenuButton.tsx b/src/components/ui/sidebar-menu/SidebarMenuButton.tsx new file mode 100644 index 0000000..676864b --- /dev/null +++ b/src/components/ui/sidebar-menu/SidebarMenuButton.tsx @@ -0,0 +1,84 @@ +"use client" + +import { ComponentProps } from "react" +import { Slot } from "@radix-ui/react-slot" +import { VariantProps, cva } from "class-variance-authority" +import { cn } from "@/lib/utils" +import { + Tooltip, + TooltipContent, + TooltipTrigger, +} from "@/components/ui/tooltip" +import { useSidebar } from "../sidebar-context" + +const sidebarMenuButtonVariants = cva( + "peer/menu-button flex w-full items-center gap-2 overflow-hidden rounded-md p-2 text-left text-sm outline-hidden ring-sidebar-ring transition-[width,height,padding] hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 active:bg-sidebar-accent active:text-sidebar-accent-foreground disabled:pointer-events-none disabled:opacity-50 group-has-data-[sidebar=menu-action]/menu-item:pr-8 aria-disabled:pointer-events-none aria-disabled:opacity-50 data-[active=true]:bg-sidebar-accent data-[active=true]:font-medium data-[active=true]:text-sidebar-accent-foreground data-[state=open]:hover:bg-sidebar-accent data-[state=open]:hover:text-sidebar-accent-foreground group-data-[collapsible=icon]:size-8! group-data-[collapsible=icon]:p-2! [&>span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0", + { + variants: { + variant: { + default: "hover:bg-sidebar-accent hover:text-sidebar-accent-foreground", + outline: + "bg-background shadow-[0_0_0_1px_hsl(var(--sidebar-border))] hover:bg-sidebar-accent hover:text-sidebar-accent-foreground hover:shadow-[0_0_0_1px_hsl(var(--sidebar-accent))]", + }, + size: { + default: "h-8 text-sm", + sm: "h-7 text-xs", + lg: "h-12 text-sm group-data-[collapsible=icon]:p-0!", + }, + }, + defaultVariants: { + variant: "default", + size: "default", + }, + } +) + +export function SidebarMenuButton({ + asChild = false, + isActive = false, + variant = "default", + size = "default", + tooltip, + className, + ...props +}: ComponentProps<"button"> & { + asChild?: boolean + isActive?: boolean + tooltip?: string | ComponentProps +} & VariantProps) { + const Comp = asChild ? Slot : "button" + const { isMobile, state } = useSidebar() + + const button = ( + + ) + + if (!tooltip) { + return button + } + + if (typeof tooltip === "string") { + tooltip = { + children: tooltip, + } + } + + return ( + + {button} + + ) +} diff --git a/src/components/ui/sidebar-menu/SidebarMenuItem.tsx b/src/components/ui/sidebar-menu/SidebarMenuItem.tsx new file mode 100644 index 0000000..cd2a992 --- /dev/null +++ b/src/components/ui/sidebar-menu/SidebarMenuItem.tsx @@ -0,0 +1,15 @@ +"use client" + +import { ComponentProps } from "react" +import { cn } from "@/lib/utils" + +export function SidebarMenuItem({ className, ...props }: ComponentProps<"li">) { + return ( +
      • + ) +} diff --git a/src/components/ui/sidebar-menu/SidebarMenuSkeleton.tsx b/src/components/ui/sidebar-menu/SidebarMenuSkeleton.tsx new file mode 100644 index 0000000..8a3d870 --- /dev/null +++ b/src/components/ui/sidebar-menu/SidebarMenuSkeleton.tsx @@ -0,0 +1,43 @@ +"use client" + +import { CSSProperties, ComponentProps, useMemo } from "react" +import { cn } from "@/lib/utils" +import { Skeleton } from "@/components/ui/skeleton" + +export function SidebarMenuSkeleton({ + className, + showIcon = false, + ...props +}: ComponentProps<"div"> & { + showIcon?: boolean +}) { + // Random width between 50 to 90%. + const width = useMemo(() => { + return `${Math.floor(Math.random() * 40) + 50}%` + }, []) + + return ( +
        + {showIcon && ( + + )} + +
        + ) +} diff --git a/src/components/ui/sidebar-menu/SidebarMenuSub.tsx b/src/components/ui/sidebar-menu/SidebarMenuSub.tsx new file mode 100644 index 0000000..19c13b1 --- /dev/null +++ b/src/components/ui/sidebar-menu/SidebarMenuSub.tsx @@ -0,0 +1,19 @@ +"use client" + +import { ComponentProps } from "react" +import { cn } from "@/lib/utils" + +export function SidebarMenuSub({ className, ...props }: ComponentProps<"ul">) { + return ( +
          + ) +} diff --git a/src/components/ui/sidebar-menu/SidebarMenuSubButton.tsx b/src/components/ui/sidebar-menu/SidebarMenuSubButton.tsx new file mode 100644 index 0000000..1168080 --- /dev/null +++ b/src/components/ui/sidebar-menu/SidebarMenuSubButton.tsx @@ -0,0 +1,37 @@ +"use client" + +import { ComponentProps } from "react" +import { Slot } from "@radix-ui/react-slot" +import { cn } from "@/lib/utils" + +export function SidebarMenuSubButton({ + asChild = false, + size = "md", + isActive = false, + className, + ...props +}: ComponentProps<"a"> & { + asChild?: boolean + size?: "sm" | "md" + isActive?: boolean +}) { + const Comp = asChild ? Slot : "a" + + return ( + svg]:text-sidebar-accent-foreground flex h-7 min-w-0 -translate-x-px items-center gap-2 overflow-hidden rounded-md px-2 outline-hidden focus-visible:ring-2 disabled:pointer-events-none disabled:opacity-50 aria-disabled:pointer-events-none aria-disabled:opacity-50 [&>span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0", + "data-[active=true]:bg-sidebar-accent data-[active=true]:text-sidebar-accent-foreground", + size === "sm" && "text-xs", + size === "md" && "text-sm", + "group-data-[collapsible=icon]:hidden", + className + )} + {...props} + /> + ) +} diff --git a/src/components/ui/sidebar-menu/SidebarMenuSubItem.tsx b/src/components/ui/sidebar-menu/SidebarMenuSubItem.tsx new file mode 100644 index 0000000..3dafc35 --- /dev/null +++ b/src/components/ui/sidebar-menu/SidebarMenuSubItem.tsx @@ -0,0 +1,18 @@ +"use client" + +import { ComponentProps } from "react" +import { cn } from "@/lib/utils" + +export function SidebarMenuSubItem({ + className, + ...props +}: ComponentProps<"li">) { + return ( +
        • + ) +} diff --git a/src/components/ui/sidebar.tsx b/src/components/ui/sidebar.tsx index 1fe6940..a8d7a3e 100644 --- a/src/components/ui/sidebar.tsx +++ b/src/components/ui/sidebar.tsx @@ -9,18 +9,15 @@ export { SidebarContent, SidebarGroup, } from "./sidebar-parts" -export { - SidebarGroupLabel, - SidebarGroupAction, - SidebarGroupContent, - SidebarMenu, - SidebarMenuItem, - SidebarMenuButton, - SidebarMenuAction, - SidebarMenuBadge, - SidebarMenuSkeleton, - SidebarMenuSub, - SidebarMenuSubItem, - SidebarMenuSubButton, -} from "./sidebar-menu" - +export { SidebarGroupLabel } from "./sidebar-menu/SidebarGroupLabel" +export { SidebarGroupAction } from "./sidebar-menu/SidebarGroupAction" +export { SidebarGroupContent } from "./sidebar-menu/SidebarGroupContent" +export { SidebarMenu } from "./sidebar-menu/SidebarMenu" +export { SidebarMenuItem } from "./sidebar-menu/SidebarMenuItem" +export { SidebarMenuButton } from "./sidebar-menu/SidebarMenuButton" +export { SidebarMenuAction } from "./sidebar-menu/SidebarMenuAction" +export { SidebarMenuBadge } from "./sidebar-menu/SidebarMenuBadge" +export { SidebarMenuSkeleton } from "./sidebar-menu/SidebarMenuSkeleton" +export { SidebarMenuSub } from "./sidebar-menu/SidebarMenuSub" +export { SidebarMenuSubItem } from "./sidebar-menu/SidebarMenuSubItem" +export { SidebarMenuSubButton } from "./sidebar-menu/SidebarMenuSubButton" diff --git a/src/lib/db-core.ts b/src/lib/db-core.ts deleted file mode 100644 index 3583fbe..0000000 --- a/src/lib/db-core.ts +++ /dev/null @@ -1,178 +0,0 @@ -/** - * Core database initialization and management - */ - -import initSqlJs, { Database } from 'sql.js' -import { loadFromIndexedDB, saveToIndexedDB, openIndexedDB, deleteFromIndexedDB } from './db-indexeddb' -import { loadFromLocalStorage, saveToLocalStorage, deleteFromLocalStorage } from './db-localstorage' -import { validateSchema, createTables } from './db-schema' -import { getStorageConfig, FlaskStorageAdapter, loadStorageConfig } from './storage' -import { DB_KEY } from './db-constants' - -let dbInstance: Database | null = null -let sqlInstance: any = null -let flaskAdapter: FlaskStorageAdapter | null = null -let configLoaded = false - -async function wipeAndRecreateDB(): Promise { - console.warn('Wiping corrupted database and creating fresh schema...') - - await saveToIndexedDB(new Uint8Array()) - saveToLocalStorage(new Uint8Array()) - - await deleteFromIndexedDB() - deleteFromLocalStorage() - - dbInstance = null -} - -export async function initDB(): Promise { - if (dbInstance) return dbInstance - - if (!sqlInstance) { - sqlInstance = await initSqlJs({ - locateFile: (file) => `https://sql.js.org/dist/${file}` - }) - } - - let loadedData: Uint8Array | null = null - let schemaValid = false - - loadedData = await loadFromIndexedDB() - - if (!loadedData) { - loadedData = loadFromLocalStorage() - } - - if (loadedData && loadedData.length > 0) { - try { - const testDb = new sqlInstance.Database(loadedData) - schemaValid = await validateSchema(testDb) - - if (schemaValid) { - dbInstance = testDb - } else { - console.warn('Schema validation failed, wiping database') - testDb.close() - await wipeAndRecreateDB() - dbInstance = new sqlInstance.Database() - } - } catch (error) { - console.error('Failed to load saved database, creating new one:', error) - await wipeAndRecreateDB() - dbInstance = new sqlInstance.Database() - } - } else { - dbInstance = new sqlInstance.Database() - } - - if (!dbInstance) { - throw new Error('Failed to initialize database') - } - - createTables(dbInstance) - await saveDB() - - return dbInstance -} - -export async function saveDB() { - if (!dbInstance) return - - try { - const data = dbInstance.export() - - const savedToIDB = await saveToIndexedDB(data) - - if (!savedToIDB) { - saveToLocalStorage(data) - } - } catch (error) { - console.error('Failed to save database:', error) - } -} - -export function getFlaskAdapter(): FlaskStorageAdapter | null { - if (!configLoaded) { - loadStorageConfig() - configLoaded = true - } - - const config = getStorageConfig() - if (config.backend === 'flask' && config.flaskUrl) { - try { - if (!flaskAdapter || flaskAdapter['baseUrl'] !== config.flaskUrl) { - flaskAdapter = new FlaskStorageAdapter(config.flaskUrl) - } - return flaskAdapter - } catch (error) { - console.warn('Failed to create Flask adapter:', error) - return null - } - } - return null -} - -export async function exportDatabase(): Promise { - const db = await initDB() - return db.export() -} - -export async function importDatabase(data: Uint8Array): Promise { - if (!sqlInstance) { - sqlInstance = await initSqlJs({ - locateFile: (file) => `https://sql.js.org/dist/${file}` - }) - } - - try { - dbInstance = new sqlInstance.Database(data) - await saveDB() - } catch (error) { - console.error('Failed to import database:', error) - throw error - } -} - -export async function getDatabaseStats(): Promise<{ - snippetCount: number - templateCount: number - storageType: 'indexeddb' | 'localstorage' | 'none' - databaseSize: number -}> { - const db = await initDB() - - const snippetResult = db.exec('SELECT COUNT(*) as count FROM snippets') - const templateResult = db.exec('SELECT COUNT(*) as count FROM snippet_templates') - - const snippetCount = snippetResult[0]?.values[0]?.[0] as number || 0 - const templateCount = templateResult[0]?.values[0]?.[0] as number || 0 - - const data = db.export() - const databaseSize = data.length - - const hasIDB = await openIndexedDB() - const hasLocalStorage = typeof localStorage !== 'undefined' && localStorage.getItem(DB_KEY) !== null - const storageType = hasIDB ? 'indexeddb' : (hasLocalStorage ? 'localstorage' : 'none') - - return { - snippetCount, - templateCount, - storageType, - databaseSize - } -} - -export async function clearDatabase(): Promise { - const adapter = getFlaskAdapter() - if (adapter) { - await adapter.wipeDatabase() - return - } - - await deleteFromIndexedDB() - deleteFromLocalStorage() - - dbInstance = null - await initDB() -} diff --git a/src/lib/db-core/clearDatabase.ts b/src/lib/db-core/clearDatabase.ts new file mode 100644 index 0000000..300fbca --- /dev/null +++ b/src/lib/db-core/clearDatabase.ts @@ -0,0 +1,19 @@ +import { deleteFromIndexedDB } from '../db-indexeddb' +import { deleteFromLocalStorage } from '../db-localstorage' +import { getFlaskAdapter } from './getFlaskAdapter' +import { initDB } from './initDB' +import { dbState } from './state' + +export async function clearDatabase(): Promise { + const adapter = getFlaskAdapter() + if (adapter) { + await adapter.wipeDatabase() + return + } + + await deleteFromIndexedDB() + deleteFromLocalStorage() + + dbState.dbInstance = null + await initDB() +} diff --git a/src/lib/db-core/exportDatabase.ts b/src/lib/db-core/exportDatabase.ts new file mode 100644 index 0000000..94937d2 --- /dev/null +++ b/src/lib/db-core/exportDatabase.ts @@ -0,0 +1,6 @@ +import { initDB } from './initDB' + +export async function exportDatabase(): Promise { + const db = await initDB() + return db.export() +} diff --git a/src/lib/db-core/getDatabaseStats.ts b/src/lib/db-core/getDatabaseStats.ts new file mode 100644 index 0000000..d545113 --- /dev/null +++ b/src/lib/db-core/getDatabaseStats.ts @@ -0,0 +1,32 @@ +import { openIndexedDB } from '../db-indexeddb' +import { DB_KEY } from '../db-constants' +import { initDB } from './initDB' + +export async function getDatabaseStats(): Promise<{ + snippetCount: number + templateCount: number + storageType: 'indexeddb' | 'localstorage' | 'none' + databaseSize: number +}> { + const db = await initDB() + + const snippetResult = db.exec('SELECT COUNT(*) as count FROM snippets') + const templateResult = db.exec('SELECT COUNT(*) as count FROM snippet_templates') + + const snippetCount = snippetResult[0]?.values[0]?.[0] as number || 0 + const templateCount = templateResult[0]?.values[0]?.[0] as number || 0 + + const data = db.export() + const databaseSize = data.length + + const hasIDB = await openIndexedDB() + const hasLocalStorage = typeof localStorage !== 'undefined' && localStorage.getItem(DB_KEY) !== null + const storageType = hasIDB ? 'indexeddb' : (hasLocalStorage ? 'localstorage' : 'none') + + return { + snippetCount, + templateCount, + storageType, + databaseSize, + } +} diff --git a/src/lib/db-core/getFlaskAdapter.ts b/src/lib/db-core/getFlaskAdapter.ts new file mode 100644 index 0000000..c2bd475 --- /dev/null +++ b/src/lib/db-core/getFlaskAdapter.ts @@ -0,0 +1,23 @@ +import { FlaskStorageAdapter, getStorageConfig, loadStorageConfig } from '../storage' +import { dbState } from './state' + +export function getFlaskAdapter(): FlaskStorageAdapter | null { + if (!dbState.configLoaded) { + loadStorageConfig() + dbState.configLoaded = true + } + + const config = getStorageConfig() + if (config.backend === 'flask' && config.flaskUrl) { + try { + if (!dbState.flaskAdapter || dbState.flaskAdapter['baseUrl'] !== config.flaskUrl) { + dbState.flaskAdapter = new FlaskStorageAdapter(config.flaskUrl) + } + return dbState.flaskAdapter + } catch (error) { + console.warn('Failed to create Flask adapter:', error) + return null + } + } + return null +} diff --git a/src/lib/db-core/importDatabase.ts b/src/lib/db-core/importDatabase.ts new file mode 100644 index 0000000..32ddc0a --- /dev/null +++ b/src/lib/db-core/importDatabase.ts @@ -0,0 +1,19 @@ +import initSqlJs from 'sql.js' +import { saveDB } from './saveDB' +import { dbState } from './state' + +export async function importDatabase(data: Uint8Array): Promise { + if (!dbState.sqlInstance) { + dbState.sqlInstance = await initSqlJs({ + locateFile: (file) => `https://sql.js.org/dist/${file}`, + }) + } + + try { + dbState.dbInstance = new dbState.sqlInstance.Database(data) + await saveDB() + } catch (error) { + console.error('Failed to import database:', error) + throw error + } +} diff --git a/src/lib/db-core/initDB.ts b/src/lib/db-core/initDB.ts new file mode 100644 index 0000000..62bd660 --- /dev/null +++ b/src/lib/db-core/initDB.ts @@ -0,0 +1,57 @@ +import initSqlJs, { Database } from 'sql.js' +import { loadFromIndexedDB } from '../db-indexeddb' +import { loadFromLocalStorage } from '../db-localstorage' +import { createTables, validateSchema } from '../db-schema' +import { saveDB } from './saveDB' +import { dbState } from './state' +import { wipeAndRecreateDB } from './wipeAndRecreateDB' + +export async function initDB(): Promise { + if (dbState.dbInstance) return dbState.dbInstance + + if (!dbState.sqlInstance) { + dbState.sqlInstance = await initSqlJs({ + locateFile: (file) => `https://sql.js.org/dist/${file}`, + }) + } + + let loadedData: Uint8Array | null = null + let schemaValid = false + + loadedData = await loadFromIndexedDB() + + if (!loadedData) { + loadedData = loadFromLocalStorage() + } + + if (loadedData && loadedData.length > 0) { + try { + const testDb = new dbState.sqlInstance.Database(loadedData) + schemaValid = await validateSchema(testDb) + + if (schemaValid) { + dbState.dbInstance = testDb + } else { + console.warn('Schema validation failed, wiping database') + testDb.close() + await wipeAndRecreateDB() + dbState.dbInstance = new dbState.sqlInstance.Database() + } + } catch (error) { + console.error('Failed to load saved database, creating new one:', error) + await wipeAndRecreateDB() + dbState.dbInstance = new dbState.sqlInstance.Database() + } + } else { + dbState.dbInstance = new dbState.sqlInstance.Database() + } + + if (!dbState.dbInstance) { + throw new Error('Failed to initialize database') + } + + createTables(dbState.dbInstance) + await saveDB() + + return dbState.dbInstance +} diff --git a/src/lib/db-core/saveDB.ts b/src/lib/db-core/saveDB.ts new file mode 100644 index 0000000..8584a4d --- /dev/null +++ b/src/lib/db-core/saveDB.ts @@ -0,0 +1,19 @@ +import { saveToIndexedDB } from '../db-indexeddb' +import { saveToLocalStorage } from '../db-localstorage' +import { dbState } from './state' + +export async function saveDB() { + if (!dbState.dbInstance) return + + try { + const data = dbState.dbInstance.export() + + const savedToIDB = await saveToIndexedDB(data) + + if (!savedToIDB) { + saveToLocalStorage(data) + } + } catch (error) { + console.error('Failed to save database:', error) + } +} diff --git a/src/lib/db-core/state.ts b/src/lib/db-core/state.ts new file mode 100644 index 0000000..3524251 --- /dev/null +++ b/src/lib/db-core/state.ts @@ -0,0 +1,9 @@ +import type { Database } from 'sql.js' +import type { FlaskStorageAdapter } from '../storage' + +export const dbState = { + dbInstance: null as Database | null, + sqlInstance: null as any, + flaskAdapter: null as FlaskStorageAdapter | null, + configLoaded: false, +} diff --git a/src/lib/db-core/wipeAndRecreateDB.ts b/src/lib/db-core/wipeAndRecreateDB.ts new file mode 100644 index 0000000..0765b00 --- /dev/null +++ b/src/lib/db-core/wipeAndRecreateDB.ts @@ -0,0 +1,15 @@ +import { deleteFromIndexedDB, saveToIndexedDB } from '../db-indexeddb' +import { deleteFromLocalStorage, saveToLocalStorage } from '../db-localstorage' +import { dbState } from './state' + +export async function wipeAndRecreateDB(): Promise { + console.warn('Wiping corrupted database and creating fresh schema...') + + await saveToIndexedDB(new Uint8Array()) + saveToLocalStorage(new Uint8Array()) + + await deleteFromIndexedDB() + deleteFromLocalStorage() + + dbState.dbInstance = null +} diff --git a/src/lib/db-namespaces.ts b/src/lib/db-namespaces.ts deleted file mode 100644 index 292e220..0000000 --- a/src/lib/db-namespaces.ts +++ /dev/null @@ -1,108 +0,0 @@ -/** - * Namespace operations for organizing snippets - */ - -import type { Namespace } from './types' -import { initDB, saveDB, getFlaskAdapter } from './db-core' -import { mapRowToObject, mapRowsToObjects } from './db-mapper' - -export async function getAllNamespaces(): Promise { - const adapter = getFlaskAdapter() - if (adapter) { - return await adapter.getAllNamespaces() - } - - const db = await initDB() - const results = db.exec('SELECT * FROM namespaces ORDER BY isDefault DESC, name ASC') - - return mapRowsToObjects(results) -} - -export async function createNamespace(name: string): Promise { - const namespace: Namespace = { - id: Date.now().toString(), - name, - createdAt: Date.now(), - isDefault: false - } - - const adapter = getFlaskAdapter() - if (adapter) { - await adapter.createNamespace(namespace) - return namespace - } - - const db = await initDB() - - db.run( - `INSERT INTO namespaces (id, name, createdAt, isDefault) - VALUES (?, ?, ?, ?)`, - [namespace.id, namespace.name, namespace.createdAt, namespace.isDefault ? 1 : 0] - ) - - await saveDB() - return namespace -} - -export async function deleteNamespace(id: string): Promise { - const adapter = getFlaskAdapter() - if (adapter) { - return await adapter.deleteNamespace(id) - } - - const db = await initDB() - - const defaultNamespace = db.exec('SELECT id FROM namespaces WHERE isDefault = 1') - if (defaultNamespace.length === 0 || defaultNamespace[0].values.length === 0) { - throw new Error('Default namespace not found') - } - - const defaultId = defaultNamespace[0].values[0][0] as string - - const checkDefault = db.exec('SELECT isDefault FROM namespaces WHERE id = ?', [id]) - if (checkDefault.length > 0 && checkDefault[0].values[0]?.[0] === 1) { - throw new Error('Cannot delete default namespace') - } - - db.run('UPDATE snippets SET namespaceId = ? WHERE namespaceId = ?', [defaultId, id]) - - db.run('DELETE FROM namespaces WHERE id = ?', [id]) - - await saveDB() -} - -export async function ensureDefaultNamespace(): Promise { - const db = await initDB() - - const results = db.exec('SELECT COUNT(*) as count FROM namespaces WHERE isDefault = 1') - const count = results[0]?.values[0]?.[0] as number || 0 - - if (count === 0) { - const defaultNamespace: Namespace = { - id: 'default', - name: 'Default', - createdAt: Date.now(), - isDefault: true - } - - db.run( - `INSERT INTO namespaces (id, name, createdAt, isDefault) - VALUES (?, ?, ?, ?)`, - [defaultNamespace.id, defaultNamespace.name, defaultNamespace.createdAt, 1] - ) - - await saveDB() - } -} - -export async function getNamespaceById(id: string): Promise { - const db = await initDB() - const results = db.exec('SELECT * FROM namespaces WHERE id = ?', [id]) - - if (results.length === 0 || results[0].values.length === 0) return null - - const columns = results[0].columns - const row = results[0].values[0] - - return mapRowToObject(row, columns) -} diff --git a/src/lib/db-namespaces/createNamespace.ts b/src/lib/db-namespaces/createNamespace.ts new file mode 100644 index 0000000..5079e22 --- /dev/null +++ b/src/lib/db-namespaces/createNamespace.ts @@ -0,0 +1,30 @@ +import type { Namespace } from '../types' +import { initDB } from '../db-core/initDB' +import { saveDB } from '../db-core/saveDB' +import { getFlaskAdapter } from '../db-core/getFlaskAdapter' + +export async function createNamespace(name: string): Promise { + const namespace: Namespace = { + id: Date.now().toString(), + name, + createdAt: Date.now(), + isDefault: false, + } + + const adapter = getFlaskAdapter() + if (adapter) { + await adapter.createNamespace(namespace) + return namespace + } + + const db = await initDB() + + db.run( + `INSERT INTO namespaces (id, name, createdAt, isDefault) + VALUES (?, ?, ?, ?)`, + [namespace.id, namespace.name, namespace.createdAt, namespace.isDefault ? 1 : 0] + ) + + await saveDB() + return namespace +} diff --git a/src/lib/db-namespaces/deleteNamespace.ts b/src/lib/db-namespaces/deleteNamespace.ts new file mode 100644 index 0000000..5435acc --- /dev/null +++ b/src/lib/db-namespaces/deleteNamespace.ts @@ -0,0 +1,30 @@ +import { initDB } from '../db-core/initDB' +import { saveDB } from '../db-core/saveDB' +import { getFlaskAdapter } from '../db-core/getFlaskAdapter' + +export async function deleteNamespace(id: string): Promise { + const adapter = getFlaskAdapter() + if (adapter) { + return await adapter.deleteNamespace(id) + } + + const db = await initDB() + + const defaultNamespace = db.exec('SELECT id FROM namespaces WHERE isDefault = 1') + if (defaultNamespace.length === 0 || defaultNamespace[0].values.length === 0) { + throw new Error('Default namespace not found') + } + + const defaultId = defaultNamespace[0].values[0][0] as string + + const checkDefault = db.exec('SELECT isDefault FROM namespaces WHERE id = ?', [id]) + if (checkDefault.length > 0 && checkDefault[0].values[0]?.[0] === 1) { + throw new Error('Cannot delete default namespace') + } + + db.run('UPDATE snippets SET namespaceId = ? WHERE namespaceId = ?', [defaultId, id]) + + db.run('DELETE FROM namespaces WHERE id = ?', [id]) + + await saveDB() +} diff --git a/src/lib/db-namespaces/ensureDefaultNamespace.ts b/src/lib/db-namespaces/ensureDefaultNamespace.ts new file mode 100644 index 0000000..5f8a78f --- /dev/null +++ b/src/lib/db-namespaces/ensureDefaultNamespace.ts @@ -0,0 +1,27 @@ +import type { Namespace } from '../types' +import { initDB } from '../db-core/initDB' +import { saveDB } from '../db-core/saveDB' + +export async function ensureDefaultNamespace(): Promise { + const db = await initDB() + + const results = db.exec('SELECT COUNT(*) as count FROM namespaces WHERE isDefault = 1') + const count = results[0]?.values[0]?.[0] as number || 0 + + if (count === 0) { + const defaultNamespace: Namespace = { + id: 'default', + name: 'Default', + createdAt: Date.now(), + isDefault: true, + } + + db.run( + `INSERT INTO namespaces (id, name, createdAt, isDefault) + VALUES (?, ?, ?, ?)`, + [defaultNamespace.id, defaultNamespace.name, defaultNamespace.createdAt, 1] + ) + + await saveDB() + } +} diff --git a/src/lib/db-namespaces/getAllNamespaces.ts b/src/lib/db-namespaces/getAllNamespaces.ts new file mode 100644 index 0000000..0001875 --- /dev/null +++ b/src/lib/db-namespaces/getAllNamespaces.ts @@ -0,0 +1,16 @@ +import type { Namespace } from '../types' +import { initDB } from '../db-core/initDB' +import { getFlaskAdapter } from '../db-core/getFlaskAdapter' +import { mapRowsToObjects } from '../db-mapper' + +export async function getAllNamespaces(): Promise { + const adapter = getFlaskAdapter() + if (adapter) { + return await adapter.getAllNamespaces() + } + + const db = await initDB() + const results = db.exec('SELECT * FROM namespaces ORDER BY isDefault DESC, name ASC') + + return mapRowsToObjects(results) +} diff --git a/src/lib/db-namespaces/getNamespaceById.ts b/src/lib/db-namespaces/getNamespaceById.ts new file mode 100644 index 0000000..d05ca11 --- /dev/null +++ b/src/lib/db-namespaces/getNamespaceById.ts @@ -0,0 +1,15 @@ +import type { Namespace } from '../types' +import { initDB } from '../db-core/initDB' +import { mapRowToObject } from '../db-mapper' + +export async function getNamespaceById(id: string): Promise { + const db = await initDB() + const results = db.exec('SELECT * FROM namespaces WHERE id = ?', [id]) + + if (results.length === 0 || results[0].values.length === 0) return null + + const columns = results[0].columns + const row = results[0].values[0] + + return mapRowToObject(row, columns) +} diff --git a/src/lib/db-snippets.ts b/src/lib/db-snippets.ts deleted file mode 100644 index bf3c07f..0000000 --- a/src/lib/db-snippets.ts +++ /dev/null @@ -1,241 +0,0 @@ -/** - * Snippet CRUD operations and templates management - */ - -import type { Snippet, SnippetTemplate } from './types' -import { initDB, saveDB, getFlaskAdapter } from './db-core' -import { mapRowToObject, mapRowsToObjects } from './db-mapper' -import { ensureDefaultNamespace } from './db-namespaces' -import seedSnippetsData from '@/data/seed-snippets.json' -import seedTemplatesData from '@/data/seed-templates.json' - -export async function getAllSnippets(): Promise { - const adapter = getFlaskAdapter() - if (adapter) { - return await adapter.getAllSnippets() - } - - const db = await initDB() - const results = db.exec('SELECT * FROM snippets ORDER BY updatedAt DESC') - - return mapRowsToObjects(results) -} - -export async function getSnippet(id: string): Promise { - const adapter = getFlaskAdapter() - if (adapter) { - return await adapter.getSnippet(id) - } - - const db = await initDB() - const results = db.exec('SELECT * FROM snippets WHERE id = ?', [id]) - - if (results.length === 0 || results[0].values.length === 0) return null - - const columns = results[0].columns - const row = results[0].values[0] - - return mapRowToObject(row, columns) -} - -export async function createSnippet(snippet: Snippet): Promise { - const adapter = getFlaskAdapter() - if (adapter) { - return await adapter.createSnippet(snippet) - } - - const db = await initDB() - - db.run( - `INSERT INTO snippets (id, title, description, code, language, category, namespaceId, hasPreview, functionName, inputParameters, createdAt, updatedAt) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, - [ - snippet.id, - snippet.title, - snippet.description, - snippet.code, - snippet.language, - snippet.category, - snippet.namespaceId || null, - snippet.hasPreview ? 1 : 0, - snippet.functionName || null, - snippet.inputParameters ? JSON.stringify(snippet.inputParameters) : null, - snippet.createdAt, - snippet.updatedAt - ] - ) - - await saveDB() -} - -export async function updateSnippet(snippet: Snippet): Promise { - const adapter = getFlaskAdapter() - if (adapter) { - return await adapter.updateSnippet(snippet) - } - - const db = await initDB() - - db.run( - `UPDATE snippets - SET title = ?, description = ?, code = ?, language = ?, category = ?, namespaceId = ?, hasPreview = ?, functionName = ?, inputParameters = ?, updatedAt = ? - WHERE id = ?`, - [ - snippet.title, - snippet.description, - snippet.code, - snippet.language, - snippet.category, - snippet.namespaceId || null, - snippet.hasPreview ? 1 : 0, - snippet.functionName || null, - snippet.inputParameters ? JSON.stringify(snippet.inputParameters) : null, - snippet.updatedAt, - snippet.id - ] - ) - - await saveDB() -} - -export async function deleteSnippet(id: string): Promise { - const adapter = getFlaskAdapter() - if (adapter) { - return await adapter.deleteSnippet(id) - } - - const db = await initDB() - - db.run('DELETE FROM snippets WHERE id = ?', [id]) - - await saveDB() -} - -export async function getSnippetsByNamespace(namespaceId: string): Promise { - const db = await initDB() - const results = db.exec('SELECT * FROM snippets WHERE namespaceId = ? OR (namespaceId IS NULL AND ? = (SELECT id FROM namespaces WHERE isDefault = 1)) ORDER BY updatedAt DESC', [namespaceId, namespaceId]) - - return mapRowsToObjects(results) -} - -export async function moveSnippetToNamespace(snippetId: string, targetNamespaceId: string): Promise { - const adapter = getFlaskAdapter() - if (adapter) { - const snippet = await adapter.getSnippet(snippetId) - if (snippet) { - snippet.namespaceId = targetNamespaceId - snippet.updatedAt = Date.now() - await adapter.updateSnippet(snippet) - } - return - } - - const db = await initDB() - - db.run( - 'UPDATE snippets SET namespaceId = ?, updatedAt = ? WHERE id = ?', - [targetNamespaceId, Date.now(), snippetId] - ) - - await saveDB() -} - -export async function bulkMoveSnippets(snippetIds: string[], targetNamespaceId: string): Promise { - const adapter = getFlaskAdapter() - if (adapter) { - await adapter.bulkMoveSnippets(snippetIds, targetNamespaceId) - return - } - - const db = await initDB() - const now = Date.now() - - for (const snippetId of snippetIds) { - db.run( - 'UPDATE snippets SET namespaceId = ?, updatedAt = ? WHERE id = ?', - [targetNamespaceId, now, snippetId] - ) - } - - await saveDB() -} - -export async function getAllTemplates(): Promise { - const db = await initDB() - const results = db.exec('SELECT * FROM snippet_templates') - - return mapRowsToObjects(results) -} - -export async function createTemplate(template: SnippetTemplate): Promise { - const db = await initDB() - - db.run( - `INSERT INTO snippet_templates (id, title, description, code, language, category, hasPreview, functionName, inputParameters) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`, - [ - template.id, - template.title, - template.description, - template.code, - template.language, - template.category, - template.hasPreview ? 1 : 0, - template.functionName || null, - template.inputParameters ? JSON.stringify(template.inputParameters) : null - ] - ) - - await saveDB() -} - -export async function syncTemplatesFromJSON(templates: SnippetTemplate[]): Promise { - const db = await initDB() - - const existingTemplates = db.exec('SELECT id FROM snippet_templates') - const existingIds = new Set( - existingTemplates[0]?.values.map(row => row[0] as string) || [] - ) - - let addedCount = 0 - for (const template of templates) { - if (!existingIds.has(template.id)) { - await createTemplate(template) - addedCount++ - } - } -} - -export async function seedDatabase(): Promise { - const db = await initDB() - - await ensureDefaultNamespace() - - const checkSnippets = db.exec('SELECT COUNT(*) as count FROM snippets') - const snippetCount = checkSnippets[0]?.values[0]?.[0] as number - - if (snippetCount > 0) { - return - } - - const now = Date.now() - - const seedSnippets: Snippet[] = seedSnippetsData.map((snippet, index) => { - const timestamp = now - index * 1000 - return { - ...snippet, - createdAt: timestamp, - updatedAt: timestamp - } - }) - - for (const snippet of seedSnippets) { - await createSnippet(snippet) - } - - const seedTemplates: SnippetTemplate[] = seedTemplatesData - - for (const template of seedTemplates) { - await createTemplate(template) - } -} diff --git a/src/lib/db-snippets/bulkMoveSnippets.ts b/src/lib/db-snippets/bulkMoveSnippets.ts new file mode 100644 index 0000000..262701c --- /dev/null +++ b/src/lib/db-snippets/bulkMoveSnippets.ts @@ -0,0 +1,23 @@ +import { initDB } from '../db-core/initDB' +import { saveDB } from '../db-core/saveDB' +import { getFlaskAdapter } from '../db-core/getFlaskAdapter' + +export async function bulkMoveSnippets(snippetIds: string[], targetNamespaceId: string): Promise { + const adapter = getFlaskAdapter() + if (adapter) { + await adapter.bulkMoveSnippets(snippetIds, targetNamespaceId) + return + } + + const db = await initDB() + const now = Date.now() + + for (const snippetId of snippetIds) { + db.run( + 'UPDATE snippets SET namespaceId = ?, updatedAt = ? WHERE id = ?', + [targetNamespaceId, now, snippetId] + ) + } + + await saveDB() +} diff --git a/src/lib/db-snippets/createSnippet.ts b/src/lib/db-snippets/createSnippet.ts new file mode 100644 index 0000000..acc8c90 --- /dev/null +++ b/src/lib/db-snippets/createSnippet.ts @@ -0,0 +1,34 @@ +import type { Snippet } from '../types' +import { initDB } from '../db-core/initDB' +import { saveDB } from '../db-core/saveDB' +import { getFlaskAdapter } from '../db-core/getFlaskAdapter' + +export async function createSnippet(snippet: Snippet): Promise { + const adapter = getFlaskAdapter() + if (adapter) { + return await adapter.createSnippet(snippet) + } + + const db = await initDB() + + db.run( + `INSERT INTO snippets (id, title, description, code, language, category, namespaceId, hasPreview, functionName, inputParameters, createdAt, updatedAt) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, + [ + snippet.id, + snippet.title, + snippet.description, + snippet.code, + snippet.language, + snippet.category, + snippet.namespaceId || null, + snippet.hasPreview ? 1 : 0, + snippet.functionName || null, + snippet.inputParameters ? JSON.stringify(snippet.inputParameters) : null, + snippet.createdAt, + snippet.updatedAt, + ] + ) + + await saveDB() +} diff --git a/src/lib/db-snippets/createTemplate.ts b/src/lib/db-snippets/createTemplate.ts new file mode 100644 index 0000000..7014b4c --- /dev/null +++ b/src/lib/db-snippets/createTemplate.ts @@ -0,0 +1,25 @@ +import type { SnippetTemplate } from '../types' +import { initDB } from '../db-core/initDB' +import { saveDB } from '../db-core/saveDB' + +export async function createTemplate(template: SnippetTemplate): Promise { + const db = await initDB() + + db.run( + `INSERT INTO snippet_templates (id, title, description, code, language, category, hasPreview, functionName, inputParameters) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`, + [ + template.id, + template.title, + template.description, + template.code, + template.language, + template.category, + template.hasPreview ? 1 : 0, + template.functionName || null, + template.inputParameters ? JSON.stringify(template.inputParameters) : null, + ] + ) + + await saveDB() +} diff --git a/src/lib/db-snippets/deleteSnippet.ts b/src/lib/db-snippets/deleteSnippet.ts new file mode 100644 index 0000000..bb89acc --- /dev/null +++ b/src/lib/db-snippets/deleteSnippet.ts @@ -0,0 +1,16 @@ +import { initDB } from '../db-core/initDB' +import { saveDB } from '../db-core/saveDB' +import { getFlaskAdapter } from '../db-core/getFlaskAdapter' + +export async function deleteSnippet(id: string): Promise { + const adapter = getFlaskAdapter() + if (adapter) { + return await adapter.deleteSnippet(id) + } + + const db = await initDB() + + db.run('DELETE FROM snippets WHERE id = ?', [id]) + + await saveDB() +} diff --git a/src/lib/db-snippets/getAllSnippets.ts b/src/lib/db-snippets/getAllSnippets.ts new file mode 100644 index 0000000..d924b4e --- /dev/null +++ b/src/lib/db-snippets/getAllSnippets.ts @@ -0,0 +1,16 @@ +import type { Snippet } from '../types' +import { initDB } from '../db-core/initDB' +import { getFlaskAdapter } from '../db-core/getFlaskAdapter' +import { mapRowsToObjects } from '../db-mapper' + +export async function getAllSnippets(): Promise { + const adapter = getFlaskAdapter() + if (adapter) { + return await adapter.getAllSnippets() + } + + const db = await initDB() + const results = db.exec('SELECT * FROM snippets ORDER BY updatedAt DESC') + + return mapRowsToObjects(results) +} diff --git a/src/lib/db-snippets/getAllTemplates.ts b/src/lib/db-snippets/getAllTemplates.ts new file mode 100644 index 0000000..247af39 --- /dev/null +++ b/src/lib/db-snippets/getAllTemplates.ts @@ -0,0 +1,10 @@ +import type { SnippetTemplate } from '../types' +import { initDB } from '../db-core/initDB' +import { mapRowsToObjects } from '../db-mapper' + +export async function getAllTemplates(): Promise { + const db = await initDB() + const results = db.exec('SELECT * FROM snippet_templates') + + return mapRowsToObjects(results) +} diff --git a/src/lib/db-snippets/getSnippet.ts b/src/lib/db-snippets/getSnippet.ts new file mode 100644 index 0000000..2943335 --- /dev/null +++ b/src/lib/db-snippets/getSnippet.ts @@ -0,0 +1,21 @@ +import type { Snippet } from '../types' +import { initDB } from '../db-core/initDB' +import { getFlaskAdapter } from '../db-core/getFlaskAdapter' +import { mapRowToObject } from '../db-mapper' + +export async function getSnippet(id: string): Promise { + const adapter = getFlaskAdapter() + if (adapter) { + return await adapter.getSnippet(id) + } + + const db = await initDB() + const results = db.exec('SELECT * FROM snippets WHERE id = ?', [id]) + + if (results.length === 0 || results[0].values.length === 0) return null + + const columns = results[0].columns + const row = results[0].values[0] + + return mapRowToObject(row, columns) +} diff --git a/src/lib/db-snippets/getSnippetsByNamespace.ts b/src/lib/db-snippets/getSnippetsByNamespace.ts new file mode 100644 index 0000000..ffc2dff --- /dev/null +++ b/src/lib/db-snippets/getSnippetsByNamespace.ts @@ -0,0 +1,13 @@ +import type { Snippet } from '../types' +import { initDB } from '../db-core/initDB' +import { mapRowsToObjects } from '../db-mapper' + +export async function getSnippetsByNamespace(namespaceId: string): Promise { + const db = await initDB() + const results = db.exec( + 'SELECT * FROM snippets WHERE namespaceId = ? OR (namespaceId IS NULL AND ? = (SELECT id FROM namespaces WHERE isDefault = 1)) ORDER BY updatedAt DESC', + [namespaceId, namespaceId] + ) + + return mapRowsToObjects(results) +} diff --git a/src/lib/db-snippets/moveSnippetToNamespace.ts b/src/lib/db-snippets/moveSnippetToNamespace.ts new file mode 100644 index 0000000..bf87927 --- /dev/null +++ b/src/lib/db-snippets/moveSnippetToNamespace.ts @@ -0,0 +1,25 @@ +import { initDB } from '../db-core/initDB' +import { saveDB } from '../db-core/saveDB' +import { getFlaskAdapter } from '../db-core/getFlaskAdapter' + +export async function moveSnippetToNamespace(snippetId: string, targetNamespaceId: string): Promise { + const adapter = getFlaskAdapter() + if (adapter) { + const snippet = await adapter.getSnippet(snippetId) + if (snippet) { + snippet.namespaceId = targetNamespaceId + snippet.updatedAt = Date.now() + await adapter.updateSnippet(snippet) + } + return + } + + const db = await initDB() + + db.run( + 'UPDATE snippets SET namespaceId = ?, updatedAt = ? WHERE id = ?', + [targetNamespaceId, Date.now(), snippetId] + ) + + await saveDB() +} diff --git a/src/lib/db-snippets/seedDatabase.ts b/src/lib/db-snippets/seedDatabase.ts new file mode 100644 index 0000000..5c2647a --- /dev/null +++ b/src/lib/db-snippets/seedDatabase.ts @@ -0,0 +1,41 @@ +import type { Snippet, SnippetTemplate } from '../types' +import { initDB } from '../db-core/initDB' +import { createSnippet } from './createSnippet' +import { createTemplate } from './createTemplate' +import { ensureDefaultNamespace } from '../db-namespaces/ensureDefaultNamespace' +import seedSnippetsData from '@/data/seed-snippets.json' +import seedTemplatesData from '@/data/seed-templates.json' + +export async function seedDatabase(): Promise { + const db = await initDB() + + await ensureDefaultNamespace() + + const checkSnippets = db.exec('SELECT COUNT(*) as count FROM snippets') + const snippetCount = checkSnippets[0]?.values[0]?.[0] as number + + if (snippetCount > 0) { + return + } + + const now = Date.now() + + const seedSnippets: Snippet[] = seedSnippetsData.map((snippet, index) => { + const timestamp = now - index * 1000 + return { + ...snippet, + createdAt: timestamp, + updatedAt: timestamp, + } + }) + + for (const snippet of seedSnippets) { + await createSnippet(snippet) + } + + const seedTemplates: SnippetTemplate[] = seedTemplatesData + + for (const template of seedTemplates) { + await createTemplate(template) + } +} diff --git a/src/lib/db-snippets/syncTemplatesFromJSON.ts b/src/lib/db-snippets/syncTemplatesFromJSON.ts new file mode 100644 index 0000000..cb31d95 --- /dev/null +++ b/src/lib/db-snippets/syncTemplatesFromJSON.ts @@ -0,0 +1,20 @@ +import type { SnippetTemplate } from '../types' +import { initDB } from '../db-core/initDB' +import { createTemplate } from './createTemplate' + +export async function syncTemplatesFromJSON(templates: SnippetTemplate[]): Promise { + const db = await initDB() + + const existingTemplates = db.exec('SELECT id FROM snippet_templates') + const existingIds = new Set( + existingTemplates[0]?.values.map(row => row[0] as string) || [] + ) + + let addedCount = 0 + for (const template of templates) { + if (!existingIds.has(template.id)) { + await createTemplate(template) + addedCount++ + } + } +} diff --git a/src/lib/db-snippets/updateSnippet.ts b/src/lib/db-snippets/updateSnippet.ts new file mode 100644 index 0000000..a8afcb3 --- /dev/null +++ b/src/lib/db-snippets/updateSnippet.ts @@ -0,0 +1,34 @@ +import type { Snippet } from '../types' +import { initDB } from '../db-core/initDB' +import { saveDB } from '../db-core/saveDB' +import { getFlaskAdapter } from '../db-core/getFlaskAdapter' + +export async function updateSnippet(snippet: Snippet): Promise { + const adapter = getFlaskAdapter() + if (adapter) { + return await adapter.updateSnippet(snippet) + } + + const db = await initDB() + + db.run( + `UPDATE snippets + SET title = ?, description = ?, code = ?, language = ?, category = ?, namespaceId = ?, hasPreview = ?, functionName = ?, inputParameters = ?, updatedAt = ? + WHERE id = ?`, + [ + snippet.title, + snippet.description, + snippet.code, + snippet.language, + snippet.category, + snippet.namespaceId || null, + snippet.hasPreview ? 1 : 0, + snippet.functionName || null, + snippet.inputParameters ? JSON.stringify(snippet.inputParameters) : null, + snippet.updatedAt, + snippet.id, + ] + ) + + await saveDB() +} diff --git a/src/lib/db.ts b/src/lib/db.ts index 7059bb4..eeab88e 100644 --- a/src/lib/db.ts +++ b/src/lib/db.ts @@ -4,32 +4,33 @@ */ // Re-export core database functions -export { initDB, saveDB, exportDatabase, importDatabase, getDatabaseStats, clearDatabase } from './db-core' +export { initDB } from './db-core/initDB' +export { saveDB } from './db-core/saveDB' +export { exportDatabase } from './db-core/exportDatabase' +export { importDatabase } from './db-core/importDatabase' +export { getDatabaseStats } from './db-core/getDatabaseStats' +export { clearDatabase } from './db-core/clearDatabase' // Re-export snippet operations -export { - getAllSnippets, - getSnippet, - createSnippet, - updateSnippet, - deleteSnippet, - getSnippetsByNamespace, - moveSnippetToNamespace, - bulkMoveSnippets, - getAllTemplates, - createTemplate, - syncTemplatesFromJSON, - seedDatabase -} from './db-snippets' +export { getAllSnippets } from './db-snippets/getAllSnippets' +export { getSnippet } from './db-snippets/getSnippet' +export { createSnippet } from './db-snippets/createSnippet' +export { updateSnippet } from './db-snippets/updateSnippet' +export { deleteSnippet } from './db-snippets/deleteSnippet' +export { getSnippetsByNamespace } from './db-snippets/getSnippetsByNamespace' +export { moveSnippetToNamespace } from './db-snippets/moveSnippetToNamespace' +export { bulkMoveSnippets } from './db-snippets/bulkMoveSnippets' +export { getAllTemplates } from './db-snippets/getAllTemplates' +export { createTemplate } from './db-snippets/createTemplate' +export { syncTemplatesFromJSON } from './db-snippets/syncTemplatesFromJSON' +export { seedDatabase } from './db-snippets/seedDatabase' // Re-export namespace operations -export { - getAllNamespaces, - createNamespace, - deleteNamespace, - ensureDefaultNamespace, - getNamespaceById -} from './db-namespaces' +export { getAllNamespaces } from './db-namespaces/getAllNamespaces' +export { createNamespace } from './db-namespaces/createNamespace' +export { deleteNamespace } from './db-namespaces/deleteNamespace' +export { ensureDefaultNamespace } from './db-namespaces/ensureDefaultNamespace' +export { getNamespaceById } from './db-namespaces/getNamespaceById' // Re-export schema validation export { validateDatabaseSchema } from './db-schema'