diff --git a/package-lock.json b/package-lock.json index 068ff7a..7cfd83d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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": { diff --git a/package.json b/package.json index 12be3c3..8e12322 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/components/SnippetManager.tsx b/src/components/SnippetManager.tsx index c3ab1c3..74c2efa 100644 --- a/src/components/SnippetManager.tsx +++ b/src/components/SnippetManager.tsx @@ -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('snippets', []) + const [snippets, setSnippets] = useState([]) + const [loading, setLoading] = useState(true) const [searchQuery, setSearchQuery] = useState('') const [dialogOpen, setDialogOpen] = useState(false) const [viewerOpen, setViewerOpen] = useState(false) const [editingSnippet, setEditingSnippet] = useState(null) const [viewingSnippet, setViewingSnippet] = useState(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) => { - 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) => { + 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 ( +
+

Loading snippets...

+
+ ) + } + if (allSnippets.length === 0) { return ( <> diff --git a/src/lib/db.ts b/src/lib/db.ts new file mode 100644 index 0000000..d4ec1f0 --- /dev/null +++ b/src/lib/db.ts @@ -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 { + 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 { + 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 { + 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 { + 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 { + 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 { + const db = await initDB() + + db.run('DELETE FROM snippets WHERE id = ?', [id]) + + saveDB() +} + +export async function getAllTemplates(): Promise { + 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 { + 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 { + 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 ( +
+

Count: {count}

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

My Todos

+
+ setInput(e.target.value)} + onKeyPress={(e) => e.key === 'Enter' && addTodo()} + placeholder="Add a new todo..." + /> + +
+
+ {todos.map(todo => ( +
+ toggleTodo(todo.id)} + /> + + {todo.text} + + +
+ ))} +
+
+ ) +} + +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 ( + + + + {title} + {description} + + +

This card has smooth animations on hover and tap!

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

Contact Form

+
+
+ + + {errors.name && ( +

{errors.name.message}

+ )} +
+ +
+ + + {errors.email && ( +

{errors.email.message}

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

Hello World

+

This is a basic component.

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

{message}, {name}!

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

Value: {value}

+ +
+ ) +} + +export default StatefulComponent`, + language: 'tsx', + category: 'Templates', + hasPreview: true, + functionName: 'StatefulComponent', + inputParameters: [] + } + ] + + for (const template of seedTemplates) { + await createTemplate(template) + } +} diff --git a/src/pages/AtomsPage.tsx b/src/pages/AtomsPage.tsx index a593788..b3a1f19 100644 --- a/src/pages/AtomsPage.tsx +++ b/src/pages/AtomsPage.tsx @@ -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('snippets', []) - - const handleSaveSnippet = useCallback((snippetData: Omit) => { - const newSnippet: Snippet = { - ...snippetData, - id: Date.now().toString(), - createdAt: Date.now(), - updatedAt: Date.now(), + const handleSaveSnippet = useCallback(async (snippetData: Omit) => { + 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 ( ('snippets', []) - - const handleSaveSnippet = useCallback((snippetData: Omit) => { - const newSnippet: Snippet = { - ...snippetData, - id: Date.now().toString(), - createdAt: Date.now(), - updatedAt: Date.now(), + const handleSaveSnippet = useCallback(async (snippetData: Omit) => { + 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 ( ('snippets', []) - - const handleSaveSnippet = useCallback((snippetData: Omit) => { - const newSnippet: Snippet = { - ...snippetData, - id: Date.now().toString(), - createdAt: Date.now(), - updatedAt: Date.now(), + const handleSaveSnippet = useCallback(async (snippetData: Omit) => { + 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 ( ('snippets', []) - - const handleSaveSnippet = useCallback((snippetData: Omit) => { - const newSnippet: Snippet = { - ...snippetData, - id: Date.now().toString(), - createdAt: Date.now(), - updatedAt: Date.now(), + const handleSaveSnippet = useCallback(async (snippetData: Omit) => { + 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 (