mirror of
https://github.com/johndoe6345789/snippet-pastebin.git
synced 2026-04-24 13:34:55 +00:00
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:
@@ -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;
|
||||
}
|
||||
19
index.html
19
index.html
@@ -1,19 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>CodeSnippet - Share & Run Code (Python, React & More)</title>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Bricolage+Grotesque:wght@400;500;600;700&family=Inter:wght@400;500;600&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
|
||||
<link href="/src/main.css" rel="stylesheet" />
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/main.tsx"></script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
14
next.config.js
Normal file
14
next.config.js
Normal file
@@ -0,0 +1,14 @@
|
||||
/** @type {import('next').NextConfig} */
|
||||
const nextConfig = {
|
||||
output: process.env.BUILD_STATIC ? 'export' : 'standalone',
|
||||
basePath: process.env.NEXT_PUBLIC_BASE_PATH || '',
|
||||
turbopack: {},
|
||||
images: {
|
||||
unoptimized: true,
|
||||
},
|
||||
experimental: {
|
||||
optimizePackageImports: ['@radix-ui/react-icons', '@phosphor-icons/react'],
|
||||
},
|
||||
};
|
||||
|
||||
export default nextConfig;
|
||||
@@ -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;
|
||||
29
nginx.conf
29
nginx.conf
@@ -1,29 +0,0 @@
|
||||
server {
|
||||
listen 3000;
|
||||
server_name localhost;
|
||||
root /usr/share/nginx/html;
|
||||
index index.html;
|
||||
|
||||
location / {
|
||||
try_files $uri $uri/ /index.html;
|
||||
|
||||
add_header Cache-Control "no-cache, no-store, must-revalidate";
|
||||
add_header Pragma "no-cache";
|
||||
add_header Expires "0";
|
||||
}
|
||||
|
||||
location /api {
|
||||
proxy_pass http://backend:5000;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection 'upgrade';
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_cache_bypass $http_upgrade;
|
||||
|
||||
proxy_buffering off;
|
||||
proxy_request_buffering off;
|
||||
}
|
||||
}
|
||||
268
package-lock.json
generated
268
package-lock.json
generated
@@ -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": {
|
||||
|
||||
@@ -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
5
postcss.config.js
Normal file
@@ -0,0 +1,5 @@
|
||||
module.exports = {
|
||||
plugins: {
|
||||
'@tailwindcss/postcss': {},
|
||||
},
|
||||
}
|
||||
115
src/App.tsx
115
src/App.tsx
@@ -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
|
||||
@@ -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
49
src/app/globals.css
Normal file
@@ -0,0 +1,49 @@
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
@layer base {
|
||||
:root {
|
||||
--background: 222.2 84% 4.9%;
|
||||
--foreground: 210 40% 98%;
|
||||
--card: 222.2 84% 4.9%;
|
||||
--card-foreground: 210 40% 98%;
|
||||
--popover: 222.2 84% 4.9%;
|
||||
--popover-foreground: 210 40% 98%;
|
||||
--primary: 263 70% 50%;
|
||||
--primary-foreground: 210 40% 98%;
|
||||
--secondary: 217.2 32.6% 17.5%;
|
||||
--secondary-foreground: 210 40% 98%;
|
||||
--muted: 217.2 32.6% 17.5%;
|
||||
--muted-foreground: 215 20.2% 65.1%;
|
||||
--accent: 195 100% 70%;
|
||||
--accent-foreground: 222.2 84% 4.9%;
|
||||
--destructive: 0 62.8% 30.6%;
|
||||
--destructive-foreground: 210 40% 98%;
|
||||
--border: 217.2 32.6% 17.5%;
|
||||
--input: 217.2 32.6% 17.5%;
|
||||
--ring: 195 100% 70%;
|
||||
--radius: 0.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
@layer base {
|
||||
* {
|
||||
@apply border-border;
|
||||
}
|
||||
body {
|
||||
@apply bg-background text-foreground;
|
||||
}
|
||||
}
|
||||
|
||||
h1, h2, h3, h4, h5, h6 {
|
||||
font-family: var(--font-inter), 'Inter', sans-serif;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: var(--font-inter), 'Inter', sans-serif;
|
||||
}
|
||||
|
||||
code, pre {
|
||||
font-family: var(--font-jetbrains-mono), 'JetBrains Mono', monospace;
|
||||
}
|
||||
@@ -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
262
src/app/theme.css
Normal file
@@ -0,0 +1,262 @@
|
||||
@import "tailwindcss";
|
||||
|
||||
@import '@radix-ui/colors/sage-dark.css' layer(base);
|
||||
@import '@radix-ui/colors/olive.css' layer(base);
|
||||
@import '@radix-ui/colors/olive-dark.css' layer(base);
|
||||
@import '@radix-ui/colors/sand.css' layer(base);
|
||||
@import '@radix-ui/colors/sand-dark.css' layer(base);
|
||||
@import '@radix-ui/colors/red.css' layer(base);
|
||||
@import '@radix-ui/colors/red-dark.css' layer(base);
|
||||
@import '@radix-ui/colors/ruby.css' layer(base);
|
||||
@import '@radix-ui/colors/ruby-dark.css' layer(base);
|
||||
@import '@radix-ui/colors/crimson.css' layer(base);
|
||||
@import '@radix-ui/colors/crimson-dark.css' layer(base);
|
||||
@import '@radix-ui/colors/pink.css' layer(base);
|
||||
@import '@radix-ui/colors/pink-dark.css' layer(base);
|
||||
@import '@radix-ui/colors/plum.css' layer(base);
|
||||
@import '@radix-ui/colors/plum-dark.css' layer(base);
|
||||
@import '@radix-ui/colors/purple.css' layer(base);
|
||||
@import '@radix-ui/colors/purple-dark.css' layer(base);
|
||||
@import '@radix-ui/colors/violet.css' layer(base);
|
||||
@import '@radix-ui/colors/violet-dark.css' layer(base);
|
||||
@import '@radix-ui/colors/iris.css' layer(base);
|
||||
@import '@radix-ui/colors/iris-dark.css' layer(base);
|
||||
@import '@radix-ui/colors/indigo.css' layer(base);
|
||||
@import '@radix-ui/colors/indigo-dark.css' layer(base);
|
||||
@import '@radix-ui/colors/blue.css' layer(base);
|
||||
@import '@radix-ui/colors/blue-dark.css' layer(base);
|
||||
@import '@radix-ui/colors/cyan.css' layer(base);
|
||||
@import '@radix-ui/colors/cyan-dark.css' layer(base);
|
||||
@import '@radix-ui/colors/teal.css' layer(base);
|
||||
@import '@radix-ui/colors/teal-dark.css' layer(base);
|
||||
@import '@radix-ui/colors/jade.css' layer(base);
|
||||
@import '@radix-ui/colors/jade-dark.css' layer(base);
|
||||
@import '@radix-ui/colors/green.css' layer(base);
|
||||
@import '@radix-ui/colors/green-dark.css' layer(base);
|
||||
@import '@radix-ui/colors/grass.css' layer(base);
|
||||
@import '@radix-ui/colors/grass-dark.css' layer(base);
|
||||
@import '@radix-ui/colors/bronze.css' layer(base);
|
||||
@import '@radix-ui/colors/bronze-dark.css' layer(base);
|
||||
@import '@radix-ui/colors/gold.css' layer(base);
|
||||
@import '@radix-ui/colors/gold-dark.css' layer(base);
|
||||
@import '@radix-ui/colors/brown.css' layer(base);
|
||||
@import '@radix-ui/colors/brown-dark.css' layer(base);
|
||||
@import '@radix-ui/colors/orange.css' layer(base);
|
||||
@import '@radix-ui/colors/orange-dark.css' layer(base);
|
||||
@import '@radix-ui/colors/amber.css' layer(base);
|
||||
@import '@radix-ui/colors/amber-dark.css' layer(base);
|
||||
@import '@radix-ui/colors/yellow.css' layer(base);
|
||||
@import '@radix-ui/colors/yellow-dark.css' layer(base);
|
||||
@import '@radix-ui/colors/lime.css' layer(base);
|
||||
@import '@radix-ui/colors/lime-dark.css' layer(base);
|
||||
@import '@radix-ui/colors/mint.css' layer(base);
|
||||
@import '@radix-ui/colors/mint-dark.css' layer(base);
|
||||
@import '@radix-ui/colors/sky.css' layer(base);
|
||||
@import '@radix-ui/colors/sky-dark.css' layer(base);
|
||||
@import '@radix-ui/colors/tomato.css' layer(base);
|
||||
@import '@radix-ui/colors/tomato-dark.css' layer(base);
|
||||
@import '@radix-ui/colors/gray.css' layer(base);
|
||||
@import '@radix-ui/colors/gray-dark.css' layer(base);
|
||||
@import '@radix-ui/colors/mauve.css' layer(base);
|
||||
@import '@radix-ui/colors/mauve-dark.css' layer(base);
|
||||
@import '@radix-ui/colors/slate.css' layer(base);
|
||||
@import '@radix-ui/colors/slate-dark.css' layer(base);
|
||||
@import '@radix-ui/colors/slate-alpha.css' layer(base);
|
||||
@import '@radix-ui/colors/slate-dark-alpha.css' layer(base);
|
||||
|
||||
@import 'tailwindcss/theme' layer(theme);
|
||||
|
||||
@import 'tailwindcss/preflight' layer(base);
|
||||
|
||||
/*
|
||||
The default border color has changed to `currentColor` in Tailwind CSS v4,
|
||||
so we've added these compatibility styles to make sure everything still
|
||||
looks the same as it did with Tailwind CSS v3.
|
||||
|
||||
If we ever want to remove these styles, we need to add an explicit border
|
||||
color utility to any element that depends on these defaults.
|
||||
*/
|
||||
@layer base {
|
||||
*,
|
||||
::after,
|
||||
::before,
|
||||
::backdrop,
|
||||
::file-selector-button {
|
||||
border-color: var(--color-gray-200, currentColor);
|
||||
}
|
||||
}
|
||||
|
||||
@layer base {
|
||||
#spark-app {
|
||||
--tomato-contrast: #fff;
|
||||
--red-contrast: #fff;
|
||||
--ruby-contrast: #fff;
|
||||
--crimson-contrast: #fff;
|
||||
--pink-contrast: #fff;
|
||||
--plum-contrast: #fff;
|
||||
--purple-contrast: #fff;
|
||||
--violet-contrast: #fff;
|
||||
--iris-contrast: #fff;
|
||||
--indigo-contrast: #fff;
|
||||
--blue-contrast: #fff;
|
||||
--cyan-contrast: #fff;
|
||||
--teal-contrast: #fff;
|
||||
--jade-contrast: #fff;
|
||||
--green-contrast: #fff;
|
||||
--grass-contrast: #fff;
|
||||
--bronze-contrast: #fff;
|
||||
--gold-contrast: #fff;
|
||||
--brown-contrast: #fff;
|
||||
--orange-contrast: #fff;
|
||||
--amber-contrast: #000;
|
||||
--yellow-contrast: #000;
|
||||
--lime-contrast: #000;
|
||||
--mint-contrast: #000;
|
||||
--sky-contrast: #000;
|
||||
--gray-contrast: #fff;
|
||||
--mauve-contrast: #fff;
|
||||
--slate-contrast: #fff;
|
||||
--sage-contrast: #fff;
|
||||
--olive-contrast: #fff;
|
||||
--sand-contrast: #fff;
|
||||
|
||||
/**
|
||||
* Spacing scale
|
||||
*
|
||||
* These variables define a spacing scale based on Tailwind's default.
|
||||
* We've introduced a --size-scale variable as a multiplier.
|
||||
* By adjusting this single value, we can proportionally
|
||||
* scale all spacing throughout the entire application.
|
||||
*
|
||||
* https://tailwindcss.com/docs/customizing-spacing#default-spacing-scale
|
||||
*/
|
||||
--size-scale: 1;
|
||||
--size-0: 0px;
|
||||
--size-px: 1px;
|
||||
--size-0-5: calc(0.125rem * var(--size-scale));
|
||||
--size-1: calc(0.25rem * var(--size-scale));
|
||||
--size-1-5: calc(0.375rem * var(--size-scale));
|
||||
--size-2: calc(0.5rem * var(--size-scale));
|
||||
--size-2-5: calc(0.625rem * var(--size-scale));
|
||||
--size-3: calc(0.75rem * var(--size-scale));
|
||||
--size-3-5: calc(0.875rem * var(--size-scale));
|
||||
--size-4: calc(1rem * var(--size-scale));
|
||||
--size-5: calc(1.25rem * var(--size-scale));
|
||||
--size-6: calc(1.5rem * var(--size-scale));
|
||||
--size-7: calc(1.75rem * var(--size-scale));
|
||||
--size-8: calc(2rem * var(--size-scale));
|
||||
--size-9: calc(2.25rem * var(--size-scale));
|
||||
--size-10: calc(2.5rem * var(--size-scale));
|
||||
--size-11: calc(2.75rem * var(--size-scale));
|
||||
--size-12: calc(3rem * var(--size-scale));
|
||||
--size-14: calc(3.5rem * var(--size-scale));
|
||||
--size-16: calc(4rem * var(--size-scale));
|
||||
--size-20: calc(5rem * var(--size-scale));
|
||||
--size-24: calc(6rem * var(--size-scale));
|
||||
--size-28: calc(7rem * var(--size-scale));
|
||||
--size-32: calc(8rem * var(--size-scale));
|
||||
--size-36: calc(9rem * var(--size-scale));
|
||||
--size-40: calc(10rem * var(--size-scale));
|
||||
--size-44: calc(11rem * var(--size-scale));
|
||||
--size-48: calc(12rem * var(--size-scale));
|
||||
--size-52: calc(13rem * var(--size-scale));
|
||||
--size-56: calc(14rem * var(--size-scale));
|
||||
--size-60: calc(15rem * var(--size-scale));
|
||||
--size-64: calc(16rem * var(--size-scale));
|
||||
--size-72: calc(18rem * var(--size-scale));
|
||||
--size-80: calc(20rem * var(--size-scale));
|
||||
--size-96: calc(24rem * var(--size-scale));
|
||||
|
||||
/* Border radii */
|
||||
--radius-factor: 1;
|
||||
--radius-sm: calc(2px * var(--radius-factor) * var(--size-scale));
|
||||
--radius-md: calc(6px * var(--radius-factor) * var(--size-scale));
|
||||
--radius-lg: calc(8px * var(--radius-factor) * var(--size-scale));
|
||||
--radius-xl: calc(12px * var(--radius-factor) * var(--size-scale));
|
||||
--radius-2xl: calc(16px * var(--radius-factor) * var(--size-scale));
|
||||
--radius-full: 9999px;
|
||||
|
||||
/* Neutral colors */
|
||||
--color-neutral-1: var(--slate-1);
|
||||
--color-neutral-2: var(--slate-2);
|
||||
--color-neutral-3: var(--slate-3);
|
||||
--color-neutral-4: var(--slate-4);
|
||||
--color-neutral-5: var(--slate-5);
|
||||
--color-neutral-6: var(--slate-6);
|
||||
--color-neutral-7: var(--slate-7);
|
||||
--color-neutral-8: var(--slate-8);
|
||||
--color-neutral-9: var(--slate-9);
|
||||
--color-neutral-10: var(--slate-10);
|
||||
--color-neutral-11: var(--slate-11);
|
||||
--color-neutral-12: var(--slate-12);
|
||||
--color-neutral-a1: var(--slate-a1);
|
||||
--color-neutral-a2: var(--slate-a2);
|
||||
--color-neutral-a3: var(--slate-a3);
|
||||
--color-neutral-a4: var(--slate-a4);
|
||||
--color-neutral-a5: var(--slate-a5);
|
||||
--color-neutral-a6: var(--slate-a6);
|
||||
--color-neutral-a7: var(--slate-a7);
|
||||
--color-neutral-a8: var(--slate-a8);
|
||||
--color-neutral-a9: var(--slate-a9);
|
||||
--color-neutral-a10: var(--slate-a10);
|
||||
--color-neutral-a11: var(--slate-a11);
|
||||
--color-neutral-a12: var(--slate-a12);
|
||||
--color-neutral-contrast: var(--slate-contrast);
|
||||
|
||||
/* Accent colors */
|
||||
--color-accent-1: var(--blue-1);
|
||||
--color-accent-2: var(--blue-2);
|
||||
--color-accent-3: var(--blue-3);
|
||||
--color-accent-4: var(--blue-4);
|
||||
--color-accent-5: var(--blue-5);
|
||||
--color-accent-6: var(--blue-6);
|
||||
--color-accent-7: var(--blue-7);
|
||||
--color-accent-8: var(--blue-8);
|
||||
--color-accent-9: var(--blue-9);
|
||||
--color-accent-10: var(--blue-10);
|
||||
--color-accent-11: var(--blue-11);
|
||||
--color-accent-12: var(--blue-12);
|
||||
--color-accent-contrast: var(--blue-contrast);
|
||||
|
||||
/* Secondary accent colors */
|
||||
--color-accent-secondary-1: var(--violet-1);
|
||||
--color-accent-secondary-2: var(--violet-2);
|
||||
--color-accent-secondary-3: var(--violet-3);
|
||||
--color-accent-secondary-4: var(--violet-4);
|
||||
--color-accent-secondary-5: var(--violet-5);
|
||||
--color-accent-secondary-6: var(--violet-6);
|
||||
--color-accent-secondary-7: var(--violet-7);
|
||||
--color-accent-secondary-8: var(--violet-8);
|
||||
--color-accent-secondary-9: var(--violet-9);
|
||||
--color-accent-secondary-10: var(--violet-10);
|
||||
--color-accent-secondary-11: var(--violet-11);
|
||||
--color-accent-secondary-12: var(--violet-12);
|
||||
--color-accent-secondary-contrast: var(--violet-contrast);
|
||||
|
||||
/* Foreground colors */
|
||||
--color-fg: var(--color-neutral-12);
|
||||
--color-fg-secondary: var(--color-neutral-a11);
|
||||
|
||||
/* Background colors */
|
||||
--color-bg: #ffffff;
|
||||
--color-bg-inset: var(--color-neutral-2);
|
||||
--color-bg-overlay: #ffffff;
|
||||
|
||||
/* Focus ring */
|
||||
--color-focus-ring: var(--color-accent-9);
|
||||
|
||||
/* Fonts */
|
||||
--font-sans-serif: ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji",
|
||||
"Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
|
||||
--font-serif: ui-serif, Georgia, Cambria, "Times New Roman", Times, serif;
|
||||
--font-monospace: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas,
|
||||
"Liberation Mono", "Courier New", monospace;
|
||||
--font-family: var(--font-sans-serif);
|
||||
}
|
||||
|
||||
#spark-app.dark-theme {
|
||||
--color-bg: var(--color-neutral-1);
|
||||
--color-bg-inset: #000000;
|
||||
--color-bg-overlay: var(--color-neutral-3);
|
||||
}
|
||||
}
|
||||
@@ -59,7 +59,13 @@ export function NamespaceSelector({ selectedNamespaceId, onNamespaceChange }: Na
|
||||
|
||||
setLoading(true)
|
||||
try {
|
||||
const newNamespace = await createNamespace(newNamespaceName.trim())
|
||||
const newNamespace: Namespace = {
|
||||
id: Date.now().toString(),
|
||||
name: newNamespaceName.trim(),
|
||||
createdAt: Date.now(),
|
||||
isDefault: false,
|
||||
}
|
||||
await createNamespace(newNamespace)
|
||||
setNamespaces(prev => [...prev, newNamespace])
|
||||
setNewNamespaceName('')
|
||||
setCreateDialogOpen(false)
|
||||
|
||||
@@ -34,7 +34,7 @@ export function SnippetViewer({ snippet, open, onOpenChange, onEdit, onCopy }: S
|
||||
onEdit(snippet)
|
||||
}
|
||||
|
||||
const canPreview = snippet.hasPreview && appConfig.previewEnabledLanguages.includes(snippet.language)
|
||||
const canPreview = !!(snippet.hasPreview && appConfig.previewEnabledLanguages.includes(snippet.language))
|
||||
const isPython = snippet.language === 'Python'
|
||||
|
||||
return (
|
||||
|
||||
@@ -37,11 +37,7 @@ export function useDatabaseOperations() {
|
||||
setCheckingSchema(true)
|
||||
try {
|
||||
const result = await validateDatabaseSchema()
|
||||
setSchemaHealth(result.valid ? 'healthy' : 'corrupted')
|
||||
|
||||
if (!result.valid) {
|
||||
console.warn('Schema validation failed:', result.issues)
|
||||
}
|
||||
setSchemaHealth(result ? 'healthy' : 'corrupted')
|
||||
} catch (error) {
|
||||
console.error('Schema check failed:', error)
|
||||
setSchemaHealth('corrupted')
|
||||
@@ -52,12 +48,12 @@ export function useDatabaseOperations() {
|
||||
|
||||
const handleExport = useCallback(async () => {
|
||||
try {
|
||||
const data = await exportDatabase()
|
||||
const blob = new Blob([new Uint8Array(data)], { type: 'application/octet-stream' })
|
||||
const jsonData = await exportDatabase()
|
||||
const blob = new Blob([jsonData], { type: 'application/json' })
|
||||
const url = URL.createObjectURL(blob)
|
||||
const a = document.createElement('a')
|
||||
a.href = url
|
||||
a.download = `codesnippet-backup-${Date.now()}.db`
|
||||
a.download = `codesnippet-backup-${Date.now()}.json`
|
||||
document.body.appendChild(a)
|
||||
a.click()
|
||||
document.body.removeChild(a)
|
||||
@@ -74,9 +70,8 @@ export function useDatabaseOperations() {
|
||||
if (!file) return
|
||||
|
||||
try {
|
||||
const arrayBuffer = await file.arrayBuffer()
|
||||
const data = new Uint8Array(arrayBuffer)
|
||||
await importDatabase(data)
|
||||
const text = await file.text()
|
||||
await importDatabase(text)
|
||||
toast.success('Database imported successfully')
|
||||
await loadStats()
|
||||
} catch (error) {
|
||||
|
||||
@@ -51,6 +51,10 @@ export function useSettingsState() {
|
||||
await migrateToFlask(flaskUrl, loadStats)
|
||||
}
|
||||
|
||||
const handleMigrateToIndexedDBWrapper = async () => {
|
||||
await handleMigrateToIndexedDB(flaskUrl)
|
||||
}
|
||||
|
||||
return {
|
||||
stats,
|
||||
loading,
|
||||
@@ -72,7 +76,7 @@ export function useSettingsState() {
|
||||
handleTestConnection,
|
||||
handleSaveStorageConfig,
|
||||
handleMigrateToFlask,
|
||||
handleMigrateToIndexedDB,
|
||||
handleMigrateToIndexedDB: handleMigrateToIndexedDBWrapper,
|
||||
checkSchemaHealth,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
/**
|
||||
* Database constants shared across modules
|
||||
*/
|
||||
|
||||
export const DB_KEY = 'codesnippet-db'
|
||||
export const IDB_NAME = 'CodeSnippetDB'
|
||||
export const IDB_STORE = 'database'
|
||||
export const IDB_VERSION = 1
|
||||
@@ -1,19 +0,0 @@
|
||||
import { deleteFromIndexedDB } from '../db-indexeddb'
|
||||
import { deleteFromLocalStorage } from '../db-localstorage'
|
||||
import { getFlaskAdapter } from './getFlaskAdapter'
|
||||
import { initDB } from './initDB'
|
||||
import { dbState } from './state'
|
||||
|
||||
export async function clearDatabase(): Promise<void> {
|
||||
const adapter = getFlaskAdapter()
|
||||
if (adapter) {
|
||||
await adapter.wipeDatabase()
|
||||
return
|
||||
}
|
||||
|
||||
await deleteFromIndexedDB()
|
||||
deleteFromLocalStorage()
|
||||
|
||||
dbState.dbInstance = null
|
||||
await initDB()
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
import { initDB } from './initDB'
|
||||
|
||||
export async function exportDatabase(): Promise<Uint8Array> {
|
||||
const db = await initDB()
|
||||
return db.export()
|
||||
}
|
||||
@@ -1,32 +0,0 @@
|
||||
import { openIndexedDB } from '../db-indexeddb'
|
||||
import { DB_KEY } from '../db-constants'
|
||||
import { initDB } from './initDB'
|
||||
|
||||
export async function getDatabaseStats(): Promise<{
|
||||
snippetCount: number
|
||||
templateCount: number
|
||||
storageType: 'indexeddb' | 'localstorage' | 'none'
|
||||
databaseSize: number
|
||||
}> {
|
||||
const db = await initDB()
|
||||
|
||||
const snippetResult = db.exec('SELECT COUNT(*) as count FROM snippets')
|
||||
const templateResult = db.exec('SELECT COUNT(*) as count FROM snippet_templates')
|
||||
|
||||
const snippetCount = snippetResult[0]?.values[0]?.[0] as number || 0
|
||||
const templateCount = templateResult[0]?.values[0]?.[0] as number || 0
|
||||
|
||||
const data = db.export()
|
||||
const databaseSize = data.length
|
||||
|
||||
const hasIDB = await openIndexedDB()
|
||||
const hasLocalStorage = typeof localStorage !== 'undefined' && localStorage.getItem(DB_KEY) !== null
|
||||
const storageType = hasIDB ? 'indexeddb' : (hasLocalStorage ? 'localstorage' : 'none')
|
||||
|
||||
return {
|
||||
snippetCount,
|
||||
templateCount,
|
||||
storageType,
|
||||
databaseSize,
|
||||
}
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
import { FlaskStorageAdapter, getStorageConfig, loadStorageConfig } from '../storage'
|
||||
import { dbState } from './state'
|
||||
|
||||
export function getFlaskAdapter(): FlaskStorageAdapter | null {
|
||||
if (!dbState.configLoaded) {
|
||||
loadStorageConfig()
|
||||
dbState.configLoaded = true
|
||||
}
|
||||
|
||||
const config = getStorageConfig()
|
||||
if (config.backend === 'flask' && config.flaskUrl) {
|
||||
try {
|
||||
if (!dbState.flaskAdapter || dbState.flaskAdapter['baseUrl'] !== config.flaskUrl) {
|
||||
dbState.flaskAdapter = new FlaskStorageAdapter(config.flaskUrl)
|
||||
}
|
||||
return dbState.flaskAdapter
|
||||
} catch (error) {
|
||||
console.warn('Failed to create Flask adapter:', error)
|
||||
return null
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
import initSqlJs from 'sql.js'
|
||||
import { saveDB } from './saveDB'
|
||||
import { dbState } from './state'
|
||||
|
||||
export async function importDatabase(data: Uint8Array): Promise<void> {
|
||||
if (!dbState.sqlInstance) {
|
||||
dbState.sqlInstance = await initSqlJs({
|
||||
locateFile: (file) => `https://sql.js.org/dist/${file}`,
|
||||
})
|
||||
}
|
||||
|
||||
try {
|
||||
dbState.dbInstance = new dbState.sqlInstance.Database(data)
|
||||
await saveDB()
|
||||
} catch (error) {
|
||||
console.error('Failed to import database:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
@@ -1,57 +0,0 @@
|
||||
import initSqlJs, { Database } from 'sql.js'
|
||||
import { loadFromIndexedDB } from '../db-indexeddb'
|
||||
import { loadFromLocalStorage } from '../db-localstorage'
|
||||
import { createTables, validateSchema } from '../db-schema'
|
||||
import { saveDB } from './saveDB'
|
||||
import { dbState } from './state'
|
||||
import { wipeAndRecreateDB } from './wipeAndRecreateDB'
|
||||
|
||||
export async function initDB(): Promise<Database> {
|
||||
if (dbState.dbInstance) return dbState.dbInstance
|
||||
|
||||
if (!dbState.sqlInstance) {
|
||||
dbState.sqlInstance = await initSqlJs({
|
||||
locateFile: (file) => `https://sql.js.org/dist/${file}`,
|
||||
})
|
||||
}
|
||||
|
||||
let loadedData: Uint8Array | null = null
|
||||
let schemaValid = false
|
||||
|
||||
loadedData = await loadFromIndexedDB()
|
||||
|
||||
if (!loadedData) {
|
||||
loadedData = loadFromLocalStorage()
|
||||
}
|
||||
|
||||
if (loadedData && loadedData.length > 0) {
|
||||
try {
|
||||
const testDb = new dbState.sqlInstance.Database(loadedData)
|
||||
schemaValid = await validateSchema(testDb)
|
||||
|
||||
if (schemaValid) {
|
||||
dbState.dbInstance = testDb
|
||||
} else {
|
||||
console.warn('Schema validation failed, wiping database')
|
||||
testDb.close()
|
||||
await wipeAndRecreateDB()
|
||||
dbState.dbInstance = new dbState.sqlInstance.Database()
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to load saved database, creating new one:', error)
|
||||
await wipeAndRecreateDB()
|
||||
dbState.dbInstance = new dbState.sqlInstance.Database()
|
||||
}
|
||||
} else {
|
||||
dbState.dbInstance = new dbState.sqlInstance.Database()
|
||||
}
|
||||
|
||||
if (!dbState.dbInstance) {
|
||||
throw new Error('Failed to initialize database')
|
||||
}
|
||||
|
||||
createTables(dbState.dbInstance)
|
||||
await saveDB()
|
||||
|
||||
return dbState.dbInstance
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
import { saveToIndexedDB } from '../db-indexeddb'
|
||||
import { saveToLocalStorage } from '../db-localstorage'
|
||||
import { dbState } from './state'
|
||||
|
||||
export async function saveDB() {
|
||||
if (!dbState.dbInstance) return
|
||||
|
||||
try {
|
||||
const data = dbState.dbInstance.export()
|
||||
|
||||
const savedToIDB = await saveToIndexedDB(data)
|
||||
|
||||
if (!savedToIDB) {
|
||||
saveToLocalStorage(data)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to save database:', error)
|
||||
}
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
import type { Database } from 'sql.js'
|
||||
import type { FlaskStorageAdapter } from '../storage'
|
||||
|
||||
export const dbState = {
|
||||
dbInstance: null as Database | null,
|
||||
sqlInstance: null as any,
|
||||
flaskAdapter: null as FlaskStorageAdapter | null,
|
||||
configLoaded: false,
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
import { deleteFromIndexedDB, saveToIndexedDB } from '../db-indexeddb'
|
||||
import { deleteFromLocalStorage, saveToLocalStorage } from '../db-localstorage'
|
||||
import { dbState } from './state'
|
||||
|
||||
export async function wipeAndRecreateDB(): Promise<void> {
|
||||
console.warn('Wiping corrupted database and creating fresh schema...')
|
||||
|
||||
await saveToIndexedDB(new Uint8Array())
|
||||
saveToLocalStorage(new Uint8Array())
|
||||
|
||||
await deleteFromIndexedDB()
|
||||
deleteFromLocalStorage()
|
||||
|
||||
dbState.dbInstance = null
|
||||
}
|
||||
@@ -1,100 +0,0 @@
|
||||
/**
|
||||
* IndexedDB operations for database persistence
|
||||
*/
|
||||
|
||||
import { DB_KEY, IDB_NAME, IDB_STORE, IDB_VERSION } from './db-constants'
|
||||
|
||||
export async function openIndexedDB(): Promise<IDBDatabase | null> {
|
||||
if (typeof indexedDB === 'undefined') return null
|
||||
|
||||
return new Promise((resolve) => {
|
||||
try {
|
||||
const request = indexedDB.open(IDB_NAME, IDB_VERSION)
|
||||
|
||||
request.onerror = () => {
|
||||
console.warn('IndexedDB not available, falling back to localStorage')
|
||||
resolve(null)
|
||||
}
|
||||
|
||||
request.onupgradeneeded = (event) => {
|
||||
const db = (event.target as IDBOpenDBRequest).result
|
||||
if (!db.objectStoreNames.contains(IDB_STORE)) {
|
||||
db.createObjectStore(IDB_STORE)
|
||||
}
|
||||
}
|
||||
|
||||
request.onsuccess = (event) => {
|
||||
resolve((event.target as IDBOpenDBRequest).result)
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('IndexedDB error:', error)
|
||||
resolve(null)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
export async function loadFromIndexedDB(): Promise<Uint8Array | null> {
|
||||
const db = await openIndexedDB()
|
||||
if (!db) return null
|
||||
|
||||
return new Promise((resolve) => {
|
||||
try {
|
||||
const transaction = db.transaction([IDB_STORE], 'readonly')
|
||||
const store = transaction.objectStore(IDB_STORE)
|
||||
const request = store.get(DB_KEY)
|
||||
|
||||
request.onsuccess = () => {
|
||||
const data = request.result
|
||||
resolve(data ? new Uint8Array(data) : null)
|
||||
}
|
||||
|
||||
request.onerror = () => {
|
||||
console.warn('Failed to load from IndexedDB')
|
||||
resolve(null)
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('IndexedDB read error:', error)
|
||||
resolve(null)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
export async function saveToIndexedDB(data: Uint8Array): Promise<boolean> {
|
||||
const db = await openIndexedDB()
|
||||
if (!db) return false
|
||||
|
||||
return new Promise((resolve) => {
|
||||
try {
|
||||
const transaction = db.transaction([IDB_STORE], 'readwrite')
|
||||
const store = transaction.objectStore(IDB_STORE)
|
||||
const request = store.put(data, DB_KEY)
|
||||
|
||||
request.onsuccess = () => resolve(true)
|
||||
request.onerror = () => {
|
||||
console.warn('Failed to save to IndexedDB')
|
||||
resolve(false)
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('IndexedDB write error:', error)
|
||||
resolve(false)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
export async function deleteFromIndexedDB(): Promise<void> {
|
||||
const db = await openIndexedDB()
|
||||
if (!db) return
|
||||
|
||||
return new Promise((resolve) => {
|
||||
try {
|
||||
const transaction = db.transaction([IDB_STORE], 'readwrite')
|
||||
const store = transaction.objectStore(IDB_STORE)
|
||||
const request = store.delete(DB_KEY)
|
||||
request.onsuccess = () => resolve()
|
||||
request.onerror = () => resolve()
|
||||
} catch (error) {
|
||||
console.warn('Error clearing IndexedDB:', error)
|
||||
resolve()
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -1,36 +0,0 @@
|
||||
/**
|
||||
* LocalStorage operations for database persistence
|
||||
*/
|
||||
|
||||
import { DB_KEY } from './db-constants'
|
||||
|
||||
export function loadFromLocalStorage(): Uint8Array | null {
|
||||
try {
|
||||
const savedData = localStorage.getItem(DB_KEY)
|
||||
if (savedData) {
|
||||
return new Uint8Array(JSON.parse(savedData))
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('Failed to load from localStorage:', error)
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
export function saveToLocalStorage(data: Uint8Array): boolean {
|
||||
try {
|
||||
const dataArray = Array.from(data)
|
||||
localStorage.setItem(DB_KEY, JSON.stringify(dataArray))
|
||||
return true
|
||||
} catch (error) {
|
||||
console.warn('Failed to save to localStorage (quota exceeded?):', error)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
export function deleteFromLocalStorage(): void {
|
||||
try {
|
||||
localStorage.removeItem(DB_KEY)
|
||||
} catch (error) {
|
||||
console.warn('Error clearing localStorage:', error)
|
||||
}
|
||||
}
|
||||
@@ -1,52 +0,0 @@
|
||||
/**
|
||||
* Database row-to-object mapping utilities
|
||||
* Handles conversion of SQL query results to typed objects
|
||||
*/
|
||||
|
||||
/**
|
||||
* Maps a SQL query result row to a typed object
|
||||
* Handles special conversions for boolean and JSON fields
|
||||
*/
|
||||
export function mapRowToObject<T>(row: any[], columns: string[]): T {
|
||||
const obj: any = {}
|
||||
|
||||
columns.forEach((col, idx) => {
|
||||
const value = row[idx]
|
||||
|
||||
// Convert integer boolean fields to actual booleans
|
||||
if (col === 'hasPreview' || col === 'isDefault') {
|
||||
obj[col] = value === 1
|
||||
}
|
||||
// Parse JSON string fields with error handling
|
||||
else if (col === 'inputParameters') {
|
||||
if (value) {
|
||||
try {
|
||||
obj[col] = JSON.parse(value as string)
|
||||
} catch (error) {
|
||||
console.warn(`Failed to parse JSON for ${col}:`, error)
|
||||
obj[col] = undefined
|
||||
}
|
||||
} else {
|
||||
obj[col] = undefined
|
||||
}
|
||||
}
|
||||
// All other fields pass through as-is
|
||||
else {
|
||||
obj[col] = value
|
||||
}
|
||||
})
|
||||
|
||||
return obj as T
|
||||
}
|
||||
|
||||
/**
|
||||
* Maps multiple SQL result rows to an array of typed objects
|
||||
*/
|
||||
export function mapRowsToObjects<T>(results: any[]): T[] {
|
||||
if (results.length === 0) return []
|
||||
|
||||
const columns = results[0].columns
|
||||
const values = results[0].values
|
||||
|
||||
return values.map(row => mapRowToObject<T>(row, columns))
|
||||
}
|
||||
@@ -1,30 +0,0 @@
|
||||
import type { Namespace } from '../types'
|
||||
import { initDB } from '../db-core/initDB'
|
||||
import { saveDB } from '../db-core/saveDB'
|
||||
import { getFlaskAdapter } from '../db-core/getFlaskAdapter'
|
||||
|
||||
export async function createNamespace(name: string): Promise<Namespace> {
|
||||
const namespace: Namespace = {
|
||||
id: Date.now().toString(),
|
||||
name,
|
||||
createdAt: Date.now(),
|
||||
isDefault: false,
|
||||
}
|
||||
|
||||
const adapter = getFlaskAdapter()
|
||||
if (adapter) {
|
||||
await adapter.createNamespace(namespace)
|
||||
return namespace
|
||||
}
|
||||
|
||||
const db = await initDB()
|
||||
|
||||
db.run(
|
||||
`INSERT INTO namespaces (id, name, createdAt, isDefault)
|
||||
VALUES (?, ?, ?, ?)`,
|
||||
[namespace.id, namespace.name, namespace.createdAt, namespace.isDefault ? 1 : 0]
|
||||
)
|
||||
|
||||
await saveDB()
|
||||
return namespace
|
||||
}
|
||||
@@ -1,30 +0,0 @@
|
||||
import { initDB } from '../db-core/initDB'
|
||||
import { saveDB } from '../db-core/saveDB'
|
||||
import { getFlaskAdapter } from '../db-core/getFlaskAdapter'
|
||||
|
||||
export async function deleteNamespace(id: string): Promise<void> {
|
||||
const adapter = getFlaskAdapter()
|
||||
if (adapter) {
|
||||
return await adapter.deleteNamespace(id)
|
||||
}
|
||||
|
||||
const db = await initDB()
|
||||
|
||||
const defaultNamespace = db.exec('SELECT id FROM namespaces WHERE isDefault = 1')
|
||||
if (defaultNamespace.length === 0 || defaultNamespace[0].values.length === 0) {
|
||||
throw new Error('Default namespace not found')
|
||||
}
|
||||
|
||||
const defaultId = defaultNamespace[0].values[0][0] as string
|
||||
|
||||
const checkDefault = db.exec('SELECT isDefault FROM namespaces WHERE id = ?', [id])
|
||||
if (checkDefault.length > 0 && checkDefault[0].values[0]?.[0] === 1) {
|
||||
throw new Error('Cannot delete default namespace')
|
||||
}
|
||||
|
||||
db.run('UPDATE snippets SET namespaceId = ? WHERE namespaceId = ?', [defaultId, id])
|
||||
|
||||
db.run('DELETE FROM namespaces WHERE id = ?', [id])
|
||||
|
||||
await saveDB()
|
||||
}
|
||||
@@ -1,27 +0,0 @@
|
||||
import type { Namespace } from '../types'
|
||||
import { initDB } from '../db-core/initDB'
|
||||
import { saveDB } from '../db-core/saveDB'
|
||||
|
||||
export async function ensureDefaultNamespace(): Promise<void> {
|
||||
const db = await initDB()
|
||||
|
||||
const results = db.exec('SELECT COUNT(*) as count FROM namespaces WHERE isDefault = 1')
|
||||
const count = results[0]?.values[0]?.[0] as number || 0
|
||||
|
||||
if (count === 0) {
|
||||
const defaultNamespace: Namespace = {
|
||||
id: 'default',
|
||||
name: 'Default',
|
||||
createdAt: Date.now(),
|
||||
isDefault: true,
|
||||
}
|
||||
|
||||
db.run(
|
||||
`INSERT INTO namespaces (id, name, createdAt, isDefault)
|
||||
VALUES (?, ?, ?, ?)`,
|
||||
[defaultNamespace.id, defaultNamespace.name, defaultNamespace.createdAt, 1]
|
||||
)
|
||||
|
||||
await saveDB()
|
||||
}
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
import type { Namespace } from '../types'
|
||||
import { initDB } from '../db-core/initDB'
|
||||
import { getFlaskAdapter } from '../db-core/getFlaskAdapter'
|
||||
import { mapRowsToObjects } from '../db-mapper'
|
||||
|
||||
export async function getAllNamespaces(): Promise<Namespace[]> {
|
||||
const adapter = getFlaskAdapter()
|
||||
if (adapter) {
|
||||
return await adapter.getAllNamespaces()
|
||||
}
|
||||
|
||||
const db = await initDB()
|
||||
const results = db.exec('SELECT * FROM namespaces ORDER BY isDefault DESC, name ASC')
|
||||
|
||||
return mapRowsToObjects<Namespace>(results)
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
import type { Namespace } from '../types'
|
||||
import { initDB } from '../db-core/initDB'
|
||||
import { mapRowToObject } from '../db-mapper'
|
||||
|
||||
export async function getNamespaceById(id: string): Promise<Namespace | null> {
|
||||
const db = await initDB()
|
||||
const results = db.exec('SELECT * FROM namespaces WHERE id = ?', [id])
|
||||
|
||||
if (results.length === 0 || results[0].values.length === 0) return null
|
||||
|
||||
const columns = results[0].columns
|
||||
const row = results[0].values[0]
|
||||
|
||||
return mapRowToObject<Namespace>(row, columns)
|
||||
}
|
||||
@@ -1,106 +0,0 @@
|
||||
/**
|
||||
* Database schema management and validation
|
||||
*/
|
||||
|
||||
import type { Database } from 'sql.js'
|
||||
|
||||
export async function validateSchema(db: Database): Promise<boolean> {
|
||||
try {
|
||||
const snippetsCheck = db.exec("PRAGMA table_info(snippets)")
|
||||
if (snippetsCheck.length === 0) return true
|
||||
|
||||
const columns = snippetsCheck[0].values.map(row => row[1] as string)
|
||||
const requiredColumns = ['id', 'title', 'code', 'language', 'category', 'namespaceId', 'createdAt', 'updatedAt']
|
||||
|
||||
for (const col of requiredColumns) {
|
||||
if (!columns.includes(col)) {
|
||||
console.warn(`Schema validation failed: missing column '${col}'`)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
const namespacesCheck = db.exec("PRAGMA table_info(namespaces)")
|
||||
if (namespacesCheck.length === 0) {
|
||||
console.warn('Schema validation failed: namespaces table missing')
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
} catch (error) {
|
||||
console.error('Schema validation error:', error)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
export function createTables(db: Database): void {
|
||||
db.run(`
|
||||
CREATE TABLE IF NOT EXISTS namespaces (
|
||||
id TEXT PRIMARY KEY,
|
||||
name TEXT NOT NULL,
|
||||
createdAt INTEGER NOT NULL,
|
||||
isDefault INTEGER DEFAULT 0
|
||||
)
|
||||
`)
|
||||
|
||||
db.run(`
|
||||
CREATE TABLE IF NOT EXISTS snippets (
|
||||
id TEXT PRIMARY KEY,
|
||||
title TEXT NOT NULL,
|
||||
description TEXT,
|
||||
code TEXT NOT NULL,
|
||||
language TEXT NOT NULL,
|
||||
category TEXT NOT NULL,
|
||||
namespaceId TEXT,
|
||||
hasPreview INTEGER DEFAULT 0,
|
||||
functionName TEXT,
|
||||
inputParameters TEXT,
|
||||
createdAt INTEGER NOT NULL,
|
||||
updatedAt INTEGER NOT NULL,
|
||||
FOREIGN KEY (namespaceId) REFERENCES namespaces(id)
|
||||
)
|
||||
`)
|
||||
|
||||
db.run(`
|
||||
CREATE TABLE IF NOT EXISTS snippet_templates (
|
||||
id TEXT PRIMARY KEY,
|
||||
title TEXT NOT NULL,
|
||||
description TEXT,
|
||||
code TEXT NOT NULL,
|
||||
language TEXT NOT NULL,
|
||||
category TEXT NOT NULL,
|
||||
hasPreview INTEGER DEFAULT 0,
|
||||
functionName TEXT,
|
||||
inputParameters TEXT
|
||||
)
|
||||
`)
|
||||
}
|
||||
|
||||
export async function validateDatabaseSchema(db: Database): Promise<{ valid: boolean; issues: string[] }> {
|
||||
try {
|
||||
const issues: string[] = []
|
||||
|
||||
const snippetsCheck = db.exec("PRAGMA table_info(snippets)")
|
||||
if (snippetsCheck.length === 0) {
|
||||
issues.push('Snippets table missing')
|
||||
return { valid: false, issues }
|
||||
}
|
||||
|
||||
const columns = snippetsCheck[0].values.map(row => row[1] as string)
|
||||
const requiredColumns = ['id', 'title', 'code', 'language', 'category', 'namespaceId', 'createdAt', 'updatedAt']
|
||||
|
||||
for (const col of requiredColumns) {
|
||||
if (!columns.includes(col)) {
|
||||
issues.push(`Missing column '${col}' in snippets table`)
|
||||
}
|
||||
}
|
||||
|
||||
const namespacesCheck = db.exec("SELECT name FROM sqlite_master WHERE type='table' AND name='namespaces'")
|
||||
if (namespacesCheck.length === 0) {
|
||||
issues.push('Namespaces table missing')
|
||||
}
|
||||
|
||||
return { valid: issues.length === 0, issues }
|
||||
} catch (error) {
|
||||
return { valid: false, issues: ['Failed to validate schema: ' + (error as Error).message] }
|
||||
}
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
import { initDB } from '../db-core/initDB'
|
||||
import { saveDB } from '../db-core/saveDB'
|
||||
import { getFlaskAdapter } from '../db-core/getFlaskAdapter'
|
||||
|
||||
export async function bulkMoveSnippets(snippetIds: string[], targetNamespaceId: string): Promise<void> {
|
||||
const adapter = getFlaskAdapter()
|
||||
if (adapter) {
|
||||
await adapter.bulkMoveSnippets(snippetIds, targetNamespaceId)
|
||||
return
|
||||
}
|
||||
|
||||
const db = await initDB()
|
||||
const now = Date.now()
|
||||
|
||||
for (const snippetId of snippetIds) {
|
||||
db.run(
|
||||
'UPDATE snippets SET namespaceId = ?, updatedAt = ? WHERE id = ?',
|
||||
[targetNamespaceId, now, snippetId]
|
||||
)
|
||||
}
|
||||
|
||||
await saveDB()
|
||||
}
|
||||
@@ -1,34 +0,0 @@
|
||||
import type { Snippet } from '../types'
|
||||
import { initDB } from '../db-core/initDB'
|
||||
import { saveDB } from '../db-core/saveDB'
|
||||
import { getFlaskAdapter } from '../db-core/getFlaskAdapter'
|
||||
|
||||
export async function createSnippet(snippet: Snippet): Promise<void> {
|
||||
const adapter = getFlaskAdapter()
|
||||
if (adapter) {
|
||||
return await adapter.createSnippet(snippet)
|
||||
}
|
||||
|
||||
const db = await initDB()
|
||||
|
||||
db.run(
|
||||
`INSERT INTO snippets (id, title, description, code, language, category, namespaceId, hasPreview, functionName, inputParameters, createdAt, updatedAt)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||
[
|
||||
snippet.id,
|
||||
snippet.title,
|
||||
snippet.description,
|
||||
snippet.code,
|
||||
snippet.language,
|
||||
snippet.category,
|
||||
snippet.namespaceId || null,
|
||||
snippet.hasPreview ? 1 : 0,
|
||||
snippet.functionName || null,
|
||||
snippet.inputParameters ? JSON.stringify(snippet.inputParameters) : null,
|
||||
snippet.createdAt,
|
||||
snippet.updatedAt,
|
||||
]
|
||||
)
|
||||
|
||||
await saveDB()
|
||||
}
|
||||
@@ -1,25 +0,0 @@
|
||||
import type { SnippetTemplate } from '../types'
|
||||
import { initDB } from '../db-core/initDB'
|
||||
import { saveDB } from '../db-core/saveDB'
|
||||
|
||||
export async function createTemplate(template: SnippetTemplate): Promise<void> {
|
||||
const db = await initDB()
|
||||
|
||||
db.run(
|
||||
`INSERT INTO snippet_templates (id, title, description, code, language, category, hasPreview, functionName, inputParameters)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||
[
|
||||
template.id,
|
||||
template.title,
|
||||
template.description,
|
||||
template.code,
|
||||
template.language,
|
||||
template.category,
|
||||
template.hasPreview ? 1 : 0,
|
||||
template.functionName || null,
|
||||
template.inputParameters ? JSON.stringify(template.inputParameters) : null,
|
||||
]
|
||||
)
|
||||
|
||||
await saveDB()
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
import { initDB } from '../db-core/initDB'
|
||||
import { saveDB } from '../db-core/saveDB'
|
||||
import { getFlaskAdapter } from '../db-core/getFlaskAdapter'
|
||||
|
||||
export async function deleteSnippet(id: string): Promise<void> {
|
||||
const adapter = getFlaskAdapter()
|
||||
if (adapter) {
|
||||
return await adapter.deleteSnippet(id)
|
||||
}
|
||||
|
||||
const db = await initDB()
|
||||
|
||||
db.run('DELETE FROM snippets WHERE id = ?', [id])
|
||||
|
||||
await saveDB()
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
import type { Snippet } from '../types'
|
||||
import { initDB } from '../db-core/initDB'
|
||||
import { getFlaskAdapter } from '../db-core/getFlaskAdapter'
|
||||
import { mapRowsToObjects } from '../db-mapper'
|
||||
|
||||
export async function getAllSnippets(): Promise<Snippet[]> {
|
||||
const adapter = getFlaskAdapter()
|
||||
if (adapter) {
|
||||
return await adapter.getAllSnippets()
|
||||
}
|
||||
|
||||
const db = await initDB()
|
||||
const results = db.exec('SELECT * FROM snippets ORDER BY updatedAt DESC')
|
||||
|
||||
return mapRowsToObjects<Snippet>(results)
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
import type { SnippetTemplate } from '../types'
|
||||
import { initDB } from '../db-core/initDB'
|
||||
import { mapRowsToObjects } from '../db-mapper'
|
||||
|
||||
export async function getAllTemplates(): Promise<SnippetTemplate[]> {
|
||||
const db = await initDB()
|
||||
const results = db.exec('SELECT * FROM snippet_templates')
|
||||
|
||||
return mapRowsToObjects<SnippetTemplate>(results)
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
import type { Snippet } from '../types'
|
||||
import { initDB } from '../db-core/initDB'
|
||||
import { getFlaskAdapter } from '../db-core/getFlaskAdapter'
|
||||
import { mapRowToObject } from '../db-mapper'
|
||||
|
||||
export async function getSnippet(id: string): Promise<Snippet | null> {
|
||||
const adapter = getFlaskAdapter()
|
||||
if (adapter) {
|
||||
return await adapter.getSnippet(id)
|
||||
}
|
||||
|
||||
const db = await initDB()
|
||||
const results = db.exec('SELECT * FROM snippets WHERE id = ?', [id])
|
||||
|
||||
if (results.length === 0 || results[0].values.length === 0) return null
|
||||
|
||||
const columns = results[0].columns
|
||||
const row = results[0].values[0]
|
||||
|
||||
return mapRowToObject<Snippet>(row, columns)
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
import type { Snippet } from '../types'
|
||||
import { initDB } from '../db-core/initDB'
|
||||
import { mapRowsToObjects } from '../db-mapper'
|
||||
|
||||
export async function getSnippetsByNamespace(namespaceId: string): Promise<Snippet[]> {
|
||||
const db = await initDB()
|
||||
const results = db.exec(
|
||||
'SELECT * FROM snippets WHERE namespaceId = ? OR (namespaceId IS NULL AND ? = (SELECT id FROM namespaces WHERE isDefault = 1)) ORDER BY updatedAt DESC',
|
||||
[namespaceId, namespaceId]
|
||||
)
|
||||
|
||||
return mapRowsToObjects<Snippet>(results)
|
||||
}
|
||||
@@ -1,25 +0,0 @@
|
||||
import { initDB } from '../db-core/initDB'
|
||||
import { saveDB } from '../db-core/saveDB'
|
||||
import { getFlaskAdapter } from '../db-core/getFlaskAdapter'
|
||||
|
||||
export async function moveSnippetToNamespace(snippetId: string, targetNamespaceId: string): Promise<void> {
|
||||
const adapter = getFlaskAdapter()
|
||||
if (adapter) {
|
||||
const snippet = await adapter.getSnippet(snippetId)
|
||||
if (snippet) {
|
||||
snippet.namespaceId = targetNamespaceId
|
||||
snippet.updatedAt = Date.now()
|
||||
await adapter.updateSnippet(snippet)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
const db = await initDB()
|
||||
|
||||
db.run(
|
||||
'UPDATE snippets SET namespaceId = ?, updatedAt = ? WHERE id = ?',
|
||||
[targetNamespaceId, Date.now(), snippetId]
|
||||
)
|
||||
|
||||
await saveDB()
|
||||
}
|
||||
@@ -1,41 +0,0 @@
|
||||
import type { Snippet, SnippetTemplate } from '../types'
|
||||
import { initDB } from '../db-core/initDB'
|
||||
import { createSnippet } from './createSnippet'
|
||||
import { createTemplate } from './createTemplate'
|
||||
import { ensureDefaultNamespace } from '../db-namespaces/ensureDefaultNamespace'
|
||||
import seedSnippetsData from '@/data/seed-snippets.json'
|
||||
import seedTemplatesData from '@/data/seed-templates.json'
|
||||
|
||||
export async function seedDatabase(): Promise<void> {
|
||||
const db = await initDB()
|
||||
|
||||
await ensureDefaultNamespace()
|
||||
|
||||
const checkSnippets = db.exec('SELECT COUNT(*) as count FROM snippets')
|
||||
const snippetCount = checkSnippets[0]?.values[0]?.[0] as number
|
||||
|
||||
if (snippetCount > 0) {
|
||||
return
|
||||
}
|
||||
|
||||
const now = Date.now()
|
||||
|
||||
const seedSnippets: Snippet[] = seedSnippetsData.map((snippet, index) => {
|
||||
const timestamp = now - index * 1000
|
||||
return {
|
||||
...snippet,
|
||||
createdAt: timestamp,
|
||||
updatedAt: timestamp,
|
||||
}
|
||||
})
|
||||
|
||||
for (const snippet of seedSnippets) {
|
||||
await createSnippet(snippet)
|
||||
}
|
||||
|
||||
const seedTemplates: SnippetTemplate[] = seedTemplatesData
|
||||
|
||||
for (const template of seedTemplates) {
|
||||
await createTemplate(template)
|
||||
}
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
import type { SnippetTemplate } from '../types'
|
||||
import { initDB } from '../db-core/initDB'
|
||||
import { createTemplate } from './createTemplate'
|
||||
|
||||
export async function syncTemplatesFromJSON(templates: SnippetTemplate[]): Promise<void> {
|
||||
const db = await initDB()
|
||||
|
||||
const existingTemplates = db.exec('SELECT id FROM snippet_templates')
|
||||
const existingIds = new Set(
|
||||
existingTemplates[0]?.values.map(row => row[0] as string) || []
|
||||
)
|
||||
|
||||
let addedCount = 0
|
||||
for (const template of templates) {
|
||||
if (!existingIds.has(template.id)) {
|
||||
await createTemplate(template)
|
||||
addedCount++
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,34 +0,0 @@
|
||||
import type { Snippet } from '../types'
|
||||
import { initDB } from '../db-core/initDB'
|
||||
import { saveDB } from '../db-core/saveDB'
|
||||
import { getFlaskAdapter } from '../db-core/getFlaskAdapter'
|
||||
|
||||
export async function updateSnippet(snippet: Snippet): Promise<void> {
|
||||
const adapter = getFlaskAdapter()
|
||||
if (adapter) {
|
||||
return await adapter.updateSnippet(snippet)
|
||||
}
|
||||
|
||||
const db = await initDB()
|
||||
|
||||
db.run(
|
||||
`UPDATE snippets
|
||||
SET title = ?, description = ?, code = ?, language = ?, category = ?, namespaceId = ?, hasPreview = ?, functionName = ?, inputParameters = ?, updatedAt = ?
|
||||
WHERE id = ?`,
|
||||
[
|
||||
snippet.title,
|
||||
snippet.description,
|
||||
snippet.code,
|
||||
snippet.language,
|
||||
snippet.category,
|
||||
snippet.namespaceId || null,
|
||||
snippet.hasPreview ? 1 : 0,
|
||||
snippet.functionName || null,
|
||||
snippet.inputParameters ? JSON.stringify(snippet.inputParameters) : null,
|
||||
snippet.updatedAt,
|
||||
snippet.id,
|
||||
]
|
||||
)
|
||||
|
||||
await saveDB()
|
||||
}
|
||||
247
src/lib/db.ts
247
src/lib/db.ts
@@ -1,38 +1,223 @@
|
||||
/**
|
||||
* Main database module - Re-exports from focused modules
|
||||
* This file maintains backward compatibility while delegating to specialized modules
|
||||
* Unified storage interface - routes to IndexedDB or Flask based on configuration
|
||||
*/
|
||||
|
||||
// Re-export core database functions
|
||||
export { initDB } from './db-core/initDB'
|
||||
export { saveDB } from './db-core/saveDB'
|
||||
export { exportDatabase } from './db-core/exportDatabase'
|
||||
export { importDatabase } from './db-core/importDatabase'
|
||||
export { getDatabaseStats } from './db-core/getDatabaseStats'
|
||||
export { clearDatabase } from './db-core/clearDatabase'
|
||||
import type { Snippet, Namespace } from './types';
|
||||
import { getStorageConfig, FlaskStorageAdapter } from './storage';
|
||||
import * as IndexedDBStorage from './indexeddb-storage';
|
||||
|
||||
// Re-export snippet operations
|
||||
export { getAllSnippets } from './db-snippets/getAllSnippets'
|
||||
export { getSnippet } from './db-snippets/getSnippet'
|
||||
export { createSnippet } from './db-snippets/createSnippet'
|
||||
export { updateSnippet } from './db-snippets/updateSnippet'
|
||||
export { deleteSnippet } from './db-snippets/deleteSnippet'
|
||||
export { getSnippetsByNamespace } from './db-snippets/getSnippetsByNamespace'
|
||||
export { moveSnippetToNamespace } from './db-snippets/moveSnippetToNamespace'
|
||||
export { bulkMoveSnippets } from './db-snippets/bulkMoveSnippets'
|
||||
export { getAllTemplates } from './db-snippets/getAllTemplates'
|
||||
export { createTemplate } from './db-snippets/createTemplate'
|
||||
export { syncTemplatesFromJSON } from './db-snippets/syncTemplatesFromJSON'
|
||||
export { seedDatabase } from './db-snippets/seedDatabase'
|
||||
// Helper to get the active storage backend
|
||||
function getActiveStorage() {
|
||||
const config = getStorageConfig();
|
||||
|
||||
if (config.backend === 'flask' && config.flaskUrl) {
|
||||
return new FlaskStorageAdapter(config.flaskUrl);
|
||||
}
|
||||
|
||||
return null; // Use IndexedDB
|
||||
}
|
||||
|
||||
// Re-export namespace operations
|
||||
export { getAllNamespaces } from './db-namespaces/getAllNamespaces'
|
||||
export { createNamespace } from './db-namespaces/createNamespace'
|
||||
export { deleteNamespace } from './db-namespaces/deleteNamespace'
|
||||
export { ensureDefaultNamespace } from './db-namespaces/ensureDefaultNamespace'
|
||||
export { getNamespaceById } from './db-namespaces/getNamespaceById'
|
||||
// Snippet operations
|
||||
export async function getAllSnippets(): Promise<Snippet[]> {
|
||||
const flask = getActiveStorage();
|
||||
if (flask) {
|
||||
return await flask.getAllSnippets();
|
||||
}
|
||||
return await IndexedDBStorage.getAllSnippets();
|
||||
}
|
||||
|
||||
// Re-export schema validation
|
||||
export { validateDatabaseSchema } from './db-schema'
|
||||
export async function getSnippet(id: string): Promise<Snippet | null> {
|
||||
const flask = getActiveStorage();
|
||||
if (flask) {
|
||||
return await flask.getSnippet(id);
|
||||
}
|
||||
return await IndexedDBStorage.getSnippet(id);
|
||||
}
|
||||
|
||||
// Note: saveDB is intentionally not exported as it's used internally by the modules
|
||||
export async function createSnippet(snippet: Snippet): Promise<void> {
|
||||
const flask = getActiveStorage();
|
||||
if (flask) {
|
||||
return await flask.createSnippet(snippet);
|
||||
}
|
||||
return await IndexedDBStorage.createSnippet(snippet);
|
||||
}
|
||||
|
||||
export async function updateSnippet(snippet: Snippet): Promise<void> {
|
||||
const flask = getActiveStorage();
|
||||
if (flask) {
|
||||
return await flask.updateSnippet(snippet);
|
||||
}
|
||||
return await IndexedDBStorage.updateSnippet(snippet);
|
||||
}
|
||||
|
||||
export async function deleteSnippet(id: string): Promise<void> {
|
||||
const flask = getActiveStorage();
|
||||
if (flask) {
|
||||
return await flask.deleteSnippet(id);
|
||||
}
|
||||
return await IndexedDBStorage.deleteSnippet(id);
|
||||
}
|
||||
|
||||
export async function getSnippetsByNamespace(namespaceId: string): Promise<Snippet[]> {
|
||||
const flask = getActiveStorage();
|
||||
if (flask) {
|
||||
return await flask.getSnippetsByNamespace(namespaceId);
|
||||
}
|
||||
return await IndexedDBStorage.getSnippetsByNamespace(namespaceId);
|
||||
}
|
||||
|
||||
export async function moveSnippetToNamespace(snippetId: string, namespaceId: string): Promise<void> {
|
||||
const snippet = await getSnippet(snippetId);
|
||||
if (!snippet) throw new Error('Snippet not found');
|
||||
|
||||
snippet.namespaceId = namespaceId;
|
||||
snippet.updatedAt = Date.now();
|
||||
|
||||
await updateSnippet(snippet);
|
||||
}
|
||||
|
||||
export async function bulkMoveSnippets(snippetIds: string[], namespaceId: string): Promise<void> {
|
||||
for (const id of snippetIds) {
|
||||
await moveSnippetToNamespace(id, namespaceId);
|
||||
}
|
||||
}
|
||||
|
||||
export async function getAllTemplates(): Promise<Snippet[]> {
|
||||
const snippets = await getAllSnippets();
|
||||
return snippets.filter(s => s.isTemplate);
|
||||
}
|
||||
|
||||
export async function createTemplate(snippet: Omit<Snippet, 'id' | 'createdAt' | 'updatedAt'>): Promise<void> {
|
||||
const template: Snippet = {
|
||||
...snippet,
|
||||
id: Date.now().toString(),
|
||||
createdAt: Date.now(),
|
||||
updatedAt: Date.now(),
|
||||
isTemplate: true,
|
||||
};
|
||||
await createSnippet(template);
|
||||
}
|
||||
|
||||
export async function syncTemplatesFromJSON(templates: any[]): Promise<void> {
|
||||
// This would sync predefined templates - implement as needed
|
||||
console.log('Syncing templates', templates.length);
|
||||
}
|
||||
|
||||
export async function seedDatabase(): Promise<void> {
|
||||
// Seed with default namespace if needed
|
||||
const namespaces = await getAllNamespaces();
|
||||
if (namespaces.length === 0) {
|
||||
await ensureDefaultNamespace();
|
||||
}
|
||||
}
|
||||
|
||||
// Namespace operations
|
||||
export async function getAllNamespaces(): Promise<Namespace[]> {
|
||||
const flask = getActiveStorage();
|
||||
if (flask) {
|
||||
return await flask.getAllNamespaces();
|
||||
}
|
||||
return await IndexedDBStorage.getAllNamespaces();
|
||||
}
|
||||
|
||||
export async function getNamespaceById(id: string): Promise<Namespace | null> {
|
||||
const flask = getActiveStorage();
|
||||
if (flask) {
|
||||
return await flask.getNamespace(id);
|
||||
}
|
||||
return await IndexedDBStorage.getNamespace(id);
|
||||
}
|
||||
|
||||
export async function createNamespace(namespace: Namespace): Promise<void> {
|
||||
const flask = getActiveStorage();
|
||||
if (flask) {
|
||||
return await flask.createNamespace(namespace);
|
||||
}
|
||||
return await IndexedDBStorage.createNamespace(namespace);
|
||||
}
|
||||
|
||||
export async function deleteNamespace(id: string): Promise<void> {
|
||||
const flask = getActiveStorage();
|
||||
if (flask) {
|
||||
return await flask.deleteNamespace(id);
|
||||
}
|
||||
return await IndexedDBStorage.deleteNamespace(id);
|
||||
}
|
||||
|
||||
export async function ensureDefaultNamespace(): Promise<Namespace> {
|
||||
const namespaces = await getAllNamespaces();
|
||||
let defaultNs = namespaces.find(ns => ns.isDefault);
|
||||
|
||||
if (!defaultNs) {
|
||||
defaultNs = {
|
||||
id: 'default',
|
||||
name: 'Default',
|
||||
createdAt: Date.now(),
|
||||
isDefault: true,
|
||||
};
|
||||
await createNamespace(defaultNs);
|
||||
}
|
||||
|
||||
return defaultNs;
|
||||
}
|
||||
|
||||
// Database operations
|
||||
export async function initDB(): Promise<void> {
|
||||
// Initialize IndexedDB or verify Flask connection
|
||||
const flask = getActiveStorage();
|
||||
if (flask) {
|
||||
const connected = await flask.testConnection();
|
||||
if (!connected) {
|
||||
throw new Error('Failed to connect to Flask backend');
|
||||
}
|
||||
} else {
|
||||
// Initialize IndexedDB
|
||||
await IndexedDBStorage.openDB();
|
||||
}
|
||||
|
||||
// Ensure default namespace exists
|
||||
await ensureDefaultNamespace();
|
||||
}
|
||||
|
||||
export async function clearDatabase(): Promise<void> {
|
||||
const flask = getActiveStorage();
|
||||
if (flask) {
|
||||
return await flask.clearDatabase();
|
||||
}
|
||||
return await IndexedDBStorage.clearDatabase();
|
||||
}
|
||||
|
||||
export async function getDatabaseStats() {
|
||||
const flask = getActiveStorage();
|
||||
if (flask) {
|
||||
return await flask.getStats();
|
||||
}
|
||||
return await IndexedDBStorage.getDatabaseStats();
|
||||
}
|
||||
|
||||
export async function exportDatabase(): Promise<string> {
|
||||
const flask = getActiveStorage();
|
||||
if (flask) {
|
||||
const data = await flask.exportDatabase();
|
||||
return JSON.stringify(data, null, 2);
|
||||
}
|
||||
const data = await IndexedDBStorage.exportDatabase();
|
||||
return JSON.stringify(data, null, 2);
|
||||
}
|
||||
|
||||
export async function importDatabase(jsonData: string): Promise<void> {
|
||||
const data = JSON.parse(jsonData);
|
||||
const flask = getActiveStorage();
|
||||
if (flask) {
|
||||
return await flask.importDatabase(data);
|
||||
}
|
||||
await IndexedDBStorage.importDatabase(data);
|
||||
}
|
||||
|
||||
export function validateDatabaseSchema(): Promise<boolean> {
|
||||
// With IndexedDB, schema is always valid
|
||||
return Promise.resolve(true);
|
||||
}
|
||||
|
||||
// For backward compatibility
|
||||
export const saveDB = async () => { /* No-op with IndexedDB */ };
|
||||
|
||||
38
src/lib/db.ts.old
Normal file
38
src/lib/db.ts.old
Normal file
@@ -0,0 +1,38 @@
|
||||
/**
|
||||
* Main database module - Re-exports from focused modules
|
||||
* This file maintains backward compatibility while delegating to specialized modules
|
||||
*/
|
||||
|
||||
// Re-export core database functions
|
||||
export { initDB } from './db-core/initDB'
|
||||
export { saveDB } from './db-core/saveDB'
|
||||
export { exportDatabase } from './db-core/exportDatabase'
|
||||
export { importDatabase } from './db-core/importDatabase'
|
||||
export { getDatabaseStats } from './db-core/getDatabaseStats'
|
||||
export { clearDatabase } from './db-core/clearDatabase'
|
||||
|
||||
// Re-export snippet operations
|
||||
export { getAllSnippets } from './db-snippets/getAllSnippets'
|
||||
export { getSnippet } from './db-snippets/getSnippet'
|
||||
export { createSnippet } from './db-snippets/createSnippet'
|
||||
export { updateSnippet } from './db-snippets/updateSnippet'
|
||||
export { deleteSnippet } from './db-snippets/deleteSnippet'
|
||||
export { getSnippetsByNamespace } from './db-snippets/getSnippetsByNamespace'
|
||||
export { moveSnippetToNamespace } from './db-snippets/moveSnippetToNamespace'
|
||||
export { bulkMoveSnippets } from './db-snippets/bulkMoveSnippets'
|
||||
export { getAllTemplates } from './db-snippets/getAllTemplates'
|
||||
export { createTemplate } from './db-snippets/createTemplate'
|
||||
export { syncTemplatesFromJSON } from './db-snippets/syncTemplatesFromJSON'
|
||||
export { seedDatabase } from './db-snippets/seedDatabase'
|
||||
|
||||
// Re-export namespace operations
|
||||
export { getAllNamespaces } from './db-namespaces/getAllNamespaces'
|
||||
export { createNamespace } from './db-namespaces/createNamespace'
|
||||
export { deleteNamespace } from './db-namespaces/deleteNamespace'
|
||||
export { ensureDefaultNamespace } from './db-namespaces/ensureDefaultNamespace'
|
||||
export { getNamespaceById } from './db-namespaces/getNamespaceById'
|
||||
|
||||
// Re-export schema validation
|
||||
export { validateDatabaseSchema } from './db-schema'
|
||||
|
||||
// Note: saveDB is intentionally not exported as it's used internally by the modules
|
||||
241
src/lib/indexeddb-storage.ts
Normal file
241
src/lib/indexeddb-storage.ts
Normal file
@@ -0,0 +1,241 @@
|
||||
/**
|
||||
* IndexedDB Storage - Direct storage of snippets and namespaces
|
||||
*/
|
||||
|
||||
import type { Snippet, Namespace } from './types';
|
||||
|
||||
const DB_NAME = 'codesnippet-db';
|
||||
const DB_VERSION = 2;
|
||||
const SNIPPETS_STORE = 'snippets';
|
||||
const NAMESPACES_STORE = 'namespaces';
|
||||
|
||||
let dbInstance: IDBDatabase | null = null;
|
||||
|
||||
export async function openDB(): Promise<IDBDatabase> {
|
||||
if (dbInstance) return dbInstance;
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const request = indexedDB.open(DB_NAME, DB_VERSION);
|
||||
|
||||
request.onerror = () => reject(request.error);
|
||||
|
||||
request.onupgradeneeded = (event) => {
|
||||
const db = (event.target as IDBOpenDBRequest).result;
|
||||
|
||||
// Create snippets store if it doesn't exist
|
||||
if (!db.objectStoreNames.contains(SNIPPETS_STORE)) {
|
||||
const snippetsStore = db.createObjectStore(SNIPPETS_STORE, { keyPath: 'id' });
|
||||
snippetsStore.createIndex('namespaceId', 'namespaceId', { unique: false });
|
||||
snippetsStore.createIndex('createdAt', 'createdAt', { unique: false });
|
||||
}
|
||||
|
||||
// Create namespaces store if it doesn't exist
|
||||
if (!db.objectStoreNames.contains(NAMESPACES_STORE)) {
|
||||
db.createObjectStore(NAMESPACES_STORE, { keyPath: 'id' });
|
||||
}
|
||||
};
|
||||
|
||||
request.onsuccess = () => {
|
||||
dbInstance = request.result;
|
||||
resolve(dbInstance);
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
// Snippet operations
|
||||
export async function getAllSnippets(): Promise<Snippet[]> {
|
||||
const db = await openDB();
|
||||
return new Promise((resolve, reject) => {
|
||||
const transaction = db.transaction([SNIPPETS_STORE], 'readonly');
|
||||
const store = transaction.objectStore(SNIPPETS_STORE);
|
||||
const request = store.getAll();
|
||||
|
||||
request.onerror = () => reject(request.error);
|
||||
request.onsuccess = () => resolve(request.result);
|
||||
});
|
||||
}
|
||||
|
||||
export async function getSnippet(id: string): Promise<Snippet | null> {
|
||||
const db = await openDB();
|
||||
return new Promise((resolve, reject) => {
|
||||
const transaction = db.transaction([SNIPPETS_STORE], 'readonly');
|
||||
const store = transaction.objectStore(SNIPPETS_STORE);
|
||||
const request = store.get(id);
|
||||
|
||||
request.onerror = () => reject(request.error);
|
||||
request.onsuccess = () => resolve(request.result || null);
|
||||
});
|
||||
}
|
||||
|
||||
export async function createSnippet(snippet: Snippet): Promise<void> {
|
||||
const db = await openDB();
|
||||
return new Promise((resolve, reject) => {
|
||||
const transaction = db.transaction([SNIPPETS_STORE], 'readwrite');
|
||||
const store = transaction.objectStore(SNIPPETS_STORE);
|
||||
const request = store.add(snippet);
|
||||
|
||||
request.onerror = () => reject(request.error);
|
||||
request.onsuccess = () => resolve();
|
||||
});
|
||||
}
|
||||
|
||||
export async function updateSnippet(snippet: Snippet): Promise<void> {
|
||||
const db = await openDB();
|
||||
return new Promise((resolve, reject) => {
|
||||
const transaction = db.transaction([SNIPPETS_STORE], 'readwrite');
|
||||
const store = transaction.objectStore(SNIPPETS_STORE);
|
||||
const request = store.put(snippet);
|
||||
|
||||
request.onerror = () => reject(request.error);
|
||||
request.onsuccess = () => resolve();
|
||||
});
|
||||
}
|
||||
|
||||
export async function deleteSnippet(id: string): Promise<void> {
|
||||
const db = await openDB();
|
||||
return new Promise((resolve, reject) => {
|
||||
const transaction = db.transaction([SNIPPETS_STORE], 'readwrite');
|
||||
const store = transaction.objectStore(SNIPPETS_STORE);
|
||||
const request = store.delete(id);
|
||||
|
||||
request.onerror = () => reject(request.error);
|
||||
request.onsuccess = () => resolve();
|
||||
});
|
||||
}
|
||||
|
||||
export async function getSnippetsByNamespace(namespaceId: string): Promise<Snippet[]> {
|
||||
const db = await openDB();
|
||||
return new Promise((resolve, reject) => {
|
||||
const transaction = db.transaction([SNIPPETS_STORE], 'readonly');
|
||||
const store = transaction.objectStore(SNIPPETS_STORE);
|
||||
const index = store.index('namespaceId');
|
||||
const request = index.getAll(namespaceId);
|
||||
|
||||
request.onerror = () => reject(request.error);
|
||||
request.onsuccess = () => resolve(request.result);
|
||||
});
|
||||
}
|
||||
|
||||
// Namespace operations
|
||||
export async function getAllNamespaces(): Promise<Namespace[]> {
|
||||
const db = await openDB();
|
||||
return new Promise((resolve, reject) => {
|
||||
const transaction = db.transaction([NAMESPACES_STORE], 'readonly');
|
||||
const store = transaction.objectStore(NAMESPACES_STORE);
|
||||
const request = store.getAll();
|
||||
|
||||
request.onerror = () => reject(request.error);
|
||||
request.onsuccess = () => resolve(request.result);
|
||||
});
|
||||
}
|
||||
|
||||
export async function getNamespace(id: string): Promise<Namespace | null> {
|
||||
const db = await openDB();
|
||||
return new Promise((resolve, reject) => {
|
||||
const transaction = db.transaction([NAMESPACES_STORE], 'readonly');
|
||||
const store = transaction.objectStore(NAMESPACES_STORE);
|
||||
const request = store.get(id);
|
||||
|
||||
request.onerror = () => reject(request.error);
|
||||
request.onsuccess = () => resolve(request.result || null);
|
||||
});
|
||||
}
|
||||
|
||||
export async function createNamespace(namespace: Namespace): Promise<void> {
|
||||
const db = await openDB();
|
||||
return new Promise((resolve, reject) => {
|
||||
const transaction = db.transaction([NAMESPACES_STORE], 'readwrite');
|
||||
const store = transaction.objectStore(NAMESPACES_STORE);
|
||||
const request = store.add(namespace);
|
||||
|
||||
request.onerror = () => reject(request.error);
|
||||
request.onsuccess = () => resolve();
|
||||
});
|
||||
}
|
||||
|
||||
export async function updateNamespace(namespace: Namespace): Promise<void> {
|
||||
const db = await openDB();
|
||||
return new Promise((resolve, reject) => {
|
||||
const transaction = db.transaction([NAMESPACES_STORE], 'readwrite');
|
||||
const store = transaction.objectStore(NAMESPACES_STORE);
|
||||
const request = store.put(namespace);
|
||||
|
||||
request.onerror = () => reject(request.error);
|
||||
request.onsuccess = () => resolve();
|
||||
});
|
||||
}
|
||||
|
||||
export async function deleteNamespace(id: string): Promise<void> {
|
||||
const db = await openDB();
|
||||
return new Promise((resolve, reject) => {
|
||||
const transaction = db.transaction([NAMESPACES_STORE], 'readwrite');
|
||||
const store = transaction.objectStore(NAMESPACES_STORE);
|
||||
const request = store.delete(id);
|
||||
|
||||
request.onerror = () => reject(request.error);
|
||||
request.onsuccess = () => resolve();
|
||||
});
|
||||
}
|
||||
|
||||
// Database operations
|
||||
export async function clearDatabase(): Promise<void> {
|
||||
const db = await openDB();
|
||||
return new Promise((resolve, reject) => {
|
||||
const transaction = db.transaction([SNIPPETS_STORE, NAMESPACES_STORE], 'readwrite');
|
||||
|
||||
const snippetsStore = transaction.objectStore(SNIPPETS_STORE);
|
||||
const namespacesStore = transaction.objectStore(NAMESPACES_STORE);
|
||||
|
||||
snippetsStore.clear();
|
||||
namespacesStore.clear();
|
||||
|
||||
transaction.onerror = () => reject(transaction.error);
|
||||
transaction.oncomplete = () => resolve();
|
||||
});
|
||||
}
|
||||
|
||||
export async function getDatabaseStats() {
|
||||
const snippets = await getAllSnippets();
|
||||
const namespaces = await getAllNamespaces();
|
||||
const templates = snippets.filter(s => s.isTemplate);
|
||||
|
||||
return {
|
||||
snippetCount: snippets.length,
|
||||
templateCount: templates.length,
|
||||
namespaceCount: namespaces.length,
|
||||
storageType: 'indexeddb' as const,
|
||||
databaseSize: 0, // IndexedDB doesn't provide easy size calculation
|
||||
};
|
||||
}
|
||||
|
||||
// Export/Import
|
||||
export async function exportDatabase(): Promise<{ snippets: Snippet[]; namespaces: Namespace[] }> {
|
||||
const snippets = await getAllSnippets();
|
||||
const namespaces = await getAllNamespaces();
|
||||
return { snippets, namespaces };
|
||||
}
|
||||
|
||||
export async function importDatabase(data: { snippets: Snippet[]; namespaces: Namespace[] }): Promise<void> {
|
||||
await clearDatabase();
|
||||
|
||||
const db = await openDB();
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const transaction = db.transaction([SNIPPETS_STORE, NAMESPACES_STORE], 'readwrite');
|
||||
const snippetsStore = transaction.objectStore(SNIPPETS_STORE);
|
||||
const namespacesStore = transaction.objectStore(NAMESPACES_STORE);
|
||||
|
||||
// Import namespaces
|
||||
for (const namespace of data.namespaces) {
|
||||
namespacesStore.add(namespace);
|
||||
}
|
||||
|
||||
// Import snippets
|
||||
for (const snippet of data.snippets) {
|
||||
snippetsStore.add(snippet);
|
||||
}
|
||||
|
||||
transaction.onerror = () => reject(transaction.error);
|
||||
transaction.oncomplete = () => resolve();
|
||||
});
|
||||
}
|
||||
@@ -249,4 +249,49 @@ export class FlaskStorageAdapter {
|
||||
throw new Error(`Failed to bulk move snippets: ${response.statusText}`)
|
||||
}
|
||||
}
|
||||
|
||||
async getSnippetsByNamespace(namespaceId: string): Promise<Snippet[]> {
|
||||
const snippets = await this.getAllSnippets();
|
||||
return snippets.filter(s => s.namespaceId === namespaceId);
|
||||
}
|
||||
|
||||
async getNamespace(id: string): Promise<import('./types').Namespace | null> {
|
||||
const namespaces = await this.getAllNamespaces();
|
||||
return namespaces.find(ns => ns.id === id) || null;
|
||||
}
|
||||
|
||||
async clearDatabase(): Promise<void> {
|
||||
return this.wipeDatabase();
|
||||
}
|
||||
|
||||
async getStats() {
|
||||
const snippets = await this.getAllSnippets();
|
||||
const namespaces = await this.getAllNamespaces();
|
||||
const templates = snippets.filter(s => s.isTemplate);
|
||||
return {
|
||||
snippetCount: snippets.length,
|
||||
templateCount: templates.length,
|
||||
namespaceCount: namespaces.length,
|
||||
storageType: 'indexeddb' as const,
|
||||
databaseSize: 0,
|
||||
};
|
||||
}
|
||||
|
||||
async exportDatabase(): Promise<{ snippets: Snippet[]; namespaces: import('./types').Namespace[] }> {
|
||||
const snippets = await this.getAllSnippets();
|
||||
const namespaces = await this.getAllNamespaces();
|
||||
return { snippets, namespaces };
|
||||
}
|
||||
|
||||
async importDatabase(data: { snippets: Snippet[]; namespaces: import('./types').Namespace[] }): Promise<void> {
|
||||
await this.wipeDatabase();
|
||||
|
||||
for (const namespace of data.namespaces) {
|
||||
await this.createNamespace(namespace);
|
||||
}
|
||||
|
||||
for (const snippet of data.snippets) {
|
||||
await this.createSnippet(snippet);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@ export interface Snippet {
|
||||
category: string
|
||||
namespaceId?: string
|
||||
hasPreview?: boolean
|
||||
isTemplate?: boolean
|
||||
functionName?: string
|
||||
inputParameters?: InputParameter[]
|
||||
createdAt: number
|
||||
|
||||
35
src/main.tsx
35
src/main.tsx
@@ -1,35 +0,0 @@
|
||||
import { createRoot } from 'react-dom/client'
|
||||
import { ErrorBoundary } from "react-error-boundary";
|
||||
import { Provider } from 'react-redux'
|
||||
import "@github/spark/spark"
|
||||
import { Toaster } from '@/components/ui/sonner'
|
||||
import { loadStorageConfig } from '@/lib/storage'
|
||||
import { store } from '@/store'
|
||||
|
||||
import App from './App.tsx'
|
||||
import { ErrorFallback } from './components/error/ErrorFallback.tsx'
|
||||
|
||||
import "./main.css"
|
||||
import "./styles/theme.css"
|
||||
import "./index.css"
|
||||
|
||||
loadStorageConfig()
|
||||
|
||||
const logErrorToConsole = (error: Error, info: { componentStack?: string }) => {
|
||||
console.error('Application Error:', error);
|
||||
if (info.componentStack) {
|
||||
console.error('Component Stack:', info.componentStack);
|
||||
}
|
||||
};
|
||||
|
||||
createRoot(document.getElementById('root')!).render(
|
||||
<Provider store={store}>
|
||||
<ErrorBoundary
|
||||
FallbackComponent={ErrorFallback}
|
||||
onError={logErrorToConsole}
|
||||
>
|
||||
<App />
|
||||
<Toaster />
|
||||
</ErrorBoundary>
|
||||
</Provider>
|
||||
)
|
||||
@@ -1,38 +0,0 @@
|
||||
import { motion } from 'framer-motion'
|
||||
import { AtomsSection } from '@/components/atoms/AtomsSection'
|
||||
import type { Snippet } from '@/lib/types'
|
||||
import { useCallback } from 'react'
|
||||
import { toast } from 'sonner'
|
||||
import { createSnippet } from '@/lib/db'
|
||||
|
||||
export function AtomsPage() {
|
||||
const handleSaveSnippet = useCallback(async (snippetData: Omit<Snippet, 'id' | 'createdAt' | 'updatedAt'>) => {
|
||||
try {
|
||||
const newSnippet: Snippet = {
|
||||
...snippetData,
|
||||
id: Date.now().toString(),
|
||||
createdAt: Date.now(),
|
||||
updatedAt: Date.now(),
|
||||
}
|
||||
await createSnippet(newSnippet)
|
||||
toast.success('Component saved as snippet!')
|
||||
} catch (error) {
|
||||
console.error('Failed to save snippet:', error)
|
||||
toast.error('Failed to save snippet')
|
||||
}
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.4 }}
|
||||
>
|
||||
<div className="mb-8">
|
||||
<h2 className="text-3xl font-bold tracking-tight mb-2">Atoms</h2>
|
||||
<p className="text-muted-foreground">Fundamental building blocks - basic HTML elements styled as reusable components</p>
|
||||
</div>
|
||||
<AtomsSection onSaveSnippet={handleSaveSnippet} />
|
||||
</motion.div>
|
||||
)
|
||||
}
|
||||
@@ -1,55 +0,0 @@
|
||||
import { useState } from 'react'
|
||||
import { motion } from 'framer-motion'
|
||||
import { SplitScreenEditor } from '@/components/features/snippet-editor/SplitScreenEditor'
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
|
||||
import { Sparkle } from '@phosphor-icons/react'
|
||||
import { DEMO_CODE } from './demo-constants'
|
||||
import { DemoFeatureCards } from './DemoFeatureCards'
|
||||
|
||||
export function DemoPage() {
|
||||
const [code, setCode] = useState(DEMO_CODE)
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.4 }}
|
||||
className="space-y-8"
|
||||
>
|
||||
<div className="mb-8">
|
||||
<div className="flex items-center gap-3 mb-2">
|
||||
<div className="h-10 w-10 rounded-lg bg-gradient-to-br from-accent to-primary flex items-center justify-center">
|
||||
<Sparkle className="h-5 w-5 text-primary-foreground" weight="fill" />
|
||||
</div>
|
||||
<h2 className="text-3xl font-bold tracking-tight">Split-Screen Demo</h2>
|
||||
</div>
|
||||
<p className="text-muted-foreground">
|
||||
Experience live React component editing with real-time preview. Edit the code on the left and watch it update instantly on the right.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<Card className="border-accent/20 bg-card/50 backdrop-blur">
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<Sparkle className="h-5 w-5 text-accent" weight="fill" />
|
||||
Interactive Code Editor
|
||||
</CardTitle>
|
||||
<CardDescription>
|
||||
This editor supports JSX, TSX, JavaScript, and TypeScript with live preview.
|
||||
Try switching between Code, Split, and Preview modes using the buttons above the editor.
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<SplitScreenEditor
|
||||
value={code}
|
||||
onChange={setCode}
|
||||
language="JSX"
|
||||
height="600px"
|
||||
/>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<DemoFeatureCards />
|
||||
</motion.div>
|
||||
)
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
import { motion } from 'framer-motion'
|
||||
import { SnippetManagerRedux } from '@/components/SnippetManagerRedux'
|
||||
|
||||
export function HomePage() {
|
||||
return (
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.4 }}
|
||||
>
|
||||
<div className="mb-8">
|
||||
<h2 className="text-3xl font-bold tracking-tight mb-2">My Snippets</h2>
|
||||
<p className="text-muted-foreground">Save, organize, and share your code snippets</p>
|
||||
</div>
|
||||
<SnippetManagerRedux />
|
||||
</motion.div>
|
||||
)
|
||||
}
|
||||
@@ -1,38 +0,0 @@
|
||||
import { motion } from 'framer-motion'
|
||||
import { MoleculesSection } from '@/components/molecules/MoleculesSection'
|
||||
import type { Snippet } from '@/lib/types'
|
||||
import { useCallback } from 'react'
|
||||
import { toast } from 'sonner'
|
||||
import { createSnippet } from '@/lib/db'
|
||||
|
||||
export function MoleculesPage() {
|
||||
const handleSaveSnippet = useCallback(async (snippetData: Omit<Snippet, 'id' | 'createdAt' | 'updatedAt'>) => {
|
||||
try {
|
||||
const newSnippet: Snippet = {
|
||||
...snippetData,
|
||||
id: Date.now().toString(),
|
||||
createdAt: Date.now(),
|
||||
updatedAt: Date.now(),
|
||||
}
|
||||
await createSnippet(newSnippet)
|
||||
toast.success('Component saved as snippet!')
|
||||
} catch (error) {
|
||||
console.error('Failed to save snippet:', error)
|
||||
toast.error('Failed to save snippet')
|
||||
}
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.4 }}
|
||||
>
|
||||
<div className="mb-8">
|
||||
<h2 className="text-3xl font-bold tracking-tight mb-2">Molecules</h2>
|
||||
<p className="text-muted-foreground">Simple combinations of atoms that work together as functional units</p>
|
||||
</div>
|
||||
<MoleculesSection onSaveSnippet={handleSaveSnippet} />
|
||||
</motion.div>
|
||||
)
|
||||
}
|
||||
@@ -1,38 +0,0 @@
|
||||
import { motion } from 'framer-motion'
|
||||
import { OrganismsSection } from '@/components/organisms/OrganismsSection'
|
||||
import type { Snippet } from '@/lib/types'
|
||||
import { useCallback } from 'react'
|
||||
import { toast } from 'sonner'
|
||||
import { createSnippet } from '@/lib/db'
|
||||
|
||||
export function OrganismsPage() {
|
||||
const handleSaveSnippet = useCallback(async (snippetData: Omit<Snippet, 'id' | 'createdAt' | 'updatedAt'>) => {
|
||||
try {
|
||||
const newSnippet: Snippet = {
|
||||
...snippetData,
|
||||
id: Date.now().toString(),
|
||||
createdAt: Date.now(),
|
||||
updatedAt: Date.now(),
|
||||
}
|
||||
await createSnippet(newSnippet)
|
||||
toast.success('Component saved as snippet!')
|
||||
} catch (error) {
|
||||
console.error('Failed to save snippet:', error)
|
||||
toast.error('Failed to save snippet')
|
||||
}
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.4 }}
|
||||
>
|
||||
<div className="mb-8">
|
||||
<h2 className="text-3xl font-bold tracking-tight mb-2">Organisms</h2>
|
||||
<p className="text-muted-foreground">Complex UI components composed of molecules and atoms</p>
|
||||
</div>
|
||||
<OrganismsSection onSaveSnippet={handleSaveSnippet} />
|
||||
</motion.div>
|
||||
)
|
||||
}
|
||||
@@ -1,100 +0,0 @@
|
||||
import { motion } from 'framer-motion'
|
||||
import { PersistenceSettings } from '@/components/demo/PersistenceSettings'
|
||||
import { SchemaHealthCard } from '@/components/settings/SchemaHealthCard'
|
||||
import { BackendAutoConfigCard } from '@/components/settings/BackendAutoConfigCard'
|
||||
import { StorageBackendCard } from '@/components/settings/StorageBackendCard'
|
||||
import { DatabaseStatsCard } from '@/components/settings/DatabaseStatsCard'
|
||||
import { StorageInfoCard } from '@/components/settings/StorageInfoCard'
|
||||
import { DatabaseActionsCard } from '@/components/settings/DatabaseActionsCard'
|
||||
import { useSettingsState } from '@/hooks/useSettingsState'
|
||||
|
||||
export function SettingsPage() {
|
||||
const {
|
||||
stats,
|
||||
loading,
|
||||
storageBackend,
|
||||
setStorageBackend,
|
||||
flaskUrl,
|
||||
setFlaskUrl,
|
||||
flaskConnectionStatus,
|
||||
setFlaskConnectionStatus,
|
||||
testingConnection,
|
||||
envVarSet,
|
||||
schemaHealth,
|
||||
checkingSchema,
|
||||
handleExport,
|
||||
handleImport,
|
||||
handleClear,
|
||||
handleSeed,
|
||||
formatBytes,
|
||||
handleTestConnection,
|
||||
handleSaveStorageConfig,
|
||||
handleMigrateToFlask,
|
||||
handleMigrateToIndexedDB,
|
||||
checkSchemaHealth,
|
||||
} = useSettingsState()
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.4 }}
|
||||
>
|
||||
<div className="mb-8">
|
||||
<h2 className="text-3xl font-bold tracking-tight mb-2">Settings</h2>
|
||||
<p className="text-muted-foreground">Manage your database and application settings</p>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-6 max-w-3xl">
|
||||
<PersistenceSettings />
|
||||
|
||||
<SchemaHealthCard
|
||||
schemaHealth={schemaHealth}
|
||||
checkingSchema={checkingSchema}
|
||||
onClear={handleClear}
|
||||
onCheckSchema={checkSchemaHealth}
|
||||
/>
|
||||
|
||||
<BackendAutoConfigCard
|
||||
envVarSet={envVarSet}
|
||||
flaskUrl={flaskUrl}
|
||||
flaskConnectionStatus={flaskConnectionStatus}
|
||||
testingConnection={testingConnection}
|
||||
onTestConnection={handleTestConnection}
|
||||
/>
|
||||
|
||||
<StorageBackendCard
|
||||
storageBackend={storageBackend}
|
||||
flaskUrl={flaskUrl}
|
||||
flaskConnectionStatus={flaskConnectionStatus}
|
||||
testingConnection={testingConnection}
|
||||
envVarSet={envVarSet}
|
||||
onStorageBackendChange={setStorageBackend}
|
||||
onFlaskUrlChange={(url) => {
|
||||
setFlaskUrl(url)
|
||||
setFlaskConnectionStatus('unknown')
|
||||
}}
|
||||
onTestConnection={handleTestConnection}
|
||||
onSaveConfig={handleSaveStorageConfig}
|
||||
onMigrateToFlask={handleMigrateToFlask}
|
||||
onMigrateToIndexedDB={handleMigrateToIndexedDB}
|
||||
/>
|
||||
|
||||
<DatabaseStatsCard
|
||||
loading={loading}
|
||||
stats={stats}
|
||||
formatBytes={formatBytes}
|
||||
/>
|
||||
|
||||
<StorageInfoCard storageType={stats?.storageType} />
|
||||
|
||||
<DatabaseActionsCard
|
||||
onExport={handleExport}
|
||||
onImport={handleImport}
|
||||
onSeed={handleSeed}
|
||||
onClear={handleClear}
|
||||
/>
|
||||
</div>
|
||||
</motion.div>
|
||||
)
|
||||
}
|
||||
@@ -1,38 +0,0 @@
|
||||
import { motion } from 'framer-motion'
|
||||
import { TemplatesSection } from '@/components/templates/TemplatesSection'
|
||||
import type { Snippet } from '@/lib/types'
|
||||
import { useCallback } from 'react'
|
||||
import { toast } from 'sonner'
|
||||
import { createSnippet } from '@/lib/db'
|
||||
|
||||
export function TemplatesPage() {
|
||||
const handleSaveSnippet = useCallback(async (snippetData: Omit<Snippet, 'id' | 'createdAt' | 'updatedAt'>) => {
|
||||
try {
|
||||
const newSnippet: Snippet = {
|
||||
...snippetData,
|
||||
id: Date.now().toString(),
|
||||
createdAt: Date.now(),
|
||||
updatedAt: Date.now(),
|
||||
}
|
||||
await createSnippet(newSnippet)
|
||||
toast.success('Component saved as snippet!')
|
||||
} catch (error) {
|
||||
console.error('Failed to save snippet:', error)
|
||||
toast.error('Failed to save snippet')
|
||||
}
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.4 }}
|
||||
>
|
||||
<div className="mb-8">
|
||||
<h2 className="text-3xl font-bold tracking-tight mb-2">Templates</h2>
|
||||
<p className="text-muted-foreground">Page-level layouts that combine organisms into complete interfaces</p>
|
||||
</div>
|
||||
<TemplatesSection onSaveSnippet={handleSaveSnippet} />
|
||||
</motion.div>
|
||||
)
|
||||
}
|
||||
@@ -32,7 +32,14 @@ export const fetchNamespaces = createAsyncThunk(
|
||||
export const createNamespace = createAsyncThunk(
|
||||
'namespaces/create',
|
||||
async (name: string) => {
|
||||
return await createNamespaceDB(name)
|
||||
const namespace: Namespace = {
|
||||
id: Date.now().toString(),
|
||||
name,
|
||||
createdAt: Date.now(),
|
||||
isDefault: false,
|
||||
}
|
||||
await createNamespaceDB(namespace)
|
||||
return namespace
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@@ -1,147 +1,62 @@
|
||||
import fs from "fs";
|
||||
|
||||
/** @type {import('tailwindcss').Config} */
|
||||
|
||||
let theme = {};
|
||||
try {
|
||||
const themePath = "./theme.json";
|
||||
|
||||
if (fs.existsSync(themePath)) {
|
||||
theme = JSON.parse(fs.readFileSync(themePath, "utf-8"));
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('failed to parse custom styles', err)
|
||||
module.exports = {
|
||||
darkMode: ["class"],
|
||||
content: [
|
||||
'./src/pages/**/*.{ts,tsx}',
|
||||
'./src/components/**/*.{ts,tsx}',
|
||||
'./src/app/**/*.{ts,tsx}',
|
||||
'./src/**/*.{ts,tsx}',
|
||||
],
|
||||
theme: {
|
||||
container: {
|
||||
center: true,
|
||||
padding: "2rem",
|
||||
screens: {
|
||||
"2xl": "1400px",
|
||||
},
|
||||
},
|
||||
extend: {
|
||||
colors: {
|
||||
border: "hsl(var(--border))",
|
||||
input: "hsl(var(--input))",
|
||||
ring: "hsl(var(--ring))",
|
||||
background: "hsl(var(--background))",
|
||||
foreground: "hsl(var(--foreground))",
|
||||
primary: {
|
||||
DEFAULT: "hsl(var(--primary))",
|
||||
foreground: "hsl(var(--primary-foreground))",
|
||||
},
|
||||
secondary: {
|
||||
DEFAULT: "hsl(var(--secondary))",
|
||||
foreground: "hsl(var(--secondary-foreground))",
|
||||
},
|
||||
destructive: {
|
||||
DEFAULT: "hsl(var(--destructive))",
|
||||
foreground: "hsl(var(--destructive-foreground))",
|
||||
},
|
||||
muted: {
|
||||
DEFAULT: "hsl(var(--muted))",
|
||||
foreground: "hsl(var(--muted-foreground))",
|
||||
},
|
||||
accent: {
|
||||
DEFAULT: "hsl(var(--accent))",
|
||||
foreground: "hsl(var(--accent-foreground))",
|
||||
},
|
||||
popover: {
|
||||
DEFAULT: "hsl(var(--popover))",
|
||||
foreground: "hsl(var(--popover-foreground))",
|
||||
},
|
||||
card: {
|
||||
DEFAULT: "hsl(var(--card))",
|
||||
foreground: "hsl(var(--card-foreground))",
|
||||
},
|
||||
},
|
||||
borderRadius: {
|
||||
lg: "var(--radius)",
|
||||
md: "calc(var(--radius) - 2px)",
|
||||
sm: "calc(var(--radius) - 4px)",
|
||||
},
|
||||
},
|
||||
},
|
||||
plugins: [],
|
||||
}
|
||||
const defaultTheme = {
|
||||
container: {
|
||||
center: true,
|
||||
padding: "2rem",
|
||||
},
|
||||
extend: {
|
||||
screens: {
|
||||
coarse: { raw: "(pointer: coarse)" },
|
||||
fine: { raw: "(pointer: fine)" },
|
||||
pwa: { raw: "(display-mode: standalone)" },
|
||||
},
|
||||
colors: {
|
||||
neutral: {
|
||||
1: "var(--color-neutral-1)",
|
||||
2: "var(--color-neutral-2)",
|
||||
3: "var(--color-neutral-3)",
|
||||
4: "var(--color-neutral-4)",
|
||||
5: "var(--color-neutral-5)",
|
||||
6: "var(--color-neutral-6)",
|
||||
7: "var(--color-neutral-7)",
|
||||
8: "var(--color-neutral-8)",
|
||||
9: "var(--color-neutral-9)",
|
||||
10: "var(--color-neutral-10)",
|
||||
11: "var(--color-neutral-11)",
|
||||
12: "var(--color-neutral-12)",
|
||||
a1: "var(--color-neutral-a1)",
|
||||
a2: "var(--color-neutral-a2)",
|
||||
a3: "var(--color-neutral-a3)",
|
||||
a4: "var(--color-neutral-a4)",
|
||||
a5: "var(--color-neutral-a5)",
|
||||
a6: "var(--color-neutral-a6)",
|
||||
a7: "var(--color-neutral-a7)",
|
||||
a8: "var(--color-neutral-a8)",
|
||||
a9: "var(--color-neutral-a9)",
|
||||
a10: "var(--color-neutral-a10)",
|
||||
a11: "var(--color-neutral-a11)",
|
||||
a12: "var(--color-neutral-a12)",
|
||||
contrast: "var(--color-neutral-contrast)",
|
||||
},
|
||||
accent: {
|
||||
1: "var(--color-accent-1)",
|
||||
2: "var(--color-accent-2)",
|
||||
3: "var(--color-accent-3)",
|
||||
4: "var(--color-accent-4)",
|
||||
5: "var(--color-accent-5)",
|
||||
6: "var(--color-accent-6)",
|
||||
7: "var(--color-accent-7)",
|
||||
8: "var(--color-accent-8)",
|
||||
9: "var(--color-accent-9)",
|
||||
10: "var(--color-accent-10)",
|
||||
11: "var(--color-accent-11)",
|
||||
12: "var(--color-accent-12)",
|
||||
contrast: "var(--color-accent-contrast)",
|
||||
},
|
||||
"accent-secondary": {
|
||||
1: "var(--color-accent-secondary-1)",
|
||||
2: "var(--color-accent-secondary-2)",
|
||||
3: "var(--color-accent-secondary-3)",
|
||||
4: "var(--color-accent-secondary-4)",
|
||||
5: "var(--color-accent-secondary-5)",
|
||||
6: "var(--color-accent-secondary-6)",
|
||||
7: "var(--color-accent-secondary-7)",
|
||||
8: "var(--color-accent-secondary-8)",
|
||||
9: "var(--color-accent-secondary-9)",
|
||||
10: "var(--color-accent-secondary-10)",
|
||||
11: "var(--color-accent-secondary-11)",
|
||||
12: "var(--color-accent-secondary-12)",
|
||||
contrast: "var(--color-accent-secondary-contrast)",
|
||||
},
|
||||
fg: {
|
||||
DEFAULT: "var(--color-fg)",
|
||||
secondary: "var(--color-fg-secondary)",
|
||||
},
|
||||
bg: {
|
||||
DEFAULT: "var(--color-bg)",
|
||||
inset: "var(--color-bg-inset)",
|
||||
overlay: "var(--color-bg-overlay)",
|
||||
},
|
||||
"focus-ring": "var(--color-focus-ring)",
|
||||
},
|
||||
borderRadius: {
|
||||
sm: "var(--radius-sm)",
|
||||
md: "var(--radius-md)",
|
||||
lg: "var(--radius-lg)",
|
||||
xl: "var(--radius-xl)",
|
||||
"2xl": "var(--radius-2xl)",
|
||||
full: "var(--radius-full)",
|
||||
},
|
||||
},
|
||||
spacing: {
|
||||
px: "var(--size-px)",
|
||||
0: "var(--size-0)",
|
||||
0.5: "var(--size-0-5)",
|
||||
1: "var(--size-1)",
|
||||
1.5: "var(--size-1-5)",
|
||||
2: "var(--size-2)",
|
||||
2.5: "var(--size-2-5)",
|
||||
3: "var(--size-3)",
|
||||
3.5: "var(--size-3-5)",
|
||||
4: "var(--size-4)",
|
||||
5: "var(--size-5)",
|
||||
6: "var(--size-6)",
|
||||
7: "var(--size-7)",
|
||||
8: "var(--size-8)",
|
||||
9: "var(--size-9)",
|
||||
10: "var(--size-10)",
|
||||
11: "var(--size-11)",
|
||||
12: "var(--size-12)",
|
||||
14: "var(--size-14)",
|
||||
16: "var(--size-16)",
|
||||
20: "var(--size-20)",
|
||||
24: "var(--size-24)",
|
||||
28: "var(--size-28)",
|
||||
32: "var(--size-32)",
|
||||
36: "var(--size-36)",
|
||||
40: "var(--size-40)",
|
||||
44: "var(--size-44)",
|
||||
48: "var(--size-48)",
|
||||
52: "var(--size-52)",
|
||||
56: "var(--size-56)",
|
||||
60: "var(--size-60)",
|
||||
64: "var(--size-64)",
|
||||
72: "var(--size-72)",
|
||||
80: "var(--size-80)",
|
||||
96: "var(--size-96)",
|
||||
},
|
||||
darkMode: ["selector", '[data-appearance="dark"]'],
|
||||
}
|
||||
|
||||
export default {
|
||||
content: ["./index.html", "./src/**/*.{js,ts,jsx,tsx}"],
|
||||
theme: { ...defaultTheme, ...theme },
|
||||
};
|
||||
@@ -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"
|
||||
|
||||
@@ -1,29 +0,0 @@
|
||||
import tailwindcss from "@tailwindcss/vite";
|
||||
import react from "@vitejs/plugin-react-swc";
|
||||
import { defineConfig, PluginOption } from "vite";
|
||||
|
||||
import sparkPlugin from "@github/spark/spark-vite-plugin";
|
||||
import createIconImportProxy from "@github/spark/vitePhosphorIconProxyPlugin";
|
||||
import { resolve } from 'path'
|
||||
|
||||
const projectRoot = process.env.PROJECT_ROOT || import.meta.dirname
|
||||
|
||||
// https://vite.dev/config/
|
||||
export default defineConfig({
|
||||
// Base path for GitHub Pages deployment
|
||||
// Set to '/' for custom domain or root deployment
|
||||
// Set to '/repo-name/' for GitHub Pages at username.github.io/repo-name/
|
||||
base: process.env.VITE_BASE_PATH || '/',
|
||||
plugins: [
|
||||
react(),
|
||||
tailwindcss(),
|
||||
// DO NOT REMOVE
|
||||
createIconImportProxy() as PluginOption,
|
||||
sparkPlugin() as PluginOption,
|
||||
],
|
||||
resolve: {
|
||||
alias: {
|
||||
'@': resolve(projectRoot, 'src')
|
||||
}
|
||||
},
|
||||
});
|
||||
Reference in New Issue
Block a user