mirror of
https://github.com/johndoe6345789/snippet-pastebin.git
synced 2026-04-24 13:34:55 +00:00
Convert to Next.js - core setup complete
Co-authored-by: johndoe6345789 <224850594+johndoe6345789@users.noreply.github.com>
This commit is contained in:
10
.env.example
10
.env.example
@@ -1,8 +1,12 @@
|
||||
# Frontend Configuration
|
||||
# Flask Backend URL - If set, the app will automatically use Flask backend instead of IndexedDB
|
||||
# Development: VITE_FLASK_BACKEND_URL=http://localhost:5000
|
||||
# Production: VITE_FLASK_BACKEND_URL=https://backend.example.com
|
||||
VITE_FLASK_BACKEND_URL=
|
||||
# Development: NEXT_PUBLIC_FLASK_BACKEND_URL=http://localhost:5000
|
||||
# Production: NEXT_PUBLIC_FLASK_BACKEND_URL=https://backend.example.com
|
||||
NEXT_PUBLIC_FLASK_BACKEND_URL=
|
||||
|
||||
# Base path for deployment (e.g., for GitHub Pages)
|
||||
# Leave empty for root deployment
|
||||
NEXT_PUBLIC_BASE_PATH=
|
||||
|
||||
# Backend Configuration (for backend/app.py)
|
||||
# CORS Allowed Origins - Comma-separated list of allowed frontend URLs
|
||||
|
||||
3
.eslintrc.json
Normal file
3
.eslintrc.json
Normal file
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"extends": ["next/core-web-vitals", "next/typescript"]
|
||||
}
|
||||
9
.github/workflows/deploy-pages.yml
vendored
9
.github/workflows/deploy-pages.yml
vendored
@@ -31,11 +31,12 @@ jobs:
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
|
||||
- name: Build
|
||||
- name: Build Next.js
|
||||
run: npm run build
|
||||
env:
|
||||
VITE_FLASK_BACKEND_URL: ${{ vars.VITE_FLASK_BACKEND_URL || '' }}
|
||||
VITE_BASE_PATH: ${{ vars.VITE_BASE_PATH || '/' }}
|
||||
BUILD_STATIC: 'true'
|
||||
NEXT_PUBLIC_FLASK_BACKEND_URL: ${{ vars.NEXT_PUBLIC_FLASK_BACKEND_URL || '' }}
|
||||
NEXT_PUBLIC_BASE_PATH: ${{ vars.NEXT_PUBLIC_BASE_PATH || '' }}
|
||||
|
||||
- name: Setup Pages
|
||||
uses: actions/configure-pages@v4
|
||||
@@ -43,7 +44,7 @@ jobs:
|
||||
- name: Upload artifact
|
||||
uses: actions/upload-pages-artifact@v3
|
||||
with:
|
||||
path: './dist'
|
||||
path: './out'
|
||||
|
||||
deploy:
|
||||
environment:
|
||||
|
||||
4
.github/workflows/docker-publish.yml
vendored
4
.github/workflows/docker-publish.yml
vendored
@@ -117,5 +117,5 @@ jobs:
|
||||
cache-from: type=gha,scope=frontend
|
||||
cache-to: type=gha,mode=max,scope=frontend
|
||||
build-args: |
|
||||
VITE_FLASK_BACKEND_URL=${{ vars.VITE_FLASK_BACKEND_URL || '' }}
|
||||
VITE_BASE_PATH=${{ vars.VITE_BASE_PATH || '/' }}
|
||||
NEXT_PUBLIC_FLASK_BACKEND_URL=${{ vars.NEXT_PUBLIC_FLASK_BACKEND_URL || '' }}
|
||||
NEXT_PUBLIC_BASE_PATH=${{ vars.NEXT_PUBLIC_BASE_PATH || '' }}
|
||||
|
||||
8
.gitignore
vendored
8
.gitignore
vendored
@@ -13,6 +13,12 @@ dist-ssr
|
||||
*-dist
|
||||
*.local
|
||||
|
||||
# Next.js
|
||||
/.next/
|
||||
/out/
|
||||
next-env.d.ts
|
||||
.vercel
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
@@ -25,6 +31,8 @@ dist-ssr
|
||||
*.sw?
|
||||
|
||||
.env
|
||||
.env.local
|
||||
.env*.local
|
||||
**/agent-eval-report*
|
||||
packages
|
||||
pids
|
||||
|
||||
29
Dockerfile
29
Dockerfile
@@ -3,26 +3,37 @@ FROM node:20-alpine AS builder
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# Copy package files
|
||||
COPY package*.json ./
|
||||
RUN npm ci
|
||||
|
||||
# Copy source code
|
||||
COPY . .
|
||||
|
||||
ARG VITE_FLASK_BACKEND_URL
|
||||
ENV VITE_FLASK_BACKEND_URL=$VITE_FLASK_BACKEND_URL
|
||||
|
||||
ARG VITE_BASE_PATH
|
||||
ENV VITE_BASE_PATH=$VITE_BASE_PATH
|
||||
# Build arguments for environment variables
|
||||
ARG NEXT_PUBLIC_FLASK_BACKEND_URL
|
||||
ARG NEXT_PUBLIC_BASE_PATH
|
||||
ENV NEXT_PUBLIC_FLASK_BACKEND_URL=$NEXT_PUBLIC_FLASK_BACKEND_URL
|
||||
ENV NEXT_PUBLIC_BASE_PATH=$NEXT_PUBLIC_BASE_PATH
|
||||
|
||||
# Build Next.js app
|
||||
RUN npm run build
|
||||
|
||||
# Production stage
|
||||
FROM nginx:alpine
|
||||
FROM node:20-alpine AS runner
|
||||
|
||||
COPY --from=builder /app/dist /usr/share/nginx/html
|
||||
WORKDIR /app
|
||||
|
||||
COPY nginx.conf /etc/nginx/conf.d/default.conf
|
||||
ENV NODE_ENV=production
|
||||
|
||||
# Copy necessary files from builder
|
||||
COPY --from=builder /app/public ./public
|
||||
COPY --from=builder /app/.next/standalone ./
|
||||
COPY --from=builder /app/.next/static ./.next/static
|
||||
|
||||
EXPOSE 3000
|
||||
|
||||
CMD ["nginx", "-g", "daemon off;"]
|
||||
ENV PORT=3000
|
||||
ENV HOSTNAME="0.0.0.0"
|
||||
|
||||
CMD ["node", "server.js"]
|
||||
|
||||
89
app/PageLayout.tsx
Normal file
89
app/PageLayout.tsx
Normal 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
43
app/atoms/page.tsx
Normal file
@@ -0,0 +1,43 @@
|
||||
'use client';
|
||||
|
||||
import { motion } from 'framer-motion';
|
||||
import { AtomsSection } from '@/components/atoms/AtomsSection';
|
||||
import type { Snippet } from '@/lib/types';
|
||||
import { useCallback } from 'react';
|
||||
import { toast } from 'sonner';
|
||||
import { createSnippet } from '@/lib/db';
|
||||
import { PageLayout } from '../PageLayout';
|
||||
|
||||
export default function AtomsPage() {
|
||||
const handleSaveSnippet = useCallback(async (snippetData: Omit<Snippet, 'id' | 'createdAt' | 'updatedAt'>) => {
|
||||
try {
|
||||
const newSnippet: Snippet = {
|
||||
...snippetData,
|
||||
id: Date.now().toString(),
|
||||
createdAt: Date.now(),
|
||||
updatedAt: Date.now(),
|
||||
};
|
||||
await createSnippet(newSnippet);
|
||||
toast.success('Component saved as snippet!');
|
||||
} catch (error) {
|
||||
console.error('Failed to save snippet:', error);
|
||||
toast.error('Failed to save snippet');
|
||||
}
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<PageLayout>
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.4 }}
|
||||
>
|
||||
<div className="mb-8">
|
||||
<h2 className="text-3xl font-bold tracking-tight mb-2">Atoms</h2>
|
||||
<p className="text-muted-foreground">Fundamental building blocks - basic HTML elements styled as reusable components</p>
|
||||
</div>
|
||||
<AtomsSection onSaveSnippet={handleSaveSnippet} />
|
||||
</motion.div>
|
||||
</PageLayout>
|
||||
);
|
||||
}
|
||||
60
app/demo/page.tsx
Normal file
60
app/demo/page.tsx
Normal file
@@ -0,0 +1,60 @@
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
import { motion } from 'framer-motion';
|
||||
import { SplitScreenEditor } from '@/components/features/snippet-editor/SplitScreenEditor';
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import { Sparkle } from '@phosphor-icons/react';
|
||||
import { DEMO_CODE } from '@/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
92
app/globals.css
Normal 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
40
app/layout.tsx
Normal 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
43
app/molecules/page.tsx
Normal file
@@ -0,0 +1,43 @@
|
||||
'use client';
|
||||
|
||||
import { motion } from 'framer-motion';
|
||||
import { MoleculesSection } from '@/components/molecules/MoleculesSection';
|
||||
import type { Snippet } from '@/lib/types';
|
||||
import { useCallback } from 'react';
|
||||
import { toast } from 'sonner';
|
||||
import { createSnippet } from '@/lib/db';
|
||||
import { PageLayout } from '../PageLayout';
|
||||
|
||||
export default function MoleculesPage() {
|
||||
const handleSaveSnippet = useCallback(async (snippetData: Omit<Snippet, 'id' | 'createdAt' | 'updatedAt'>) => {
|
||||
try {
|
||||
const newSnippet: Snippet = {
|
||||
...snippetData,
|
||||
id: Date.now().toString(),
|
||||
createdAt: Date.now(),
|
||||
updatedAt: Date.now(),
|
||||
};
|
||||
await createSnippet(newSnippet);
|
||||
toast.success('Component saved as snippet!');
|
||||
} catch (error) {
|
||||
console.error('Failed to save snippet:', error);
|
||||
toast.error('Failed to save snippet');
|
||||
}
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<PageLayout>
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.4 }}
|
||||
>
|
||||
<div className="mb-8">
|
||||
<h2 className="text-3xl font-bold tracking-tight mb-2">Molecules</h2>
|
||||
<p className="text-muted-foreground">Simple combinations of atoms that work together as functional units</p>
|
||||
</div>
|
||||
<MoleculesSection onSaveSnippet={handleSaveSnippet} />
|
||||
</motion.div>
|
||||
</PageLayout>
|
||||
);
|
||||
}
|
||||
43
app/organisms/page.tsx
Normal file
43
app/organisms/page.tsx
Normal file
@@ -0,0 +1,43 @@
|
||||
'use client';
|
||||
|
||||
import { motion } from 'framer-motion';
|
||||
import { OrganismsSection } from '@/components/organisms/OrganismsSection';
|
||||
import type { Snippet } from '@/lib/types';
|
||||
import { useCallback } from 'react';
|
||||
import { toast } from 'sonner';
|
||||
import { createSnippet } from '@/lib/db';
|
||||
import { PageLayout } from '../PageLayout';
|
||||
|
||||
export default function OrganismsPage() {
|
||||
const handleSaveSnippet = useCallback(async (snippetData: Omit<Snippet, 'id' | 'createdAt' | 'updatedAt'>) => {
|
||||
try {
|
||||
const newSnippet: Snippet = {
|
||||
...snippetData,
|
||||
id: Date.now().toString(),
|
||||
createdAt: Date.now(),
|
||||
updatedAt: Date.now(),
|
||||
};
|
||||
await createSnippet(newSnippet);
|
||||
toast.success('Component saved as snippet!');
|
||||
} catch (error) {
|
||||
console.error('Failed to save snippet:', error);
|
||||
toast.error('Failed to save snippet');
|
||||
}
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<PageLayout>
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.4 }}
|
||||
>
|
||||
<div className="mb-8">
|
||||
<h2 className="text-3xl font-bold tracking-tight mb-2">Organisms</h2>
|
||||
<p className="text-muted-foreground">Complex UI components composed of molecules and atoms</p>
|
||||
</div>
|
||||
<OrganismsSection onSaveSnippet={handleSaveSnippet} />
|
||||
</motion.div>
|
||||
</PageLayout>
|
||||
);
|
||||
}
|
||||
23
app/page.tsx
Normal file
23
app/page.tsx
Normal file
@@ -0,0 +1,23 @@
|
||||
'use client';
|
||||
|
||||
import { motion } from 'framer-motion';
|
||||
import { SnippetManagerRedux } from '@/components/SnippetManagerRedux';
|
||||
import { PageLayout } from './PageLayout';
|
||||
|
||||
export default function HomePage() {
|
||||
return (
|
||||
<PageLayout>
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.4 }}
|
||||
>
|
||||
<div className="mb-8">
|
||||
<h2 className="text-3xl font-bold tracking-tight mb-2">My Snippets</h2>
|
||||
<p className="text-muted-foreground">Save, organize, and share your code snippets</p>
|
||||
</div>
|
||||
<SnippetManagerRedux />
|
||||
</motion.div>
|
||||
</PageLayout>
|
||||
);
|
||||
}
|
||||
42
app/providers.tsx
Normal file
42
app/providers.tsx
Normal file
@@ -0,0 +1,42 @@
|
||||
'use client';
|
||||
|
||||
import '@github/spark/spark';
|
||||
import { ErrorBoundary } from 'react-error-boundary';
|
||||
import { Provider } from 'react-redux';
|
||||
import { Toaster } from '@/components/ui/sonner';
|
||||
import { store } from '@/store';
|
||||
import { ErrorFallback } from '@/components/error/ErrorFallback';
|
||||
import { NavigationProvider } from '@/components/layout/navigation/NavigationProvider';
|
||||
import { useEffect } from 'react';
|
||||
import { loadStorageConfig } from '@/lib/storage';
|
||||
|
||||
const logErrorToConsole = (error: Error, info: { componentStack?: string }) => {
|
||||
console.error('Application Error:', error);
|
||||
if (info.componentStack) {
|
||||
console.error('Component Stack:', info.componentStack);
|
||||
}
|
||||
};
|
||||
|
||||
function StorageInitializer() {
|
||||
useEffect(() => {
|
||||
loadStorageConfig();
|
||||
}, []);
|
||||
return null;
|
||||
}
|
||||
|
||||
export function Providers({ children }: { children: React.ReactNode }) {
|
||||
return (
|
||||
<Provider store={store}>
|
||||
<ErrorBoundary
|
||||
FallbackComponent={ErrorFallback}
|
||||
onError={logErrorToConsole}
|
||||
>
|
||||
<NavigationProvider>
|
||||
<StorageInitializer />
|
||||
{children}
|
||||
<Toaster />
|
||||
</NavigationProvider>
|
||||
</ErrorBoundary>
|
||||
</Provider>
|
||||
);
|
||||
}
|
||||
105
app/settings/page.tsx
Normal file
105
app/settings/page.tsx
Normal file
@@ -0,0 +1,105 @@
|
||||
'use client';
|
||||
|
||||
import { motion } from 'framer-motion';
|
||||
import { PersistenceSettings } from '@/components/demo/PersistenceSettings';
|
||||
import { SchemaHealthCard } from '@/components/settings/SchemaHealthCard';
|
||||
import { BackendAutoConfigCard } from '@/components/settings/BackendAutoConfigCard';
|
||||
import { StorageBackendCard } from '@/components/settings/StorageBackendCard';
|
||||
import { DatabaseStatsCard } from '@/components/settings/DatabaseStatsCard';
|
||||
import { StorageInfoCard } from '@/components/settings/StorageInfoCard';
|
||||
import { DatabaseActionsCard } from '@/components/settings/DatabaseActionsCard';
|
||||
import { useSettingsState } from '@/hooks/useSettingsState';
|
||||
import { PageLayout } from '../PageLayout';
|
||||
|
||||
export default function SettingsPage() {
|
||||
const {
|
||||
stats,
|
||||
loading,
|
||||
storageBackend,
|
||||
setStorageBackend,
|
||||
flaskUrl,
|
||||
setFlaskUrl,
|
||||
flaskConnectionStatus,
|
||||
setFlaskConnectionStatus,
|
||||
testingConnection,
|
||||
envVarSet,
|
||||
schemaHealth,
|
||||
checkingSchema,
|
||||
handleExport,
|
||||
handleImport,
|
||||
handleClear,
|
||||
handleSeed,
|
||||
formatBytes,
|
||||
handleTestConnection,
|
||||
handleSaveStorageConfig,
|
||||
handleMigrateToFlask,
|
||||
handleMigrateToIndexedDB,
|
||||
checkSchemaHealth,
|
||||
} = useSettingsState();
|
||||
|
||||
return (
|
||||
<PageLayout>
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.4 }}
|
||||
>
|
||||
<div className="mb-8">
|
||||
<h2 className="text-3xl font-bold tracking-tight mb-2">Settings</h2>
|
||||
<p className="text-muted-foreground">Manage your database and application settings</p>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-6 max-w-3xl">
|
||||
<PersistenceSettings />
|
||||
|
||||
<SchemaHealthCard
|
||||
schemaHealth={schemaHealth}
|
||||
checkingSchema={checkingSchema}
|
||||
onClear={handleClear}
|
||||
onCheckSchema={checkSchemaHealth}
|
||||
/>
|
||||
|
||||
<BackendAutoConfigCard
|
||||
envVarSet={envVarSet}
|
||||
flaskUrl={flaskUrl}
|
||||
flaskConnectionStatus={flaskConnectionStatus}
|
||||
testingConnection={testingConnection}
|
||||
onTestConnection={handleTestConnection}
|
||||
/>
|
||||
|
||||
<StorageBackendCard
|
||||
storageBackend={storageBackend}
|
||||
flaskUrl={flaskUrl}
|
||||
flaskConnectionStatus={flaskConnectionStatus}
|
||||
testingConnection={testingConnection}
|
||||
envVarSet={envVarSet}
|
||||
onStorageBackendChange={setStorageBackend}
|
||||
onFlaskUrlChange={(url) => {
|
||||
setFlaskUrl(url);
|
||||
setFlaskConnectionStatus('unknown');
|
||||
}}
|
||||
onTestConnection={handleTestConnection}
|
||||
onSaveConfig={handleSaveStorageConfig}
|
||||
onMigrateToFlask={handleMigrateToFlask}
|
||||
onMigrateToIndexedDB={handleMigrateToIndexedDB}
|
||||
/>
|
||||
|
||||
<DatabaseStatsCard
|
||||
loading={loading}
|
||||
stats={stats}
|
||||
formatBytes={formatBytes}
|
||||
/>
|
||||
|
||||
<StorageInfoCard storageType={stats?.storageType} />
|
||||
|
||||
<DatabaseActionsCard
|
||||
onExport={handleExport}
|
||||
onImport={handleImport}
|
||||
onSeed={handleSeed}
|
||||
onClear={handleClear}
|
||||
/>
|
||||
</div>
|
||||
</motion.div>
|
||||
</PageLayout>
|
||||
);
|
||||
}
|
||||
43
app/templates/page.tsx
Normal file
43
app/templates/page.tsx
Normal file
@@ -0,0 +1,43 @@
|
||||
'use client';
|
||||
|
||||
import { motion } from 'framer-motion';
|
||||
import { TemplatesSection } from '@/components/templates/TemplatesSection';
|
||||
import type { Snippet } from '@/lib/types';
|
||||
import { useCallback } from 'react';
|
||||
import { toast } from 'sonner';
|
||||
import { createSnippet } from '@/lib/db';
|
||||
import { PageLayout } from '../PageLayout';
|
||||
|
||||
export default function TemplatesPage() {
|
||||
const handleSaveSnippet = useCallback(async (snippetData: Omit<Snippet, 'id' | 'createdAt' | 'updatedAt'>) => {
|
||||
try {
|
||||
const newSnippet: Snippet = {
|
||||
...snippetData,
|
||||
id: Date.now().toString(),
|
||||
createdAt: Date.now(),
|
||||
updatedAt: Date.now(),
|
||||
};
|
||||
await createSnippet(newSnippet);
|
||||
toast.success('Component saved as snippet!');
|
||||
} catch (error) {
|
||||
console.error('Failed to save snippet:', error);
|
||||
toast.error('Failed to save snippet');
|
||||
}
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<PageLayout>
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.4 }}
|
||||
>
|
||||
<div className="mb-8">
|
||||
<h2 className="text-3xl font-bold tracking-tight mb-2">Templates</h2>
|
||||
<p className="text-muted-foreground">Page-level layouts that combine organisms into complete interfaces</p>
|
||||
</div>
|
||||
<TemplatesSection onSaveSnippet={handleSaveSnippet} />
|
||||
</motion.div>
|
||||
</PageLayout>
|
||||
);
|
||||
}
|
||||
@@ -18,7 +18,7 @@ services:
|
||||
build:
|
||||
context: .
|
||||
args:
|
||||
- VITE_FLASK_BACKEND_URL=https://backend.example.com
|
||||
- NEXT_PUBLIC_FLASK_BACKEND_URL=https://backend.example.com
|
||||
ports:
|
||||
- "3000:3000"
|
||||
depends_on:
|
||||
|
||||
@@ -16,7 +16,7 @@ services:
|
||||
build:
|
||||
context: .
|
||||
args:
|
||||
- VITE_FLASK_BACKEND_URL=http://localhost:5000
|
||||
- NEXT_PUBLIC_FLASK_BACKEND_URL=http://localhost:5000
|
||||
ports:
|
||||
- "3000:3000"
|
||||
depends_on:
|
||||
|
||||
49
next.config.ts
Normal file
49
next.config.ts
Normal 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
4454
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
20
package.json
20
package.json
@@ -2,14 +2,12 @@
|
||||
"name": "spark-template",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"kill": "fuser -k 5000/tcp",
|
||||
"build": "tsc -b --noCheck && vite build",
|
||||
"lint": "eslint .",
|
||||
"optimize": "vite optimize",
|
||||
"preview": "vite preview"
|
||||
"dev": "next dev",
|
||||
"build": "next build",
|
||||
"start": "next start",
|
||||
"lint": "next lint",
|
||||
"kill": "fuser -k 5000/tcp"
|
||||
},
|
||||
"dependencies": {
|
||||
"@babel/standalone": "^7.28.6",
|
||||
@@ -48,7 +46,6 @@
|
||||
"@radix-ui/react-tooltip": "^1.1.8",
|
||||
"@reduxjs/toolkit": "^2.11.2",
|
||||
"@tailwindcss/container-queries": "^0.1.1",
|
||||
"@tailwindcss/vite": "^4.1.11",
|
||||
"@tanstack/react-query": "^5.83.1",
|
||||
"@types/sql.js": "^1.4.9",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
@@ -61,6 +58,7 @@
|
||||
"input-otp": "^1.4.2",
|
||||
"lucide-react": "^0.484.0",
|
||||
"marked": "^15.0.7",
|
||||
"next": "16.1.3",
|
||||
"next-themes": "^0.4.6",
|
||||
"octokit": "^4.1.2",
|
||||
"pyodide": "^0.29.1",
|
||||
@@ -87,15 +85,13 @@
|
||||
"@tailwindcss/postcss": "^4.1.8",
|
||||
"@types/react": "^19.0.10",
|
||||
"@types/react-dom": "^19.0.4",
|
||||
"@vitejs/plugin-react-swc": "^4.2.2",
|
||||
"eslint": "^9.28.0",
|
||||
"eslint-config-next": "^16.1.3",
|
||||
"eslint-plugin-react-hooks": "^5.2.0",
|
||||
"eslint-plugin-react-refresh": "^0.4.19",
|
||||
"globals": "^16.0.0",
|
||||
"tailwindcss": "^4.1.11",
|
||||
"typescript": "~5.7.2",
|
||||
"typescript-eslint": "^8.38.0",
|
||||
"vite": "^7.2.6"
|
||||
"typescript-eslint": "^8.38.0"
|
||||
},
|
||||
"workspaces": {
|
||||
"packages": [
|
||||
|
||||
@@ -10,7 +10,7 @@ export function BackendIndicator() {
|
||||
useEffect(() => {
|
||||
const config = getStorageConfig()
|
||||
setBackend(config.backend)
|
||||
setIsEnvConfigured(Boolean(import.meta.env.VITE_FLASK_BACKEND_URL))
|
||||
setIsEnvConfigured(Boolean(process.env.NEXT_PUBLIC_FLASK_BACKEND_URL))
|
||||
}, [])
|
||||
|
||||
if (backend === 'indexeddb') {
|
||||
|
||||
@@ -1,14 +1,17 @@
|
||||
import { motion } from 'framer-motion'
|
||||
import { Link, useLocation } from 'react-router-dom'
|
||||
import { X } from '@phosphor-icons/react'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { navigationItems } from './navigation-items'
|
||||
import { useNavigation } from './useNavigation'
|
||||
'use client';
|
||||
|
||||
import { motion } from 'framer-motion';
|
||||
import Link from 'next/link';
|
||||
import { usePathname } from 'next/navigation';
|
||||
import { X } from '@phosphor-icons/react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { navigationItems } from './navigation-items';
|
||||
import { useNavigation } from './useNavigation';
|
||||
|
||||
export function NavigationSidebar() {
|
||||
const { menuOpen, setMenuOpen } = useNavigation()
|
||||
const location = useLocation()
|
||||
const { menuOpen, setMenuOpen } = useNavigation();
|
||||
const pathname = usePathname();
|
||||
|
||||
return (
|
||||
<motion.aside
|
||||
@@ -31,11 +34,11 @@ export function NavigationSidebar() {
|
||||
<nav className="p-4">
|
||||
<ul className="space-y-2">
|
||||
{navigationItems.map((item) => {
|
||||
const Icon = item.icon
|
||||
const isActive = location.pathname === item.path
|
||||
const Icon = item.icon;
|
||||
const isActive = pathname === item.path;
|
||||
return (
|
||||
<li key={item.path}>
|
||||
<Link to={item.path}>
|
||||
<Link href={item.path}>
|
||||
<Button
|
||||
variant={isActive ? 'secondary' : 'ghost'}
|
||||
className={cn(
|
||||
@@ -48,7 +51,7 @@ export function NavigationSidebar() {
|
||||
</Button>
|
||||
</Link>
|
||||
</li>
|
||||
)
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
</nav>
|
||||
@@ -60,5 +63,5 @@ export function NavigationSidebar() {
|
||||
</div>
|
||||
</div>
|
||||
</motion.aside>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -38,7 +38,7 @@ export function BackendAutoConfigCard({
|
||||
</div>
|
||||
<div className="flex items-center justify-between py-2">
|
||||
<span className="text-sm text-muted-foreground">Configuration Source</span>
|
||||
<code className="text-sm font-mono bg-muted px-2 py-1 rounded">VITE_FLASK_BACKEND_URL</code>
|
||||
<code className="text-sm font-mono bg-muted px-2 py-1 rounded">NEXT_PUBLIC_FLASK_BACKEND_URL</code>
|
||||
</div>
|
||||
<div className="flex items-center justify-between py-2">
|
||||
<span className="text-sm text-muted-foreground">Status</span>
|
||||
|
||||
@@ -51,7 +51,7 @@ export function StorageBackendCard({
|
||||
<AlertDescription className="flex items-center gap-2">
|
||||
<CloudCheck weight="fill" size={16} className="text-accent" />
|
||||
<span>
|
||||
Storage backend is configured via <code className="px-1.5 py-0.5 rounded bg-muted text-xs font-mono">VITE_FLASK_BACKEND_URL</code> environment variable and cannot be changed here.
|
||||
Storage backend is configured via <code className="px-1.5 py-0.5 rounded bg-muted text-xs font-mono">NEXT_PUBLIC_FLASK_BACKEND_URL</code> environment variable and cannot be changed here.
|
||||
</span>
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
|
||||
@@ -32,7 +32,7 @@ export function useStorageConfig() {
|
||||
|
||||
const loadConfig = useCallback(() => {
|
||||
const config = loadStorageConfig()
|
||||
const envFlaskUrl = import.meta.env.VITE_FLASK_BACKEND_URL
|
||||
const envFlaskUrl = process.env.NEXT_PUBLIC_FLASK_BACKEND_URL
|
||||
const isEnvSet = Boolean(envFlaskUrl)
|
||||
|
||||
setEnvVarSet(isEnvSet)
|
||||
|
||||
@@ -10,7 +10,7 @@ export interface StorageConfig {
|
||||
const STORAGE_CONFIG_KEY = 'codesnippet-storage-config'
|
||||
|
||||
function getDefaultConfig(): StorageConfig {
|
||||
const flaskUrl = import.meta.env.VITE_FLASK_BACKEND_URL
|
||||
const flaskUrl = process.env.NEXT_PUBLIC_FLASK_BACKEND_URL
|
||||
|
||||
if (flaskUrl) {
|
||||
return {
|
||||
|
||||
@@ -1,34 +1,44 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
|
||||
"target": "ES2020",
|
||||
"useDefineForClassFields": true,
|
||||
"lib": [
|
||||
"ES2020",
|
||||
"DOM",
|
||||
"DOM.Iterable"
|
||||
],
|
||||
"jsx": "preserve",
|
||||
"module": "ESNext",
|
||||
"skipLibCheck": true,
|
||||
"strictNullChecks": true,
|
||||
/* Bundler mode */
|
||||
"moduleResolution": "bundler",
|
||||
"allowImportingTsExtensions": true,
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"moduleDetection": "force",
|
||||
"allowJs": true,
|
||||
"skipLibCheck": true,
|
||||
"strict": false,
|
||||
"strictNullChecks": true,
|
||||
"noEmit": true,
|
||||
"jsx": "react-jsx",
|
||||
/* Linting */
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"esModuleInterop": true,
|
||||
"isolatedModules": true,
|
||||
"incremental": true,
|
||||
"noUncheckedSideEffectImports": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"allowImportingTsExtensions": false,
|
||||
"plugins": [
|
||||
{
|
||||
"name": "next"
|
||||
}
|
||||
],
|
||||
"paths": {
|
||||
"@/*": [
|
||||
"./src/*"
|
||||
]
|
||||
},
|
||||
}
|
||||
},
|
||||
"include": [
|
||||
"src"
|
||||
"next-env.d.ts",
|
||||
"**/*.ts",
|
||||
"**/*.tsx",
|
||||
".next/types/**/*.ts"
|
||||
],
|
||||
"exclude": [
|
||||
"node_modules"
|
||||
]
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user