Convert to Next.js - core setup complete

Co-authored-by: johndoe6345789 <224850594+johndoe6345789@users.noreply.github.com>
This commit is contained in:
copilot-swe-agent[bot]
2026-01-19 12:57:36 +00:00
parent 345e73b813
commit 8e93467317
29 changed files with 4935 additions and 355 deletions

View File

@@ -1,8 +1,12 @@
# Frontend Configuration # Frontend Configuration
# Flask Backend URL - If set, the app will automatically use Flask backend instead of IndexedDB # Flask Backend URL - If set, the app will automatically use Flask backend instead of IndexedDB
# Development: VITE_FLASK_BACKEND_URL=http://localhost:5000 # Development: NEXT_PUBLIC_FLASK_BACKEND_URL=http://localhost:5000
# Production: VITE_FLASK_BACKEND_URL=https://backend.example.com # Production: NEXT_PUBLIC_FLASK_BACKEND_URL=https://backend.example.com
VITE_FLASK_BACKEND_URL= NEXT_PUBLIC_FLASK_BACKEND_URL=
# Base path for deployment (e.g., for GitHub Pages)
# Leave empty for root deployment
NEXT_PUBLIC_BASE_PATH=
# Backend Configuration (for backend/app.py) # Backend Configuration (for backend/app.py)
# CORS Allowed Origins - Comma-separated list of allowed frontend URLs # CORS Allowed Origins - Comma-separated list of allowed frontend URLs

3
.eslintrc.json Normal file
View File

@@ -0,0 +1,3 @@
{
"extends": ["next/core-web-vitals", "next/typescript"]
}

View File

@@ -31,11 +31,12 @@ jobs:
- name: Install dependencies - name: Install dependencies
run: npm ci run: npm ci
- name: Build - name: Build Next.js
run: npm run build run: npm run build
env: env:
VITE_FLASK_BACKEND_URL: ${{ vars.VITE_FLASK_BACKEND_URL || '' }} BUILD_STATIC: 'true'
VITE_BASE_PATH: ${{ vars.VITE_BASE_PATH || '/' }} NEXT_PUBLIC_FLASK_BACKEND_URL: ${{ vars.NEXT_PUBLIC_FLASK_BACKEND_URL || '' }}
NEXT_PUBLIC_BASE_PATH: ${{ vars.NEXT_PUBLIC_BASE_PATH || '' }}
- name: Setup Pages - name: Setup Pages
uses: actions/configure-pages@v4 uses: actions/configure-pages@v4
@@ -43,7 +44,7 @@ jobs:
- name: Upload artifact - name: Upload artifact
uses: actions/upload-pages-artifact@v3 uses: actions/upload-pages-artifact@v3
with: with:
path: './dist' path: './out'
deploy: deploy:
environment: environment:

View File

@@ -117,5 +117,5 @@ jobs:
cache-from: type=gha,scope=frontend cache-from: type=gha,scope=frontend
cache-to: type=gha,mode=max,scope=frontend cache-to: type=gha,mode=max,scope=frontend
build-args: | build-args: |
VITE_FLASK_BACKEND_URL=${{ vars.VITE_FLASK_BACKEND_URL || '' }} NEXT_PUBLIC_FLASK_BACKEND_URL=${{ vars.NEXT_PUBLIC_FLASK_BACKEND_URL || '' }}
VITE_BASE_PATH=${{ vars.VITE_BASE_PATH || '/' }} NEXT_PUBLIC_BASE_PATH=${{ vars.NEXT_PUBLIC_BASE_PATH || '' }}

8
.gitignore vendored
View File

@@ -13,6 +13,12 @@ dist-ssr
*-dist *-dist
*.local *.local
# Next.js
/.next/
/out/
next-env.d.ts
.vercel
# Editor directories and files # Editor directories and files
.vscode/* .vscode/*
!.vscode/extensions.json !.vscode/extensions.json
@@ -25,6 +31,8 @@ dist-ssr
*.sw? *.sw?
.env .env
.env.local
.env*.local
**/agent-eval-report* **/agent-eval-report*
packages packages
pids pids

View File

