Complete Next.js conversion - removed sql.js, updated routing, fixed Tailwind

Co-authored-by: johndoe6345789 <224850594+johndoe6345789@users.noreply.github.com>
This commit is contained in:
copilot-swe-agent[bot]
2026-01-19 13:36:35 +00:00
parent 8e93467317
commit ea79d908bb
75 changed files with 1101 additions and 1942 deletions

View File

@@ -1,92 +0,0 @@
@import 'tailwindcss';
@import "tw-animate-css";
@import '../src/styles/theme.css';
/*
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.
*/
@layer base {
*,
::after,
::before,
::backdrop,
::file-selector-button {
border-color: var(--color-gray-200, currentColor);
}
* {
@apply border-border
}
}
:root {
--radius: 0.5rem;
--background: oklch(0.08 0.01 265);
--foreground: oklch(0.95 0.01 265);
--card: oklch(0.15 0.01 265);
--card-foreground: oklch(0.98 0 0);
--popover: oklch(0.15 0.01 265);
--popover-foreground: oklch(0.98 0 0);
--primary: oklch(0.35 0.15 265);
--primary-foreground: oklch(0.98 0 0);
--secondary: oklch(0.25 0.01 265);
--secondary-foreground: oklch(0.95 0.01 265);
--muted: oklch(0.20 0.01 265);
--muted-foreground: oklch(0.65 0.01 265);
--accent: oklch(0.75 0.15 195);
--accent-foreground: oklch(0.15 0.01 265);
--destructive: oklch(0.577 0.245 27.325);
--destructive-foreground: oklch(0.98 0 0);
--border: oklch(0.25 0.01 265);
--input: oklch(0.28 0.02 265);
--ring: oklch(0.75 0.15 195);
--chart-1: oklch(0.70 0.20 10);
--chart-2: oklch(0.70 0.20 160);
--chart-3: oklch(0.70 0.20 200);
--chart-4: oklch(0.70 0.20 240);
--chart-5: oklch(0.70 0.20 280);
}
@theme {
--color-background: var(--background);
--color-foreground: var(--foreground);
--color-card: var(--card);
--color-card-foreground: var(--card-foreground);
--color-popover: var(--popover);
--color-popover-foreground: var(--popover-foreground);
--color-primary: var(--primary);
--color-primary-foreground: var(--primary-foreground);
--color-secondary: var(--secondary);
--color-secondary-foreground: var(--secondary-foreground);
--color-muted: var(--muted);
--color-muted-foreground: var(--muted-foreground);
--color-accent: var(--accent);
--color-accent-foreground: var(--accent-foreground);
--color-destructive: var(--destructive);
--color-destructive-foreground: var(--destructive-foreground);
--color-border: var(--border);
--color-input: var(--input);
--color-ring: var(--ring);
--radius-sm: calc(var(--radius) * 0.5);
--radius-md: var(--radius);
--radius-lg: calc(var(--radius) * 1.5);
--radius-xl: calc(var(--radius) * 2);
--radius-2xl: calc(var(--radius) * 3);
--radius-full: 9999px;
}
h1, h2, h3, h4, h5, h6 {
font-family: var(--font-inter), sans-serif;
}
body {
font-family: var(--font-inter), sans-serif;
}
code, pre {
font-family: var(--font-jetbrains-mono), monospace;
}

View File

@@ -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
View 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;

View File

@@ -1,49 +0,0 @@
import type { NextConfig } from 'next';
const nextConfig: NextConfig = {
// Output as static HTML for GitHub Pages, or standalone for Docker
output: process.env.BUILD_STATIC ? 'export' : 'standalone',
// 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/
basePath: process.env.NEXT_PUBLIC_BASE_PATH || '',
// Configure webpack for browser-only modules
webpack: (config, { isServer }) => {
if (!isServer) {
// Externalize node modules for browser
config.resolve.fallback = {
...config.resolve.fallback,
fs: false,
path: false,
crypto: false,
'node:url': false,
'node:fs': false,
'node:fs/promises': false,
'node:vm': false,
'node:path': false,
'node:crypto': false,
'node:child_process': false,
};
}
return config;
},
// Environment variables that should be available on the client
env: {
NEXT_PUBLIC_FLASK_BACKEND_URL: process.env.NEXT_PUBLIC_FLASK_BACKEND_URL || '',
},
// Image optimization
images: {
unoptimized: true, // Required for static export
},
// Experimental features
experimental: {
optimizePackageImports: ['@radix-ui/react-icons', '@phosphor-icons/react'],
},
};
export default nextConfig;

