mirror of
https://github.com/johndoe6345789/snippet-pastebin.git
synced 2026-04-24 13:34:55 +00:00
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:
@@ -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'
|
||||
|
||||
16
src/components/layout/navigation/Navigation.tsx
Normal file
16
src/components/layout/navigation/Navigation.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
12
src/components/layout/navigation/NavigationProvider.tsx
Normal file
12
src/components/layout/navigation/NavigationProvider.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
@@ -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()
|
||||
10
src/components/layout/navigation/navigation-context.tsx
Normal file
10
src/components/layout/navigation/navigation-context.tsx
Normal file
@@ -0,0 +1,10 @@
|
||||
import { createContext } from 'react'
|
||||
|
||||
type NavigationContextType = {
|
||||
menuOpen: boolean
|
||||
setMenuOpen: (open: boolean) => void
|
||||
}
|
||||
|
||||
export const NavigationContext = createContext<NavigationContextType | undefined>(
|
||||
undefined
|
||||
)
|
||||
18
src/components/layout/navigation/navigation-items.ts
Normal file
18
src/components/layout/navigation/navigation-items.ts
Normal 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 },
|
||||
]
|
||||
10
src/components/layout/navigation/useNavigation.ts
Normal file
10
src/components/layout/navigation/useNavigation.ts
Normal 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
|
||||
}
|
||||
@@ -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}
|
||||
/>
|
||||
)
|
||||
}
|
||||
28
src/components/ui/sidebar-menu/SidebarGroupAction.tsx
Normal file
28
src/components/ui/sidebar-menu/SidebarGroupAction.tsx
Normal 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}
|
||||
/>
|
||||
)
|
||||
}
|
||||
18
src/components/ui/sidebar-menu/SidebarGroupContent.tsx
Normal file
18
src/components/ui/sidebar-menu/SidebarGroupContent.tsx
Normal 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}
|
||||
/>
|
||||
)
|
||||
}
|
||||
26
src/components/ui/sidebar-menu/SidebarGroupLabel.tsx
Normal file
26
src/components/ui/sidebar-menu/SidebarGroupLabel.tsx
Normal 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}
|
||||
/>
|
||||
)
|
||||
}
|
||||
15
src/components/ui/sidebar-menu/SidebarMenu.tsx
Normal file
15
src/components/ui/sidebar-menu/SidebarMenu.tsx
Normal 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}
|
||||
/>
|
||||
)
|
||||
}
|
||||
37
src/components/ui/sidebar-menu/SidebarMenuAction.tsx
Normal file
37
src/components/ui/sidebar-menu/SidebarMenuAction.tsx
Normal 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}
|
||||
/>
|
||||
)
|
||||
}
|
||||
26
src/components/ui/sidebar-menu/SidebarMenuBadge.tsx
Normal file
26
src/components/ui/sidebar-menu/SidebarMenuBadge.tsx
Normal 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}
|
||||
/>
|
||||
)
|
||||
}
|
||||
84
src/components/ui/sidebar-menu/SidebarMenuButton.tsx
Normal file
84
src/components/ui/sidebar-menu/SidebarMenuButton.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
15
src/components/ui/sidebar-menu/SidebarMenuItem.tsx
Normal file
15
src/components/ui/sidebar-menu/SidebarMenuItem.tsx
Normal 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}
|
||||
/>
|
||||
)
|
||||
}
|
||||
43
src/components/ui/sidebar-menu/SidebarMenuSkeleton.tsx
Normal file
43
src/components/ui/sidebar-menu/SidebarMenuSkeleton.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
19
src/components/ui/sidebar-menu/SidebarMenuSub.tsx
Normal file
19
src/components/ui/sidebar-menu/SidebarMenuSub.tsx
Normal 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}
|
||||
/>
|
||||
)
|
||||
}
|
||||
37
src/components/ui/sidebar-menu/SidebarMenuSubButton.tsx
Normal file
37
src/components/ui/sidebar-menu/SidebarMenuSubButton.tsx
Normal 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}
|
||||
/>
|
||||
)
|
||||
}
|
||||
18
src/components/ui/sidebar-menu/SidebarMenuSubItem.tsx
Normal file
18
src/components/ui/sidebar-menu/SidebarMenuSubItem.tsx
Normal 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}
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -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"
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
19
src/lib/db-core/clearDatabase.ts
Normal file
19
src/lib/db-core/clearDatabase.ts
Normal 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()
|
||||
}
|
||||
6
src/lib/db-core/exportDatabase.ts
Normal file
6
src/lib/db-core/exportDatabase.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
import { initDB } from './initDB'
|
||||
|
||||
export async function exportDatabase(): Promise<Uint8Array> {
|
||||
const db = await initDB()
|
||||
return db.export()
|
||||
}
|
||||
32
src/lib/db-core/getDatabaseStats.ts
Normal file
32
src/lib/db-core/getDatabaseStats.ts
Normal 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,
|
||||
}
|
||||
}
|
||||
23
src/lib/db-core/getFlaskAdapter.ts
Normal file
23
src/lib/db-core/getFlaskAdapter.ts
Normal 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
|
||||
}
|
||||
19
src/lib/db-core/importDatabase.ts
Normal file
19
src/lib/db-core/importDatabase.ts
Normal 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
57
src/lib/db-core/initDB.ts
Normal 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
19
src/lib/db-core/saveDB.ts
Normal 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
9
src/lib/db-core/state.ts
Normal 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,
|
||||
}
|
||||
15
src/lib/db-core/wipeAndRecreateDB.ts
Normal file
15
src/lib/db-core/wipeAndRecreateDB.ts
Normal 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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
30
src/lib/db-namespaces/createNamespace.ts
Normal file
30
src/lib/db-namespaces/createNamespace.ts
Normal 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
|
||||
}
|
||||
30
src/lib/db-namespaces/deleteNamespace.ts
Normal file
30
src/lib/db-namespaces/deleteNamespace.ts
Normal 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()
|
||||
}
|
||||
27
src/lib/db-namespaces/ensureDefaultNamespace.ts
Normal file
27
src/lib/db-namespaces/ensureDefaultNamespace.ts
Normal 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()
|
||||
}
|
||||
}
|
||||
16
src/lib/db-namespaces/getAllNamespaces.ts
Normal file
16
src/lib/db-namespaces/getAllNamespaces.ts
Normal 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)
|
||||
}
|
||||
15
src/lib/db-namespaces/getNamespaceById.ts
Normal file
15
src/lib/db-namespaces/getNamespaceById.ts
Normal 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)
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
23
src/lib/db-snippets/bulkMoveSnippets.ts
Normal file
23
src/lib/db-snippets/bulkMoveSnippets.ts
Normal 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()
|
||||
}
|
||||
34
src/lib/db-snippets/createSnippet.ts
Normal file
34
src/lib/db-snippets/createSnippet.ts
Normal 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()
|
||||
}
|
||||
25
src/lib/db-snippets/createTemplate.ts
Normal file
25
src/lib/db-snippets/createTemplate.ts
Normal 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()
|
||||
}
|
||||
16
src/lib/db-snippets/deleteSnippet.ts
Normal file
16
src/lib/db-snippets/deleteSnippet.ts
Normal 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()
|
||||
}
|
||||
16
src/lib/db-snippets/getAllSnippets.ts
Normal file
16
src/lib/db-snippets/getAllSnippets.ts
Normal 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)
|
||||
}
|
||||
10
src/lib/db-snippets/getAllTemplates.ts
Normal file
10
src/lib/db-snippets/getAllTemplates.ts
Normal 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)
|
||||
}
|
||||
21
src/lib/db-snippets/getSnippet.ts
Normal file
21
src/lib/db-snippets/getSnippet.ts
Normal 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)
|
||||
}
|
||||
13
src/lib/db-snippets/getSnippetsByNamespace.ts
Normal file
13
src/lib/db-snippets/getSnippetsByNamespace.ts
Normal 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)
|
||||
}
|
||||
25
src/lib/db-snippets/moveSnippetToNamespace.ts
Normal file
25
src/lib/db-snippets/moveSnippetToNamespace.ts
Normal 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()
|
||||
}
|
||||
41
src/lib/db-snippets/seedDatabase.ts
Normal file
41
src/lib/db-snippets/seedDatabase.ts
Normal 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)
|
||||
}
|
||||
}
|
||||
20
src/lib/db-snippets/syncTemplatesFromJSON.ts
Normal file
20
src/lib/db-snippets/syncTemplatesFromJSON.ts
Normal 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++
|
||||
}
|
||||
}
|
||||
}
|
||||
34
src/lib/db-snippets/updateSnippet.ts
Normal file
34
src/lib/db-snippets/updateSnippet.ts
Normal 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()
|
||||
}
|
||||
@@ -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'
|
||||
|
||||
Reference in New Issue
Block a user