@@ -3,26 +3,37 @@ FROM node:20-alpine AS builder
WORKDIR /app WORKDIR /app
# Copy package files
COPY package*.json ./ COPY package*.json ./
RUN npm ci RUN npm ci
# Copy source code
COPY . . COPY . .
ARG VITE_FLASK_BACKEND_URL # Build arguments for environment variables
ENV VITE_FLASK_BACKEND_URL=$VITE_FLASK_BACKEND_URL ARG NEXT_PUBLIC_FLASK_BACKEND_URL
ARG NEXT_PUBLIC_BASE_PATH
ARG VITE_BASE_PATH ENV NEXT_PUBLIC_FLASK_BACKEND_URL=$NEXT_PUBLIC_FLASK_BACKEND_URL
ENV VITE_BASE_PATH=$VITE_BASE_PATH ENV NEXT_PUBLIC_BASE_PATH=$NEXT_PUBLIC_BASE_PATH
# Build Next.js app
RUN npm run build RUN npm run build
# Production stage # Production stage
FROM nginx:alpine FROM node:20-alpine AS runner
COPY --from=builder /app/dist /usr/share/nginx/html WORKDIR /app
COPY nginx.conf /etc/nginx/conf.d/default.conf ENV NODE_ENV=production
# Copy necessary files from builder
COPY --from=builder /app/public ./public
COPY --from=builder /app/.next/standalone ./
COPY --from=builder /app/.next/static ./.next/static
EXPOSE 3000 EXPOSE 3000
CMD ["nginx", "-g", "daemon off;"] ENV PORT=3000
ENV HOSTNAME="0.0.0.0"
CMD ["node", "server.js"]

89
app/PageLayout.tsx Normal file
View File

@@ -0,0 +1,89 @@
'use client';
import { motion } from 'framer-motion';
import { Code } from '@phosphor-icons/react';
import { Navigation } from '@/components/layout/navigation/Navigation';
import { NavigationSidebar } from '@/components/layout/navigation/NavigationSidebar';
import { useNavigation } from '@/components/layout/navigation/useNavigation';
import { BackendIndicator } from '@/components/layout/BackendIndicator';
import { ReactNode } from 'react';
export function PageLayout({ children }: { children: ReactNode }) {
const { menuOpen } = useNavigation();
return (
<div className="min-h-screen bg-background">
<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">
{children}
</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>
);
}

43
app/atoms/page.tsx Normal file
View File

@@ -0,0 +1,43 @@
'use client';
import { motion } from 'framer-motion';
import { AtomsSection } from '@/components/atoms/AtomsSection';
import type { Snippet } from '@/lib/types';
import { useCallback } from 'react';
import { toast } from 'sonner';
import { createSnippet } from '@/lib/db';
import { PageLayout } from '../PageLayout';
export default function AtomsPage() {
const handleSaveSnippet = useCallback(async (snippetData: Omit<Snippet, 'id' | 'createdAt' | 'updatedAt'>) => {
try {
const newSnippet: Snippet = {
...snippetData,
id: Date.now().toString(),
createdAt: Date.now(),
updatedAt: Date.now(),
};
await createSnippet(newSnippet);
toast.success('Component saved as snippet!');
} catch (error) {
console.error('Failed to save snippet:', error);
toast.error('Failed to save snippet');
}
}, []);
return (
<PageLayout>
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.4 }}
>
<div className="mb-8">
<h2 className="text-3xl font-bold tracking-tight mb-2">Atoms</h2>
<p className="text-muted-foreground">Fundamental building blocks - basic HTML elements styled as reusable components</p>
</div>
<AtomsSection onSaveSnippet={handleSaveSnippet} />
</motion.div>
</PageLayout>
);
}

60
app/demo/page.tsx Normal file
View File

