Merge pull request #10 from johndoe6345789/codex/split-functions-into-separate-files

Split multi-function modules into single-function files for navigation, sidebar-menu, and DB helpers
This commit is contained in:
2026-01-18 00:11:37 +00:00
committed by GitHub
51 changed files with 1071 additions and 944 deletions

View File

@@ -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'

View File

@@ -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 (
<Button
variant="ghost"
size="icon"
onClick={() => setMenuOpen(!menuOpen)}
>
<List className="h-5 w-5" />
</Button>
)
}

View File

@@ -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 (
<NavigationContext.Provider value={{ menuOpen, setMenuOpen }}>
{children}
</NavigationContext.Provider>
)
}

View File

@@ -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<NavigationContextType | undefined>(undefined)
export function NavigationProvider({ children }: { children: React.ReactNode }) {
const [menuOpen, setMenuOpen] = useState(false)
return (
<NavigationContext.Provider value={{ menuOpen, setMenuOpen }}>
{children}
</NavigationContext.Provider>
)
}
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 (
<Button
variant="ghost"
size="icon"
onClick={() => setMenuOpen(!menuOpen)}
>
<List className="h-5 w-5" />
</Button>
)
}
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()

View File

@@ -0,0 +1,10 @@
import { createContext } from 'react'
type NavigationContextType = {
menuOpen: boolean
setMenuOpen: (open: boolean) => void
}
export const NavigationContext = createContext<NavigationContextType | undefined>(
undefined
)

View File

@@ -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 },
]

View File

@@ -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
}

View File