View File

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

268
package-lock.json generated
View File

@@ -70,7 +70,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",
@@ -80,9 +79,10 @@
},
"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",
"autoprefixer": "^10.4.23",
"eslint": "^9.28.0",
"eslint-config-next": "^16.1.3",
"eslint-plugin-react-hooks": "^5.2.0",
@@ -4726,9 +4726,9 @@
}
},
"node_modules/@tailwindcss/node": {
"version": "4.1.17",
"resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.1.17.tgz",
"integrity": "sha512-csIkHIgLb3JisEFQ0vxr2Y57GUNYh447C8xzwj89U/8fdW8LhProdxvnVH6U8M2Y73QKiTIH+LWbK3V2BBZsAg==",
"version": "4.1.18",
"resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.1.18.tgz",
"integrity": "sha512-DoR7U1P7iYhw16qJ49fgXUlry1t4CpXeErJHnQ44JgTSKMaZUdf17cfn5mHchfJ4KRBZRFA/Coo+MUF5+gOaCQ==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -4738,37 +4738,37 @@
"lightningcss": "1.30.2",
"magic-string": "^0.30.21",
"source-map-js": "^1.2.1",
"tailwindcss": "4.1.17"
"tailwindcss": "4.1.18"
}
},
"node_modules/@tailwindcss/oxide": {
"version": "4.1.17",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.1.17.tgz",
"integrity": "sha512-F0F7d01fmkQhsTjXezGBLdrl1KresJTcI3DB8EkScCldyKp3Msz4hub4uyYaVnk88BAS1g5DQjjF6F5qczheLA==",
"version": "4.1.18",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.1.18.tgz",
"integrity": "sha512-EgCR5tTS5bUSKQgzeMClT6iCY3ToqE1y+ZB0AKldj809QXk1Y+3jB0upOYZrn9aGIzPtUsP7sX4QQ4XtjBB95A==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 10"
},
"optionalDependencies": {
"@tailwindcss/oxide-android-arm64": "4.1.17",
"@tailwindcss/oxide-darwin-arm64": "4.1.17",
"@tailwindcss/oxide-darwin-x64": "4.1.17",
"@tailwindcss/oxide-freebsd-x64": "4.1.17",
"@tailwindcss/oxide-linux-arm-gnueabihf": "4.1.17",
"@tailwindcss/oxide-linux-arm64-gnu": "4.1.17",
"@tailwindcss/oxide-linux-arm64-musl": "4.1.17",
"@tailwindcss/oxide-linux-x64-gnu": "4.1.17",
"@tailwindcss/oxide-linux-x64-musl": "4.1.17",
"@tailwindcss/oxide-wasm32-wasi": "4.1.17",
"@tailwindcss/oxide-win32-arm64-msvc": "4.1.17",
"@tailwindcss/oxide-win32-x64-msvc": "4.1.17"
"@tailwindcss/oxide-android-arm64": "4.1.18",
"@tailwindcss/oxide-darwin-arm64": "4.1.18",
"@tailwindcss/oxide-darwin-x64": "4.1.18",
"@tailwindcss/oxide-freebsd-x64": "4.1.18",
"@tailwindcss/oxide-linux-arm-gnueabihf": "4.1.18",
"@tailwindcss/oxide-linux-arm64-gnu": "4.1.18",
"@tailwindcss/oxide-linux-arm64-musl": "4.1.18",
"@tailwindcss/oxide-linux-x64-gnu": "4.1.18",
"@tailwindcss/oxide-linux-x64-musl": "4.1.18",
"@tailwindcss/oxide-wasm32-wasi": "4.1.18",
"@tailwindcss/oxide-win32-arm64-msvc": "4.1.18",
"@tailwindcss/oxide-win32-x64-msvc": "4.1.18"
}
},
"node_modules/@tailwindcss/oxide-android-arm64": {
"version": "4.1.17",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.1.17.tgz",
"integrity": "sha512-BMqpkJHgOZ5z78qqiGE6ZIRExyaHyuxjgrJ6eBO5+hfrfGkuya0lYfw8fRHG77gdTjWkNWEEm+qeG2cDMxArLQ==",
"version": "4.1.18",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.1.18.tgz",
"integrity": "sha512-dJHz7+Ugr9U/diKJA0W6N/6/cjI+ZTAoxPf9Iz9BFRF2GzEX8IvXxFIi/dZBloVJX/MZGvRuFA9rqwdiIEZQ0Q==",
"cpu": [
"arm64"
],
@@ -4783,9 +4783,9 @@
}
},
"node_modules/@tailwindcss/oxide-darwin-arm64": {
"version": "4.1.17",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.1.17.tgz",
"integrity": "sha512-EquyumkQweUBNk1zGEU/wfZo2qkp/nQKRZM8bUYO0J+Lums5+wl2CcG1f9BgAjn/u9pJzdYddHWBiFXJTcxmOg==",
"version": "4.1.18",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.1.18.tgz",
"integrity": "sha512-Gc2q4Qhs660bhjyBSKgq6BYvwDz4G+BuyJ5H1xfhmDR3D8HnHCmT/BSkvSL0vQLy/nkMLY20PQ2OoYMO15Jd0A==",
"cpu": [
"arm64"
],
@@ -4800,9 +4800,9 @@
}
},
"node_modules/@tailwindcss/oxide-darwin-x64": {
"version": "4.1.17",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.1.17.tgz",
"integrity": "sha512-gdhEPLzke2Pog8s12oADwYu0IAw04Y2tlmgVzIN0+046ytcgx8uZmCzEg4VcQh+AHKiS7xaL8kGo/QTiNEGRog==",
"version": "4.1.18",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.1.18.tgz",
"integrity": "sha512-FL5oxr2xQsFrc3X9o1fjHKBYBMD1QZNyc1Xzw/h5Qu4XnEBi3dZn96HcHm41c/euGV+GRiXFfh2hUCyKi/e+yw==",
"cpu": [
"x64"
],
@@ -4817,9 +4817,9 @@
}
},
"node_modules/@tailwindcss/oxide-freebsd-x64": {
"version": "4.1.17",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.1.17.tgz",
"integrity": "sha512-hxGS81KskMxML9DXsaXT1H0DyA+ZBIbyG/sSAjWNe2EDl7TkPOBI42GBV3u38itzGUOmFfCzk1iAjDXds8Oh0g==",
"version": "4.1.18",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.1.18.tgz",
"integrity": "sha512-Fj+RHgu5bDodmV1dM9yAxlfJwkkWvLiRjbhuO2LEtwtlYlBgiAT4x/j5wQr1tC3SANAgD+0YcmWVrj8R9trVMA==",
"cpu": [
"x64"
],
@@ -4834,9 +4834,9 @@
}
},
"node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": {
"version": "4.1.17",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.1.17.tgz",
"integrity": "sha512-k7jWk5E3ldAdw0cNglhjSgv501u7yrMf8oeZ0cElhxU6Y2o7f8yqelOp3fhf7evjIS6ujTI3U8pKUXV2I4iXHQ==",
"version": "4.1.18",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.1.18.tgz",
"integrity": "sha512-Fp+Wzk/Ws4dZn+LV2Nqx3IilnhH51YZoRaYHQsVq3RQvEl+71VGKFpkfHrLM/Li+kt5c0DJe/bHXK1eHgDmdiA==",
"cpu": [
"arm"
],
@@ -4851,9 +4851,9 @@
}
},
"node_modules/@tailwindcss/oxide-linux-arm64-gnu": {
"version": "4.1.17",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.1.17.tgz",
"integrity": "sha512-HVDOm/mxK6+TbARwdW17WrgDYEGzmoYayrCgmLEw7FxTPLcp/glBisuyWkFz/jb7ZfiAXAXUACfyItn+nTgsdQ==",
"version": "4.1.18",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.1.18.tgz",
"integrity": "sha512-S0n3jboLysNbh55Vrt7pk9wgpyTTPD0fdQeh7wQfMqLPM/Hrxi+dVsLsPrycQjGKEQk85Kgbx+6+QnYNiHalnw==",
"cpu": [
"arm64"
],
@@ -4868,9 +4868,9 @@
}
},
"node_modules/@tailwindcss/oxide-linux-arm64-musl": {
"version": "4.1.17",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.1.17.tgz",
"integrity": "sha512-HvZLfGr42i5anKtIeQzxdkw/wPqIbpeZqe7vd3V9vI3RQxe3xU1fLjss0TjyhxWcBaipk7NYwSrwTwK1hJARMg==",
"version": "4.1.18",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.1.18.tgz",
"integrity": "sha512-1px92582HkPQlaaCkdRcio71p8bc8i/ap5807tPRDK/uw953cauQBT8c5tVGkOwrHMfc2Yh6UuxaH4vtTjGvHg==",
"cpu": [
"arm64"
],
@@ -4885,9 +4885,9 @@
}
},
"node_modules/@tailwindcss/oxide-linux-x64-gnu": {
"version": "4.1.17",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.1.17.tgz",
"integrity": "sha512-M3XZuORCGB7VPOEDH+nzpJ21XPvK5PyjlkSFkFziNHGLc5d6g3di2McAAblmaSUNl8IOmzYwLx9NsE7bplNkwQ==",
"version": "4.1.18",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.1.18.tgz",
"integrity": "sha512-v3gyT0ivkfBLoZGF9LyHmts0Isc8jHZyVcbzio6Wpzifg/+5ZJpDiRiUhDLkcr7f/r38SWNe7ucxmGW3j3Kb/g==",
"cpu": [
"x64"
],
@@ -4902,9 +4902,9 @@
}
},
"node_modules/@tailwindcss/oxide-linux-x64-musl": {
"version": "4.1.17",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.1.17.tgz",
"integrity": "sha512-k7f+pf9eXLEey4pBlw+8dgfJHY4PZ5qOUFDyNf7SI6lHjQ9Zt7+NcscjpwdCEbYi6FI5c2KDTDWyf2iHcCSyyQ==",
"version": "4.1.18",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.1.18.tgz",
"integrity": "sha512-bhJ2y2OQNlcRwwgOAGMY0xTFStt4/wyU6pvI6LSuZpRgKQwxTec0/3Scu91O8ir7qCR3AuepQKLU/kX99FouqQ==",
"cpu": [
"x64"
],
@@ -4919,9 +4919,9 @@
}
},
"node_modules/@tailwindcss/oxide-wasm32-wasi": {
"version": "4.1.17",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.1.17.tgz",
"integrity": "sha512-cEytGqSSoy7zK4JRWiTCx43FsKP/zGr0CsuMawhH67ONlH+T79VteQeJQRO/X7L0juEUA8ZyuYikcRBf0vsxhg==",
"version": "4.1.18",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.1.18.tgz",
"integrity": "sha512-LffYTvPjODiP6PT16oNeUQJzNVyJl1cjIebq/rWWBF+3eDst5JGEFSc5cWxyRCJ0Mxl+KyIkqRxk1XPEs9x8TA==",
"bundleDependencies": [
"@napi-rs/wasm-runtime",
"@emnapi/core",
@@ -4937,10 +4937,10 @@
"license": "MIT",
"optional": true,
"dependencies": {
"@emnapi/core": "^1.6.0",
"@emnapi/runtime": "^1.6.0",
"@emnapi/core": "^1.7.1",
"@emnapi/runtime": "^1.7.1",
"@emnapi/wasi-threads": "^1.1.0",
"@napi-rs/wasm-runtime": "^1.0.7",
"@napi-rs/wasm-runtime": "^1.1.0",
"@tybys/wasm-util": "^0.10.1",
"tslib": "^2.4.0"
},
@@ -4948,70 +4948,10 @@
"node": ">=14.0.0"
}
},
"node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/core": {
"version": "1.6.0",
"dev": true,
"inBundle": true,
"license": "MIT",
"optional": true,
"dependencies": {
"@emnapi/wasi-threads": "1.1.0",
"tslib": "^2.4.0"
}
},
"node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/runtime": {
"version": "1.6.0",
"dev": true,
"inBundle": true,
"license": "MIT",
"optional": true,
"dependencies": {
"tslib": "^2.4.0"
}
},
"node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/wasi-threads": {
"version": "1.1.0",
"dev": true,
"inBundle": true,
"license": "MIT",
"optional": true,
"dependencies": {
"tslib": "^2.4.0"
}
},
"node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@napi-rs/wasm-runtime": {
"version": "1.0.7",
"dev": true,
"inBundle": true,
"license": "MIT",
"optional": true,
"dependencies": {
"@emnapi/core": "^1.5.0",
"@emnapi/runtime": "^1.5.0",
"@tybys/wasm-util": "^0.10.1"
}
},
"node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@tybys/wasm-util": {
"version": "0.10.1",
"dev": true,
"inBundle": true,
"license": "MIT",
"optional": true,
"dependencies": {
"tslib": "^2.4.0"
}
},
"node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/tslib": {
"version": "2.8.1",
"dev": true,
"inBundle": true,
"license": "0BSD",
"optional": true
},
"node_modules/@tailwindcss/oxide-win32-arm64-msvc": {
"version": "4.1.17",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.1.17.tgz",
"integrity": "sha512-JU5AHr7gKbZlOGvMdb4722/0aYbU+tN6lv1kONx0JK2cGsh7g148zVWLM0IKR3NeKLv+L90chBVYcJ8uJWbC9A==",
"version": "4.1.18",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.1.18.tgz",
"integrity": "sha512-HjSA7mr9HmC8fu6bdsZvZ+dhjyGCLdotjVOgLA2vEqxEBZaQo9YTX4kwgEvPCpRh8o4uWc4J/wEoFzhEmjvPbA==",
"cpu": [
"arm64"
],
@@ -5026,9 +4966,9 @@
}
},
"node_modules/@tailwindcss/oxide-win32-x64-msvc": {
"version": "4.1.17",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.1.17.tgz",
"integrity": "sha512-SKWM4waLuqx0IH+FMDUw6R66Hu4OuTALFgnleKbqhgGU30DY20NORZMZUKgLRjQXNN2TLzKvh48QXTig4h4bGw==",
"version": "4.1.18",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.1.18.tgz",
"integrity": "sha512-bJWbyYpUlqamC8dpR7pfjA0I7vdF6t5VpUGMWRkXVE3AXgIZjYUYAK7II1GNaxR8J1SSrSrppRar8G++JekE3Q==",
"cpu": [
"x64"
],
@@ -5043,17 +4983,17 @@
}
},
"node_modules/@tailwindcss/postcss": {
"version": "4.1.17",
"resolved": "https://registry.npmjs.org/@tailwindcss/postcss/-/postcss-4.1.17.tgz",
"integrity": "sha512-+nKl9N9mN5uJ+M7dBOOCzINw94MPstNR/GtIhz1fpZysxL/4a+No64jCBD6CPN+bIHWFx3KWuu8XJRrj/572Dw==",
"version": "4.1.18",
"resolved": "https://registry.npmjs.org/@tailwindcss/postcss/-/postcss-4.1.18.tgz",
"integrity": "sha512-Ce0GFnzAOuPyfV5SxjXGn0CubwGcuDB0zcdaPuCSzAa/2vII24JTkH+I6jcbXLb1ctjZMZZI6OjDaLPJQL1S0g==",
"dev": true,
"license": "MIT",
"dependencies": {
"@alloc/quick-lru": "^5.2.0",
"@tailwindcss/node": "4.1.17",
"@tailwindcss/oxide": "4.1.17",
"@tailwindcss/node": "4.1.18",
"@tailwindcss/oxide": "4.1.18",
"postcss": "^8.4.41",
"tailwindcss": "4.1.17"
"tailwindcss": "4.1.18"
}
},
"node_modules/@tanstack/query-core": {
@@ -6142,6 +6082,43 @@
"node": ">= 0.4"
}
},
"node_modules/autoprefixer": {
"version": "10.4.23",
"resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.23.tgz",
"integrity": "sha512-YYTXSFulfwytnjAPlw8QHncHJmlvFKtczb8InXaAx9Q0LbfDnfEYDE55omerIJKihhmU61Ft+cAOSzQVaBUmeA==",
"dev": true,
"funding": [
{
"type": "opencollective",
"url": "https://opencollective.com/postcss/"
},
{
"type": "tidelift",
"url": "https://tidelift.com/funding/github/npm/autoprefixer"
},
{
"type": "github",
"url": "https://github.com/sponsors/ai"
}
],
"license": "MIT",
"dependencies": {
"browserslist": "^4.28.1",
"caniuse-lite": "^1.0.30001760",
"fraction.js": "^5.3.4",
"picocolors": "^1.1.1",
"postcss-value-parser": "^4.2.0"
},
"bin": {
"autoprefixer": "bin/autoprefixer"
},
"engines": {
"node": "^10 || ^12 || >=14"
},
"peerDependencies": {
"postcss": "^8.1.0"
}
},
"node_modules/available-typed-arrays": {
"version": "1.0.7",
"resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz",
@@ -7236,9 +7213,9 @@
}
},
"node_modules/enhanced-resolve": {
"version": "5.18.3",
"resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.3.tgz",
"integrity": "sha512-d4lC8xfavMeBjzGr2vECC3fsGXziXZQyJxD868h2M/mBI3PwAuODxAkLkq5HYuvrPYcUtiLzsTo8U3PgX3Ocww==",
"version": "5.18.4",
"resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.4.tgz",
"integrity": "sha512-LgQMM4WXU3QI+SYgEc2liRgznaD5ojbmY3sb8LxyguVkIg5FxdpTkvk72te2R38/TGKxH634oLxXRGY6d7AP+Q==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -8417,6 +8394,20 @@
"node": ">= 0.6"
}
},
"node_modules/fraction.js": {
"version": "5.3.4",
"resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-5.3.4.tgz",
"integrity": "sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ==",
"dev": true,
"license": "MIT",
"engines": {
"node": "*"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/rawify"
}
},
"node_modules/framer-motion": {
"version": "12.23.25",
"resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-12.23.25.tgz",
@@ -10494,6 +10485,13 @@
"node": "^10 || ^12 || >=14"
}
},
"node_modules/postcss-value-parser": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz",
"integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==",
"dev": true,
"license": "MIT"
},
"node_modules/prelude-ls": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz",
@@ -11513,12 +11511,6 @@
"node": ">=0.10.0"
}
},
"node_modules/sql.js": {
"version": "1.13.0",
"resolved": "https://registry.npmjs.org/sql.js/-/sql.js-1.13.0.tgz",
"integrity": "sha512-RJbVP1HRDlUUXahJ7VMTcu9Rm1Nzw+EBpoPr94vnbD4LwR715F3CcxE2G2k45PewcaZ57pjetYa+LoSJLAASgA==",
"license": "MIT"
},
"node_modules/stable-hash": {
"version": "0.0.5",
"resolved": "https://registry.npmjs.org/stable-hash/-/stable-hash-0.0.5.tgz",
@@ -11751,9 +11743,9 @@
}
},
"node_modules/tailwindcss": {
"version": "4.1.17",
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.17.tgz",
"integrity": "sha512-j9Ee2YjuQqYT9bbRTfTZht9W/ytp5H+jJpZKiYdP/bpnXARAuELt9ofP0lPnmHjbga7SNQIxdTAXCmtKVYjN+Q==",
"version": "4.1.18",
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.18.tgz",
"integrity": "sha512-4+Z+0yiYyEtUVCScyfHCxOYP06L5Ne+JiHhY2IjR2KWMIWhJOYZKLSGZaP5HkZ8+bY0cxfzwDE5uOmzFXyIwxw==",
"license": "MIT"
},
"node_modules/tapable": {

View File

@@ -72,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",
@@ -82,9 +81,10 @@
},
"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",
"autoprefixer": "^10.4.23",
"eslint": "^9.28.0",
"eslint-config-next": "^16.1.3",
"eslint-plugin-react-hooks": "^5.2.0",