@@ -0,0 +1,60 @@
'use client';
import { useState } from 'react';
import { motion } from 'framer-motion';
import { SplitScreenEditor } from '@/components/features/snippet-editor/SplitScreenEditor';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
import { Sparkle } from '@phosphor-icons/react';
import { DEMO_CODE } from '@/pages/demo-constants';
import { DemoFeatureCards } from '@/pages/DemoFeatureCards';
import { PageLayout } from '../PageLayout';
export default function DemoPage() {
const [code, setCode] = useState(DEMO_CODE);
return (
<PageLayout>
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.4 }}
className="space-y-8"
>
<div className="mb-8">
<div className="flex items-center gap-3 mb-2">
<div className="h-10 w-10 rounded-lg bg-gradient-to-br from-accent to-primary flex items-center justify-center">
<Sparkle className="h-5 w-5 text-primary-foreground" weight="fill" />
</div>
<h2 className="text-3xl font-bold tracking-tight">Split-Screen Demo</h2>
</div>
<p className="text-muted-foreground">
Experience live React component editing with real-time preview. Edit the code on the left and watch it update instantly on the right.
</p>
</div>
<Card className="border-accent/20 bg-card/50 backdrop-blur">
<CardHeader>
<CardTitle className="flex items-center gap-2">
<Sparkle className="h-5 w-5 text-accent" weight="fill" />
Interactive Code Editor
</CardTitle>
<CardDescription>
This editor supports JSX, TSX, JavaScript, and TypeScript with live preview.
Try switching between Code, Split, and Preview modes using the buttons above the editor.
</CardDescription>
</CardHeader>
<CardContent>
<SplitScreenEditor
value={code}
onChange={setCode}
language="JSX"
height="600px"
/>
</CardContent>
</Card>
<DemoFeatureCards />
</motion.div>
</PageLayout>
);
}

92
app/globals.css Normal file
View File

@@ -0,0 +1,92 @@
@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;
}

40
app/layout.tsx Normal file
View File

@@ -0,0 +1,40 @@
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',
};
export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
return (
<html lang="en" suppressHydrationWarning>
<body className={`${inter.variable} ${jetbrainsMono.variable} ${bricolageGrotesque.variable}`}>
<Providers>
{children}
</Providers>
</body>
</html>
);
}

43
app/molecules/page.tsx Normal file
View File

@@ -0,0 +1,43 @@
'use client';
import { motion } from 'framer-motion';
import { MoleculesSection } from '@/components/molecules/MoleculesSection';
import type { Snippet } from '@/lib/types';
import { useCallback } from 'react';
import { toast } from 'sonner';
import { createSnippet } from '@/lib/db';
import { PageLayout } from '../PageLayout';
export default function MoleculesPage() {
const handleSaveSnippet = useCallback(async (snippetData: Omit<Snippet, 'id' | 'createdAt' | 'updatedAt'>) => {
try {
const newSnippet: Snippet = {
...snippetData,
id: Date.now().toString(),
createdAt: Date.now(),
updatedAt: Date.now(),
};
await createSnippet(newSnippet);
toast.success('Component saved as snippet!');
} catch (error) {
console.error('Failed to save snippet:', error);
toast.error('Failed to save snippet');
}
}, []);
return (
<PageLayout>
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.4 }}
>
<div className="mb-8">
<h2 className="text-3xl font-bold tracking-tight mb-2">Molecules</h2>
<p className="text-muted-foreground">Simple combinations of atoms that work together as functional units</p>
</div>
<MoleculesSection onSaveSnippet={handleSaveSnippet} />
</motion.div>
</PageLayout>
);
}

43
app/organisms/page.tsx Normal file
View File

@@ -0,0 +1,43 @@
'use client';
import { motion } from 'framer-motion';
import { OrganismsSection } from '@/components/organisms/OrganismsSection';
import type { Snippet } from '@/lib/types';
import { useCallback } from 'react';
import { toast } from 'sonner';
import { createSnippet } from '@/lib/db';
import { PageLayout } from '../PageLayout';
export default function OrganismsPage() {
const handleSaveSnippet = useCallback(async (snippetData: Omit<Snippet, 'id' | 'createdAt' | 'updatedAt'>) => {
try {
const newSnippet: Snippet = {
...snippetData,
id: Date.now().toString(),
createdAt: Date.now(),
updatedAt: Date.now(),
};
await createSnippet(newSnippet);
toast.success('Component saved as snippet!');
} catch (error) {
console.error('Failed to save snippet:', error);
toast.error('Failed to save snippet');
}
}, []);
return (
<PageLayout>
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.4 }}
>
<div className="mb-8">
<h2 className="text-3xl font-bold tracking-tight mb-2">Organisms</h2>
<p className="text-muted-foreground">Complex UI components composed of molecules and atoms</p>
</div>
<OrganismsSection onSaveSnippet={handleSaveSnippet} />
</motion.div>
</PageLayout>
);
}

