mirror of
https://github.com/johndoe6345789/snippet-pastebin.git
synced 2026-04-24 13:34:55 +00:00
Generated by Spark: Reduce reliance on spark database. Just use sqlite.
This commit is contained in:
26
package-lock.json
generated
26
package-lock.json
generated
@@ -45,6 +45,7 @@
|
||||
"@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",
|
||||
"clsx": "^2.1.1",
|
||||
"cmdk": "^1.1.1",
|
||||
@@ -66,6 +67,7 @@
|
||||
"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",
|
||||
@@ -4322,6 +4324,12 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/emscripten": {
|
||||
"version": "1.41.5",
|
||||
"resolved": "https://registry.npmjs.org/@types/emscripten/-/emscripten-1.41.5.tgz",
|
||||
"integrity": "sha512-cMQm7pxu6BxtHyqJ7mQZ2kXWV5SLmugybFdHCBbJ5eHzOo6VhBckEgAT3//rP5FwPHNPeEiq4SmQ5ucBwsOo4Q==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/estree": {
|
||||
"version": "1.0.8",
|
||||
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
|
||||
@@ -4371,7 +4379,6 @@
|
||||
"version": "22.19.3",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.3.tgz",
|
||||
"integrity": "sha512-1N9SBnWYOJTrNZCdh/yJE+t910Y128BoyY+zBLWhL3r0TYzlTmFdXrPwHL9DyFZmlEXNQQolTZh3KHV31QDhyA==",
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"undici-types": "~6.21.0"
|
||||
@@ -4439,6 +4446,16 @@
|
||||
"@types/node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/sql.js": {
|
||||
"version": "1.4.9",
|
||||
"resolved": "https://registry.npmjs.org/@types/sql.js/-/sql.js-1.4.9.tgz",
|
||||
"integrity": "sha512-ep8b36RKHlgWPqjNG9ToUrPiwkhwh0AEzy883mO5Xnd+cL6VBH1EvSjBAAuxLUFF2Vn/moE3Me6v9E1Lo+48GQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/emscripten": "*",
|
||||
"@types/node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/trusted-types": {
|
||||
"version": "2.0.7",
|
||||
"resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz",
|
||||
@@ -9325,6 +9342,12 @@
|
||||
"source-map": "^0.6.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/stackback": {
|
||||
"version": "0.0.2",
|
||||
"resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz",
|
||||
@@ -9764,7 +9787,6 @@
|
||||
"version": "6.21.0",
|
||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
|
||||
"integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
|
||||
"devOptional": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/unicorn-magic": {
|
||||
|
||||
@@ -49,6 +49,7 @@
|
||||
"@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",
|
||||
"clsx": "^2.1.1",
|
||||
"cmdk": "^1.1.1",
|
||||
@@ -70,6 +71,7 @@
|
||||
"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",
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { useState, useMemo, useCallback } from 'react'
|
||||
import { useKV } from '@github/spark/hooks'
|
||||
import { useState, useMemo, useCallback, useEffect } from 'react'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { Plus, MagnifyingGlass, CaretDown } from '@phosphor-icons/react'
|
||||
@@ -19,17 +18,43 @@ import { Snippet, SnippetTemplate } from '@/lib/types'
|
||||
import { toast } from 'sonner'
|
||||
import { strings } from '@/lib/config'
|
||||
import templatesData from '@/data/templates.json'
|
||||
import {
|
||||
getAllSnippets,
|
||||
createSnippet,
|
||||
updateSnippet,
|
||||
deleteSnippet as deleteSnippetDB,
|
||||
seedDatabase
|
||||
} from '@/lib/db'
|
||||
|
||||
const templates = templatesData as SnippetTemplate[]
|
||||
|
||||
export function SnippetManager() {
|
||||
const [snippets, setSnippets] = useKV<Snippet[]>('snippets', [])
|
||||
const [snippets, setSnippets] = useState<Snippet[]>([])
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [searchQuery, setSearchQuery] = useState('')
|
||||
const [dialogOpen, setDialogOpen] = useState(false)
|
||||
const [viewerOpen, setViewerOpen] = useState(false)
|
||||
const [editingSnippet, setEditingSnippet] = useState<Snippet | null>(null)
|
||||
const [viewingSnippet, setViewingSnippet] = useState<Snippet | null>(null)
|
||||
|
||||
useEffect(() => {
|
||||
const loadSnippets = async () => {
|
||||
setLoading(true)
|
||||
try {
|
||||
await seedDatabase()
|
||||
const loadedSnippets = await getAllSnippets()
|
||||
setSnippets(loadedSnippets)
|
||||
} catch (error) {
|
||||
console.error('Failed to load snippets:', error)
|
||||
toast.error('Failed to load snippets')
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
loadSnippets()
|
||||
}, [])
|
||||
|
||||
const filteredSnippets = useMemo(() => {
|
||||
const allSnippets = snippets || []
|
||||
if (!searchQuery.trim()) return allSnippets
|
||||
@@ -44,46 +69,52 @@ export function SnippetManager() {
|
||||
)
|
||||
}, [snippets, searchQuery])
|
||||
|
||||
const handleSaveSnippet = useCallback((snippetData: Omit<Snippet, 'id' | 'createdAt' | 'updatedAt'>) => {
|
||||
if (editingSnippet?.id) {
|
||||
setSnippets((currentSnippets) => {
|
||||
const allSnippets = currentSnippets || []
|
||||
return allSnippets.map((s) =>
|
||||
s.id === editingSnippet.id
|
||||
? {
|
||||
...s,
|
||||
...snippetData,
|
||||
updatedAt: Date.now(),
|
||||
}
|
||||
: s
|
||||
const handleSaveSnippet = useCallback(async (snippetData: Omit<Snippet, 'id' | 'createdAt' | 'updatedAt'>) => {
|
||||
try {
|
||||
if (editingSnippet?.id) {
|
||||
const updatedSnippet = {
|
||||
...editingSnippet,
|
||||
...snippetData,
|
||||
updatedAt: Date.now(),
|
||||
}
|
||||
await updateSnippet(updatedSnippet)
|
||||
setSnippets((current) =>
|
||||
current.map((s) => s.id === editingSnippet.id ? updatedSnippet : s)
|
||||
)
|
||||
})
|
||||
toast.success(strings.toast.snippetUpdated)
|
||||
} else {
|
||||
const newSnippet: Snippet = {
|
||||
...snippetData,
|
||||
id: Date.now().toString(),
|
||||
createdAt: Date.now(),
|
||||
updatedAt: Date.now(),
|
||||
toast.success(strings.toast.snippetUpdated)
|
||||
} else {
|
||||
const newSnippet: Snippet = {
|
||||
...snippetData,
|
||||
id: Date.now().toString(),
|
||||
createdAt: Date.now(),
|
||||
updatedAt: Date.now(),
|
||||
}
|
||||
await createSnippet(newSnippet)
|
||||
setSnippets((current) => [newSnippet, ...current])
|
||||
toast.success(strings.toast.snippetCreated)
|
||||
}
|
||||
setSnippets((currentSnippets) => [newSnippet, ...(currentSnippets || [])])
|
||||
toast.success(strings.toast.snippetCreated)
|
||||
setEditingSnippet(null)
|
||||
} catch (error) {
|
||||
console.error('Failed to save snippet:', error)
|
||||
toast.error('Failed to save snippet')
|
||||
}
|
||||
setEditingSnippet(null)
|
||||
}, [editingSnippet, setSnippets])
|
||||
}, [editingSnippet])
|
||||
|
||||
const handleEditSnippet = useCallback((snippet: Snippet) => {
|
||||
setEditingSnippet(snippet)
|
||||
setDialogOpen(true)
|
||||
}, [])
|
||||
|
||||
const handleDeleteSnippet = useCallback((id: string) => {
|
||||
setSnippets((currentSnippets) => {
|
||||
const allSnippets = currentSnippets || []
|
||||
return allSnippets.filter((s) => s.id !== id)
|
||||
})
|
||||
toast.success(strings.toast.snippetDeleted)
|
||||
}, [setSnippets])
|
||||
const handleDeleteSnippet = useCallback(async (id: string) => {
|
||||
try {
|
||||
await deleteSnippetDB(id)
|
||||
setSnippets((current) => current.filter((s) => s.id !== id))
|
||||
toast.success(strings.toast.snippetDeleted)
|
||||
} catch (error) {
|
||||
console.error('Failed to delete snippet:', error)
|
||||
toast.error('Failed to delete snippet')
|
||||
}
|
||||
}, [])
|
||||
|
||||
const handleCopyCode = useCallback((code: string) => {
|
||||
navigator.clipboard.writeText(code)
|
||||
@@ -127,6 +158,14 @@ export function SnippetManager() {
|
||||
|
||||
const allSnippets = snippets || []
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="text-center py-20">
|
||||
<p className="text-muted-foreground">Loading snippets...</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
if (allSnippets.length === 0) {
|
||||
return (
|
||||
<>
|
||||
|
||||
611
src/lib/db.ts
Normal file
611
src/lib/db.ts
Normal file
@@ -0,0 +1,611 @@
|
||||
import initSqlJs, { Database } from 'sql.js'
|
||||
import type { Snippet, SnippetTemplate } from './types'
|
||||
|
||||
let dbInstance: Database | null = null
|
||||
let sqlInstance: any = null
|
||||
|
||||
const DB_KEY = 'codesnippet-db'
|
||||
|
||||
export async function initDB(): Promise<Database> {
|
||||
if (dbInstance) return dbInstance
|
||||
|
||||
if (!sqlInstance) {
|
||||
sqlInstance = await initSqlJs({
|
||||
locateFile: (file) => `https://sql.js.org/dist/${file}`
|
||||
})
|
||||
}
|
||||
|
||||
const savedData = localStorage.getItem(DB_KEY)
|
||||
|
||||
if (savedData) {
|
||||
try {
|
||||
const uint8Array = new Uint8Array(JSON.parse(savedData))
|
||||
dbInstance = new sqlInstance.Database(uint8Array)
|
||||
} catch (error) {
|
||||
console.error('Failed to load saved database, creating new one:', error)
|
||||
dbInstance = new sqlInstance.Database()
|
||||
}
|
||||
} else {
|
||||
dbInstance = new sqlInstance.Database()
|
||||
}
|
||||
|
||||
if (!dbInstance) {
|
||||
throw new Error('Failed to initialize database')
|
||||
}
|
||||
|
||||
dbInstance.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,
|
||||
hasPreview INTEGER DEFAULT 0,
|
||||
functionName TEXT,
|
||||
inputParameters TEXT,
|
||||
createdAt INTEGER NOT NULL,
|
||||
updatedAt INTEGER NOT NULL
|
||||
)
|
||||
`)
|
||||
|
||||
dbInstance.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
|
||||
)
|
||||
`)
|
||||
|
||||
saveDB()
|
||||
|
||||
return dbInstance
|
||||
}
|
||||
|
||||
function saveDB() {
|
||||
if (!dbInstance) return
|
||||
|
||||
try {
|
||||
const data = dbInstance.export()
|
||||
const dataArray = Array.from(data)
|
||||
localStorage.setItem(DB_KEY, JSON.stringify(dataArray))
|
||||
} catch (error) {
|
||||
console.error('Failed to save database:', error)
|
||||
}
|
||||
}
|
||||
|
||||
export async function getAllSnippets(): Promise<Snippet[]> {
|
||||
const db = await initDB()
|
||||
|
||||
const results = db.exec('SELECT * FROM snippets ORDER BY updatedAt DESC')
|
||||
|
||||
if (results.length === 0) return []
|
||||
|
||||
const columns = results[0].columns
|
||||
const values = results[0].values
|
||||
|
||||
return values.map(row => {
|
||||
const snippet: any = {}
|
||||
columns.forEach((col, idx) => {
|
||||
if (col === 'hasPreview') {
|
||||
snippet[col] = row[idx] === 1
|
||||
} else if (col === 'inputParameters') {
|
||||
snippet[col] = row[idx] ? JSON.parse(row[idx] as string) : undefined
|
||||
} else {
|
||||
snippet[col] = row[idx]
|
||||
}
|
||||
})
|
||||
return snippet as Snippet
|
||||
})
|
||||
}
|
||||
|
||||
export async function getSnippet(id: string): Promise<Snippet | null> {
|
||||
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]
|
||||
|
||||
const snippet: any = {}
|
||||
columns.forEach((col, idx) => {
|
||||
if (col === 'hasPreview') {
|
||||
snippet[col] = row[idx] === 1
|
||||
} else if (col === 'inputParameters') {
|
||||
snippet[col] = row[idx] ? JSON.parse(row[idx] as string) : undefined
|
||||
} else {
|
||||
snippet[col] = row[idx]
|
||||
}
|
||||
})
|
||||
|
||||
return snippet as Snippet
|
||||
}
|
||||
|
||||
export async function createSnippet(snippet: Snippet): Promise<void> {
|
||||
const db = await initDB()
|
||||
|
||||
db.run(
|
||||
`INSERT INTO snippets (id, title, description, code, language, category, hasPreview, functionName, inputParameters, createdAt, updatedAt)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||
[
|
||||
snippet.id,
|
||||
snippet.title,
|
||||
snippet.description,
|
||||
snippet.code,
|
||||
snippet.language,
|
||||
snippet.category,
|
||||
snippet.hasPreview ? 1 : 0,
|
||||
snippet.functionName || null,
|
||||
snippet.inputParameters ? JSON.stringify(snippet.inputParameters) : null,
|
||||
snippet.createdAt,
|
||||
snippet.updatedAt
|
||||
]
|
||||
)
|
||||
|
||||
saveDB()
|
||||
}
|
||||
|
||||
export async function updateSnippet(snippet: Snippet): Promise<void> {
|
||||
const db = await initDB()
|
||||
|
||||
db.run(
|
||||
`UPDATE snippets
|
||||
SET title = ?, description = ?, code = ?, language = ?, category = ?, hasPreview = ?, functionName = ?, inputParameters = ?, updatedAt = ?
|
||||
WHERE id = ?`,
|
||||
[
|
||||
snippet.title,
|
||||
snippet.description,
|
||||
snippet.code,
|
||||
snippet.language,
|
||||
snippet.category,
|
||||
snippet.hasPreview ? 1 : 0,
|
||||
snippet.functionName || null,
|
||||
snippet.inputParameters ? JSON.stringify(snippet.inputParameters) : null,
|
||||
snippet.updatedAt,
|
||||
snippet.id
|
||||
]
|
||||
)
|
||||
|
||||
saveDB()
|
||||
}
|
||||
|
||||
export async function deleteSnippet(id: string): Promise<void> {
|
||||
const db = await initDB()
|
||||
|
||||
db.run('DELETE FROM snippets WHERE id = ?', [id])
|
||||
|
||||
saveDB()
|
||||
}
|
||||
|
||||
export async function getAllTemplates(): Promise<SnippetTemplate[]> {
|
||||
const db = await initDB()
|
||||
|
||||
const results = db.exec('SELECT * FROM snippet_templates')
|
||||
|
||||
if (results.length === 0) return []
|
||||
|
||||
const columns = results[0].columns
|
||||
const values = results[0].values
|
||||
|
||||
return values.map(row => {
|
||||
const template: any = {}
|
||||
columns.forEach((col, idx) => {
|
||||
if (col === 'hasPreview') {
|
||||
template[col] = row[idx] === 1
|
||||
} else if (col === 'inputParameters') {
|
||||
template[col] = row[idx] ? JSON.parse(row[idx] as string) : undefined
|
||||
} else {
|
||||
template[col] = row[idx]
|
||||
}
|
||||
})
|
||||
return template as SnippetTemplate
|
||||
})
|
||||
}
|
||||
|
||||
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
|
||||
]
|
||||
)
|
||||
|
||||
saveDB()
|
||||
}
|
||||
|
||||
export async function seedDatabase(): Promise<void> {
|
||||
const db = await initDB()
|
||||
|
||||
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[] = [
|
||||
{
|
||||
id: 'seed-1',
|
||||
title: 'React Counter Hook',
|
||||
description: 'Basic state management with useState',
|
||||
code: `import { useState } from 'react'
|
||||
|
||||
function Counter() {
|
||||
const [count, setCount] = useState(0)
|
||||
|
||||
return (
|
||||
<div className="p-6 space-y-4">
|
||||
<p className="text-2xl font-bold">Count: {count}</p>
|
||||
<div className="flex gap-2">
|
||||
<button
|
||||
onClick={() => setCount(count + 1)}
|
||||
className="px-4 py-2 bg-primary text-primary-foreground rounded"
|
||||
>
|
||||
Increment
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setCount(count - 1)}
|
||||
className="px-4 py-2 bg-secondary text-secondary-foreground rounded"
|
||||
>
|
||||
Decrement
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default Counter`,
|
||||
language: 'tsx',
|
||||
category: 'React Hooks',
|
||||
hasPreview: true,
|
||||
functionName: 'Counter',
|
||||
inputParameters: [],
|
||||
createdAt: now,
|
||||
updatedAt: now
|
||||
},
|
||||
{
|
||||
id: 'seed-2',
|
||||
title: 'Todo List Component',
|
||||
description: 'Complete todo list with add, toggle, and delete',
|
||||
code: `import { useState } from 'react'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { Card } from '@/components/ui/card'
|
||||
import { Checkbox } from '@/components/ui/checkbox'
|
||||
import { Trash2 } from '@phosphor-icons/react'
|
||||
|
||||
function TodoList() {
|
||||
const [todos, setTodos] = useState([
|
||||
{ id: 1, text: 'Learn React', completed: false },
|
||||
{ id: 2, text: 'Build a project', completed: false }
|
||||
])
|
||||
const [input, setInput] = useState('')
|
||||
|
||||
const addTodo = () => {
|
||||
if (input.trim()) {
|
||||
setTodos([...todos, { id: Date.now(), text: input, completed: false }])
|
||||
setInput('')
|
||||
}
|
||||
}
|
||||
|
||||
const toggleTodo = (id) => {
|
||||
setTodos(todos.map(todo =>
|
||||
todo.id === id ? { ...todo, completed: !todo.completed } : todo
|
||||
))
|
||||
}
|
||||
|
||||
const deleteTodo = (id) => {
|
||||
setTodos(todos.filter(todo => todo.id !== id))
|
||||
}
|
||||
|
||||
return (
|
||||
<Card className="p-6 max-w-md mx-auto">
|
||||
<h2 className="text-2xl font-bold mb-4">My Todos</h2>
|
||||
<div className="flex gap-2 mb-4">
|
||||
<Input
|
||||
value={input}
|
||||
onChange={(e) => setInput(e.target.value)}
|
||||
onKeyPress={(e) => e.key === 'Enter' && addTodo()}
|
||||
placeholder="Add a new todo..."
|
||||
/>
|
||||
<Button onClick={addTodo}>Add</Button>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
{todos.map(todo => (
|
||||
<div key={todo.id} className="flex items-center gap-2 p-2 hover:bg-muted rounded">
|
||||
<Checkbox
|
||||
checked={todo.completed}
|
||||
onCheckedChange={() => toggleTodo(todo.id)}
|
||||
/>
|
||||
<span className={todo.completed ? 'line-through text-muted-foreground flex-1' : 'flex-1'}>
|
||||
{todo.text}
|
||||
</span>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={() => deleteTodo(todo.id)}
|
||||
>
|
||||
<Trash2 size={16} />
|
||||
</Button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
|
||||
export default TodoList`,
|
||||
language: 'tsx',
|
||||
category: 'Components',
|
||||
hasPreview: true,
|
||||
functionName: 'TodoList',
|
||||
inputParameters: [],
|
||||
createdAt: now - 1000,
|
||||
updatedAt: now - 1000
|
||||
},
|
||||
{
|
||||
id: 'seed-3',
|
||||
title: 'Fetch Data Hook',
|
||||
description: 'Custom hook for API data fetching',
|
||||
code: `import { useState, useEffect } from 'react'
|
||||
|
||||
function useFetch(url) {
|
||||
const [data, setData] = useState(null)
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [error, setError] = useState(null)
|
||||
|
||||
useEffect(() => {
|
||||
const fetchData = async () => {
|
||||
try {
|
||||
setLoading(true)
|
||||
const response = await fetch(url)
|
||||
if (!response.ok) throw new Error('Network response was not ok')
|
||||
const json = await response.json()
|
||||
setData(json)
|
||||
setError(null)
|
||||
} catch (err) {
|
||||
setError(err.message)
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
fetchData()
|
||||
}, [url])
|
||||
|
||||
return { data, loading, error }
|
||||
}`,
|
||||
language: 'tsx',
|
||||
category: 'React Hooks',
|
||||
hasPreview: false,
|
||||
createdAt: now - 2000,
|
||||
updatedAt: now - 2000
|
||||
},
|
||||
{
|
||||
id: 'seed-4',
|
||||
title: 'Animated Card',
|
||||
description: 'Card with hover animation using Framer Motion',
|
||||
code: `import { motion } from 'framer-motion'
|
||||
import { Card, CardHeader, CardTitle, CardDescription, CardContent } from '@/components/ui/card'
|
||||
|
||||
function AnimatedCard({ title = 'Animated Card', description = 'Hover to see the effect' }) {
|
||||
return (
|
||||
<motion.div
|
||||
whileHover={{ scale: 1.05, rotateZ: 2 }}
|
||||
whileTap={{ scale: 0.95 }}
|
||||
transition={{ type: 'spring', stiffness: 300, damping: 20 }}
|
||||
>
|
||||
<Card className="cursor-pointer">
|
||||
<CardHeader>
|
||||
<CardTitle>{title}</CardTitle>
|
||||
<CardDescription>{description}</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<p>This card has smooth animations on hover and tap!</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</motion.div>
|
||||
)
|
||||
}
|
||||
|
||||
export default AnimatedCard`,
|
||||
language: 'tsx',
|
||||
category: 'Components',
|
||||
hasPreview: true,
|
||||
functionName: 'AnimatedCard',
|
||||
inputParameters: [
|
||||
{
|
||||
name: 'title',
|
||||
type: 'string',
|
||||
defaultValue: 'Animated Card',
|
||||
description: 'Card title'
|
||||
},
|
||||
{
|
||||
name: 'description',
|
||||
type: 'string',
|
||||
defaultValue: 'Hover to see the effect',
|
||||
description: 'Card description'
|
||||
}
|
||||
],
|
||||
createdAt: now - 3000,
|
||||
updatedAt: now - 3000
|
||||
},
|
||||
{
|
||||
id: 'seed-5',
|
||||
title: 'Form Validation',
|
||||
description: 'Form with react-hook-form validation',
|
||||
code: `import { useForm } from 'react-hook-form'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { Label } from '@/components/ui/label'
|
||||
import { Card } from '@/components/ui/card'
|
||||
|
||||
function ContactForm() {
|
||||
const { register, handleSubmit, formState: { errors } } = useForm()
|
||||
|
||||
const onSubmit = (data) => {
|
||||
console.log('Form data:', data)
|
||||
alert('Form submitted successfully!')
|
||||
}
|
||||
|
||||
return (
|
||||
<Card className="p-6 max-w-md mx-auto">
|
||||
<h2 className="text-2xl font-bold mb-4">Contact Form</h2>
|
||||
<form onSubmit={handleSubmit(onSubmit)} className="space-y-4">
|
||||
<div>
|
||||
<Label htmlFor="name">Name</Label>
|
||||
<Input
|
||||
id="name"
|
||||
{...register('name', { required: 'Name is required' })}
|
||||
/>
|
||||
{errors.name && (
|
||||
<p className="text-destructive text-sm mt-1">{errors.name.message}</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label htmlFor="email">Email</Label>
|
||||
<Input
|
||||
id="email"
|
||||
type="email"
|
||||
{...register('email', {
|
||||
required: 'Email is required',
|
||||
pattern: {
|
||||
value: /^[A-Z0-9._%+-]+@[A-Z0-9.-]+\\.[A-Z]{2,}$/i,
|
||||
message: 'Invalid email address'
|
||||
}
|
||||
})}
|
||||
/>
|
||||
{errors.email && (
|
||||
<p className="text-destructive text-sm mt-1">{errors.email.message}</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<Button type="submit" className="w-full">Submit</Button>
|
||||
</form>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
|
||||
export default ContactForm`,
|
||||
language: 'tsx',
|
||||
category: 'Forms',
|
||||
hasPreview: true,
|
||||
functionName: 'ContactForm',
|
||||
inputParameters: [],
|
||||
createdAt: now - 4000,
|
||||
updatedAt: now - 4000
|
||||
}
|
||||
]
|
||||
|
||||
for (const snippet of seedSnippets) {
|
||||
await createSnippet(snippet)
|
||||
}
|
||||
|
||||
const seedTemplates: SnippetTemplate[] = [
|
||||
{
|
||||
id: 'template-1',
|
||||
title: 'Basic React Component',
|
||||
description: 'Simple functional component template',
|
||||
code: `function MyComponent() {
|
||||
return (
|
||||
<div className="p-4">
|
||||
<h2 className="text-xl font-bold">Hello World</h2>
|
||||
<p>This is a basic component.</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default MyComponent`,
|
||||
language: 'tsx',
|
||||
category: 'Templates',
|
||||
hasPreview: true,
|
||||
functionName: 'MyComponent',
|
||||
inputParameters: []
|
||||
},
|
||||
{
|
||||
id: 'template-2',
|
||||
title: 'Component with Props',
|
||||
description: 'Component template with configurable props',
|
||||
code: `function Greeting({ name = 'World', message = 'Hello' }) {
|
||||
return (
|
||||
<div className="p-4">
|
||||
<h2 className="text-xl font-bold">{message}, {name}!</h2>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default Greeting`,
|
||||
language: 'tsx',
|
||||
category: 'Templates',
|
||||
hasPreview: true,
|
||||
functionName: 'Greeting',
|
||||
inputParameters: [
|
||||
{
|
||||
name: 'name',
|
||||
type: 'string',
|
||||
defaultValue: 'World',
|
||||
description: 'Name to greet'
|
||||
},
|
||||
{
|
||||
name: 'message',
|
||||
type: 'string',
|
||||
defaultValue: 'Hello',
|
||||
description: 'Greeting message'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 'template-3',
|
||||
title: 'useState Hook Template',
|
||||
description: 'Component with state management',
|
||||
code: `import { useState } from 'react'
|
||||
import { Button } from '@/components/ui/button'
|
||||
|
||||
function StatefulComponent() {
|
||||
const [value, setValue] = useState(0)
|
||||
|
||||
return (
|
||||
<div className="p-4 space-y-4">
|
||||
<p className="text-lg">Value: {value}</p>
|
||||
<Button onClick={() => setValue(value + 1)}>
|
||||
Increment
|
||||
</Button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default StatefulComponent`,
|
||||
language: 'tsx',
|
||||
category: 'Templates',
|
||||
hasPreview: true,
|
||||
functionName: 'StatefulComponent',
|
||||
inputParameters: []
|
||||
}
|
||||
]
|
||||
|
||||
for (const template of seedTemplates) {
|
||||
await createTemplate(template)
|
||||
}
|
||||
}
|
||||
@@ -1,23 +1,26 @@
|
||||
import { motion } from 'framer-motion'
|
||||
import { AtomsSection } from '@/components/atoms/AtomsSection'
|
||||
import type { Snippet } from '@/lib/types'
|
||||
import { useKV } from '@github/spark/hooks'
|
||||
import { useCallback } from 'react'
|
||||
import { toast } from 'sonner'
|
||||
import { createSnippet } from '@/lib/db'
|
||||
|
||||
export function AtomsPage() {
|
||||
const [snippets, setSnippets] = useKV<Snippet[]>('snippets', [])
|
||||
|
||||
const handleSaveSnippet = useCallback((snippetData: Omit<Snippet, 'id' | 'createdAt' | 'updatedAt'>) => {
|
||||
const newSnippet: Snippet = {
|
||||
...snippetData,
|
||||
id: Date.now().toString(),
|
||||
createdAt: Date.now(),
|
||||
updatedAt: Date.now(),
|
||||
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')
|
||||
}
|
||||
setSnippets((currentSnippets) => [newSnippet, ...(currentSnippets || [])])
|
||||
toast.success('Component saved as snippet!')
|
||||
}, [setSnippets])
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
|
||||
@@ -1,23 +1,26 @@
|
||||
import { motion } from 'framer-motion'
|
||||
import { MoleculesSection } from '@/components/molecules/MoleculesSection'
|
||||
import type { Snippet } from '@/lib/types'
|
||||
import { useKV } from '@github/spark/hooks'
|
||||
import { useCallback } from 'react'
|
||||
import { toast } from 'sonner'
|
||||
import { createSnippet } from '@/lib/db'
|
||||
|
||||
export function MoleculesPage() {
|
||||
const [snippets, setSnippets] = useKV<Snippet[]>('snippets', [])
|
||||
|
||||
const handleSaveSnippet = useCallback((snippetData: Omit<Snippet, 'id' | 'createdAt' | 'updatedAt'>) => {
|
||||
const newSnippet: Snippet = {
|
||||
...snippetData,
|
||||
id: Date.now().toString(),
|
||||
createdAt: Date.now(),
|
||||
updatedAt: Date.now(),
|
||||
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')
|
||||
}
|
||||
setSnippets((currentSnippets) => [newSnippet, ...(currentSnippets || [])])
|
||||
toast.success('Component saved as snippet!')
|
||||
}, [setSnippets])
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
|
||||
@@ -1,23 +1,26 @@
|
||||
import { motion } from 'framer-motion'
|
||||
import { OrganismsSection } from '@/components/organisms/OrganismsSection'
|
||||
import type { Snippet } from '@/lib/types'
|
||||
import { useKV } from '@github/spark/hooks'
|
||||
import { useCallback } from 'react'
|
||||
import { toast } from 'sonner'
|
||||
import { createSnippet } from '@/lib/db'
|
||||
|
||||
export function OrganismsPage() {
|
||||
const [snippets, setSnippets] = useKV<Snippet[]>('snippets', [])
|
||||
|
||||
const handleSaveSnippet = useCallback((snippetData: Omit<Snippet, 'id' | 'createdAt' | 'updatedAt'>) => {
|
||||
const newSnippet: Snippet = {
|
||||
...snippetData,
|
||||
id: Date.now().toString(),
|
||||
createdAt: Date.now(),
|
||||
updatedAt: Date.now(),
|
||||
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')
|
||||
}
|
||||
setSnippets((currentSnippets) => [newSnippet, ...(currentSnippets || [])])
|
||||
toast.success('Component saved as snippet!')
|
||||
}, [setSnippets])
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
|
||||
@@ -1,23 +1,26 @@
|
||||
import { motion } from 'framer-motion'
|
||||
import { TemplatesSection } from '@/components/templates/TemplatesSection'
|
||||
import type { Snippet } from '@/lib/types'
|
||||
import { useKV } from '@github/spark/hooks'
|
||||
import { useCallback } from 'react'
|
||||
import { toast } from 'sonner'
|
||||
import { createSnippet } from '@/lib/db'
|
||||
|
||||
export function TemplatesPage() {
|
||||
const [snippets, setSnippets] = useKV<Snippet[]>('snippets', [])
|
||||
|
||||
const handleSaveSnippet = useCallback((snippetData: Omit<Snippet, 'id' | 'createdAt' | 'updatedAt'>) => {
|
||||
const newSnippet: Snippet = {
|
||||
...snippetData,
|
||||
id: Date.now().toString(),
|
||||
createdAt: Date.now(),
|
||||
updatedAt: Date.now(),
|
||||
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')
|
||||
}
|
||||
setSnippets((currentSnippets) => [newSnippet, ...(currentSnippets || [])])
|
||||
toast.success('Component saved as snippet!')
|
||||
}, [setSnippets])
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
|
||||
Reference in New Issue
Block a user