5
postcss.config.js Normal file
View File

@@ -0,0 +1,5 @@
module.exports = {
plugins: {
'@tailwindcss/postcss': {},
},
}

View File

@@ -1,115 +0,0 @@
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'
function AppContent() {
const { menuOpen } = useNavigation()
return (
<div className="min-h-screen bg-background">
<div
className="fixed inset-0 opacity-[0.03] pointer-events-none"
style={{
backgroundImage: `
repeating-linear-gradient(
0deg,
transparent,
transparent 40px,
oklch(0.75 0.18 200) 40px,
oklch(0.75 0.18 200) 41px
),
repeating-linear-gradient(
90deg,
transparent,
transparent 40px,
oklch(0.75 0.18 200) 40px,
oklch(0.75 0.18 200) 41px
)
`,
}}
/>
<NavigationSidebar />
<motion.div
initial={false}
animate={{ marginLeft: menuOpen ? 320 : 0 }}
transition={{ type: 'spring', damping: 30, stiffness: 300 }}
className="relative z-10"
>
<header className="border-b border-border bg-background/90 backdrop-blur-md sticky top-0 z-20">
<div className="container mx-auto px-6 py-6">
<div className="flex items-center justify-between">
<motion.div
initial={{ opacity: 0, x: -20 }}
animate={{ opacity: 1, x: 0 }}
transition={{ duration: 0.4 }}
className="flex items-center gap-3"
>
<Navigation />
<div className="h-10 w-10 rounded-lg bg-gradient-to-br from-primary to-accent flex items-center justify-center">
<Code className="h-5 w-5 text-primary-foreground" weight="bold" />
</div>
<h1 className="text-2xl font-bold tracking-tight bg-gradient-to-r from-primary via-accent to-primary bg-clip-text text-transparent">
CodeSnippet
</h1>
</motion.div>
<motion.div
initial={{ opacity: 0, x: 20 }}
animate={{ opacity: 1, x: 0 }}
transition={{ duration: 0.4, delay: 0.1 }}
>
<BackendIndicator />
</motion.div>
</div>
</div>
</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>
</main>
<footer className="border-t border-border mt-24">
<div className="container mx-auto px-6 py-8">
<div className="text-center text-sm text-muted-foreground">
<p>Save, organize, and share your code snippets with beautiful syntax highlighting and live execution</p>
<p className="mt-2 text-xs">Supports React preview and Python execution via Pyodide</p>
</div>
</div>
</footer>
</motion.div>
</div>
)
}
function App() {
return (
<Router>
<NavigationProvider>
<AppContent />
</NavigationProvider>
</Router>
)
}
export default App