23
app/page.tsx Normal file
View File

@@ -0,0 +1,23 @@
'use client';
import { motion } from 'framer-motion';
import { SnippetManagerRedux } from '@/components/SnippetManagerRedux';
import { PageLayout } from './PageLayout';
export default function HomePage() {
return (
<PageLayout>
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.4 }}
>
<div className="mb-8">
<h2 className="text-3xl font-bold tracking-tight mb-2">My Snippets</h2>
<p className="text-muted-foreground">Save, organize, and share your code snippets</p>
</div>
<SnippetManagerRedux />
</motion.div>
</PageLayout>
);
}

42
app/providers.tsx Normal file
View File

@@ -0,0 +1,42 @@
'use client';
import '@github/spark/spark';
import { ErrorBoundary } from 'react-error-boundary';
import { Provider } from 'react-redux';
import { Toaster } from '@/components/ui/sonner';
import { store } from '@/store';
import { ErrorFallback } from '@/components/error/ErrorFallback';
import { NavigationProvider } from '@/components/layout/navigation/NavigationProvider';
import { useEffect } from 'react';
import { loadStorageConfig } from '@/lib/storage';
const logErrorToConsole = (error: Error, info: { componentStack?: string }) => {
console.error('Application Error:', error);
if (info.componentStack) {
console.error('Component Stack:', info.componentStack);
}
};
function StorageInitializer() {
useEffect(() => {
loadStorageConfig();
}, []);
return null;
}
export function Providers({ children }: { children: React.ReactNode }) {
return (
<Provider store={store}>
<ErrorBoundary
FallbackComponent={ErrorFallback}
onError={logErrorToConsole}
>
<NavigationProvider>
<StorageInitializer />
{children}
<Toaster />
</NavigationProvider>
</ErrorBoundary>
</Provider>
);
}

105
app/settings/page.tsx Normal file
View File

@@ -0,0 +1,105 @@
'use client';
import { motion } from 'framer-motion';
import { PersistenceSettings } from '@/components/demo/PersistenceSettings';
import { SchemaHealthCard } from '@/components/settings/SchemaHealthCard';
import { BackendAutoConfigCard } from '@/components/settings/BackendAutoConfigCard';
import { StorageBackendCard } from '@/components/settings/StorageBackendCard';
import { DatabaseStatsCard } from '@/components/settings/DatabaseStatsCard';
import { StorageInfoCard } from '@/components/settings/StorageInfoCard';
import { DatabaseActionsCard } from '@/components/settings/DatabaseActionsCard';
import { useSettingsState } from '@/hooks/useSettingsState';
import { PageLayout } from '../PageLayout';
export default function SettingsPage() {
const {
stats,
loading,
storageBackend,
setStorageBackend,
flaskUrl,
setFlaskUrl,
flaskConnectionStatus,
setFlaskConnectionStatus,
testingConnection,
envVarSet,
schemaHealth,
checkingSchema,
handleExport,
handleImport,
handleClear,
handleSeed,
formatBytes,
handleTestConnection,
handleSaveStorageConfig,
handleMigrateToFlask,
handleMigrateToIndexedDB,
checkSchemaHealth,
} = useSettingsState();
return (
<PageLayout>
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.4 }}
>
<div className="mb-8">
<h2 className="text-3xl font-bold tracking-tight mb-2">Settings</h2>
<p className="text-muted-foreground">Manage your database and application settings</p>
</div>
<div className="grid gap-6 max-w-3xl">
<PersistenceSettings />
<SchemaHealthCard
schemaHealth={schemaHealth}
checkingSchema={checkingSchema}
onClear={handleClear}
onCheckSchema={checkSchemaHealth}
/>
<BackendAutoConfigCard
envVarSet={envVarSet}
flaskUrl={flaskUrl}
flaskConnectionStatus={flaskConnectionStatus}
testingConnection={testingConnection}
onTestConnection={handleTestConnection}
/>
<StorageBackendCard
storageBackend={storageBackend}
flaskUrl={flaskUrl}
flaskConnectionStatus={flaskConnectionStatus}
testingConnection={testingConnection}
envVarSet={envVarSet}
onStorageBackendChange={setStorageBackend}
onFlaskUrlChange={(url) => {
setFlaskUrl(url);
setFlaskConnectionStatus('unknown');
}}
onTestConnection={handleTestConnection}
onSaveConfig={handleSaveStorageConfig}
onMigrateToFlask={handleMigrateToFlask}
onMigrateToIndexedDB={handleMigrateToIndexedDB}
/>
<DatabaseStatsCard
loading={loading}
stats={stats}
formatBytes={formatBytes}
/>
<StorageInfoCard storageType={stats?.storageType} />
<DatabaseActionsCard
onExport={handleExport}
onImport={handleImport}
onSeed={handleSeed}
onClear={handleClear}
/>
</div>
</motion.div>
</PageLayout>
);
}