@@ -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 (
<Comp
data-slot="sidebar-group-label"
data-sidebar="group-label"
className={cn(
"text-sidebar-foreground/70 ring-sidebar-ring flex h-8 shrink-0 items-center rounded-md px-2 text-xs font-medium outline-hidden transition-[margin,opacity] duration-200 ease-linear focus-visible:ring-2 [&>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 (
<Comp
data-slot="sidebar-group-action"
data-sidebar="group-action"
className={cn(
"text-sidebar-foreground ring-sidebar-ring hover:bg-sidebar-accent hover:text-sidebar-accent-foreground absolute top-3.5 right-3 flex aspect-square w-5 items-center justify-center rounded-md p-0 outline-hidden transition-transform focus-visible:ring-2 [&>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 (
<div
data-slot="sidebar-group-content"
data-sidebar="group-content"
className={cn("w-full text-sm", className)}
{...props}
/>
)
}
export function SidebarMenu({ className, ...props }: ComponentProps<"ul">) {
return (
<ul
data-slot="sidebar-menu"
data-sidebar="menu"
className={cn("flex w-full min-w-0 flex-col gap-1", className)}
{...props}
/>
)
}
export function SidebarMenuItem({ className, ...props }: ComponentProps<"li">) {
return (
<li
data-slot="sidebar-menu-item"
data-sidebar="menu-item"
className={cn("group/menu-item relative", className)}
{...props}
/>
)
}
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<typeof TooltipContent>
} & VariantProps<typeof sidebarMenuButtonVariants>) {
const Comp = asChild ? Slot : "button"
const { isMobile, state } = useSidebar()
const button = (
<Comp
data-slot="sidebar-menu-button"
data-sidebar="menu-button"
data-size={size}
data-active={isActive}
className={cn(sidebarMenuButtonVariants({ variant, size }), className)}
{...props}
/>
)
if (!tooltip) {
return button
}
if (typeof tooltip === "string") {
tooltip = {
children: tooltip,
}
}
return (
<Tooltip>
<TooltipTrigger asChild>{button}</TooltipTrigger>
<TooltipContent
side="right"
align="center"
hidden={state !== "collapsed" || isMobile}
{...tooltip}
/>
</Tooltip>
)
}
export function SidebarMenuAction({
className,
asChild = false,
showOnHover = false,
...props
}: ComponentProps<"button"> & {
asChild?: boolean
showOnHover?: boolean
}) {
const Comp = asChild ? Slot : "button"
return (
<Comp
data-slot="sidebar-menu-action"
data-sidebar="menu-action"
className={cn(
"text-sidebar-foreground ring-sidebar-ring hover:bg-sidebar-accent hover:text-sidebar-accent-foreground peer-hover/menu-button:text-sidebar-accent-foreground absolute top-1.5 right-1 flex aspect-square w-5 items-center justify-center rounded-md p-0 outline-hidden transition-transform focus-visible:ring-2 [&>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 (
<div
data-slot="sidebar-menu-badge"
data-sidebar="menu-badge"
className={cn(
"text-sidebar-foreground pointer-events-none absolute right-1 flex h-5 min-w-5 items-center justify-center rounded-md px-1 text-xs font-medium tabular-nums select-none",
"peer-hover/menu-button:text-sidebar-accent-foreground peer-data-[active=true]/menu-button:text-sidebar-accent-foreground",
"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",
className
)}
{...props}
/>
)
}
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 (
<div
data-slot="sidebar-menu-skeleton"
data-sidebar="menu-skeleton"
className={cn("flex h-8 items-center gap-2 rounded-md px-2", className)}
{...props}
>
{showIcon && (
<Skeleton
className="size-4 rounded-md"
data-sidebar="menu-skeleton-icon"
/>
)}
<Skeleton
className="h-4 max-w-(--skeleton-width) flex-1"
data-sidebar="menu-skeleton-text"
style={
{
"--skeleton-width": width,
} as CSSProperties
}
/>
</div>
)
}
export function SidebarMenuSub({ className, ...props }: ComponentProps<"ul">) {
return (
<ul
data-slot="sidebar-menu-sub"
data-sidebar="menu-sub"
className={cn(
"border-sidebar-border mx-3.5 flex min-w-0 translate-x-px flex-col gap-1 border-l px-2.5 py-0.5",
"group-data-[collapsible=icon]:hidden",
className
)}
{...props}
/>
)
}
export function SidebarMenuSubItem({
className,
...props
}: ComponentProps<"li">) {
return (
<li
data-slot="sidebar-menu-sub-item"
data-sidebar="menu-sub-item"
className={cn("group/menu-sub-item relative", className)}
{...props}
/>
)
}
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 (
<Comp
data-slot="sidebar-menu-sub-button"
data-sidebar="menu-sub-button"
data-size={size}
data-active={isActive}
className={cn(
"text-sidebar-foreground ring-sidebar-ring hover:bg-sidebar-accent hover:text-sidebar-accent-foreground active:bg-sidebar-accent active:text-sidebar-accent-foreground [&>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}
/>
)
}

View File

@@ -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 (
<Comp
data-slot="sidebar-group-action"
data-sidebar="group-action"
className={cn(
"text-sidebar-foreground ring-sidebar-ring hover:bg-sidebar-accent hover:text-sidebar-accent-foreground absolute top-3.5 right-3 flex aspect-square w-5 items-center justify-center rounded-md p-0 outline-hidden transition-transform focus-visible:ring-2 [&>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}
/>
)
}

View File

@@ -0,0 +1,18 @@
"use client"
import { ComponentProps } from "react"
import { cn } from "@/lib/utils"
export function SidebarGroupContent({
className,
...props
}: ComponentProps<"div">) {
return (
<div
data-slot="sidebar-group-content"
data-sidebar="group-content"
className={cn("w-full text-sm", className)}
{...props}
/>
)
}

View File

@@ -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 (
<Comp
data-slot="sidebar-group-label"
data-sidebar="group-label"
className={cn(
"text-sidebar-foreground/70 ring-sidebar-ring flex h-8 shrink-0 items-center rounded-md px-2 text-xs font-medium outline-hidden transition-[margin,opacity] duration-200 ease-linear focus-visible:ring-2 [&>svg]:size-4 [&>svg]:shrink-0",
"group-data-[collapsible=icon]:-mt-8 group-data-[collapsible=icon]:opacity-0",
className
)}
{...props}
/>
)
}

View File

@@ -0,0 +1,15 @@
"use client"
import { ComponentProps } from "react"
import { cn } from "@/lib/utils"
export function SidebarMenu({ className, ...props }: ComponentProps<"ul">) {
return (
<ul
data-slot="sidebar-menu"
data-sidebar="menu"
className={cn("flex w-full min-w-0 flex-col gap-1", className)}
{...props}
/>
)
}

View File

@@ -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 (
<Comp
data-slot="sidebar-menu-action"
data-sidebar="menu-action"
className={cn(
"text-sidebar-foreground ring-sidebar-ring hover:bg-sidebar-accent hover:text-sidebar-accent-foreground peer-hover/menu-button:text-sidebar-accent-foreground absolute top-1.5 right-1 flex aspect-square w-5 items-center justify-center rounded-md p-0 outline-hidden transition-transform focus-visible:ring-2 [&>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}
/>
)
}

View File

@@ -0,0 +1,26 @@
"use client"
import { ComponentProps } from "react"
import { cn } from "@/lib/utils"
export function SidebarMenuBadge({
className,
...props
}: ComponentProps<"div">) {
return (
<div
data-slot="sidebar-menu-badge"
data-sidebar="menu-badge"
className={cn(
"text-sidebar-foreground pointer-events-none absolute right-1 flex h-5 min-w-5 items-center justify-center rounded-md px-1 text-xs font-medium tabular-nums select-none",
"peer-hover/menu-button:text-sidebar-accent-foreground peer-data-[active=true]/menu-button:text-sidebar-accent-foreground",
"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",
className
)}
{...props}
/>
)
}

View File

@@ -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<typeof TooltipContent>
} & VariantProps<typeof sidebarMenuButtonVariants>) {
const Comp = asChild ? Slot : "button"
const { isMobile, state } = useSidebar()
const button = (
<Comp
data-slot="sidebar-menu-button"
data-sidebar="menu-button"
data-size={size}
data-active={isActive}
className={cn(sidebarMenuButtonVariants({ variant, size }), className)}
{...props}
/>
)
if (!tooltip) {
return button
}
if (typeof tooltip === "string") {
tooltip = {
children: tooltip,
}
}
return (
<Tooltip>
<TooltipTrigger asChild>{button}</TooltipTrigger>
<TooltipContent
side="right"
align="center"
hidden={state !== "collapsed" || isMobile}
{...tooltip}
/>
</Tooltip>
)
}

View File

@@ -0,0 +1,15 @@
"use client"
import { ComponentProps } from "react"
import { cn } from "@/lib/utils"
export function SidebarMenuItem({ className, ...props }: ComponentProps<"li">) {
return (
<li
data-slot="sidebar-menu-item"
data-sidebar="menu-item"
className={cn("group/menu-item relative", className)}
{...props}
/>
)
}

View File

@@ -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 (
<div
data-slot="sidebar-menu-skeleton"
data-sidebar="menu-skeleton"
className={cn("flex h-8 items-center gap-2 rounded-md px-2", className)}
{...props}
>
{showIcon && (
<Skeleton
className="size-4 rounded-md"
data-sidebar="menu-skeleton-icon"
/>
)}
<Skeleton
className="h-4 max-w-(--skeleton-width) flex-1"
data-sidebar="menu-skeleton-text"
style={
{
"--skeleton-width": width,
} as CSSProperties
}
/>
</div>
)
}

View File

@@ -0,0 +1,19 @@
"use client"
import { ComponentProps } from "react"
import { cn } from "@/lib/utils"
export function SidebarMenuSub({ className, ...props }: ComponentProps<"ul">) {
return (
<ul
data-slot="sidebar-menu-sub"
data-sidebar="menu-sub"
className={cn(
"border-sidebar-border mx-3.5 flex min-w-0 translate-x-px flex-col gap-1 border-l px-2.5 py-0.5",
"group-data-[collapsible=icon]:hidden",
className
)}
{...props}
/>
)
}

View File

@@ -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 (
<Comp
data-slot="sidebar-menu-sub-button"
data-sidebar="menu-sub-button"
data-size={size}
data-active={isActive}
className={cn(
"text-sidebar-foreground ring-sidebar-ring hover:bg-sidebar-accent hover:text-sidebar-accent-foreground active:bg-sidebar-accent active:text-sidebar-accent-foreground [&>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}
/>
)
}

View File

@@ -0,0 +1,18 @@
"use client"
import { ComponentProps } from "react"
import { cn } from "@/lib/utils"
export function SidebarMenuSubItem({
className,
...props
}: ComponentProps<"li">) {
return (
<li
data-slot="sidebar-menu-sub-item"
data-sidebar="menu-sub-item"
className={cn("group/menu-sub-item relative", className)}
{...props}
/>
)
}

View File

@@ -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"

View File

@@ -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<void> {
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<Database> {
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<Uint8Array> {
const db = await initDB()
return db.export()
}
export async function importDatabase(data: Uint8Array): Promise<void> {
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<void> {
const adapter = getFlaskAdapter()
if (adapter) {
await adapter.wipeDatabase()
return
}
await deleteFromIndexedDB()
deleteFromLocalStorage()
dbInstance = null
await initDB()
}

View File

@@ -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<void> {
const adapter = getFlaskAdapter()
if (adapter) {
await adapter.wipeDatabase()
return
}
await deleteFromIndexedDB()
deleteFromLocalStorage()
dbState.dbInstance = null
await initDB()
}

View File

@@ -0,0 +1,6 @@
import { initDB } from './initDB'
export async function exportDatabase(): Promise<Uint8Array> {
const db = await initDB()
return db.export()
}

View File

@@ -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,
}
}

View File

@@ -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
}

View File

@@ -0,0 +1,19 @@
import initSqlJs from 'sql.js'
import { saveDB } from './saveDB'
import { dbState } from './state'
export async function importDatabase(data: Uint8Array): Promise<void> {
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
}
}

57
src/lib/db-core/initDB.ts Normal file
View File

@@ -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<Database> {
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
}

19
src/lib/db-core/saveDB.ts Normal file
View File

@@ -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)
}
}