View File

@@ -5,8 +5,8 @@ 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 '@/pages/demo-constants';
import { DemoFeatureCards } from '@/pages/DemoFeatureCards';
import { DEMO_CODE } from '@/components/demo/demo-constants';
import { DemoFeatureCards } from '@/components/demo/DemoFeatureCards';
import { PageLayout } from '../PageLayout';
export default function DemoPage() {

49
src/app/globals.css Normal file
View 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;
}

View File

@@ -1,23 +1,7 @@
import type { Metadata } from 'next';
import { Inter, JetBrains_Mono, Bricolage_Grotesque } from 'next/font/google';
import './globals.css';
import { Providers } from './providers';
const inter = Inter({
subsets: ['latin'],
variable: '--font-inter',
});
const jetbrainsMono = JetBrains_Mono({
subsets: ['latin'],
variable: '--font-jetbrains-mono',
});
const bricolageGrotesque = Bricolage_Grotesque({
subsets: ['latin'],
variable: '--font-bricolage-grotesque',
});
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',
@@ -30,7 +14,12 @@ export default function RootLayout({
}>) {
return (
<html lang="en" suppressHydrationWarning>
<body className={`${inter.variable} ${jetbrainsMono.variable} ${bricolageGrotesque.variable}`}>
<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>

262
src/app/theme.css Normal file
View 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);
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View 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();
});
}

View File

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

View File

@@ -16,6 +16,7 @@ export interface Snippet {
category: string
namespaceId?: string
hasPreview?: boolean
isTemplate?: boolean
functionName?: string
inputParameters?: InputParameter[]
createdAt: number

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -6,7 +6,7 @@
"DOM",
"DOM.Iterable"
],
"jsx": "preserve",
"jsx": "react-jsx",
"module": "ESNext",
"moduleResolution": "bundler",
"resolveJsonModule": true,
@@ -36,7 +36,8 @@
"next-env.d.ts",
"**/*.ts",
"**/*.tsx",
".next/types/**/*.ts"
".next/types/**/*.ts",
".next/dev/types/**/*.ts"
],
"exclude": [
"node_modules"

View File

@@ -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')
}
},
});