43
app/templates/page.tsx Normal file
View File

@@ -0,0 +1,43 @@
'use client';
import { motion } from 'framer-motion';
import { TemplatesSection } from '@/components/templates/TemplatesSection';
import type { Snippet } from '@/lib/types';
import { useCallback } from 'react';
import { toast } from 'sonner';
import { createSnippet } from '@/lib/db';
import { PageLayout } from '../PageLayout';
export default function TemplatesPage() {
const handleSaveSnippet = useCallback(async (snippetData: Omit<Snippet, 'id' | 'createdAt' | 'updatedAt'>) => {
try {
const newSnippet: Snippet = {
...snippetData,
id: Date.now().toString(),
createdAt: Date.now(),
updatedAt: Date.now(),
};
await createSnippet(newSnippet);
toast.success('Component saved as snippet!');
} catch (error) {
console.error('Failed to save snippet:', error);
toast.error('Failed to save snippet');
}
}, []);
return (
<PageLayout>
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.4 }}
>
<div className="mb-8">
<h2 className="text-3xl font-bold tracking-tight mb-2">Templates</h2>
<p className="text-muted-foreground">Page-level layouts that combine organisms into complete interfaces</p>
</div>
<TemplatesSection onSaveSnippet={handleSaveSnippet} />
</motion.div>
</PageLayout>
);
}

View File

@@ -18,7 +18,7 @@ services:
build: build:
context: . context: .
args: args:
- VITE_FLASK_BACKEND_URL=https://backend.example.com - NEXT_PUBLIC_FLASK_BACKEND_URL=https://backend.example.com
ports: ports:
- "3000:3000" - "3000:3000"
depends_on: depends_on:

View File

@@ -16,7 +16,7 @@ services:
build: build:
context: . context: .
args: args:
- VITE_FLASK_BACKEND_URL=http://localhost:5000 - NEXT_PUBLIC_FLASK_BACKEND_URL=http://localhost:5000
ports: ports:
- "3000:3000" - "3000:3000"
depends_on: depends_on:

49
next.config.ts Normal file
View File

@@ -0,0 +1,49 @@
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;