9
src/lib/db-core/state.ts Normal file
View File

@@ -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,
}

View File

@@ -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<void> {
console.warn('Wiping corrupted database and creating fresh schema...')
await saveToIndexedDB(new Uint8Array())
saveToLocalStorage(new Uint8Array())
await deleteFromIndexedDB()
deleteFromLocalStorage()
dbState.dbInstance = null
}

View File

@@ -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<Namespace[]> {
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<Namespace>(results)
}
export async function createNamespace(name: string): Promise<Namespace> {
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<void> {
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<void> {
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<Namespace | null> {
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<Namespace>(row, columns)
}

View File

@@ -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<Namespace> {
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
}

View File

@@ -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<void> {
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()
}

View File

@@ -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<void> {
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()
}
}

View File

@@ -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<Namespace[]> {
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<Namespace>(results)
}

View File

@@ -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<Namespace | null> {
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<Namespace>(row, columns)
}

View File

@@ -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<Snippet[]> {
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<Snippet>(results)
}
export async function getSnippet(id: string): Promise<Snippet | null> {
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<Snippet>(row, columns)
}
export async function createSnippet(snippet: Snippet): Promise<void> {
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<void> {
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<void> {
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<Snippet[]> {
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<Snippet>(results)
}
export async function moveSnippetToNamespace(snippetId: string, targetNamespaceId: string): Promise<void> {
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<void> {
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<SnippetTemplate[]> {
const db = await initDB()
const results = db.exec('SELECT * FROM snippet_templates')
return mapRowsToObjects<SnippetTemplate>(results)
}
export async function createTemplate(template: SnippetTemplate): Promise<void> {
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<void> {
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<void> {
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)
}
}

View File

@@ -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<void> {
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()
}

View File

@@ -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<void> {
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()
}

View File

@@ -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<void> {
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()
}

View File

@@ -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<void> {
const adapter = getFlaskAdapter()
if (adapter) {
return await adapter.deleteSnippet(id)
}
const db = await initDB()
db.run('DELETE FROM snippets WHERE id = ?', [id])
await saveDB()
}

View File

@@ -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<Snippet[]> {
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<Snippet>(results)
}

View File

@@ -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<SnippetTemplate[]> {
const db = await initDB()
const results = db.exec('SELECT * FROM snippet_templates')
return mapRowsToObjects<SnippetTemplate>(results)
}

View File

@@ -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<Snippet | null> {
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<Snippet>(row, columns)
}

View File

@@ -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<Snippet[]> {
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<Snippet>(results)
}

View File

@@ -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<void> {
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()
}

View File

@@ -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<void> {
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)
}
}

View File

@@ -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<void> {
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++
}
}
}

View File

@@ -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<void> {
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()
}

View File

@@ -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'