mirror of
https://github.com/johndoe6345789/snippet-pastebin.git
synced 2026-04-24 13:34:55 +00:00
Merge pull request #17 from johndoe6345789/copilot/convert-to-nextjs-update-files
Convert from Vite to Next.js, remove sql.js, use IndexedDB directly
This commit is contained in:
10
.env.example
10
.env.example
@@ -1,8 +1,12 @@
|
||||
# Frontend Configuration
|
||||
# Flask Backend URL - If set, the app will automatically use Flask backend instead of IndexedDB
|
||||
# Development: VITE_FLASK_BACKEND_URL=http://localhost:5000
|
||||
# Production: VITE_FLASK_BACKEND_URL=https://backend.example.com
|
||||
VITE_FLASK_BACKEND_URL=
|
||||
# Development: NEXT_PUBLIC_FLASK_BACKEND_URL=http://localhost:5000
|
||||
# Production: NEXT_PUBLIC_FLASK_BACKEND_URL=https://backend.example.com
|
||||
NEXT_PUBLIC_FLASK_BACKEND_URL=
|
||||
|
||||
# Base path for deployment (e.g., for GitHub Pages)
|
||||
# Leave empty for root deployment
|
||||
NEXT_PUBLIC_BASE_PATH=
|
||||
|
||||
# Backend Configuration (for backend/app.py)
|
||||
# CORS Allowed Origins - Comma-separated list of allowed frontend URLs
|
||||
|
||||
3
.eslintrc.json
Normal file
3
.eslintrc.json
Normal file
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"extends": ["next/core-web-vitals", "next/typescript"]
|
||||
}
|
||||
9
.github/workflows/deploy-pages.yml
vendored
9
.github/workflows/deploy-pages.yml
vendored
@@ -31,11 +31,12 @@ jobs:
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
|
||||
- name: Build
|
||||
- name: Build Next.js
|
||||
run: npm run build
|
||||
env:
|
||||
VITE_FLASK_BACKEND_URL: ${{ vars.VITE_FLASK_BACKEND_URL || '' }}
|
||||
VITE_BASE_PATH: ${{ vars.VITE_BASE_PATH || '/' }}
|
||||
BUILD_STATIC: 'true'
|
||||
NEXT_PUBLIC_FLASK_BACKEND_URL: ${{ vars.NEXT_PUBLIC_FLASK_BACKEND_URL || '' }}
|
||||
NEXT_PUBLIC_BASE_PATH: ${{ vars.NEXT_PUBLIC_BASE_PATH || '' }}
|
||||
|
||||
- name: Setup Pages
|
||||
uses: actions/configure-pages@v4
|
||||
@@ -43,7 +44,7 @@ jobs:
|
||||
- name: Upload artifact
|
||||
uses: actions/upload-pages-artifact@v3
|
||||
with:
|
||||
path: './dist'
|
||||
path: './out'
|
||||
|
||||
deploy:
|
||||
environment:
|
||||
|
||||
4
.github/workflows/docker-publish.yml
vendored
4
.github/workflows/docker-publish.yml
vendored
@@ -117,5 +117,5 @@ jobs:
|
||||
cache-from: type=gha,scope=frontend
|
||||
cache-to: type=gha,mode=max,scope=frontend
|
||||
build-args: |
|
||||
VITE_FLASK_BACKEND_URL=${{ vars.VITE_FLASK_BACKEND_URL || '' }}
|
||||
VITE_BASE_PATH=${{ vars.VITE_BASE_PATH || '/' }}
|
||||
NEXT_PUBLIC_FLASK_BACKEND_URL=${{ vars.NEXT_PUBLIC_FLASK_BACKEND_URL || '' }}
|
||||
NEXT_PUBLIC_BASE_PATH=${{ vars.NEXT_PUBLIC_BASE_PATH || '' }}
|
||||
|
||||
8
.gitignore
vendored
8
.gitignore
vendored
@@ -13,6 +13,12 @@ dist-ssr
|
||||
*-dist
|
||||
*.local
|
||||
|
||||
# Next.js
|
||||
/.next/
|
||||
/out/
|
||||
next-env.d.ts
|
||||
.vercel
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
@@ -25,6 +31,8 @@ dist-ssr
|
||||
*.sw?
|
||||
|
||||
.env
|
||||
.env.local
|
||||
.env*.local
|
||||
**/agent-eval-report*
|
||||
packages
|
||||
pids
|
||||
|
||||
29
Dockerfile
29
Dockerfile
@@ -3,26 +3,37 @@ FROM node:20-alpine AS builder
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# Copy package files
|
||||
COPY package*.json ./
|
||||
RUN npm ci
|
||||
|
||||
# Copy source code
|
||||
COPY . .
|
||||
|
||||
ARG VITE_FLASK_BACKEND_URL
|
||||
ENV VITE_FLASK_BACKEND_URL=$VITE_FLASK_BACKEND_URL
|
||||
|
||||
ARG VITE_BASE_PATH
|
||||
ENV VITE_BASE_PATH=$VITE_BASE_PATH
|
||||
# Build arguments for environment variables
|
||||
ARG NEXT_PUBLIC_FLASK_BACKEND_URL
|
||||
ARG NEXT_PUBLIC_BASE_PATH
|
||||
ENV NEXT_PUBLIC_FLASK_BACKEND_URL=$NEXT_PUBLIC_FLASK_BACKEND_URL
|
||||
ENV NEXT_PUBLIC_BASE_PATH=$NEXT_PUBLIC_BASE_PATH
|
||||
|
||||
# Build Next.js app
|
||||
RUN npm run build
|
||||
|
||||
# Production stage
|
||||
FROM nginx:alpine
|
||||
FROM node:20-alpine AS runner
|
||||
|
||||
COPY --from=builder /app/dist /usr/share/nginx/html
|
||||
WORKDIR /app
|
||||
|
||||
COPY nginx.conf /etc/nginx/conf.d/default.conf
|
||||
ENV NODE_ENV=production
|
||||
|
||||
# Copy necessary files from builder
|
||||
COPY --from=builder /app/public ./public
|
||||
COPY --from=builder /app/.next/standalone ./
|
||||
COPY --from=builder /app/.next/static ./.next/static
|
||||
|
||||
EXPOSE 3000
|
||||
|
||||
CMD ["nginx", "-g", "daemon off;"]
|
||||
ENV PORT=3000
|
||||
ENV HOSTNAME="0.0.0.0"
|
||||
|
||||
CMD ["node", "server.js"]
|
||||
|
||||
@@ -18,7 +18,7 @@ services:
|
||||
build:
|
||||
context: .
|
||||
args:
|
||||
- VITE_FLASK_BACKEND_URL=https://backend.example.com
|
||||
- NEXT_PUBLIC_FLASK_BACKEND_URL=https://backend.example.com
|
||||
ports:
|
||||
- "3000:3000"
|
||||
depends_on:
|
||||
|
||||
@@ -16,7 +16,7 @@ services:
|
||||
build:
|
||||
context: .
|
||||
args:
|
||||
- VITE_FLASK_BACKEND_URL=http://localhost:5000
|
||||
- NEXT_PUBLIC_FLASK_BACKEND_URL=http://localhost:5000
|
||||
ports:
|
||||
- "3000:3000"
|
||||
depends_on:
|
||||
|
||||
19
index.html
19
index.html
@@ -1,19 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>CodeSnippet - Share & Run Code (Python, React & More)</title>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Bricolage+Grotesque:wght@400;500;600;700&family=Inter:wght@400;500;600&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
|
||||
<link href="/src/main.css" rel="stylesheet" />
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/main.tsx"></script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
14
next.config.js
Normal file
14
next.config.js
Normal file
@@ -0,0 +1,14 @@
|
||||
/** @type {import('next').NextConfig} */
|
||||
const nextConfig = {
|
||||
output: process.env.BUILD_STATIC ? 'export' : 'standalone',
|
||||
basePath: process.env.NEXT_PUBLIC_BASE_PATH || '',
|
||||
turbopack: {},
|
||||
images: {
|
||||
unoptimized: true,
|
||||
},
|
||||
experimental: {
|
||||
optimizePackageImports: ['@radix-ui/react-icons', '@phosphor-icons/react'],
|
||||
},
|
||||
};
|
||||
|
||||
export default nextConfig;
|
||||
29
nginx.conf
29
nginx.conf
@@ -1,29 +0,0 @@
|
||||
server {
|
||||
listen 3000;
|
||||
server_name localhost;
|
||||
root /usr/share/nginx/html;
|
||||
index index.html;
|
||||
|
||||
location / {
|
||||
try_files $uri $uri/ /index.html;
|
||||
|
||||
add_header Cache-Control "no-cache, no-store, must-revalidate";
|
||||
add_header Pragma "no-cache";
|
||||
add_header Expires "0";
|
||||
}
|
||||
|
||||
location /api {
|
||||
proxy_pass http://backend:5000;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection 'upgrade';
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_cache_bypass $http_upgrade;
|
||||
|
||||
proxy_buffering off;
|
||||
proxy_request_buffering off;
|
||||
}
|
||||
}
|
||||
4708
package-lock.json
generated
4708
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
24
package.json
24
package.json
@@ -2,14 +2,12 @@
|
||||
"name": "spark-template",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"kill": "fuser -k 5000/tcp",
|
||||
"build": "tsc -b --noCheck && vite build",
|
||||
"lint": "eslint .",
|
||||
"optimize": "vite optimize",
|
||||
"preview": "vite preview"
|
||||
"dev": "next dev",
|
||||
"build": "next build",
|
||||
"start": "next start",
|
||||
"lint": "next lint",
|
||||
"kill": "fuser -k 5000/tcp"
|
||||
},
|
||||
"dependencies": {
|
||||
"@babel/standalone": "^7.28.6",
|
||||
@@ -48,7 +46,6 @@
|
||||
"@radix-ui/react-tooltip": "^1.1.8",
|
||||
"@reduxjs/toolkit": "^2.11.2",
|
||||
"@tailwindcss/container-queries": "^0.1.1",
|
||||
"@tailwindcss/vite": "^4.1.11",
|
||||
"@tanstack/react-query": "^5.83.1",
|
||||
"@types/sql.js": "^1.4.9",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
@@ -61,6 +58,7 @@
|
||||
"input-otp": "^1.4.2",
|
||||
"lucide-react": "^0.484.0",
|
||||
"marked": "^15.0.7",
|
||||
"next": "16.1.3",
|
||||
"next-themes": "^0.4.6",
|
||||
"octokit": "^4.1.2",
|
||||
"pyodide": "^0.29.1",
|
||||
@@ -74,7 +72,6 @@
|
||||
"react-router-dom": "^7.12.0",
|
||||
"recharts": "^2.15.1",
|
||||
"sonner": "^2.0.1",
|
||||
"sql.js": "^1.13.0",
|
||||
"tailwind-merge": "^3.0.2",
|
||||
"three": "^0.175.0",
|
||||
"tw-animate-css": "^1.2.4",
|
||||
@@ -84,18 +81,17 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.21.0",
|
||||
"@tailwindcss/postcss": "^4.1.8",
|
||||
"@tailwindcss/postcss": "^4.1.18",
|
||||
"@types/react": "^19.0.10",
|
||||
"@types/react-dom": "^19.0.4",
|
||||
"@vitejs/plugin-react-swc": "^4.2.2",
|
||||
"autoprefixer": "^10.4.23",
|
||||
"eslint": "^9.28.0",
|
||||
"eslint-config-next": "^16.1.3",
|
||||
"eslint-plugin-react-hooks": "^5.2.0",
|
||||
"eslint-plugin-react-refresh": "^0.4.19",
|
||||
"globals": "^16.0.0",
|
||||
"tailwindcss": "^4.1.11",
|
||||
"typescript": "~5.7.2",
|
||||
"typescript-eslint": "^8.38.0",
|
||||
"vite": "^7.2.6"
|
||||
"typescript-eslint": "^8.38.0"
|
||||
},
|
||||
"workspaces": {
|
||||
"packages": [
|
||||
|
||||
5
postcss.config.js
Normal file
5
postcss.config.js
Normal file
@@ -0,0 +1,5 @@
|
||||
module.exports = {
|
||||
plugins: {
|
||||
'@tailwindcss/postcss': {},
|
||||
},
|
||||
}
|
||||
@@ -1,21 +1,15 @@
|
||||
import { BrowserRouter as Router, Routes, Route } from 'react-router-dom'
|
||||
import { motion } from 'framer-motion'
|
||||
import { Code } from '@phosphor-icons/react'
|
||||
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'
|
||||
import { AtomsPage } from '@/pages/AtomsPage'
|
||||
import { MoleculesPage } from '@/pages/MoleculesPage'
|
||||
import { OrganismsPage } from '@/pages/OrganismsPage'
|
||||
import { TemplatesPage } from '@/pages/TemplatesPage'
|
||||
import { SettingsPage } from '@/pages/SettingsPage'
|
||||
'use client';
|
||||
|
||||
function AppContent() {
|
||||
const { menuOpen } = useNavigation()
|
||||
import { motion } from 'framer-motion';
|
||||
import { Code } from '@phosphor-icons/react';
|
||||
import { Navigation } from '@/components/layout/navigation/Navigation';
|
||||
import { NavigationSidebar } from '@/components/layout/navigation/NavigationSidebar';
|
||||
import { useNavigation } from '@/components/layout/navigation/useNavigation';
|
||||
import { BackendIndicator } from '@/components/layout/BackendIndicator';
|
||||
import { ReactNode } from 'react';
|
||||
|
||||
export function PageLayout({ children }: { children: ReactNode }) {
|
||||
const { menuOpen } = useNavigation();
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-background">
|
||||
@@ -78,15 +72,7 @@ function AppContent() {
|
||||
</header>
|
||||
|
||||
<main className="container mx-auto px-6 py-8">
|
||||
<Routes>
|
||||
<Route path="/" element={<HomePage />} />
|
||||
<Route path="/demo" element={<DemoPage />} />
|
||||
<Route path="/atoms" element={<AtomsPage />} />
|
||||
<Route path="/molecules" element={<MoleculesPage />} />
|
||||
<Route path="/organisms" element={<OrganismsPage />} />
|
||||
<Route path="/templates" element={<TemplatesPage />} />
|
||||
<Route path="/settings" element={<SettingsPage />} />
|
||||
</Routes>
|
||||
{children}
|
||||
</main>
|
||||
|
||||
<footer className="border-t border-border mt-24">
|
||||
@@ -99,17 +85,5 @@ function AppContent() {
|
||||
</footer>
|
||||
</motion.div>
|
||||
</div>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function App() {
|
||||
return (
|
||||
<Router>
|
||||
<NavigationProvider>
|
||||
<AppContent />
|
||||
</NavigationProvider>
|
||||
</Router>
|
||||
)
|
||||
}
|
||||
|
||||
export default App
|
||||
43
src/app/atoms/page.tsx
Normal file
43
src/app/atoms/page.tsx
Normal file
@@ -0,0 +1,43 @@
|
||||
'use client';
|
||||
|
||||
import { motion } from 'framer-motion';
|
||||
import { AtomsSection } from '@/components/atoms/AtomsSection';
|
||||
import type { Snippet } from '@/lib/types';
|
||||
import { useCallback } from 'react';
|
||||
import { toast } from 'sonner';
|
||||
import { createSnippet } from '@/lib/db';
|
||||
import { PageLayout } from '../PageLayout';
|
||||
|
||||
export default function AtomsPage() {
|
||||
const handleSaveSnippet = useCallback(async (snippetData: Omit<Snippet, 'id' | 'createdAt' | 'updatedAt'>) => {
|
||||
try {
|
||||
const newSnippet: Snippet = {
|
||||
...snippetData,
|
||||
id: Date.now().toString(),
|
||||
createdAt: Date.now(),
|
||||
updatedAt: Date.now(),
|
||||
};
|
||||
await createSnippet(newSnippet);
|
||||
toast.success('Component saved as snippet!');
|
||||
} catch (error) {
|
||||
console.error('Failed to save snippet:', error);
|
||||
toast.error('Failed to save snippet');
|
||||
}
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<PageLayout>
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.4 }}
|
||||
>
|
||||
<div className="mb-8">
|
||||
<h2 className="text-3xl font-bold tracking-tight mb-2">Atoms</h2>
|
||||
<p className="text-muted-foreground">Fundamental building blocks - basic HTML elements styled as reusable components</p>
|
||||
</div>
|
||||
<AtomsSection onSaveSnippet={handleSaveSnippet} />
|
||||
</motion.div>
|
||||
</PageLayout>
|
||||
);
|
||||
}
|
||||
60
src/app/demo/page.tsx
Normal file
60
src/app/demo/page.tsx
Normal file
@@ -0,0 +1,60 @@
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
import { motion } from 'framer-motion';
|
||||
import { SplitScreenEditor } from '@/components/features/snippet-editor/SplitScreenEditor';
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import { Sparkle } from '@phosphor-icons/react';
|
||||
import { DEMO_CODE } from '@/components/demo/demo-constants';
|
||||
import { DemoFeatureCards } from '@/components/demo/DemoFeatureCards';
|
||||
import { PageLayout } from '../PageLayout';
|
||||
|
||||
export default function DemoPage() {
|
||||
const [code, setCode] = useState(DEMO_CODE);
|
||||
|
||||
return (
|
||||
<PageLayout>
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.4 }}
|
||||
className="space-y-8"
|
||||
>
|
||||
<div className="mb-8">
|
||||
<div className="flex items-center gap-3 mb-2">
|
||||
<div className="h-10 w-10 rounded-lg bg-gradient-to-br from-accent to-primary flex items-center justify-center">
|
||||
<Sparkle className="h-5 w-5 text-primary-foreground" weight="fill" />
|
||||
</div>
|
||||
<h2 className="text-3xl font-bold tracking-tight">Split-Screen Demo</h2>
|
||||
</div>
|
||||
<p className="text-muted-foreground">
|
||||
Experience live React component editing with real-time preview. Edit the code on the left and watch it update instantly on the right.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<Card className="border-accent/20 bg-card/50 backdrop-blur">
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<Sparkle className="h-5 w-5 text-accent" weight="fill" />
|
||||
Interactive Code Editor
|
||||
</CardTitle>
|
||||
<CardDescription>
|
||||
This editor supports JSX, TSX, JavaScript, and TypeScript with live preview.
|
||||
Try switching between Code, Split, and Preview modes using the buttons above the editor.
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<SplitScreenEditor
|
||||
value={code}
|
||||
onChange={setCode}
|
||||
language="JSX"
|
||||
height="600px"
|
||||
/>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<DemoFeatureCards />
|
||||
</motion.div>
|
||||
</PageLayout>
|
||||
);
|
||||
}
|
||||
49
src/app/globals.css
Normal file
49
src/app/globals.css
Normal file
@@ -0,0 +1,49 @@
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
@layer base {
|
||||
:root {
|
||||
--background: 222.2 84% 4.9%;
|
||||
--foreground: 210 40% 98%;
|
||||
--card: 222.2 84% 4.9%;
|
||||
--card-foreground: 210 40% 98%;
|
||||
--popover: 222.2 84% 4.9%;
|
||||
--popover-foreground: 210 40% 98%;
|
||||
--primary: 263 70% 50%;
|
||||
--primary-foreground: 210 40% 98%;
|
||||
--secondary: 217.2 32.6% 17.5%;
|
||||
--secondary-foreground: 210 40% 98%;
|
||||
--muted: 217.2 32.6% 17.5%;
|
||||
--muted-foreground: 215 20.2% 65.1%;
|
||||
--accent: 195 100% 70%;
|
||||
--accent-foreground: 222.2 84% 4.9%;
|
||||
--destructive: 0 62.8% 30.6%;
|
||||
--destructive-foreground: 210 40% 98%;
|
||||
--border: 217.2 32.6% 17.5%;
|
||||
--input: 217.2 32.6% 17.5%;
|
||||
--ring: 195 100% 70%;
|
||||
--radius: 0.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
@layer base {
|
||||
* {
|
||||
@apply border-border;
|
||||
}
|
||||
body {
|
||||
@apply bg-background text-foreground;
|
||||
}
|
||||
}
|
||||
|
||||
h1, h2, h3, h4, h5, h6 {
|
||||
font-family: var(--font-inter), 'Inter', sans-serif;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: var(--font-inter), 'Inter', sans-serif;
|
||||
}
|
||||
|
||||
code, pre {
|
||||
font-family: var(--font-jetbrains-mono), 'JetBrains Mono', monospace;
|
||||
}
|
||||
29
src/app/layout.tsx
Normal file
29
src/app/layout.tsx
Normal file
@@ -0,0 +1,29 @@
|
||||
import type { Metadata } from 'next';
|
||||
import './globals.css';
|
||||
import { Providers } from './providers';
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: 'CodeSnippet - Share & Run Code (Python, React & More)',
|
||||
description: 'Save, organize, and share your code snippets with beautiful syntax highlighting and live execution',
|
||||
};
|
||||
|
||||
export default function RootLayout({
|
||||
children,
|
||||
}: Readonly<{
|
||||
children: React.ReactNode;
|
||||
}>) {
|
||||
return (
|
||||
<html lang="en" suppressHydrationWarning>
|
||||
<head>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossOrigin="anonymous" />
|
||||
<link href="https://fonts.googleapis.com/css2?family=Bricolage+Grotesque:wght@400;500;600;700&family=Inter:wght@400;500;600&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet" />
|
||||
</head>
|
||||
<body>
|
||||
<Providers>
|
||||
{children}
|
||||
</Providers>
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
43
src/app/molecules/page.tsx
Normal file
43
src/app/molecules/page.tsx
Normal file
@@ -0,0 +1,43 @@
|
||||
'use client';
|
||||
|
||||
import { motion } from 'framer-motion';
|
||||
import { MoleculesSection } from '@/components/molecules/MoleculesSection';
|
||||
import type { Snippet } from '@/lib/types';
|
||||
import { useCallback } from 'react';
|
||||
import { toast } from 'sonner';
|
||||
import { createSnippet } from '@/lib/db';
|
||||
import { PageLayout } from '../PageLayout';
|
||||
|
||||
export default function MoleculesPage() {
|
||||
const handleSaveSnippet = useCallback(async (snippetData: Omit<Snippet, 'id' | 'createdAt' | 'updatedAt'>) => {
|
||||
try {
|
||||
const newSnippet: Snippet = {
|
||||
...snippetData,
|
||||
id: Date.now().toString(),
|
||||
createdAt: Date.now(),
|
||||
updatedAt: Date.now(),
|
||||
};
|
||||
await createSnippet(newSnippet);
|
||||
toast.success('Component saved as snippet!');
|
||||
} catch (error) {
|
||||
console.error('Failed to save snippet:', error);
|
||||
toast.error('Failed to save snippet');
|
||||
}
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<PageLayout>
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.4 }}
|
||||
>
|
||||
<div className="mb-8">
|
||||
<h2 className="text-3xl font-bold tracking-tight mb-2">Molecules</h2>
|
||||
<p className="text-muted-foreground">Simple combinations of atoms that work together as functional units</p>
|
||||
</div>
|
||||
<MoleculesSection onSaveSnippet={handleSaveSnippet} />
|
||||
</motion.div>
|
||||
</PageLayout>
|
||||
);
|
||||
}
|
||||
43
src/app/organisms/page.tsx
Normal file
43
src/app/organisms/page.tsx
Normal file
@@ -0,0 +1,43 @@
|
||||
'use client';
|
||||
|
||||
import { motion } from 'framer-motion';
|
||||
import { OrganismsSection } from '@/components/organisms/OrganismsSection';
|
||||
import type { Snippet } from '@/lib/types';
|
||||
import { useCallback } from 'react';
|
||||
import { toast } from 'sonner';
|
||||
import { createSnippet } from '@/lib/db';
|
||||
import { PageLayout } from '../PageLayout';
|
||||
|
||||
export default function OrganismsPage() {
|
||||
const handleSaveSnippet = useCallback(async (snippetData: Omit<Snippet, 'id' | 'createdAt' | 'updatedAt'>) => {
|
||||
try {
|
||||
const newSnippet: Snippet = {
|
||||
...snippetData,
|
||||
id: Date.now().toString(),
|
||||
createdAt: Date.now(),
|
||||
updatedAt: Date.now(),
|
||||
};
|
||||
await createSnippet(newSnippet);
|
||||
toast.success('Component saved as snippet!');
|
||||
} catch (error) {
|
||||
console.error('Failed to save snippet:', error);
|
||||
toast.error('Failed to save snippet');
|
||||
}
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<PageLayout>
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.4 }}
|
||||
>
|
||||
<div className="mb-8">
|
||||
<h2 className="text-3xl font-bold tracking-tight mb-2">Organisms</h2>
|
||||
<p className="text-muted-foreground">Complex UI components composed of molecules and atoms</p>
|
||||
</div>
|
||||
<OrganismsSection onSaveSnippet={handleSaveSnippet} />
|
||||
</motion.div>
|
||||
</PageLayout>
|
||||
);
|
||||
}
|
||||
23
src/app/page.tsx
Normal file
23
src/app/page.tsx
Normal file
@@ -0,0 +1,23 @@
|
||||
'use client';
|
||||
|
||||
import { motion } from 'framer-motion';
|
||||
import { SnippetManagerRedux } from '@/components/SnippetManagerRedux';
|
||||
import { PageLayout } from './PageLayout';
|
||||
|
||||
export default function HomePage() {
|
||||
return (
|
||||
<PageLayout>
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.4 }}
|
||||
>
|
||||
<div className="mb-8">
|
||||
<h2 className="text-3xl font-bold tracking-tight mb-2">My Snippets</h2>
|
||||
<p className="text-muted-foreground">Save, organize, and share your code snippets</p>
|
||||
</div>
|
||||
<SnippetManagerRedux />
|
||||
</motion.div>
|
||||
</PageLayout>
|
||||
);
|
||||
}
|
||||
42
src/app/providers.tsx
Normal file
42
src/app/providers.tsx
Normal file
@@ -0,0 +1,42 @@
|
||||
'use client';
|
||||
|
||||
import '@github/spark/spark';
|
||||
import { ErrorBoundary } from 'react-error-boundary';
|
||||
import { Provider } from 'react-redux';
|
||||
import { Toaster } from '@/components/ui/sonner';
|
||||
import { store } from '@/store';
|
||||
import { ErrorFallback } from '@/components/error/ErrorFallback';
|
||||
import { NavigationProvider } from '@/components/layout/navigation/NavigationProvider';
|
||||
import { useEffect } from 'react';
|
||||
import { loadStorageConfig } from '@/lib/storage';
|
||||
|
||||
const logErrorToConsole = (error: Error, info: { componentStack?: string }) => {
|
||||
console.error('Application Error:', error);
|
||||
if (info.componentStack) {
|
||||
console.error('Component Stack:', info.componentStack);
|
||||
}
|
||||
};
|
||||
|
||||
function StorageInitializer() {
|
||||
useEffect(() => {
|
||||
loadStorageConfig();
|
||||
}, []);
|
||||
return null;
|
||||
}
|
||||
|
||||
export function Providers({ children }: { children: React.ReactNode }) {
|
||||
return (
|
||||
<Provider store={store}>
|
||||
<ErrorBoundary
|
||||
FallbackComponent={ErrorFallback}
|
||||
onError={logErrorToConsole}
|
||||
>
|
||||
<NavigationProvider>
|
||||
<StorageInitializer />
|
||||
{children}
|
||||
<Toaster />
|
||||
</NavigationProvider>
|
||||
</ErrorBoundary>
|
||||
</Provider>
|
||||
);
|
||||
}
|
||||
105
src/app/settings/page.tsx
Normal file
105
src/app/settings/page.tsx
Normal file
@@ -0,0 +1,105 @@
|
||||
'use client';
|
||||
|
||||
import { motion } from 'framer-motion';
|
||||
import { PersistenceSettings } from '@/components/demo/PersistenceSettings';
|
||||
import { SchemaHealthCard } from '@/components/settings/SchemaHealthCard';
|
||||
import { BackendAutoConfigCard } from '@/components/settings/BackendAutoConfigCard';
|
||||
import { StorageBackendCard } from '@/components/settings/StorageBackendCard';
|
||||
import { DatabaseStatsCard } from '@/components/settings/DatabaseStatsCard';
|
||||
import { StorageInfoCard } from '@/components/settings/StorageInfoCard';
|
||||
import { DatabaseActionsCard } from '@/components/settings/DatabaseActionsCard';
|
||||
import { useSettingsState } from '@/hooks/useSettingsState';
|
||||
import { PageLayout } from '../PageLayout';
|
||||
|
||||
export default function SettingsPage() {
|
||||
const {
|
||||
stats,
|
||||
loading,
|
||||
storageBackend,
|
||||
setStorageBackend,
|
||||
flaskUrl,
|
||||
setFlaskUrl,
|
||||
flaskConnectionStatus,
|
||||
setFlaskConnectionStatus,
|
||||
testingConnection,
|
||||
envVarSet,
|
||||
schemaHealth,
|
||||
checkingSchema,
|
||||
handleExport,
|
||||
handleImport,
|
||||
handleClear,
|
||||
handleSeed,
|
||||
formatBytes,
|
||||
handleTestConnection,
|
||||
handleSaveStorageConfig,
|
||||
handleMigrateToFlask,
|
||||
handleMigrateToIndexedDB,
|
||||
checkSchemaHealth,
|
||||
} = useSettingsState();
|
||||
|
||||
return (
|
||||
<PageLayout>
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.4 }}
|
||||
>
|
||||
<div className="mb-8">
|
||||
<h2 className="text-3xl font-bold tracking-tight mb-2">Settings</h2>
|
||||
<p className="text-muted-foreground">Manage your database and application settings</p>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-6 max-w-3xl">
|
||||
<PersistenceSettings />
|
||||
|
||||
<SchemaHealthCard
|
||||
schemaHealth={schemaHealth}
|
||||
checkingSchema={checkingSchema}
|
||||
onClear={handleClear}
|
||||
onCheckSchema={checkSchemaHealth}
|
||||
/>
|
||||
|
||||
<BackendAutoConfigCard
|
||||
envVarSet={envVarSet}
|
||||
flaskUrl={flaskUrl}
|
||||
flaskConnectionStatus={flaskConnectionStatus}
|
||||
testingConnection={testingConnection}
|
||||
onTestConnection={handleTestConnection}
|
||||
/>
|
||||
|
||||
<StorageBackendCard
|
||||
storageBackend={storageBackend}
|
||||
flaskUrl={flaskUrl}
|
||||
flaskConnectionStatus={flaskConnectionStatus}
|
||||
testingConnection={testingConnection}
|
||||
envVarSet={envVarSet}
|
||||
onStorageBackendChange={setStorageBackend}
|
||||
onFlaskUrlChange={(url) => {
|
||||
setFlaskUrl(url);
|
||||
setFlaskConnectionStatus('unknown');
|
||||
}}
|
||||
onTestConnection={handleTestConnection}
|
||||
onSaveConfig={handleSaveStorageConfig}
|
||||
onMigrateToFlask={handleMigrateToFlask}
|
||||
onMigrateToIndexedDB={handleMigrateToIndexedDB}
|
||||
/>
|
||||
|
||||
<DatabaseStatsCard
|
||||
loading={loading}
|
||||
stats={stats}
|
||||
formatBytes={formatBytes}
|
||||
/>
|
||||
|
||||
<StorageInfoCard storageType={stats?.storageType} />
|
||||
|
||||
<DatabaseActionsCard
|
||||
onExport={handleExport}
|
||||
onImport={handleImport}
|
||||
onSeed={handleSeed}
|
||||
onClear={handleClear}
|
||||
/>
|
||||
</div>
|
||||
</motion.div>
|
||||
</PageLayout>
|
||||
);
|
||||
}
|
||||
43
src/app/templates/page.tsx
Normal file
43
src/app/templates/page.tsx
Normal file
@@ -0,0 +1,43 @@
|
||||
'use client';
|
||||
|
||||
import { motion } from 'framer-motion';
|
||||
import { TemplatesSection } from '@/components/templates/TemplatesSection';
|
||||
import type { Snippet } from '@/lib/types';
|
||||
import { useCallback } from 'react';
|
||||
import { toast } from 'sonner';
|
||||
import { createSnippet } from '@/lib/db';
|
||||
import { PageLayout } from '../PageLayout';
|
||||
|
||||
export default function TemplatesPage() {
|
||||
const handleSaveSnippet = useCallback(async (snippetData: Omit<Snippet, 'id' | 'createdAt' | 'updatedAt'>) => {
|
||||
try {
|
||||
const newSnippet: Snippet = {
|
||||
...snippetData,
|
||||
id: Date.now().toString(),
|
||||
createdAt: Date.now(),
|
||||
updatedAt: Date.now(),
|
||||
};
|
||||
await createSnippet(newSnippet);
|
||||
toast.success('Component saved as snippet!');
|
||||
} catch (error) {
|
||||
console.error('Failed to save snippet:', error);
|
||||
toast.error('Failed to save snippet');
|
||||
}
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<PageLayout>
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.4 }}
|
||||
>
|
||||
<div className="mb-8">
|
||||
<h2 className="text-3xl font-bold tracking-tight mb-2">Templates</h2>
|
||||
<p className="text-muted-foreground">Page-level layouts that combine organisms into complete interfaces</p>
|
||||
</div>
|
||||
<TemplatesSection onSaveSnippet={handleSaveSnippet} />
|
||||
</motion.div>
|
||||
</PageLayout>
|
||||
);
|
||||
}
|
||||
262
src/app/theme.css
Normal file
262
src/app/theme.css
Normal file
@@ -0,0 +1,262 @@
|
||||
@import "tailwindcss";
|
||||
|
||||
@import '@radix-ui/colors/sage-dark.css' layer(base);
|
||||
@import '@radix-ui/colors/olive.css' layer(base);
|
||||
@import '@radix-ui/colors/olive-dark.css' layer(base);
|
||||
@import '@radix-ui/colors/sand.css' layer(base);
|
||||
@import '@radix-ui/colors/sand-dark.css' layer(base);
|
||||
@import '@radix-ui/colors/red.css' layer(base);
|
||||
@import '@radix-ui/colors/red-dark.css' layer(base);
|
||||
@import '@radix-ui/colors/ruby.css' layer(base);
|
||||
@import '@radix-ui/colors/ruby-dark.css' layer(base);
|
||||
@import '@radix-ui/colors/crimson.css' layer(base);
|
||||
@import '@radix-ui/colors/crimson-dark.css' layer(base);
|
||||
@import '@radix-ui/colors/pink.css' layer(base);
|
||||
@import '@radix-ui/colors/pink-dark.css' layer(base);
|
||||
@import '@radix-ui/colors/plum.css' layer(base);
|
||||
@import '@radix-ui/colors/plum-dark.css' layer(base);
|
||||
@import '@radix-ui/colors/purple.css' layer(base);
|
||||
@import '@radix-ui/colors/purple-dark.css' layer(base);
|
||||
@import '@radix-ui/colors/violet.css' layer(base);
|
||||
@import '@radix-ui/colors/violet-dark.css' layer(base);
|
||||
@import '@radix-ui/colors/iris.css' layer(base);
|
||||
@import '@radix-ui/colors/iris-dark.css' layer(base);
|
||||
@import '@radix-ui/colors/indigo.css' layer(base);
|
||||
@import '@radix-ui/colors/indigo-dark.css' layer(base);
|
||||
@import '@radix-ui/colors/blue.css' layer(base);
|
||||
@import '@radix-ui/colors/blue-dark.css' layer(base);
|
||||
@import '@radix-ui/colors/cyan.css' layer(base);
|
||||
@import '@radix-ui/colors/cyan-dark.css' layer(base);
|
||||
@import '@radix-ui/colors/teal.css' layer(base);
|
||||
@import '@radix-ui/colors/teal-dark.css' layer(base);
|
||||
@import '@radix-ui/colors/jade.css' layer(base);
|
||||
@import '@radix-ui/colors/jade-dark.css' layer(base);
|
||||
@import '@radix-ui/colors/green.css' layer(base);
|
||||
@import '@radix-ui/colors/green-dark.css' layer(base);
|
||||
@import '@radix-ui/colors/grass.css' layer(base);
|
||||
@import '@radix-ui/colors/grass-dark.css' layer(base);
|
||||
@import '@radix-ui/colors/bronze.css' layer(base);
|
||||
@import '@radix-ui/colors/bronze-dark.css' layer(base);
|
||||
@import '@radix-ui/colors/gold.css' layer(base);
|
||||
@import '@radix-ui/colors/gold-dark.css' layer(base);
|
||||
@import '@radix-ui/colors/brown.css' layer(base);
|
||||
@import '@radix-ui/colors/brown-dark.css' layer(base);
|
||||
@import '@radix-ui/colors/orange.css' layer(base);
|
||||
@import '@radix-ui/colors/orange-dark.css' layer(base);
|
||||
@import '@radix-ui/colors/amber.css' layer(base);
|
||||
@import '@radix-ui/colors/amber-dark.css' layer(base);
|
||||
@import '@radix-ui/colors/yellow.css' layer(base);
|
||||
@import '@radix-ui/colors/yellow-dark.css' layer(base);
|
||||
@import '@radix-ui/colors/lime.css' layer(base);
|
||||
@import '@radix-ui/colors/lime-dark.css' layer(base);
|
||||
@import '@radix-ui/colors/mint.css' layer(base);
|
||||
@import '@radix-ui/colors/mint-dark.css' layer(base);
|
||||
@import '@radix-ui/colors/sky.css' layer(base);
|
||||
@import '@radix-ui/colors/sky-dark.css' layer(base);
|
||||
@import '@radix-ui/colors/tomato.css' layer(base);
|
||||
@import '@radix-ui/colors/tomato-dark.css' layer(base);
|
||||
@import '@radix-ui/colors/gray.css' layer(base);
|
||||
@import '@radix-ui/colors/gray-dark.css' layer(base);
|
||||
@import '@radix-ui/colors/mauve.css' layer(base);
|
||||
@import '@radix-ui/colors/mauve-dark.css' layer(base);
|
||||
@import '@radix-ui/colors/slate.css' layer(base);
|
||||
@import '@radix-ui/colors/slate-dark.css' layer(base);
|
||||
@import '@radix-ui/colors/slate-alpha.css' layer(base);
|
||||
@import '@radix-ui/colors/slate-dark-alpha.css' layer(base);
|
||||
|
||||
@import 'tailwindcss/theme' layer(theme);
|
||||
|
||||
@import 'tailwindcss/preflight' layer(base);
|
||||
|
||||
/*
|
||||
The default border color has changed to `currentColor` in Tailwind CSS v4,
|
||||
so we've added these compatibility styles to make sure everything still
|
||||
looks the same as it did with Tailwind CSS v3.
|
||||
|
||||
If we ever want to remove these styles, we need to add an explicit border
|
||||
color utility to any element that depends on these defaults.
|
||||
*/
|
||||
@layer base {
|
||||
*,
|
||||
::after,
|
||||
::before,
|
||||
::backdrop,
|
||||
::file-selector-button {
|
||||
border-color: var(--color-gray-200, currentColor);
|
||||
}
|
||||
}
|
||||
|
||||
@layer base {
|
||||
#spark-app {
|
||||
--tomato-contrast: #fff;
|
||||
--red-contrast: #fff;
|
||||
--ruby-contrast: #fff;
|
||||
--crimson-contrast: #fff;
|
||||
--pink-contrast: #fff;
|
||||
--plum-contrast: #fff;
|
||||
--purple-contrast: #fff;
|
||||
--violet-contrast: #fff;
|
||||
--iris-contrast: #fff;
|
||||
--indigo-contrast: #fff;
|
||||
--blue-contrast: #fff;
|
||||
--cyan-contrast: #fff;
|
||||
--teal-contrast: #fff;
|
||||
--jade-contrast: #fff;
|
||||
--green-contrast: #fff;
|
||||
--grass-contrast: #fff;
|
||||
--bronze-contrast: #fff;
|
||||
--gold-contrast: #fff;
|
||||
--brown-contrast: #fff;
|
||||
--orange-contrast: #fff;
|
||||
--amber-contrast: #000;
|
||||
--yellow-contrast: #000;
|
||||
--lime-contrast: #000;
|
||||
--mint-contrast: #000;
|
||||
--sky-contrast: #000;
|
||||
--gray-contrast: #fff;
|
||||
--mauve-contrast: #fff;
|
||||
--slate-contrast: #fff;
|
||||
--sage-contrast: #fff;
|
||||
--olive-contrast: #fff;
|
||||
--sand-contrast: #fff;
|
||||
|
||||
/**
|
||||
* Spacing scale
|
||||
*
|
||||
* These variables define a spacing scale based on Tailwind's default.
|
||||
* We've introduced a --size-scale variable as a multiplier.
|
||||
* By adjusting this single value, we can proportionally
|
||||
* scale all spacing throughout the entire application.
|
||||
*
|
||||
* https://tailwindcss.com/docs/customizing-spacing#default-spacing-scale
|
||||
*/
|
||||
--size-scale: 1;
|
||||
--size-0: 0px;
|
||||
--size-px: 1px;
|
||||
--size-0-5: calc(0.125rem * var(--size-scale));
|
||||
--size-1: calc(0.25rem * var(--size-scale));
|
||||
--size-1-5: calc(0.375rem * var(--size-scale));
|
||||
--size-2: calc(0.5rem * var(--size-scale));
|
||||
--size-2-5: calc(0.625rem * var(--size-scale));
|
||||
--size-3: calc(0.75rem * var(--size-scale));
|
||||
--size-3-5: calc(0.875rem * var(--size-scale));
|
||||
--size-4: calc(1rem * var(--size-scale));
|
||||
--size-5: calc(1.25rem * var(--size-scale));
|
||||
--size-6: calc(1.5rem * var(--size-scale));
|
||||
--size-7: calc(1.75rem * var(--size-scale));
|
||||
--size-8: calc(2rem * var(--size-scale));
|
||||
--size-9: calc(2.25rem * var(--size-scale));
|
||||
--size-10: calc(2.5rem * var(--size-scale));
|
||||
--size-11: calc(2.75rem * var(--size-scale));
|
||||
--size-12: calc(3rem * var(--size-scale));
|
||||
--size-14: calc(3.5rem * var(--size-scale));
|
||||
--size-16: calc(4rem * var(--size-scale));
|
||||
--size-20: calc(5rem * var(--size-scale));
|
||||
--size-24: calc(6rem * var(--size-scale));
|
||||
--size-28: calc(7rem * var(--size-scale));
|
||||
--size-32: calc(8rem * var(--size-scale));
|
||||
--size-36: calc(9rem * var(--size-scale));
|
||||
--size-40: calc(10rem * var(--size-scale));
|
||||
--size-44: calc(11rem * var(--size-scale));
|
||||
--size-48: calc(12rem * var(--size-scale));
|
||||
--size-52: calc(13rem * var(--size-scale));
|
||||
--size-56: calc(14rem * var(--size-scale));
|
||||
--size-60: calc(15rem * var(--size-scale));
|
||||
--size-64: calc(16rem * var(--size-scale));
|
||||
--size-72: calc(18rem * var(--size-scale));
|
||||
--size-80: calc(20rem * var(--size-scale));
|
||||
--size-96: calc(24rem * var(--size-scale));
|
||||
|
||||
/* Border radii */
|
||||
--radius-factor: 1;
|
||||
--radius-sm: calc(2px * var(--radius-factor) * var(--size-scale));
|
||||
--radius-md: calc(6px * var(--radius-factor) * var(--size-scale));
|
||||
--radius-lg: calc(8px * var(--radius-factor) * var(--size-scale));
|
||||
--radius-xl: calc(12px * var(--radius-factor) * var(--size-scale));
|
||||
--radius-2xl: calc(16px * var(--radius-factor) * var(--size-scale));
|
||||
--radius-full: 9999px;
|
||||
|
||||
/* Neutral colors */
|
||||
--color-neutral-1: var(--slate-1);
|
||||
--color-neutral-2: var(--slate-2);
|
||||
--color-neutral-3: var(--slate-3);
|
||||
--color-neutral-4: var(--slate-4);
|
||||
--color-neutral-5: var(--slate-5);
|
||||
--color-neutral-6: var(--slate-6);
|
||||
--color-neutral-7: var(--slate-7);
|
||||
--color-neutral-8: var(--slate-8);
|
||||
--color-neutral-9: var(--slate-9);
|
||||
--color-neutral-10: var(--slate-10);
|
||||
--color-neutral-11: var(--slate-11);
|
||||
--color-neutral-12: var(--slate-12);
|
||||
--color-neutral-a1: var(--slate-a1);
|
||||
--color-neutral-a2: var(--slate-a2);
|
||||
--color-neutral-a3: var(--slate-a3);
|
||||
--color-neutral-a4: var(--slate-a4);
|
||||
--color-neutral-a5: var(--slate-a5);
|
||||
--color-neutral-a6: var(--slate-a6);
|
||||
--color-neutral-a7: var(--slate-a7);
|
||||
--color-neutral-a8: var(--slate-a8);
|
||||
--color-neutral-a9: var(--slate-a9);
|
||||
--color-neutral-a10: var(--slate-a10);
|
||||
--color-neutral-a11: var(--slate-a11);
|
||||
--color-neutral-a12: var(--slate-a12);
|
||||
--color-neutral-contrast: var(--slate-contrast);
|
||||
|
||||
/* Accent colors */
|
||||
--color-accent-1: var(--blue-1);
|
||||
--color-accent-2: var(--blue-2);
|
||||
--color-accent-3: var(--blue-3);
|
||||
--color-accent-4: var(--blue-4);
|
||||
--color-accent-5: var(--blue-5);
|
||||
--color-accent-6: var(--blue-6);
|
||||
--color-accent-7: var(--blue-7);
|
||||
--color-accent-8: var(--blue-8);
|
||||
--color-accent-9: var(--blue-9);
|
||||
--color-accent-10: var(--blue-10);
|
||||
--color-accent-11: var(--blue-11);
|
||||
--color-accent-12: var(--blue-12);
|
||||
--color-accent-contrast: var(--blue-contrast);
|
||||
|
||||
/* Secondary accent colors */
|
||||
--color-accent-secondary-1: var(--violet-1);
|
||||
--color-accent-secondary-2: var(--violet-2);
|
||||
--color-accent-secondary-3: var(--violet-3);
|
||||
--color-accent-secondary-4: var(--violet-4);
|
||||
--color-accent-secondary-5: var(--violet-5);
|
||||
--color-accent-secondary-6: var(--violet-6);
|
||||
--color-accent-secondary-7: var(--violet-7);
|
||||
--color-accent-secondary-8: var(--violet-8);
|
||||
--color-accent-secondary-9: var(--violet-9);
|
||||
--color-accent-secondary-10: var(--violet-10);
|
||||
--color-accent-secondary-11: var(--violet-11);
|
||||
--color-accent-secondary-12: var(--violet-12);
|
||||
--color-accent-secondary-contrast: var(--violet-contrast);
|
||||
|
||||
/* Foreground colors */
|
||||
--color-fg: var(--color-neutral-12);
|
||||
--color-fg-secondary: var(--color-neutral-a11);
|
||||
|
||||
/* Background colors */
|
||||
--color-bg: #ffffff;
|
||||
--color-bg-inset: var(--color-neutral-2);
|
||||
--color-bg-overlay: #ffffff;
|
||||
|
||||
/* Focus ring */
|
||||
--color-focus-ring: var(--color-accent-9);
|
||||
|
||||
/* Fonts */
|
||||
--font-sans-serif: ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji",
|
||||
"Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
|
||||
--font-serif: ui-serif, Georgia, Cambria, "Times New Roman", Times, serif;
|
||||
--font-monospace: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas,
|
||||
"Liberation Mono", "Courier New", monospace;
|
||||
--font-family: var(--font-sans-serif);
|
||||
}
|
||||
|
||||
#spark-app.dark-theme {
|
||||
--color-bg: var(--color-neutral-1);
|
||||
--color-bg-inset: #000000;
|
||||
--color-bg-overlay: var(--color-neutral-3);
|
||||
}
|
||||
}
|
||||
@@ -59,7 +59,13 @@ export function NamespaceSelector({ selectedNamespaceId, onNamespaceChange }: Na
|
||||
|
||||
setLoading(true)
|
||||
try {
|
||||
const newNamespace = await createNamespace(newNamespaceName.trim())
|
||||
const newNamespace: Namespace = {
|
||||
id: Date.now().toString(),
|
||||
name: newNamespaceName.trim(),
|
||||
createdAt: Date.now(),
|
||||
isDefault: false,
|
||||
}
|
||||
await createNamespace(newNamespace)
|
||||
setNamespaces(prev => [...prev, newNamespace])
|
||||
setNewNamespaceName('')
|
||||
setCreateDialogOpen(false)
|
||||
|
||||
@@ -34,7 +34,7 @@ export function SnippetViewer({ snippet, open, onOpenChange, onEdit, onCopy }: S
|
||||
onEdit(snippet)
|
||||
}
|
||||
|
||||
const canPreview = snippet.hasPreview && appConfig.previewEnabledLanguages.includes(snippet.language)
|
||||
const canPreview = !!(snippet.hasPreview && appConfig.previewEnabledLanguages.includes(snippet.language))
|
||||
const isPython = snippet.language === 'Python'
|
||||
|
||||
return (
|
||||
|
||||
@@ -10,7 +10,7 @@ export function BackendIndicator() {
|
||||
useEffect(() => {
|
||||
const config = getStorageConfig()
|
||||
setBackend(config.backend)
|
||||
setIsEnvConfigured(Boolean(import.meta.env.VITE_FLASK_BACKEND_URL))
|
||||
setIsEnvConfigured(Boolean(process.env.NEXT_PUBLIC_FLASK_BACKEND_URL))
|
||||
}, [])
|
||||
|
||||
if (backend === 'indexeddb') {
|
||||
|
||||
@@ -1,14 +1,17 @@
|
||||
import { motion } from 'framer-motion'
|
||||
import { Link, useLocation } from 'react-router-dom'
|
||||
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'
|
||||
'use client';
|
||||
|
||||
import { motion } from 'framer-motion';
|
||||
import Link from 'next/link';
|
||||
import { usePathname } from 'next/navigation';
|
||||
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()
|
||||
const location = useLocation()
|
||||
const { menuOpen, setMenuOpen } = useNavigation();
|
||||
const pathname = usePathname();
|
||||
|
||||
return (
|
||||
<motion.aside
|
||||
@@ -31,11 +34,11 @@ export function NavigationSidebar() {
|
||||
<nav className="p-4">
|
||||
<ul className="space-y-2">
|
||||
{navigationItems.map((item) => {
|
||||
const Icon = item.icon
|
||||
const isActive = location.pathname === item.path
|
||||
const Icon = item.icon;
|
||||
const isActive = pathname === item.path;
|
||||
return (
|
||||
<li key={item.path}>
|
||||
<Link to={item.path}>
|
||||
<Link href={item.path}>
|
||||
<Button
|
||||
variant={isActive ? 'secondary' : 'ghost'}
|
||||
className={cn(
|
||||
@@ -48,7 +51,7 @@ export function NavigationSidebar() {
|
||||
</Button>
|
||||
</Link>
|
||||
</li>
|
||||
)
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
</nav>
|
||||
@@ -60,5 +63,5 @@ export function NavigationSidebar() {
|
||||
</div>
|
||||
</div>
|
||||
</motion.aside>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -38,7 +38,7 @@ export function BackendAutoConfigCard({
|
||||
</div>
|
||||
<div className="flex items-center justify-between py-2">
|
||||
<span className="text-sm text-muted-foreground">Configuration Source</span>
|
||||
<code className="text-sm font-mono bg-muted px-2 py-1 rounded">VITE_FLASK_BACKEND_URL</code>
|
||||
<code className="text-sm font-mono bg-muted px-2 py-1 rounded">NEXT_PUBLIC_FLASK_BACKEND_URL</code>
|
||||
</div>
|
||||
<div className="flex items-center justify-between py-2">
|
||||
<span className="text-sm text-muted-foreground">Status</span>
|
||||
|
||||
@@ -51,7 +51,7 @@ export function StorageBackendCard({
|
||||
<AlertDescription className="flex items-center gap-2">
|
||||
<CloudCheck weight="fill" size={16} className="text-accent" />
|
||||
<span>
|
||||
Storage backend is configured via <code className="px-1.5 py-0.5 rounded bg-muted text-xs font-mono">VITE_FLASK_BACKEND_URL</code> environment variable and cannot be changed here.
|
||||
Storage backend is configured via <code className="px-1.5 py-0.5 rounded bg-muted text-xs font-mono">NEXT_PUBLIC_FLASK_BACKEND_URL</code> environment variable and cannot be changed here.
|
||||
</span>
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
|
||||
@@ -37,11 +37,7 @@ export function useDatabaseOperations() {
|
||||
setCheckingSchema(true)
|
||||
try {
|
||||
const result = await validateDatabaseSchema()
|
||||
setSchemaHealth(result.valid ? 'healthy' : 'corrupted')
|
||||
|
||||
if (!result.valid) {
|
||||
console.warn('Schema validation failed:', result.issues)
|
||||
}
|
||||
setSchemaHealth(result ? 'healthy' : 'corrupted')
|
||||
} catch (error) {
|
||||
console.error('Schema check failed:', error)
|
||||
setSchemaHealth('corrupted')
|
||||
@@ -52,12 +48,12 @@ export function useDatabaseOperations() {
|
||||
|
||||
const handleExport = useCallback(async () => {
|
||||
try {
|
||||
const data = await exportDatabase()
|
||||
const blob = new Blob([new Uint8Array(data)], { type: 'application/octet-stream' })
|
||||
const jsonData = await exportDatabase()
|
||||
const blob = new Blob([jsonData], { type: 'application/json' })
|
||||
const url = URL.createObjectURL(blob)
|
||||
const a = document.createElement('a')
|
||||
a.href = url
|
||||
a.download = `codesnippet-backup-${Date.now()}.db`
|
||||
a.download = `codesnippet-backup-${Date.now()}.json`
|
||||
document.body.appendChild(a)
|
||||
a.click()
|
||||
document.body.removeChild(a)
|
||||
@@ -74,9 +70,8 @@ export function useDatabaseOperations() {
|
||||
if (!file) return
|
||||
|
||||
try {
|
||||
const arrayBuffer = await file.arrayBuffer()
|
||||
const data = new Uint8Array(arrayBuffer)
|
||||
await importDatabase(data)
|
||||
const text = await file.text()
|
||||
await importDatabase(text)
|
||||
toast.success('Database imported successfully')
|
||||
await loadStats()
|
||||
} catch (error) {
|
||||
|
||||
@@ -51,6 +51,10 @@ export function useSettingsState() {
|
||||
await migrateToFlask(flaskUrl, loadStats)
|
||||
}
|
||||
|
||||
const handleMigrateToIndexedDBWrapper = async () => {
|
||||
await handleMigrateToIndexedDB(flaskUrl)
|
||||
}
|
||||
|
||||
return {
|
||||
stats,
|
||||
loading,
|
||||
@@ -72,7 +76,7 @@ export function useSettingsState() {
|
||||
handleTestConnection,
|
||||
handleSaveStorageConfig,
|
||||
handleMigrateToFlask,
|
||||
handleMigrateToIndexedDB,
|
||||
handleMigrateToIndexedDB: handleMigrateToIndexedDBWrapper,
|
||||
checkSchemaHealth,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,7 +32,7 @@ export function useStorageConfig() {
|
||||
|
||||
const loadConfig = useCallback(() => {
|
||||
const config = loadStorageConfig()
|
||||
const envFlaskUrl = import.meta.env.VITE_FLASK_BACKEND_URL
|
||||
const envFlaskUrl = process.env.NEXT_PUBLIC_FLASK_BACKEND_URL
|
||||
const isEnvSet = Boolean(envFlaskUrl)
|
||||
|
||||
setEnvVarSet(isEnvSet)
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
/**
|
||||
* Database constants shared across modules
|
||||
*/
|
||||
|
||||
export const DB_KEY = 'codesnippet-db'
|
||||
export const IDB_NAME = 'CodeSnippetDB'
|
||||
export const IDB_STORE = 'database'
|
||||
export const IDB_VERSION = 1
|
||||
@@ -1,19 +0,0 @@
|
||||
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()
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
import { initDB } from './initDB'
|
||||
|
||||
export async function exportDatabase(): Promise<Uint8Array> {
|
||||
const db = await initDB()
|
||||
return db.export()
|
||||
}
|
||||
@@ -1,32 +0,0 @@
|
||||
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,
|
||||
}
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
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
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
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
|
||||
}
|
||||
}
|
||||
@@ -1,57 +0,0 @@
|
||||
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
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
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,
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
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,100 +0,0 @@
|
||||
/**
|
||||
* IndexedDB operations for database persistence
|
||||
*/
|
||||
|
||||
import { DB_KEY, IDB_NAME, IDB_STORE, IDB_VERSION } from './db-constants'
|
||||
|
||||
export async function openIndexedDB(): Promise<IDBDatabase | null> {
|
||||
if (typeof indexedDB === 'undefined') return null
|
||||
|
||||
return new Promise((resolve) => {
|
||||
try {
|
||||
const request = indexedDB.open(IDB_NAME, IDB_VERSION)
|
||||
|
||||
request.onerror = () => {
|
||||
console.warn('IndexedDB not available, falling back to localStorage')
|
||||
resolve(null)
|
||||
}
|
||||
|
||||
request.onupgradeneeded = (event) => {
|
||||
const db = (event.target as IDBOpenDBRequest).result
|
||||
if (!db.objectStoreNames.contains(IDB_STORE)) {
|
||||
db.createObjectStore(IDB_STORE)
|
||||
}
|
||||
}
|
||||
|
||||
request.onsuccess = (event) => {
|
||||
resolve((event.target as IDBOpenDBRequest).result)
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('IndexedDB error:', error)
|
||||
resolve(null)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
export async function loadFromIndexedDB(): Promise<Uint8Array | null> {
|
||||
const db = await openIndexedDB()
|
||||
if (!db) return null
|
||||
|
||||
return new Promise((resolve) => {
|
||||
try {
|
||||
const transaction = db.transaction([IDB_STORE], 'readonly')
|
||||
const store = transaction.objectStore(IDB_STORE)
|
||||
const request = store.get(DB_KEY)
|
||||
|
||||
request.onsuccess = () => {
|
||||
const data = request.result
|
||||
resolve(data ? new Uint8Array(data) : null)
|
||||
}
|
||||
|
||||
request.onerror = () => {
|
||||
console.warn('Failed to load from IndexedDB')
|
||||
resolve(null)
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('IndexedDB read error:', error)
|
||||
resolve(null)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
export async function saveToIndexedDB(data: Uint8Array): Promise<boolean> {
|
||||
const db = await openIndexedDB()
|
||||
if (!db) return false
|
||||
|
||||
return new Promise((resolve) => {
|
||||
try {
|
||||
const transaction = db.transaction([IDB_STORE], 'readwrite')
|
||||
const store = transaction.objectStore(IDB_STORE)
|
||||
const request = store.put(data, DB_KEY)
|
||||
|
||||
request.onsuccess = () => resolve(true)
|
||||
request.onerror = () => {
|
||||
console.warn('Failed to save to IndexedDB')
|
||||
resolve(false)
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('IndexedDB write error:', error)
|
||||
resolve(false)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
export async function deleteFromIndexedDB(): Promise<void> {
|
||||
const db = await openIndexedDB()
|
||||
if (!db) return
|
||||
|
||||
return new Promise((resolve) => {
|
||||
try {
|
||||
const transaction = db.transaction([IDB_STORE], 'readwrite')
|
||||
const store = transaction.objectStore(IDB_STORE)
|
||||
const request = store.delete(DB_KEY)
|
||||
request.onsuccess = () => resolve()
|
||||
request.onerror = () => resolve()
|
||||
} catch (error) {
|
||||
console.warn('Error clearing IndexedDB:', error)
|
||||
resolve()
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -1,36 +0,0 @@
|
||||
/**
|
||||
* LocalStorage operations for database persistence
|
||||
*/
|
||||
|
||||
import { DB_KEY } from './db-constants'
|
||||
|
||||
export function loadFromLocalStorage(): Uint8Array | null {
|
||||
try {
|
||||
const savedData = localStorage.getItem(DB_KEY)
|
||||
if (savedData) {
|
||||
return new Uint8Array(JSON.parse(savedData))
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('Failed to load from localStorage:', error)
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
export function saveToLocalStorage(data: Uint8Array): boolean {
|
||||
try {
|
||||
const dataArray = Array.from(data)
|
||||
localStorage.setItem(DB_KEY, JSON.stringify(dataArray))
|
||||
return true
|
||||
} catch (error) {
|
||||
console.warn('Failed to save to localStorage (quota exceeded?):', error)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
export function deleteFromLocalStorage(): void {
|
||||
try {
|
||||
localStorage.removeItem(DB_KEY)
|
||||
} catch (error) {
|
||||
console.warn('Error clearing localStorage:', error)
|
||||
}
|
||||
}
|
||||
@@ -1,52 +0,0 @@
|
||||
/**
|
||||
* Database row-to-object mapping utilities
|
||||
* Handles conversion of SQL query results to typed objects
|
||||
*/
|
||||
|
||||
/**
|
||||
* Maps a SQL query result row to a typed object
|
||||
* Handles special conversions for boolean and JSON fields
|
||||
*/
|
||||
export function mapRowToObject<T>(row: any[], columns: string[]): T {
|
||||
const obj: any = {}
|
||||
|
||||
columns.forEach((col, idx) => {
|
||||
const value = row[idx]
|
||||
|
||||
// Convert integer boolean fields to actual booleans
|
||||
if (col === 'hasPreview' || col === 'isDefault') {
|
||||
obj[col] = value === 1
|
||||
}
|
||||
// Parse JSON string fields with error handling
|
||||
else if (col === 'inputParameters') {
|
||||
if (value) {
|
||||
try {
|
||||
obj[col] = JSON.parse(value as string)
|
||||
} catch (error) {
|
||||
console.warn(`Failed to parse JSON for ${col}:`, error)
|
||||
obj[col] = undefined
|
||||
}
|
||||
} else {
|
||||
obj[col] = undefined
|
||||
}
|
||||
}
|
||||
// All other fields pass through as-is
|
||||
else {
|
||||
obj[col] = value
|
||||
}
|
||||
})
|
||||
|
||||
return obj as T
|
||||
}
|
||||
|
||||
/**
|
||||
* Maps multiple SQL result rows to an array of typed objects
|
||||
*/
|
||||
export function mapRowsToObjects<T>(results: any[]): T[] {
|
||||
if (results.length === 0) return []
|
||||
|
||||
const columns = results[0].columns
|
||||
const values = results[0].values
|
||||
|
||||
return values.map(row => mapRowToObject<T>(row, columns))
|
||||
}
|
||||
@@ -1,30 +0,0 @@
|
||||
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
|
||||
}
|
||||
@@ -1,30 +0,0 @@
|
||||
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()
|
||||
}
|
||||
@@ -1,27 +0,0 @@
|
||||
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()
|
||||
}
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
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)
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
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,106 +0,0 @@
|
||||
/**
|
||||
* Database schema management and validation
|
||||
*/
|
||||
|
||||
import type { Database } from 'sql.js'
|
||||
|
||||
export async function validateSchema(db: Database): Promise<boolean> {
|
||||
try {
|
||||
const snippetsCheck = db.exec("PRAGMA table_info(snippets)")
|
||||
if (snippetsCheck.length === 0) return true
|
||||
|
||||
const columns = snippetsCheck[0].values.map(row => row[1] as string)
|
||||
const requiredColumns = ['id', 'title', 'code', 'language', 'category', 'namespaceId', 'createdAt', 'updatedAt']
|
||||
|
||||
for (const col of requiredColumns) {
|
||||
if (!columns.includes(col)) {
|
||||
console.warn(`Schema validation failed: missing column '${col}'`)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
const namespacesCheck = db.exec("PRAGMA table_info(namespaces)")
|
||||
if (namespacesCheck.length === 0) {
|
||||
console.warn('Schema validation failed: namespaces table missing')
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
} catch (error) {
|
||||
console.error('Schema validation error:', error)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
export function createTables(db: Database): void {
|
||||
db.run(`
|
||||
CREATE TABLE IF NOT EXISTS namespaces (
|
||||
id TEXT PRIMARY KEY,
|
||||
name TEXT NOT NULL,
|
||||
createdAt INTEGER NOT NULL,
|
||||
isDefault INTEGER DEFAULT 0
|
||||
)
|
||||
`)
|
||||
|
||||
db.run(`
|
||||
CREATE TABLE IF NOT EXISTS snippets (
|
||||
id TEXT PRIMARY KEY,
|
||||
title TEXT NOT NULL,
|
||||
description TEXT,
|
||||
code TEXT NOT NULL,
|
||||
language TEXT NOT NULL,
|
||||
category TEXT NOT NULL,
|
||||
namespaceId TEXT,
|
||||
hasPreview INTEGER DEFAULT 0,
|
||||
functionName TEXT,
|
||||
inputParameters TEXT,
|
||||
createdAt INTEGER NOT NULL,
|
||||
updatedAt INTEGER NOT NULL,
|
||||
FOREIGN KEY (namespaceId) REFERENCES namespaces(id)
|
||||
)
|
||||
`)
|
||||
|
||||
db.run(`
|
||||
CREATE TABLE IF NOT EXISTS snippet_templates (
|
||||
id TEXT PRIMARY KEY,
|
||||
title TEXT NOT NULL,
|
||||
description TEXT,
|
||||
code TEXT NOT NULL,
|
||||
language TEXT NOT NULL,
|
||||
category TEXT NOT NULL,
|
||||
hasPreview INTEGER DEFAULT 0,
|
||||
functionName TEXT,
|
||||
inputParameters TEXT
|
||||
)
|
||||
`)
|
||||
}
|
||||
|
||||
export async function validateDatabaseSchema(db: Database): Promise<{ valid: boolean; issues: string[] }> {
|
||||
try {
|
||||
const issues: string[] = []
|
||||
|
||||
const snippetsCheck = db.exec("PRAGMA table_info(snippets)")
|
||||
if (snippetsCheck.length === 0) {
|
||||
issues.push('Snippets table missing')
|
||||
return { valid: false, issues }
|
||||
}
|
||||
|
||||
const columns = snippetsCheck[0].values.map(row => row[1] as string)
|
||||
const requiredColumns = ['id', 'title', 'code', 'language', 'category', 'namespaceId', 'createdAt', 'updatedAt']
|
||||
|
||||
for (const col of requiredColumns) {
|
||||
if (!columns.includes(col)) {
|
||||
issues.push(`Missing column '${col}' in snippets table`)
|
||||
}
|
||||
}
|
||||
|
||||
const namespacesCheck = db.exec("SELECT name FROM sqlite_master WHERE type='table' AND name='namespaces'")
|
||||
if (namespacesCheck.length === 0) {
|
||||
issues.push('Namespaces table missing')
|
||||
}
|
||||
|
||||
return { valid: issues.length === 0, issues }
|
||||
} catch (error) {
|
||||
return { valid: false, issues: ['Failed to validate schema: ' + (error as Error).message] }
|
||||
}
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
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()
|
||||
}
|
||||
@@ -1,34 +0,0 @@
|
||||
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()
|
||||
}
|
||||
@@ -1,25 +0,0 @@
|
||||
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()
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
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()
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
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)
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
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)
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
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)
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
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)
|
||||
}
|
||||
@@ -1,25 +0,0 @@
|
||||
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()
|
||||
}
|
||||
@@ -1,41 +0,0 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
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++
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,34 +0,0 @@
|
||||
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()
|
||||
}
|
||||
247
src/lib/db.ts
247
src/lib/db.ts
@@ -1,38 +1,223 @@
|
||||
/**
|
||||
* Main database module - Re-exports from focused modules
|
||||
* This file maintains backward compatibility while delegating to specialized modules
|
||||
* Unified storage interface - routes to IndexedDB or Flask based on configuration
|
||||
*/
|
||||
|
||||
// Re-export core database functions
|
||||
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'
|
||||
import type { Snippet, Namespace } from './types';
|
||||
import { getStorageConfig, FlaskStorageAdapter } from './storage';
|
||||
import * as IndexedDBStorage from './indexeddb-storage';
|
||||
|
||||
// Re-export snippet operations
|
||||
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'
|
||||
// Helper to get the active storage backend
|
||||
function getActiveStorage() {
|
||||
const config = getStorageConfig();
|
||||
|
||||
if (config.backend === 'flask' && config.flaskUrl) {
|
||||
return new FlaskStorageAdapter(config.flaskUrl);
|
||||
}
|
||||
|
||||
return null; // Use IndexedDB
|
||||
}
|
||||
|
||||
// Re-export namespace operations
|
||||
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'
|
||||
// Snippet operations
|
||||
export async function getAllSnippets(): Promise<Snippet[]> {
|
||||
const flask = getActiveStorage();
|
||||
if (flask) {
|
||||
return await flask.getAllSnippets();
|
||||
}
|
||||
return await IndexedDBStorage.getAllSnippets();
|
||||
}
|
||||
|
||||
// Re-export schema validation
|
||||
export { validateDatabaseSchema } from './db-schema'
|
||||
export async function getSnippet(id: string): Promise<Snippet | null> {
|
||||
const flask = getActiveStorage();
|
||||
if (flask) {
|
||||
return await flask.getSnippet(id);
|
||||
}
|
||||
return await IndexedDBStorage.getSnippet(id);
|
||||
}
|
||||
|
||||
// Note: saveDB is intentionally not exported as it's used internally by the modules
|
||||
export async function createSnippet(snippet: Snippet): Promise<void> {
|
||||
const flask = getActiveStorage();
|
||||
if (flask) {
|
||||
return await flask.createSnippet(snippet);
|
||||
}
|
||||
return await IndexedDBStorage.createSnippet(snippet);
|
||||
}
|
||||
|
||||
export async function updateSnippet(snippet: Snippet): Promise<void> {
|
||||
const flask = getActiveStorage();
|
||||
if (flask) {
|
||||
return await flask.updateSnippet(snippet);
|
||||
}
|
||||
return await IndexedDBStorage.updateSnippet(snippet);
|
||||
}
|
||||
|
||||
export async function deleteSnippet(id: string): Promise<void> {
|
||||
const flask = getActiveStorage();
|
||||
if (flask) {
|
||||
return await flask.deleteSnippet(id);
|
||||
}
|
||||
return await IndexedDBStorage.deleteSnippet(id);
|
||||
}
|
||||
|
||||
export async function getSnippetsByNamespace(namespaceId: string): Promise<Snippet[]> {
|
||||
const flask = getActiveStorage();
|
||||
if (flask) {
|
||||
return await flask.getSnippetsByNamespace(namespaceId);
|
||||
}
|
||||
return await IndexedDBStorage.getSnippetsByNamespace(namespaceId);
|
||||
}
|
||||
|
||||
export async function moveSnippetToNamespace(snippetId: string, namespaceId: string): Promise<void> {
|
||||
const snippet = await getSnippet(snippetId);
|
||||
if (!snippet) throw new Error('Snippet not found');
|
||||
|
||||
snippet.namespaceId = namespaceId;
|
||||
snippet.updatedAt = Date.now();
|
||||
|
||||
await updateSnippet(snippet);
|
||||
}
|
||||
|
||||
export async function bulkMoveSnippets(snippetIds: string[], namespaceId: string): Promise<void> {
|
||||
for (const id of snippetIds) {
|
||||
await moveSnippetToNamespace(id, namespaceId);
|
||||
}
|
||||
}
|
||||
|
||||
export async function getAllTemplates(): Promise<Snippet[]> {
|
||||
const snippets = await getAllSnippets();
|
||||
return snippets.filter(s => s.isTemplate);
|
||||
}
|
||||
|
||||
export async function createTemplate(snippet: Omit<Snippet, 'id' | 'createdAt' | 'updatedAt'>): Promise<void> {
|
||||
const template: Snippet = {
|
||||
...snippet,
|
||||
id: Date.now().toString(),
|
||||
createdAt: Date.now(),
|
||||
updatedAt: Date.now(),
|
||||
isTemplate: true,
|
||||
};
|
||||
await createSnippet(template);
|
||||
}
|
||||
|
||||
export async function syncTemplatesFromJSON(templates: any[]): Promise<void> {
|
||||
// This would sync predefined templates - implement as needed
|
||||
console.log('Syncing templates', templates.length);
|
||||
}
|
||||
|
||||
export async function seedDatabase(): Promise<void> {
|
||||
// Seed with default namespace if needed
|
||||
const namespaces = await getAllNamespaces();
|
||||
if (namespaces.length === 0) {
|
||||
await ensureDefaultNamespace();
|
||||
}
|
||||
}
|
||||
|
||||
// Namespace operations
|
||||
export async function getAllNamespaces(): Promise<Namespace[]> {
|
||||
const flask = getActiveStorage();
|
||||
if (flask) {
|
||||
return await flask.getAllNamespaces();
|
||||
}
|
||||
return await IndexedDBStorage.getAllNamespaces();
|
||||
}
|
||||
|
||||
export async function getNamespaceById(id: string): Promise<Namespace | null> {
|
||||
const flask = getActiveStorage();
|
||||
if (flask) {
|
||||
return await flask.getNamespace(id);
|
||||
}
|
||||
return await IndexedDBStorage.getNamespace(id);
|
||||
}
|
||||
|
||||
export async function createNamespace(namespace: Namespace): Promise<void> {
|
||||
const flask = getActiveStorage();
|
||||
if (flask) {
|
||||
return await flask.createNamespace(namespace);
|
||||
}
|
||||
return await IndexedDBStorage.createNamespace(namespace);
|
||||
}
|
||||
|
||||
export async function deleteNamespace(id: string): Promise<void> {
|
||||
const flask = getActiveStorage();
|
||||
if (flask) {
|
||||
return await flask.deleteNamespace(id);
|
||||
}
|
||||
return await IndexedDBStorage.deleteNamespace(id);
|
||||
}
|
||||
|
||||
export async function ensureDefaultNamespace(): Promise<Namespace> {
|
||||
const namespaces = await getAllNamespaces();
|
||||
let defaultNs = namespaces.find(ns => ns.isDefault);
|
||||
|
||||
if (!defaultNs) {
|
||||
defaultNs = {
|
||||
id: 'default',
|
||||
name: 'Default',
|
||||
createdAt: Date.now(),
|
||||
isDefault: true,
|
||||
};
|
||||
await createNamespace(defaultNs);
|
||||
}
|
||||
|
||||
return defaultNs;
|
||||
}
|
||||
|
||||
// Database operations
|
||||
export async function initDB(): Promise<void> {
|
||||
// Initialize IndexedDB or verify Flask connection
|
||||
const flask = getActiveStorage();
|
||||
if (flask) {
|
||||
const connected = await flask.testConnection();
|
||||
if (!connected) {
|
||||
throw new Error('Failed to connect to Flask backend');
|
||||
}
|
||||
} else {
|
||||
// Initialize IndexedDB
|
||||
await IndexedDBStorage.openDB();
|
||||
}
|
||||
|
||||
// Ensure default namespace exists
|
||||
await ensureDefaultNamespace();
|
||||
}
|
||||
|
||||
export async function clearDatabase(): Promise<void> {
|
||||
const flask = getActiveStorage();
|
||||
if (flask) {
|
||||
return await flask.clearDatabase();
|
||||
}
|
||||
return await IndexedDBStorage.clearDatabase();
|
||||
}
|
||||
|
||||
export async function getDatabaseStats() {
|
||||
const flask = getActiveStorage();
|
||||
if (flask) {
|
||||
return await flask.getStats();
|
||||
}
|
||||
return await IndexedDBStorage.getDatabaseStats();
|
||||
}
|
||||
|
||||
export async function exportDatabase(): Promise<string> {
|
||||
const flask = getActiveStorage();
|
||||
if (flask) {
|
||||
const data = await flask.exportDatabase();
|
||||
return JSON.stringify(data, null, 2);
|
||||
}
|
||||
const data = await IndexedDBStorage.exportDatabase();
|
||||
return JSON.stringify(data, null, 2);
|
||||
}
|
||||
|
||||
export async function importDatabase(jsonData: string): Promise<void> {
|
||||
const data = JSON.parse(jsonData);
|
||||
const flask = getActiveStorage();
|
||||
if (flask) {
|
||||
return await flask.importDatabase(data);
|
||||
}
|
||||
await IndexedDBStorage.importDatabase(data);
|
||||
}
|
||||
|
||||
export function validateDatabaseSchema(): Promise<boolean> {
|
||||
// With IndexedDB, schema is always valid
|
||||
return Promise.resolve(true);
|
||||
}
|
||||
|
||||
// For backward compatibility
|
||||
export const saveDB = async () => { /* No-op with IndexedDB */ };
|
||||
|
||||
38
src/lib/db.ts.old
Normal file
38
src/lib/db.ts.old
Normal file
@@ -0,0 +1,38 @@
|
||||
/**
|
||||
* Main database module - Re-exports from focused modules
|
||||
* This file maintains backward compatibility while delegating to specialized modules
|
||||
*/
|
||||
|
||||
// Re-export core database functions
|
||||
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 } 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 } 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'
|
||||
|
||||
// Note: saveDB is intentionally not exported as it's used internally by the modules
|
||||
241
src/lib/indexeddb-storage.ts
Normal file
241
src/lib/indexeddb-storage.ts
Normal file
@@ -0,0 +1,241 @@
|
||||
/**
|
||||
* IndexedDB Storage - Direct storage of snippets and namespaces
|
||||
*/
|
||||
|
||||
import type { Snippet, Namespace } from './types';
|
||||
|
||||
const DB_NAME = 'codesnippet-db';
|
||||
const DB_VERSION = 2;
|
||||
const SNIPPETS_STORE = 'snippets';
|
||||
const NAMESPACES_STORE = 'namespaces';
|
||||
|
||||
let dbInstance: IDBDatabase | null = null;
|
||||
|
||||
export async function openDB(): Promise<IDBDatabase> {
|
||||
if (dbInstance) return dbInstance;
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const request = indexedDB.open(DB_NAME, DB_VERSION);
|
||||
|
||||
request.onerror = () => reject(request.error);
|
||||
|
||||
request.onupgradeneeded = (event) => {
|
||||
const db = (event.target as IDBOpenDBRequest).result;
|
||||
|
||||
// Create snippets store if it doesn't exist
|
||||
if (!db.objectStoreNames.contains(SNIPPETS_STORE)) {
|
||||
const snippetsStore = db.createObjectStore(SNIPPETS_STORE, { keyPath: 'id' });
|
||||
snippetsStore.createIndex('namespaceId', 'namespaceId', { unique: false });
|
||||
snippetsStore.createIndex('createdAt', 'createdAt', { unique: false });
|
||||
}
|
||||
|
||||
// Create namespaces store if it doesn't exist
|
||||
if (!db.objectStoreNames.contains(NAMESPACES_STORE)) {
|
||||
db.createObjectStore(NAMESPACES_STORE, { keyPath: 'id' });
|
||||
}
|
||||
};
|
||||
|
||||
request.onsuccess = () => {
|
||||
dbInstance = request.result;
|
||||
resolve(dbInstance);
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
// Snippet operations
|
||||
export async function getAllSnippets(): Promise<Snippet[]> {
|
||||
const db = await openDB();
|
||||
return new Promise((resolve, reject) => {
|
||||
const transaction = db.transaction([SNIPPETS_STORE], 'readonly');
|
||||
const store = transaction.objectStore(SNIPPETS_STORE);
|
||||
const request = store.getAll();
|
||||
|
||||
request.onerror = () => reject(request.error);
|
||||
request.onsuccess = () => resolve(request.result);
|
||||
});
|
||||
}
|
||||
|
||||
export async function getSnippet(id: string): Promise<Snippet | null> {
|
||||
const db = await openDB();
|
||||
return new Promise((resolve, reject) => {
|
||||
const transaction = db.transaction([SNIPPETS_STORE], 'readonly');
|
||||
const store = transaction.objectStore(SNIPPETS_STORE);
|
||||
const request = store.get(id);
|
||||
|
||||
request.onerror = () => reject(request.error);
|
||||
request.onsuccess = () => resolve(request.result || null);
|
||||
});
|
||||
}
|
||||
|
||||
export async function createSnippet(snippet: Snippet): Promise<void> {
|
||||
const db = await openDB();
|
||||
return new Promise((resolve, reject) => {
|
||||
const transaction = db.transaction([SNIPPETS_STORE], 'readwrite');
|
||||
const store = transaction.objectStore(SNIPPETS_STORE);
|
||||
const request = store.add(snippet);
|
||||
|
||||
request.onerror = () => reject(request.error);
|
||||
request.onsuccess = () => resolve();
|
||||
});
|
||||
}
|
||||
|
||||
export async function updateSnippet(snippet: Snippet): Promise<void> {
|
||||
const db = await openDB();
|
||||
return new Promise((resolve, reject) => {
|
||||
const transaction = db.transaction([SNIPPETS_STORE], 'readwrite');
|
||||
const store = transaction.objectStore(SNIPPETS_STORE);
|
||||
const request = store.put(snippet);
|
||||
|
||||
request.onerror = () => reject(request.error);
|
||||
request.onsuccess = () => resolve();
|
||||
});
|
||||
}
|
||||
|
||||
export async function deleteSnippet(id: string): Promise<void> {
|
||||
const db = await openDB();
|
||||
return new Promise((resolve, reject) => {
|
||||
const transaction = db.transaction([SNIPPETS_STORE], 'readwrite');
|
||||
const store = transaction.objectStore(SNIPPETS_STORE);
|
||||
const request = store.delete(id);
|
||||
|
||||
request.onerror = () => reject(request.error);
|
||||
request.onsuccess = () => resolve();
|
||||
});
|
||||
}
|
||||
|
||||
export async function getSnippetsByNamespace(namespaceId: string): Promise<Snippet[]> {
|
||||
const db = await openDB();
|
||||
return new Promise((resolve, reject) => {
|
||||
const transaction = db.transaction([SNIPPETS_STORE], 'readonly');
|
||||
const store = transaction.objectStore(SNIPPETS_STORE);
|
||||
const index = store.index('namespaceId');
|
||||
const request = index.getAll(namespaceId);
|
||||
|
||||
request.onerror = () => reject(request.error);
|
||||
request.onsuccess = () => resolve(request.result);
|
||||
});
|
||||
}
|
||||
|
||||
// Namespace operations
|
||||
export async function getAllNamespaces(): Promise<Namespace[]> {
|
||||
const db = await openDB();
|
||||
return new Promise((resolve, reject) => {
|
||||
const transaction = db.transaction([NAMESPACES_STORE], 'readonly');
|
||||
const store = transaction.objectStore(NAMESPACES_STORE);
|
||||
const request = store.getAll();
|
||||
|
||||
request.onerror = () => reject(request.error);
|
||||
request.onsuccess = () => resolve(request.result);
|
||||
});
|
||||
}
|
||||
|
||||
export async function getNamespace(id: string): Promise<Namespace | null> {
|
||||
const db = await openDB();
|
||||
return new Promise((resolve, reject) => {
|
||||
const transaction = db.transaction([NAMESPACES_STORE], 'readonly');
|
||||
const store = transaction.objectStore(NAMESPACES_STORE);
|
||||
const request = store.get(id);
|
||||
|
||||
request.onerror = () => reject(request.error);
|
||||
request.onsuccess = () => resolve(request.result || null);
|
||||
});
|
||||
}
|
||||
|
||||
export async function createNamespace(namespace: Namespace): Promise<void> {
|
||||
const db = await openDB();
|
||||
return new Promise((resolve, reject) => {
|
||||
const transaction = db.transaction([NAMESPACES_STORE], 'readwrite');
|
||||
const store = transaction.objectStore(NAMESPACES_STORE);
|
||||
const request = store.add(namespace);
|
||||
|
||||
request.onerror = () => reject(request.error);
|
||||
request.onsuccess = () => resolve();
|
||||
});
|
||||
}
|
||||
|
||||
export async function updateNamespace(namespace: Namespace): Promise<void> {
|
||||
const db = await openDB();
|
||||
return new Promise((resolve, reject) => {
|
||||
const transaction = db.transaction([NAMESPACES_STORE], 'readwrite');
|
||||
const store = transaction.objectStore(NAMESPACES_STORE);
|
||||
const request = store.put(namespace);
|
||||
|
||||
request.onerror = () => reject(request.error);
|
||||
request.onsuccess = () => resolve();
|
||||
});
|
||||
}
|
||||
|
||||
export async function deleteNamespace(id: string): Promise<void> {
|
||||
const db = await openDB();
|
||||
return new Promise((resolve, reject) => {
|
||||
const transaction = db.transaction([NAMESPACES_STORE], 'readwrite');
|
||||
const store = transaction.objectStore(NAMESPACES_STORE);
|
||||
const request = store.delete(id);
|
||||
|
||||
request.onerror = () => reject(request.error);
|
||||
request.onsuccess = () => resolve();
|
||||
});
|
||||
}
|
||||
|
||||
// Database operations
|
||||
export async function clearDatabase(): Promise<void> {
|
||||
const db = await openDB();
|
||||
return new Promise((resolve, reject) => {
|
||||
const transaction = db.transaction([SNIPPETS_STORE, NAMESPACES_STORE], 'readwrite');
|
||||
|
||||
const snippetsStore = transaction.objectStore(SNIPPETS_STORE);
|
||||
const namespacesStore = transaction.objectStore(NAMESPACES_STORE);
|
||||
|
||||
snippetsStore.clear();
|
||||
namespacesStore.clear();
|
||||
|
||||
transaction.onerror = () => reject(transaction.error);
|
||||
transaction.oncomplete = () => resolve();
|
||||
});
|
||||
}
|
||||
|
||||
export async function getDatabaseStats() {
|
||||
const snippets = await getAllSnippets();
|
||||
const namespaces = await getAllNamespaces();
|
||||
const templates = snippets.filter(s => s.isTemplate);
|
||||
|
||||
return {
|
||||
snippetCount: snippets.length,
|
||||
templateCount: templates.length,
|
||||
namespaceCount: namespaces.length,
|
||||
storageType: 'indexeddb' as const,
|
||||
databaseSize: 0, // IndexedDB doesn't provide easy size calculation
|
||||
};
|
||||
}
|
||||
|
||||
// Export/Import
|
||||
export async function exportDatabase(): Promise<{ snippets: Snippet[]; namespaces: Namespace[] }> {
|
||||
const snippets = await getAllSnippets();
|
||||
const namespaces = await getAllNamespaces();
|
||||
return { snippets, namespaces };
|
||||
}
|
||||
|
||||
export async function importDatabase(data: { snippets: Snippet[]; namespaces: Namespace[] }): Promise<void> {
|
||||
await clearDatabase();
|
||||
|
||||
const db = await openDB();
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const transaction = db.transaction([SNIPPETS_STORE, NAMESPACES_STORE], 'readwrite');
|
||||
const snippetsStore = transaction.objectStore(SNIPPETS_STORE);
|
||||
const namespacesStore = transaction.objectStore(NAMESPACES_STORE);
|
||||
|
||||
// Import namespaces
|
||||
for (const namespace of data.namespaces) {
|
||||
namespacesStore.add(namespace);
|
||||
}
|
||||
|
||||
// Import snippets
|
||||
for (const snippet of data.snippets) {
|
||||
snippetsStore.add(snippet);
|
||||
}
|
||||
|
||||
transaction.onerror = () => reject(transaction.error);
|
||||
transaction.oncomplete = () => resolve();
|
||||
});
|
||||
}
|
||||
@@ -10,7 +10,7 @@ export interface StorageConfig {
|
||||
const STORAGE_CONFIG_KEY = 'codesnippet-storage-config'
|
||||
|
||||
function getDefaultConfig(): StorageConfig {
|
||||
const flaskUrl = import.meta.env.VITE_FLASK_BACKEND_URL
|
||||
const flaskUrl = process.env.NEXT_PUBLIC_FLASK_BACKEND_URL
|
||||
|
||||
if (flaskUrl) {
|
||||
return {
|
||||
@@ -249,4 +249,49 @@ export class FlaskStorageAdapter {
|
||||
throw new Error(`Failed to bulk move snippets: ${response.statusText}`)
|
||||
}
|
||||
}
|
||||
|
||||
async getSnippetsByNamespace(namespaceId: string): Promise<Snippet[]> {
|
||||
const snippets = await this.getAllSnippets();
|
||||
return snippets.filter(s => s.namespaceId === namespaceId);
|
||||
}
|
||||
|
||||
async getNamespace(id: string): Promise<import('./types').Namespace | null> {
|
||||
const namespaces = await this.getAllNamespaces();
|
||||
return namespaces.find(ns => ns.id === id) || null;
|
||||
}
|
||||
|
||||
async clearDatabase(): Promise<void> {
|
||||
return this.wipeDatabase();
|
||||
}
|
||||
|
||||
async getStats() {
|
||||
const snippets = await this.getAllSnippets();
|
||||
const namespaces = await this.getAllNamespaces();
|
||||
const templates = snippets.filter(s => s.isTemplate);
|
||||
return {
|
||||
snippetCount: snippets.length,
|
||||
templateCount: templates.length,
|
||||
namespaceCount: namespaces.length,
|
||||
storageType: 'indexeddb' as const,
|
||||
databaseSize: 0,
|
||||
};
|
||||
}
|
||||
|
||||
async exportDatabase(): Promise<{ snippets: Snippet[]; namespaces: import('./types').Namespace[] }> {
|
||||
const snippets = await this.getAllSnippets();
|
||||
const namespaces = await this.getAllNamespaces();
|
||||
return { snippets, namespaces };
|
||||
}
|
||||
|
||||
async importDatabase(data: { snippets: Snippet[]; namespaces: import('./types').Namespace[] }): Promise<void> {
|
||||
await this.wipeDatabase();
|
||||
|
||||
for (const namespace of data.namespaces) {
|
||||
await this.createNamespace(namespace);
|
||||
}
|
||||
|
||||
for (const snippet of data.snippets) {
|
||||
await this.createSnippet(snippet);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@ export interface Snippet {
|
||||
category: string
|
||||
namespaceId?: string
|
||||
hasPreview?: boolean
|
||||
isTemplate?: boolean
|
||||
functionName?: string
|
||||
inputParameters?: InputParameter[]
|
||||
createdAt: number
|
||||
|
||||
35
src/main.tsx
35
src/main.tsx
@@ -1,35 +0,0 @@
|
||||
import { createRoot } from 'react-dom/client'
|
||||
import { ErrorBoundary } from "react-error-boundary";
|
||||
import { Provider } from 'react-redux'
|
||||
import "@github/spark/spark"
|
||||
import { Toaster } from '@/components/ui/sonner'
|
||||
import { loadStorageConfig } from '@/lib/storage'
|
||||
import { store } from '@/store'
|
||||
|
||||
import App from './App.tsx'
|
||||
import { ErrorFallback } from './components/error/ErrorFallback.tsx'
|
||||
|
||||
import "./main.css"
|
||||
import "./styles/theme.css"
|
||||
import "./index.css"
|
||||
|
||||
loadStorageConfig()
|
||||
|
||||
const logErrorToConsole = (error: Error, info: { componentStack?: string }) => {
|
||||
console.error('Application Error:', error);
|
||||
if (info.componentStack) {
|
||||
console.error('Component Stack:', info.componentStack);
|
||||
}
|
||||
};
|
||||
|
||||
createRoot(document.getElementById('root')!).render(
|
||||
<Provider store={store}>
|
||||
<ErrorBoundary
|
||||
FallbackComponent={ErrorFallback}
|
||||
onError={logErrorToConsole}
|
||||
>
|
||||
<App />
|
||||
<Toaster />
|
||||
</ErrorBoundary>
|
||||
</Provider>
|
||||
)
|
||||
@@ -1,38 +0,0 @@
|
||||
import { motion } from 'framer-motion'
|
||||
import { AtomsSection } from '@/components/atoms/AtomsSection'
|
||||
import type { Snippet } from '@/lib/types'
|
||||
import { useCallback } from 'react'
|
||||
import { toast } from 'sonner'
|
||||
import { createSnippet } from '@/lib/db'
|
||||
|
||||
export function AtomsPage() {
|
||||
const handleSaveSnippet = useCallback(async (snippetData: Omit<Snippet, 'id' | 'createdAt' | 'updatedAt'>) => {
|
||||
try {
|
||||
const newSnippet: Snippet = {
|
||||
...snippetData,
|
||||
id: Date.now().toString(),
|
||||
createdAt: Date.now(),
|
||||
updatedAt: Date.now(),
|
||||
}
|
||||
await createSnippet(newSnippet)
|
||||
toast.success('Component saved as snippet!')
|
||||
} catch (error) {
|
||||
console.error('Failed to save snippet:', error)
|
||||
toast.error('Failed to save snippet')
|
||||
}
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.4 }}
|
||||
>
|
||||
<div className="mb-8">
|
||||
<h2 className="text-3xl font-bold tracking-tight mb-2">Atoms</h2>
|
||||
<p className="text-muted-foreground">Fundamental building blocks - basic HTML elements styled as reusable components</p>
|
||||
</div>
|
||||
<AtomsSection onSaveSnippet={handleSaveSnippet} />
|
||||
</motion.div>
|
||||
)
|
||||
}
|
||||
@@ -1,55 +0,0 @@
|
||||
import { useState } from 'react'
|
||||
import { motion } from 'framer-motion'
|
||||
import { SplitScreenEditor } from '@/components/features/snippet-editor/SplitScreenEditor'
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
|
||||
import { Sparkle } from '@phosphor-icons/react'
|
||||
import { DEMO_CODE } from './demo-constants'
|
||||
import { DemoFeatureCards } from './DemoFeatureCards'
|
||||
|
||||
export function DemoPage() {
|
||||
const [code, setCode] = useState(DEMO_CODE)
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.4 }}
|
||||
className="space-y-8"
|
||||
>
|
||||
<div className="mb-8">
|
||||
<div className="flex items-center gap-3 mb-2">
|
||||
<div className="h-10 w-10 rounded-lg bg-gradient-to-br from-accent to-primary flex items-center justify-center">
|
||||
<Sparkle className="h-5 w-5 text-primary-foreground" weight="fill" />
|
||||
</div>
|
||||
<h2 className="text-3xl font-bold tracking-tight">Split-Screen Demo</h2>
|
||||
</div>
|
||||
<p className="text-muted-foreground">
|
||||
Experience live React component editing with real-time preview. Edit the code on the left and watch it update instantly on the right.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<Card className="border-accent/20 bg-card/50 backdrop-blur">
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<Sparkle className="h-5 w-5 text-accent" weight="fill" />
|
||||
Interactive Code Editor
|
||||
</CardTitle>
|
||||
<CardDescription>
|
||||
This editor supports JSX, TSX, JavaScript, and TypeScript with live preview.
|
||||
Try switching between Code, Split, and Preview modes using the buttons above the editor.
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<SplitScreenEditor
|
||||
value={code}
|
||||
onChange={setCode}
|
||||
language="JSX"
|
||||
height="600px"
|
||||
/>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<DemoFeatureCards />
|
||||
</motion.div>
|
||||
)
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
import { motion } from 'framer-motion'
|
||||
import { SnippetManagerRedux } from '@/components/SnippetManagerRedux'
|
||||
|
||||
export function HomePage() {
|
||||
return (
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.4 }}
|
||||
>
|
||||
<div className="mb-8">
|
||||
<h2 className="text-3xl font-bold tracking-tight mb-2">My Snippets</h2>
|
||||
<p className="text-muted-foreground">Save, organize, and share your code snippets</p>
|
||||
</div>
|
||||
<SnippetManagerRedux />
|
||||
</motion.div>
|
||||
)
|
||||
}
|
||||
@@ -1,38 +0,0 @@
|
||||
import { motion } from 'framer-motion'
|
||||
import { MoleculesSection } from '@/components/molecules/MoleculesSection'
|
||||
import type { Snippet } from '@/lib/types'
|
||||
import { useCallback } from 'react'
|
||||
import { toast } from 'sonner'
|
||||
import { createSnippet } from '@/lib/db'
|
||||
|
||||
export function MoleculesPage() {
|
||||
const handleSaveSnippet = useCallback(async (snippetData: Omit<Snippet, 'id' | 'createdAt' | 'updatedAt'>) => {
|
||||
try {
|
||||
const newSnippet: Snippet = {
|
||||
...snippetData,
|
||||
id: Date.now().toString(),
|
||||
createdAt: Date.now(),
|
||||
updatedAt: Date.now(),
|
||||
}
|
||||
await createSnippet(newSnippet)
|
||||
toast.success('Component saved as snippet!')
|
||||
} catch (error) {
|
||||
console.error('Failed to save snippet:', error)
|
||||
toast.error('Failed to save snippet')
|
||||
}
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.4 }}
|
||||
>
|
||||
<div className="mb-8">
|
||||
<h2 className="text-3xl font-bold tracking-tight mb-2">Molecules</h2>
|
||||
<p className="text-muted-foreground">Simple combinations of atoms that work together as functional units</p>
|
||||
</div>
|
||||
<MoleculesSection onSaveSnippet={handleSaveSnippet} />
|
||||
</motion.div>
|
||||
)
|
||||
}
|
||||
@@ -1,38 +0,0 @@
|
||||
import { motion } from 'framer-motion'
|
||||
import { OrganismsSection } from '@/components/organisms/OrganismsSection'
|
||||
import type { Snippet } from '@/lib/types'
|
||||
import { useCallback } from 'react'
|
||||
import { toast } from 'sonner'
|
||||
import { createSnippet } from '@/lib/db'
|
||||
|
||||
export function OrganismsPage() {
|
||||
const handleSaveSnippet = useCallback(async (snippetData: Omit<Snippet, 'id' | 'createdAt' | 'updatedAt'>) => {
|
||||
try {
|
||||
const newSnippet: Snippet = {
|
||||
...snippetData,
|
||||
id: Date.now().toString(),
|
||||
createdAt: Date.now(),
|
||||
updatedAt: Date.now(),
|
||||
}
|
||||
await createSnippet(newSnippet)
|
||||
toast.success('Component saved as snippet!')
|
||||
} catch (error) {
|
||||
console.error('Failed to save snippet:', error)
|
||||
toast.error('Failed to save snippet')
|
||||
}
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.4 }}
|
||||
>
|
||||
<div className="mb-8">
|
||||
<h2 className="text-3xl font-bold tracking-tight mb-2">Organisms</h2>
|
||||
<p className="text-muted-foreground">Complex UI components composed of molecules and atoms</p>
|
||||
</div>
|
||||
<OrganismsSection onSaveSnippet={handleSaveSnippet} />
|
||||
</motion.div>
|
||||
)
|
||||
}
|
||||
@@ -1,100 +0,0 @@
|
||||
import { motion } from 'framer-motion'
|
||||
import { PersistenceSettings } from '@/components/demo/PersistenceSettings'
|
||||
import { SchemaHealthCard } from '@/components/settings/SchemaHealthCard'
|
||||
import { BackendAutoConfigCard } from '@/components/settings/BackendAutoConfigCard'
|
||||
import { StorageBackendCard } from '@/components/settings/StorageBackendCard'
|
||||
import { DatabaseStatsCard } from '@/components/settings/DatabaseStatsCard'
|
||||
import { StorageInfoCard } from '@/components/settings/StorageInfoCard'
|
||||
import { DatabaseActionsCard } from '@/components/settings/DatabaseActionsCard'
|
||||
import { useSettingsState } from '@/hooks/useSettingsState'
|
||||
|
||||
export function SettingsPage() {
|
||||
const {
|
||||
stats,
|
||||
loading,
|
||||
storageBackend,
|
||||
setStorageBackend,
|
||||
flaskUrl,
|
||||
setFlaskUrl,
|
||||
flaskConnectionStatus,
|
||||
setFlaskConnectionStatus,
|
||||
testingConnection,
|
||||
envVarSet,
|
||||
schemaHealth,
|
||||
checkingSchema,
|
||||
handleExport,
|
||||
handleImport,
|
||||
handleClear,
|
||||
handleSeed,
|
||||
formatBytes,
|
||||
handleTestConnection,
|
||||
handleSaveStorageConfig,
|
||||
handleMigrateToFlask,
|
||||
handleMigrateToIndexedDB,
|
||||
checkSchemaHealth,
|
||||
} = useSettingsState()
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.4 }}
|
||||
>
|
||||
<div className="mb-8">
|
||||
<h2 className="text-3xl font-bold tracking-tight mb-2">Settings</h2>
|
||||
<p className="text-muted-foreground">Manage your database and application settings</p>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-6 max-w-3xl">
|
||||
<PersistenceSettings />
|
||||
|
||||
<SchemaHealthCard
|
||||
schemaHealth={schemaHealth}
|
||||
checkingSchema={checkingSchema}
|
||||
onClear={handleClear}
|
||||
onCheckSchema={checkSchemaHealth}
|
||||
/>
|
||||
|
||||
<BackendAutoConfigCard
|
||||
envVarSet={envVarSet}
|
||||
flaskUrl={flaskUrl}
|
||||
flaskConnectionStatus={flaskConnectionStatus}
|
||||
testingConnection={testingConnection}
|
||||
onTestConnection={handleTestConnection}
|
||||
/>
|
||||
|
||||
<StorageBackendCard
|
||||
storageBackend={storageBackend}
|
||||
flaskUrl={flaskUrl}
|
||||
flaskConnectionStatus={flaskConnectionStatus}
|
||||
testingConnection={testingConnection}
|
||||
envVarSet={envVarSet}
|
||||
onStorageBackendChange={setStorageBackend}
|
||||
onFlaskUrlChange={(url) => {
|
||||
setFlaskUrl(url)
|
||||
setFlaskConnectionStatus('unknown')
|
||||
}}
|
||||
onTestConnection={handleTestConnection}
|
||||
onSaveConfig={handleSaveStorageConfig}
|
||||
onMigrateToFlask={handleMigrateToFlask}
|
||||
onMigrateToIndexedDB={handleMigrateToIndexedDB}
|
||||
/>
|
||||
|
||||
<DatabaseStatsCard
|
||||
loading={loading}
|
||||
stats={stats}
|
||||
formatBytes={formatBytes}
|
||||
/>
|
||||
|
||||
<StorageInfoCard storageType={stats?.storageType} />
|
||||
|
||||
<DatabaseActionsCard
|
||||
onExport={handleExport}
|
||||
onImport={handleImport}
|
||||
onSeed={handleSeed}
|
||||
onClear={handleClear}
|
||||
/>
|
||||
</div>
|
||||
</motion.div>
|
||||
)
|
||||
}
|
||||
@@ -1,38 +0,0 @@
|
||||
import { motion } from 'framer-motion'
|
||||
import { TemplatesSection } from '@/components/templates/TemplatesSection'
|
||||
import type { Snippet } from '@/lib/types'
|
||||
import { useCallback } from 'react'
|
||||
import { toast } from 'sonner'
|
||||
import { createSnippet } from '@/lib/db'
|
||||
|
||||
export function TemplatesPage() {
|
||||
const handleSaveSnippet = useCallback(async (snippetData: Omit<Snippet, 'id' | 'createdAt' | 'updatedAt'>) => {
|
||||
try {
|
||||
const newSnippet: Snippet = {
|
||||
...snippetData,
|
||||
id: Date.now().toString(),
|
||||
createdAt: Date.now(),
|
||||
updatedAt: Date.now(),
|
||||
}
|
||||
await createSnippet(newSnippet)
|
||||
toast.success('Component saved as snippet!')
|
||||
} catch (error) {
|
||||
console.error('Failed to save snippet:', error)
|
||||
toast.error('Failed to save snippet')
|
||||
}
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.4 }}
|
||||
>
|
||||
<div className="mb-8">
|
||||
<h2 className="text-3xl font-bold tracking-tight mb-2">Templates</h2>
|
||||
<p className="text-muted-foreground">Page-level layouts that combine organisms into complete interfaces</p>
|
||||
</div>
|
||||
<TemplatesSection onSaveSnippet={handleSaveSnippet} />
|
||||
</motion.div>
|
||||
)
|
||||
}
|
||||
@@ -32,7 +32,14 @@ export const fetchNamespaces = createAsyncThunk(
|
||||
export const createNamespace = createAsyncThunk(
|
||||
'namespaces/create',
|
||||
async (name: string) => {
|
||||
return await createNamespaceDB(name)
|
||||
const namespace: Namespace = {
|
||||
id: Date.now().toString(),
|
||||
name,
|
||||
createdAt: Date.now(),
|
||||
isDefault: false,
|
||||
}
|
||||
await createNamespaceDB(namespace)
|
||||
return namespace
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@@ -1,147 +1,62 @@
|
||||
import fs from "fs";
|
||||
|
||||
/** @type {import('tailwindcss').Config} */
|
||||
|
||||
let theme = {};
|
||||
try {
|
||||
const themePath = "./theme.json";
|
||||
|
||||
if (fs.existsSync(themePath)) {
|
||||
theme = JSON.parse(fs.readFileSync(themePath, "utf-8"));
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('failed to parse custom styles', err)
|
||||
module.exports = {
|
||||
darkMode: ["class"],
|
||||
content: [
|
||||
'./src/pages/**/*.{ts,tsx}',
|
||||
'./src/components/**/*.{ts,tsx}',
|
||||
'./src/app/**/*.{ts,tsx}',
|
||||
'./src/**/*.{ts,tsx}',
|
||||
],
|
||||
theme: {
|
||||
container: {
|
||||
center: true,
|
||||
padding: "2rem",
|
||||
screens: {
|
||||
"2xl": "1400px",
|
||||
},
|
||||
},
|
||||
extend: {
|
||||
colors: {
|
||||
border: "hsl(var(--border))",
|
||||
input: "hsl(var(--input))",
|
||||
ring: "hsl(var(--ring))",
|
||||
background: "hsl(var(--background))",
|
||||
foreground: "hsl(var(--foreground))",
|
||||
primary: {
|
||||
DEFAULT: "hsl(var(--primary))",
|
||||
foreground: "hsl(var(--primary-foreground))",
|
||||
},
|
||||
secondary: {
|
||||
DEFAULT: "hsl(var(--secondary))",
|
||||
foreground: "hsl(var(--secondary-foreground))",
|
||||
},
|
||||
destructive: {
|
||||
DEFAULT: "hsl(var(--destructive))",
|
||||
foreground: "hsl(var(--destructive-foreground))",
|
||||
},
|
||||
muted: {
|
||||
DEFAULT: "hsl(var(--muted))",
|
||||
foreground: "hsl(var(--muted-foreground))",
|
||||
},
|
||||
accent: {
|
||||
DEFAULT: "hsl(var(--accent))",
|
||||
foreground: "hsl(var(--accent-foreground))",
|
||||
},
|
||||
popover: {
|
||||
DEFAULT: "hsl(var(--popover))",
|
||||
foreground: "hsl(var(--popover-foreground))",
|
||||
},
|
||||
card: {
|
||||
DEFAULT: "hsl(var(--card))",
|
||||
foreground: "hsl(var(--card-foreground))",
|
||||
},
|
||||
},
|
||||
borderRadius: {
|
||||
lg: "var(--radius)",
|
||||
md: "calc(var(--radius) - 2px)",
|
||||
sm: "calc(var(--radius) - 4px)",
|
||||
},
|
||||
},
|
||||
},
|
||||
plugins: [],
|
||||
}
|
||||
const defaultTheme = {
|
||||
container: {
|
||||
center: true,
|
||||
padding: "2rem",
|
||||
},
|
||||
extend: {
|
||||
screens: {
|
||||
coarse: { raw: "(pointer: coarse)" },
|
||||
fine: { raw: "(pointer: fine)" },
|
||||
pwa: { raw: "(display-mode: standalone)" },
|
||||
},
|
||||
colors: {
|
||||
neutral: {
|
||||
1: "var(--color-neutral-1)",
|
||||
2: "var(--color-neutral-2)",
|
||||
3: "var(--color-neutral-3)",
|
||||
4: "var(--color-neutral-4)",
|
||||
5: "var(--color-neutral-5)",
|
||||
6: "var(--color-neutral-6)",
|
||||
7: "var(--color-neutral-7)",
|
||||
8: "var(--color-neutral-8)",
|
||||
9: "var(--color-neutral-9)",
|
||||
10: "var(--color-neutral-10)",
|
||||
11: "var(--color-neutral-11)",
|
||||
12: "var(--color-neutral-12)",
|
||||
a1: "var(--color-neutral-a1)",
|
||||
a2: "var(--color-neutral-a2)",
|
||||
a3: "var(--color-neutral-a3)",
|
||||
a4: "var(--color-neutral-a4)",
|
||||
a5: "var(--color-neutral-a5)",
|
||||
a6: "var(--color-neutral-a6)",
|
||||
a7: "var(--color-neutral-a7)",
|
||||
a8: "var(--color-neutral-a8)",
|
||||
a9: "var(--color-neutral-a9)",
|
||||
a10: "var(--color-neutral-a10)",
|
||||
a11: "var(--color-neutral-a11)",
|
||||
a12: "var(--color-neutral-a12)",
|
||||
contrast: "var(--color-neutral-contrast)",
|
||||
},
|
||||
accent: {
|
||||
1: "var(--color-accent-1)",
|
||||
2: "var(--color-accent-2)",
|
||||
3: "var(--color-accent-3)",
|
||||
4: "var(--color-accent-4)",
|
||||
5: "var(--color-accent-5)",
|
||||
6: "var(--color-accent-6)",
|
||||
7: "var(--color-accent-7)",
|
||||
8: "var(--color-accent-8)",
|
||||
9: "var(--color-accent-9)",
|
||||
10: "var(--color-accent-10)",
|
||||
11: "var(--color-accent-11)",
|
||||
12: "var(--color-accent-12)",
|
||||
contrast: "var(--color-accent-contrast)",
|
||||
},
|
||||
"accent-secondary": {
|
||||
1: "var(--color-accent-secondary-1)",
|
||||
2: "var(--color-accent-secondary-2)",
|
||||
3: "var(--color-accent-secondary-3)",
|
||||
4: "var(--color-accent-secondary-4)",
|
||||
5: "var(--color-accent-secondary-5)",
|
||||
6: "var(--color-accent-secondary-6)",
|
||||
7: "var(--color-accent-secondary-7)",
|
||||
8: "var(--color-accent-secondary-8)",
|
||||
9: "var(--color-accent-secondary-9)",
|
||||
10: "var(--color-accent-secondary-10)",
|
||||
11: "var(--color-accent-secondary-11)",
|
||||
12: "var(--color-accent-secondary-12)",
|
||||
contrast: "var(--color-accent-secondary-contrast)",
|
||||
},
|
||||
fg: {
|
||||
DEFAULT: "var(--color-fg)",
|
||||
secondary: "var(--color-fg-secondary)",
|
||||
},
|
||||
bg: {
|
||||
DEFAULT: "var(--color-bg)",
|
||||
inset: "var(--color-bg-inset)",
|
||||
overlay: "var(--color-bg-overlay)",
|
||||
},
|
||||
"focus-ring": "var(--color-focus-ring)",
|
||||
},
|
||||
borderRadius: {
|
||||
sm: "var(--radius-sm)",
|
||||
md: "var(--radius-md)",
|
||||
lg: "var(--radius-lg)",
|
||||
xl: "var(--radius-xl)",
|
||||
"2xl": "var(--radius-2xl)",
|
||||
full: "var(--radius-full)",
|
||||
},
|
||||
},
|
||||
spacing: {
|
||||
px: "var(--size-px)",
|
||||
0: "var(--size-0)",
|
||||
0.5: "var(--size-0-5)",
|
||||
1: "var(--size-1)",
|
||||
1.5: "var(--size-1-5)",
|
||||
2: "var(--size-2)",
|
||||
2.5: "var(--size-2-5)",
|
||||
3: "var(--size-3)",
|
||||
3.5: "var(--size-3-5)",
|
||||
4: "var(--size-4)",
|
||||
5: "var(--size-5)",
|
||||
6: "var(--size-6)",
|
||||
7: "var(--size-7)",
|
||||
8: "var(--size-8)",
|
||||
9: "var(--size-9)",
|
||||
10: "var(--size-10)",
|
||||
11: "var(--size-11)",
|
||||
12: "var(--size-12)",
|
||||
14: "var(--size-14)",
|
||||
16: "var(--size-16)",
|
||||
20: "var(--size-20)",
|
||||
24: "var(--size-24)",
|
||||
28: "var(--size-28)",
|
||||
32: "var(--size-32)",
|
||||
36: "var(--size-36)",
|
||||
40: "var(--size-40)",
|
||||
44: "var(--size-44)",
|
||||
48: "var(--size-48)",
|
||||
52: "var(--size-52)",
|
||||
56: "var(--size-56)",
|
||||
60: "var(--size-60)",
|
||||
64: "var(--size-64)",
|
||||
72: "var(--size-72)",
|
||||
80: "var(--size-80)",
|
||||
96: "var(--size-96)",
|
||||
},
|
||||
darkMode: ["selector", '[data-appearance="dark"]'],
|
||||
}
|
||||
|
||||
export default {
|
||||
content: ["./index.html", "./src/**/*.{js,ts,jsx,tsx}"],
|
||||
theme: { ...defaultTheme, ...theme },
|
||||
};
|
||||
@@ -1,34 +1,45 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
|
||||
"target": "ES2020",
|
||||
"useDefineForClassFields": true,
|
||||
"lib": [
|
||||
"ES2020",
|
||||
"DOM",
|
||||
"DOM.Iterable"
|
||||
],
|
||||
"module": "ESNext",
|
||||
"skipLibCheck": true,
|
||||
"strictNullChecks": true,
|
||||
/* Bundler mode */
|
||||
"moduleResolution": "bundler",
|
||||
"allowImportingTsExtensions": true,
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"moduleDetection": "force",
|
||||
"noEmit": true,
|
||||
"jsx": "react-jsx",
|
||||
/* Linting */
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "bundler",
|
||||
"resolveJsonModule": true,
|
||||
"allowJs": true,
|
||||
"skipLibCheck": true,
|
||||
"strict": false,
|
||||
"strictNullChecks": true,
|
||||
"noEmit": true,
|
||||
"esModuleInterop": true,
|
||||
"isolatedModules": true,
|
||||
"incremental": true,
|
||||
"noUncheckedSideEffectImports": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"allowImportingTsExtensions": false,
|
||||
"plugins": [
|
||||
{
|
||||
"name": "next"
|
||||
}
|
||||
],
|
||||
"paths": {
|
||||
"@/*": [
|
||||
"./src/*"
|
||||
]
|
||||
},
|
||||
}
|
||||
},
|
||||
"include": [
|
||||
"src"
|
||||
"next-env.d.ts",
|
||||
"**/*.ts",
|
||||
"**/*.tsx",
|
||||
".next/types/**/*.ts",
|
||||
".next/dev/types/**/*.ts"
|
||||
],
|
||||
"exclude": [
|
||||
"node_modules"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -1,29 +0,0 @@
|
||||
import tailwindcss from "@tailwindcss/vite";
|
||||
import react from "@vitejs/plugin-react-swc";
|
||||
import { defineConfig, PluginOption } from "vite";
|
||||
|
||||
import sparkPlugin from "@github/spark/spark-vite-plugin";
|
||||
import createIconImportProxy from "@github/spark/vitePhosphorIconProxyPlugin";
|
||||
import { resolve } from 'path'
|
||||
|
||||
const projectRoot = process.env.PROJECT_ROOT || import.meta.dirname
|
||||
|
||||
// https://vite.dev/config/
|
||||
export default defineConfig({
|
||||
// Base path for GitHub Pages deployment
|
||||
// Set to '/' for custom domain or root deployment
|
||||
// Set to '/repo-name/' for GitHub Pages at username.github.io/repo-name/
|
||||
base: process.env.VITE_BASE_PATH || '/',
|
||||
plugins: [
|
||||
react(),
|
||||
tailwindcss(),
|
||||
// DO NOT REMOVE
|
||||
createIconImportProxy() as PluginOption,
|
||||
sparkPlugin() as PluginOption,
|
||||
],
|
||||
resolve: {
|
||||
alias: {
|
||||
'@': resolve(projectRoot, 'src')
|
||||
}
|
||||
},
|
||||
});
|
||||
Reference in New Issue
Block a user