4454
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -2,14 +2,12 @@
"name": "spark-template", "name": "spark-template",
"private": true, "private": true,
"version": "0.0.0", "version": "0.0.0",
"type": "module",
"scripts": { "scripts": {
"dev": "vite", "dev": "next dev",
"kill": "fuser -k 5000/tcp", "build": "next build",
"build": "tsc -b --noCheck && vite build", "start": "next start",
"lint": "eslint .", "lint": "next lint",
"optimize": "vite optimize", "kill": "fuser -k 5000/tcp"
"preview": "vite preview"
}, },
"dependencies": { "dependencies": {
"@babel/standalone": "^7.28.6", "@babel/standalone": "^7.28.6",
@@ -48,7 +46,6 @@
"@radix-ui/react-tooltip": "^1.1.8", "@radix-ui/react-tooltip": "^1.1.8",
"@reduxjs/toolkit": "^2.11.2", "@reduxjs/toolkit": "^2.11.2",
"@tailwindcss/container-queries": "^0.1.1", "@tailwindcss/container-queries": "^0.1.1",
"@tailwindcss/vite": "^4.1.11",
"@tanstack/react-query": "^5.83.1", "@tanstack/react-query": "^5.83.1",
"@types/sql.js": "^1.4.9", "@types/sql.js": "^1.4.9",
"class-variance-authority": "^0.7.1", "class-variance-authority": "^0.7.1",
@@ -61,6 +58,7 @@
"input-otp": "^1.4.2", "input-otp": "^1.4.2",
"lucide-react": "^0.484.0", "lucide-react": "^0.484.0",
"marked": "^15.0.7", "marked": "^15.0.7",
"next": "16.1.3",
"next-themes": "^0.4.6", "next-themes": "^0.4.6",
"octokit": "^4.1.2", "octokit": "^4.1.2",
"pyodide": "^0.29.1", "pyodide": "^0.29.1",
@@ -87,15 +85,13 @@
"@tailwindcss/postcss": "^4.1.8", "@tailwindcss/postcss": "^4.1.8",
"@types/react": "^19.0.10", "@types/react": "^19.0.10",
"@types/react-dom": "^19.0.4", "@types/react-dom": "^19.0.4",
"@vitejs/plugin-react-swc": "^4.2.2",
"eslint": "^9.28.0", "eslint": "^9.28.0",
"eslint-config-next": "^16.1.3",
"eslint-plugin-react-hooks": "^5.2.0", "eslint-plugin-react-hooks": "^5.2.0",
"eslint-plugin-react-refresh": "^0.4.19",
"globals": "^16.0.0", "globals": "^16.0.0",
"tailwindcss": "^4.1.11", "tailwindcss": "^4.1.11",
"typescript": "~5.7.2", "typescript": "~5.7.2",
"typescript-eslint": "^8.38.0", "typescript-eslint": "^8.38.0"
"vite": "^7.2.6"
}, },
"workspaces": { "workspaces": {
"packages": [ "packages": [

View File

@@ -10,7 +10,7 @@ export function BackendIndicator() {
useEffect(() => { useEffect(() => {
const config = getStorageConfig() const config = getStorageConfig()
setBackend(config.backend) setBackend(config.backend)
setIsEnvConfigured(Boolean(import.meta.env.VITE_FLASK_BACKEND_URL)) setIsEnvConfigured(Boolean(process.env.NEXT_PUBLIC_FLASK_BACKEND_URL))
}, []) }, [])
if (backend === 'indexeddb') { if (backend === 'indexeddb') {

View File

@@ -1,14 +1,17 @@
import { motion } from 'framer-motion' 'use client';
import { Link, useLocation } from 'react-router-dom'
import { X } from '@phosphor-icons/react' import { motion } from 'framer-motion';
import { Button } from '@/components/ui/button' import Link from 'next/link';
import { cn } from '@/lib/utils' import { usePathname } from 'next/navigation';
import { navigationItems } from './navigation-items' import { X } from '@phosphor-icons/react';
import { useNavigation } from './useNavigation' import { Button } from '@/components/ui/button';
import { cn } from '@/lib/utils';
import { navigationItems } from './navigation-items';
import { useNavigation } from './useNavigation';
export function NavigationSidebar() { export function NavigationSidebar() {
const { menuOpen, setMenuOpen } = useNavigation() const { menuOpen, setMenuOpen } = useNavigation();
const location = useLocation() const pathname = usePathname();
return ( return (
<motion.aside <motion.aside
@@ -31,11 +34,11 @@ export function NavigationSidebar() {
<nav className="p-4"> <nav className="p-4">
<ul className="space-y-2"> <ul className="space-y-2">
{navigationItems.map((item) => { {navigationItems.map((item) => {
const Icon = item.icon const Icon = item.icon;
const isActive = location.pathname === item.path const isActive = pathname === item.path;
return ( return (
<li key={item.path}> <li key={item.path}>
<Link to={item.path}> <Link href={item.path}>
<Button <Button
variant={isActive ? 'secondary' : 'ghost'} variant={isActive ? 'secondary' : 'ghost'}
className={cn( className={cn(
@@ -48,7 +51,7 @@ export function NavigationSidebar() {
</Button> </Button>
</Link> </Link>
</li> </li>
) );
})} })}
</ul> </ul>
</nav> </nav>
@@ -60,5 +63,5 @@ export function NavigationSidebar() {
</div> </div>
</div> </div>
</motion.aside> </motion.aside>
) );
} }

View File

@@ -38,7 +38,7 @@ export function BackendAutoConfigCard({
</div> </div>
<div className="flex items-center justify-between py-2"> <div className="flex items-center justify-between py-2">
<span className="text-sm text-muted-foreground">Configuration Source</span> <span className="text-sm text-muted-foreground">Configuration Source</span>
<code className="text-sm font-mono bg-muted px-2 py-1 rounded">VITE_FLASK_BACKEND_URL</code> <code className="text-sm font-mono bg-muted px-2 py-1 rounded">NEXT_PUBLIC_FLASK_BACKEND_URL</code>
</div> </div>
<div className="flex items-center justify-between py-2"> <div className="flex items-center justify-between py-2">
<span className="text-sm text-muted-foreground">Status</span> <span className="text-sm text-muted-foreground">Status</span>

View File

@@ -51,7 +51,7 @@ export function StorageBackendCard({
<AlertDescription className="flex items-center gap-2"> <AlertDescription className="flex items-center gap-2">
<CloudCheck weight="fill" size={16} className="text-accent" /> <CloudCheck weight="fill" size={16} className="text-accent" />
<span> <span>
Storage backend is configured via <code className="px-1.5 py-0.5 rounded bg-muted text-xs font-mono">VITE_FLASK_BACKEND_URL</code> environment variable and cannot be changed here. Storage backend is configured via <code className="px-1.5 py-0.5 rounded bg-muted text-xs font-mono">NEXT_PUBLIC_FLASK_BACKEND_URL</code> environment variable and cannot be changed here.
</span> </span>
</AlertDescription> </AlertDescription>
</Alert> </Alert>

View File

@@ -32,7 +32,7 @@ export function useStorageConfig() {
const loadConfig = useCallback(() => { const loadConfig = useCallback(() => {
const config = loadStorageConfig() const config = loadStorageConfig()
const envFlaskUrl = import.meta.env.VITE_FLASK_BACKEND_URL const envFlaskUrl = process.env.NEXT_PUBLIC_FLASK_BACKEND_URL
const isEnvSet = Boolean(envFlaskUrl) const isEnvSet = Boolean(envFlaskUrl)
setEnvVarSet(isEnvSet) setEnvVarSet(isEnvSet)

View File

@@ -10,7 +10,7 @@ export interface StorageConfig {
const STORAGE_CONFIG_KEY = 'codesnippet-storage-config' const STORAGE_CONFIG_KEY = 'codesnippet-storage-config'
function getDefaultConfig(): StorageConfig { function getDefaultConfig(): StorageConfig {
const flaskUrl = import.meta.env.VITE_FLASK_BACKEND_URL const flaskUrl = process.env.NEXT_PUBLIC_FLASK_BACKEND_URL
if (flaskUrl) { if (flaskUrl) {
return { return {

View File

@@ -1,34 +1,44 @@
{ {
"compilerOptions": { "compilerOptions": {
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
"target": "ES2020", "target": "ES2020",
"useDefineForClassFields": true,
"lib": [ "lib": [
"ES2020", "ES2020",
"DOM", "DOM",
"DOM.Iterable" "DOM.Iterable"
], ],
"jsx": "preserve",
"module": "ESNext", "module": "ESNext",
"skipLibCheck": true,
"strictNullChecks": true,
/* Bundler mode */
"moduleResolution": "bundler", "moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"resolveJsonModule": true, "resolveJsonModule": true,
"isolatedModules": true, "allowJs": true,
"moduleDetection": "force", "skipLibCheck": true,
"strict": false,
"strictNullChecks": true,
"noEmit": true, "noEmit": true,
"jsx": "react-jsx", "esModuleInterop": true,
/* Linting */ "isolatedModules": true,
"noFallthroughCasesInSwitch": true, "incremental": true,
"noUncheckedSideEffectImports": true, "noUncheckedSideEffectImports": true,
"noFallthroughCasesInSwitch": true,
"allowImportingTsExtensions": false,
"plugins": [
{
"name": "next"
}
],
"paths": { "paths": {
"@/*": [ "@/*": [
"./src/*" "./src/*"
] ]
}, }
}, },
"include": [ "include": [
"src" "next-env.d.ts",
"**/*.ts",
"**/*.tsx",
".next/types/**/*.ts"
],
"exclude": [
"node_modules"
] ]
} }