mirror of
https://github.com/johndoe6345789/snippet-pastebin.git
synced 2026-04-24 21:44:54 +00:00
Merge pull request #7 from johndoe6345789/copilot/refactor-large-components
Refactor monolithic files into focused modules (db.ts 1085→37 LOC, component-code-snippets.ts 1006→9 LOC)
This commit is contained in:
File diff suppressed because it is too large
Load Diff
8
src/lib/db-constants.ts
Normal file
8
src/lib/db-constants.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
/**
|
||||
* Database constants shared across modules
|
||||
*/
|
||||
|
||||
export const DB_KEY = 'codesnippet-db'
|
||||
export const IDB_NAME = 'CodeSnippetDB'
|
||||
export const IDB_STORE = 'database'
|
||||
export const IDB_VERSION = 1
|
||||
178
src/lib/db-core.ts
Normal file
178
src/lib/db-core.ts
Normal file
@@ -0,0 +1,178 @@
|
||||
/**
|
||||
* Core database initialization and management
|
||||
*/
|
||||
|
||||
import initSqlJs, { Database } from 'sql.js'
|
||||
import { loadFromIndexedDB, saveToIndexedDB, openIndexedDB, deleteFromIndexedDB } from './db-indexeddb'
|
||||
import { loadFromLocalStorage, saveToLocalStorage, deleteFromLocalStorage } from './db-localstorage'
|
||||
import { validateSchema, createTables } from './db-schema'
|
||||
import { getStorageConfig, FlaskStorageAdapter, loadStorageConfig } from './storage'
|
||||
import { DB_KEY } from './db-constants'
|
||||
|
||||
let dbInstance: Database | null = null
|
||||
let sqlInstance: any = null
|
||||
let flaskAdapter: FlaskStorageAdapter | null = null
|
||||
let configLoaded = false
|
||||
|
||||
async function wipeAndRecreateDB(): Promise<void> {
|
||||
console.warn('Wiping corrupted database and creating fresh schema...')
|
||||
|
||||
await saveToIndexedDB(new Uint8Array())
|
||||
saveToLocalStorage(new Uint8Array())
|
||||
|
||||
await deleteFromIndexedDB()
|
||||
deleteFromLocalStorage()
|
||||
|
||||
dbInstance = null
|
||||
}
|
||||
|
||||
export async function initDB(): Promise<Database> {
|
||||
if (dbInstance) return dbInstance
|
||||
|
||||
if (!sqlInstance) {
|
||||
sqlInstance = await initSqlJs({
|
||||
locateFile: (file) => `https://sql.js.org/dist/${file}`
|
||||
})
|
||||
}
|
||||
|
||||
let loadedData: Uint8Array | null = null
|
||||
let schemaValid = false
|
||||
|
||||
loadedData = await loadFromIndexedDB()
|
||||
|
||||
if (!loadedData) {
|
||||
loadedData = loadFromLocalStorage()
|
||||
}
|
||||
|
||||
if (loadedData && loadedData.length > 0) {
|
||||
try {
|
||||
const testDb = new sqlInstance.Database(loadedData)
|
||||
schemaValid = await validateSchema(testDb)
|
||||
|
||||
if (schemaValid) {
|
||||
dbInstance = testDb
|
||||
} else {
|
||||
console.warn('Schema validation failed, wiping database')
|
||||
testDb.close()
|
||||
await wipeAndRecreateDB()
|
||||
dbInstance = new sqlInstance.Database()
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to load saved database, creating new one:', error)
|
||||
await wipeAndRecreateDB()
|
||||
dbInstance = new sqlInstance.Database()
|
||||
}
|
||||
} else {
|
||||
dbInstance = new sqlInstance.Database()
|
||||
}
|
||||
|
||||
if (!dbInstance) {
|
||||
throw new Error('Failed to initialize database')
|
||||
}
|
||||
|
||||
createTables(dbInstance)
|
||||
await saveDB()
|
||||
|
||||
return dbInstance
|
||||
}
|
||||
|
||||
export async function saveDB() {
|
||||
if (!dbInstance) return
|
||||
|
||||
try {
|
||||
const data = dbInstance.export()
|
||||
|
||||
const savedToIDB = await saveToIndexedDB(data)
|
||||
|
||||
if (!savedToIDB) {
|
||||
saveToLocalStorage(data)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to save database:', error)
|
||||
}
|
||||
}
|
||||
|
||||
export function getFlaskAdapter(): FlaskStorageAdapter | null {
|
||||
if (!configLoaded) {
|
||||
loadStorageConfig()
|
||||
configLoaded = true
|
||||
}
|
||||
|
||||
const config = getStorageConfig()
|
||||
if (config.backend === 'flask' && config.flaskUrl) {
|
||||
try {
|
||||
if (!flaskAdapter || flaskAdapter['baseUrl'] !== config.flaskUrl) {
|
||||
flaskAdapter = new FlaskStorageAdapter(config.flaskUrl)
|
||||
}
|
||||
return flaskAdapter
|
||||
} catch (error) {
|
||||
console.warn('Failed to create Flask adapter:', error)
|
||||
return null
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
export async function exportDatabase(): Promise<Uint8Array> {
|
||||
const db = await initDB()
|
||||
return db.export()
|
||||
}
|
||||
|
||||
export async function importDatabase(data: Uint8Array): Promise<void> {
|
||||
if (!sqlInstance) {
|
||||
sqlInstance = await initSqlJs({
|
||||
locateFile: (file) => `https://sql.js.org/dist/${file}`
|
||||
})
|
||||
}
|
||||
|
||||
try {
|
||||
dbInstance = new sqlInstance.Database(data)
|
||||
await saveDB()
|
||||
} catch (error) {
|
||||
console.error('Failed to import database:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
export async function getDatabaseStats(): Promise<{
|
||||
snippetCount: number
|
||||
templateCount: number
|
||||
storageType: 'indexeddb' | 'localstorage' | 'none'
|
||||
databaseSize: number
|
||||
}> {
|
||||
const db = await initDB()
|
||||
|
||||
const snippetResult = db.exec('SELECT COUNT(*) as count FROM snippets')
|
||||
const templateResult = db.exec('SELECT COUNT(*) as count FROM snippet_templates')
|
||||
|
||||
const snippetCount = snippetResult[0]?.values[0]?.[0] as number || 0
|
||||
const templateCount = templateResult[0]?.values[0]?.[0] as number || 0
|
||||
|
||||
const data = db.export()
|
||||
const databaseSize = data.length
|
||||
|
||||
const hasIDB = await openIndexedDB()
|
||||
const hasLocalStorage = typeof localStorage !== 'undefined' && localStorage.getItem(DB_KEY) !== null
|
||||
const storageType = hasIDB ? 'indexeddb' : (hasLocalStorage ? 'localstorage' : 'none')
|
||||
|
||||
return {
|
||||
snippetCount,
|
||||
templateCount,
|
||||
storageType,
|
||||
databaseSize
|
||||
}
|
||||
}
|
||||
|
||||
export async function clearDatabase(): Promise<void> {
|
||||
const adapter = getFlaskAdapter()
|
||||
if (adapter) {
|
||||
await adapter.wipeDatabase()
|
||||
return
|
||||
}
|
||||
|
||||
await deleteFromIndexedDB()
|
||||
deleteFromLocalStorage()
|
||||
|
||||
dbInstance = null
|
||||
await initDB()
|
||||
}
|
||||
100
src/lib/db-indexeddb.ts
Normal file
100
src/lib/db-indexeddb.ts
Normal file
@@ -0,0 +1,100 @@
|
||||
/**
|
||||
* IndexedDB operations for database persistence
|
||||
*/
|
||||
|
||||
import { DB_KEY, IDB_NAME, IDB_STORE, IDB_VERSION } from './db-constants'
|
||||
|
||||
export async function openIndexedDB(): Promise<IDBDatabase | null> {
|
||||
if (typeof indexedDB === 'undefined') return null
|
||||
|
||||
return new Promise((resolve) => {
|
||||
try {
|
||||
const request = indexedDB.open(IDB_NAME, IDB_VERSION)
|
||||
|
||||
request.onerror = () => {
|
||||
console.warn('IndexedDB not available, falling back to localStorage')
|
||||
resolve(null)
|
||||
}
|
||||
|
||||
request.onupgradeneeded = (event) => {
|
||||
const db = (event.target as IDBOpenDBRequest).result
|
||||
if (!db.objectStoreNames.contains(IDB_STORE)) {
|
||||
db.createObjectStore(IDB_STORE)
|
||||
}
|
||||
}
|
||||
|
||||
request.onsuccess = (event) => {
|
||||
resolve((event.target as IDBOpenDBRequest).result)
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('IndexedDB error:', error)
|
||||
resolve(null)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
export async function loadFromIndexedDB(): Promise<Uint8Array | null> {
|
||||
const db = await openIndexedDB()
|
||||
if (!db) return null
|
||||
|
||||
return new Promise((resolve) => {
|
||||
try {
|
||||
const transaction = db.transaction([IDB_STORE], 'readonly')
|
||||
const store = transaction.objectStore(IDB_STORE)
|
||||
const request = store.get(DB_KEY)
|
||||
|
||||
request.onsuccess = () => {
|
||||
const data = request.result
|
||||
resolve(data ? new Uint8Array(data) : null)
|
||||
}
|
||||
|
||||
request.onerror = () => {
|
||||
console.warn('Failed to load from IndexedDB')
|
||||
resolve(null)
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('IndexedDB read error:', error)
|
||||
resolve(null)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
export async function saveToIndexedDB(data: Uint8Array): Promise<boolean> {
|
||||
const db = await openIndexedDB()
|
||||
if (!db) return false
|
||||
|
||||
return new Promise((resolve) => {
|
||||
try {
|
||||
const transaction = db.transaction([IDB_STORE], 'readwrite')
|
||||
const store = transaction.objectStore(IDB_STORE)
|
||||
const request = store.put(data, DB_KEY)
|
||||
|
||||
request.onsuccess = () => resolve(true)
|
||||
request.onerror = () => {
|
||||
console.warn('Failed to save to IndexedDB')
|
||||
resolve(false)
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('IndexedDB write error:', error)
|
||||
resolve(false)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
export async function deleteFromIndexedDB(): Promise<void> {
|
||||
const db = await openIndexedDB()
|
||||
if (!db) return
|
||||
|
||||
return new Promise((resolve) => {
|
||||
try {
|
||||
const transaction = db.transaction([IDB_STORE], 'readwrite')
|
||||
const store = transaction.objectStore(IDB_STORE)
|
||||
const request = store.delete(DB_KEY)
|
||||
request.onsuccess = () => resolve()
|
||||
request.onerror = () => resolve()
|
||||
} catch (error) {
|
||||
console.warn('Error clearing IndexedDB:', error)
|
||||
resolve()
|
||||
}
|
||||
})
|
||||
}
|
||||
36
src/lib/db-localstorage.ts
Normal file
36
src/lib/db-localstorage.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
/**
|
||||
* LocalStorage operations for database persistence
|
||||
*/
|
||||
|
||||
import { DB_KEY } from './db-constants'
|
||||
|
||||
export function loadFromLocalStorage(): Uint8Array | null {
|
||||
try {
|
||||
const savedData = localStorage.getItem(DB_KEY)
|
||||
if (savedData) {
|
||||
return new Uint8Array(JSON.parse(savedData))
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('Failed to load from localStorage:', error)
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
export function saveToLocalStorage(data: Uint8Array): boolean {
|
||||
try {
|
||||
const dataArray = Array.from(data)
|
||||
localStorage.setItem(DB_KEY, JSON.stringify(dataArray))
|
||||
return true
|
||||
} catch (error) {
|
||||
console.warn('Failed to save to localStorage (quota exceeded?):', error)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
export function deleteFromLocalStorage(): void {
|
||||
try {
|
||||
localStorage.removeItem(DB_KEY)
|
||||
} catch (error) {
|
||||
console.warn('Error clearing localStorage:', error)
|
||||
}
|
||||
}
|
||||
108
src/lib/db-namespaces.ts
Normal file
108
src/lib/db-namespaces.ts
Normal file
@@ -0,0 +1,108 @@
|
||||
/**
|
||||
* Namespace operations for organizing snippets
|
||||
*/
|
||||
|
||||
import type { Namespace } from './types'
|
||||
import { initDB, saveDB, getFlaskAdapter } from './db-core'
|
||||
import { mapRowToObject, mapRowsToObjects } from './db-mapper'
|
||||
|
||||
export async function getAllNamespaces(): Promise<Namespace[]> {
|
||||
const adapter = getFlaskAdapter()
|
||||
if (adapter) {
|
||||
return await adapter.getAllNamespaces()
|
||||
}
|
||||
|
||||
const db = await initDB()
|
||||
const results = db.exec('SELECT * FROM namespaces ORDER BY isDefault DESC, name ASC')
|
||||
|
||||
return mapRowsToObjects<Namespace>(results)
|
||||
}
|
||||
|
||||
export async function createNamespace(name: string): Promise<Namespace> {
|
||||
const namespace: Namespace = {
|
||||
id: Date.now().toString(),
|
||||
name,
|
||||
createdAt: Date.now(),
|
||||
isDefault: false
|
||||
}
|
||||
|
||||
const adapter = getFlaskAdapter()
|
||||
if (adapter) {
|
||||
await adapter.createNamespace(namespace)
|
||||
return namespace
|
||||
}
|
||||
|
||||
const db = await initDB()
|
||||
|
||||
db.run(
|
||||
`INSERT INTO namespaces (id, name, createdAt, isDefault)
|
||||
VALUES (?, ?, ?, ?)`,
|
||||
[namespace.id, namespace.name, namespace.createdAt, namespace.isDefault ? 1 : 0]
|
||||
)
|
||||
|
||||
await saveDB()
|
||||
return namespace
|
||||
}
|
||||
|
||||
export async function deleteNamespace(id: string): Promise<void> {
|
||||
const adapter = getFlaskAdapter()
|
||||
if (adapter) {
|
||||
return await adapter.deleteNamespace(id)
|
||||
}
|
||||
|
||||
const db = await initDB()
|
||||
|
||||
const defaultNamespace = db.exec('SELECT id FROM namespaces WHERE isDefault = 1')
|
||||
if (defaultNamespace.length === 0 || defaultNamespace[0].values.length === 0) {
|
||||
throw new Error('Default namespace not found')
|
||||
}
|
||||
|
||||
const defaultId = defaultNamespace[0].values[0][0] as string
|
||||
|
||||
const checkDefault = db.exec('SELECT isDefault FROM namespaces WHERE id = ?', [id])
|
||||
if (checkDefault.length > 0 && checkDefault[0].values[0]?.[0] === 1) {
|
||||
throw new Error('Cannot delete default namespace')
|
||||
}
|
||||
|
||||
db.run('UPDATE snippets SET namespaceId = ? WHERE namespaceId = ?', [defaultId, id])
|
||||
|
||||
db.run('DELETE FROM namespaces WHERE id = ?', [id])
|
||||
|
||||
await saveDB()
|
||||
}
|
||||
|
||||
export async function ensureDefaultNamespace(): Promise<void> {
|
||||
const db = await initDB()
|
||||
|
||||
const results = db.exec('SELECT COUNT(*) as count FROM namespaces WHERE isDefault = 1')
|
||||
const count = results[0]?.values[0]?.[0] as number || 0
|
||||
|
||||
if (count === 0) {
|
||||
const defaultNamespace: Namespace = {
|
||||
id: 'default',
|
||||
name: 'Default',
|
||||
createdAt: Date.now(),
|
||||
isDefault: true
|
||||
}
|
||||
|
||||
db.run(
|
||||
`INSERT INTO namespaces (id, name, createdAt, isDefault)
|
||||
VALUES (?, ?, ?, ?)`,
|
||||
[defaultNamespace.id, defaultNamespace.name, defaultNamespace.createdAt, 1]
|
||||
)
|
||||
|
||||
await saveDB()
|
||||
}
|
||||
}
|
||||
|
||||
export async function getNamespaceById(id: string): Promise<Namespace | null> {
|
||||
const db = await initDB()
|
||||
const results = db.exec('SELECT * FROM namespaces WHERE id = ?', [id])
|
||||
|
||||
if (results.length === 0 || results[0].values.length === 0) return null
|
||||
|
||||
const columns = results[0].columns
|
||||
const row = results[0].values[0]
|
||||
|
||||
return mapRowToObject<Namespace>(row, columns)
|
||||
}
|
||||
106
src/lib/db-schema.ts
Normal file
106
src/lib/db-schema.ts
Normal file
@@ -0,0 +1,106 @@
|
||||
/**
|
||||
* Database schema management and validation
|
||||
*/
|
||||
|
||||
import type { Database } from 'sql.js'
|
||||
|
||||
export async function validateSchema(db: Database): Promise<boolean> {
|
||||
try {
|
||||
const snippetsCheck = db.exec("PRAGMA table_info(snippets)")
|
||||
if (snippetsCheck.length === 0) return true
|
||||
|
||||
const columns = snippetsCheck[0].values.map(row => row[1] as string)
|
||||
const requiredColumns = ['id', 'title', 'code', 'language', 'category', 'namespaceId', 'createdAt', 'updatedAt']
|
||||
|
||||
for (const col of requiredColumns) {
|
||||
if (!columns.includes(col)) {
|
||||
console.warn(`Schema validation failed: missing column '${col}'`)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
const namespacesCheck = db.exec("PRAGMA table_info(namespaces)")
|
||||
if (namespacesCheck.length === 0) {
|
||||
console.warn('Schema validation failed: namespaces table missing')
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
} catch (error) {
|
||||
console.error('Schema validation error:', error)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
export function createTables(db: Database): void {
|
||||
db.run(`
|
||||
CREATE TABLE IF NOT EXISTS namespaces (
|
||||
id TEXT PRIMARY KEY,
|
||||
name TEXT NOT NULL,
|
||||
createdAt INTEGER NOT NULL,
|
||||
isDefault INTEGER DEFAULT 0
|
||||
)
|
||||
`)
|
||||
|
||||
db.run(`
|
||||
CREATE TABLE IF NOT EXISTS snippets (
|
||||
id TEXT PRIMARY KEY,
|
||||
title TEXT NOT NULL,
|
||||
description TEXT,
|
||||
code TEXT NOT NULL,
|
||||
language TEXT NOT NULL,
|
||||
category TEXT NOT NULL,
|
||||
namespaceId TEXT,
|
||||
hasPreview INTEGER DEFAULT 0,
|
||||
functionName TEXT,
|
||||
inputParameters TEXT,
|
||||
createdAt INTEGER NOT NULL,
|
||||
updatedAt INTEGER NOT NULL,
|
||||
FOREIGN KEY (namespaceId) REFERENCES namespaces(id)
|
||||
)
|
||||
`)
|
||||
|
||||
db.run(`
|
||||
CREATE TABLE IF NOT EXISTS snippet_templates (
|
||||
id TEXT PRIMARY KEY,
|
||||
title TEXT NOT NULL,
|
||||
description TEXT,
|
||||
code TEXT NOT NULL,
|
||||
language TEXT NOT NULL,
|
||||
category TEXT NOT NULL,
|
||||
hasPreview INTEGER DEFAULT 0,
|
||||
functionName TEXT,
|
||||
inputParameters TEXT
|
||||
)
|
||||
`)
|
||||
}
|
||||
|
||||
export async function validateDatabaseSchema(db: Database): Promise<{ valid: boolean; issues: string[] }> {
|
||||
try {
|
||||
const issues: string[] = []
|
||||
|
||||
const snippetsCheck = db.exec("PRAGMA table_info(snippets)")
|
||||
if (snippetsCheck.length === 0) {
|
||||
issues.push('Snippets table missing')
|
||||
return { valid: false, issues }
|
||||
}
|
||||
|
||||
const columns = snippetsCheck[0].values.map(row => row[1] as string)
|
||||
const requiredColumns = ['id', 'title', 'code', 'language', 'category', 'namespaceId', 'createdAt', 'updatedAt']
|
||||
|
||||
for (const col of requiredColumns) {
|
||||
if (!columns.includes(col)) {
|
||||
issues.push(`Missing column '${col}' in snippets table`)
|
||||
}
|
||||
}
|
||||
|
||||
const namespacesCheck = db.exec("SELECT name FROM sqlite_master WHERE type='table' AND name='namespaces'")
|
||||
if (namespacesCheck.length === 0) {
|
||||
issues.push('Namespaces table missing')
|
||||
}
|
||||
|
||||
return { valid: issues.length === 0, issues }
|
||||
} catch (error) {
|
||||
return { valid: false, issues: ['Failed to validate schema: ' + (error as Error).message] }
|
||||
}
|
||||
}
|
||||
585
src/lib/db-snippets.ts
Normal file
585
src/lib/db-snippets.ts
Normal file
@@ -0,0 +1,585 @@
|
||||
/**
|
||||
* Snippet CRUD operations and templates management
|
||||
*/
|
||||
|
||||
import type { Snippet, SnippetTemplate } from './types'
|
||||
import { initDB, saveDB, getFlaskAdapter } from './db-core'
|
||||
import { mapRowToObject, mapRowsToObjects } from './db-mapper'
|
||||
import { ensureDefaultNamespace } from './db-namespaces'
|
||||
|
||||
export async function getAllSnippets(): Promise<Snippet[]> {
|
||||
const adapter = getFlaskAdapter()
|
||||
if (adapter) {
|
||||
return await adapter.getAllSnippets()
|
||||
}
|
||||
|
||||
const db = await initDB()
|
||||
const results = db.exec('SELECT * FROM snippets ORDER BY updatedAt DESC')
|
||||
|
||||
return mapRowsToObjects<Snippet>(results)
|
||||
}
|
||||
|
||||
export async function getSnippet(id: string): Promise<Snippet | null> {
|
||||
const adapter = getFlaskAdapter()
|
||||
if (adapter) {
|
||||
return await adapter.getSnippet(id)
|
||||
}
|
||||
|
||||
const db = await initDB()
|
||||
const results = db.exec('SELECT * FROM snippets WHERE id = ?', [id])
|
||||
|
||||
if (results.length === 0 || results[0].values.length === 0) return null
|
||||
|
||||
const columns = results[0].columns
|
||||
const row = results[0].values[0]
|
||||
|
||||
return mapRowToObject<Snippet>(row, columns)
|
||||
}
|
||||
|
||||
export async function createSnippet(snippet: Snippet): Promise<void> {
|
||||
const adapter = getFlaskAdapter()
|
||||
if (adapter) {
|
||||
return await adapter.createSnippet(snippet)
|
||||
}
|
||||
|
||||
const db = await initDB()
|
||||
|
||||
db.run(
|
||||
`INSERT INTO snippets (id, title, description, code, language, category, namespaceId, hasPreview, functionName, inputParameters, createdAt, updatedAt)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||
[
|
||||
snippet.id,
|
||||
snippet.title,
|
||||
snippet.description,
|
||||
snippet.code,
|
||||
snippet.language,
|
||||
snippet.category,
|
||||
snippet.namespaceId || null,
|
||||
snippet.hasPreview ? 1 : 0,
|
||||
snippet.functionName || null,
|
||||
snippet.inputParameters ? JSON.stringify(snippet.inputParameters) : null,
|
||||
snippet.createdAt,
|
||||
snippet.updatedAt
|
||||
]
|
||||
)
|
||||
|
||||
await saveDB()
|
||||
}
|
||||
|
||||
export async function updateSnippet(snippet: Snippet): Promise<void> {
|
||||
const adapter = getFlaskAdapter()
|
||||
if (adapter) {
|
||||
return await adapter.updateSnippet(snippet)
|
||||
}
|
||||
|
||||
const db = await initDB()
|
||||
|
||||
db.run(
|
||||
`UPDATE snippets
|
||||
SET title = ?, description = ?, code = ?, language = ?, category = ?, namespaceId = ?, hasPreview = ?, functionName = ?, inputParameters = ?, updatedAt = ?
|
||||
WHERE id = ?`,
|
||||
[
|
||||
snippet.title,
|
||||
snippet.description,
|
||||
snippet.code,
|
||||
snippet.language,
|
||||
snippet.category,
|
||||
snippet.namespaceId || null,
|
||||
snippet.hasPreview ? 1 : 0,
|
||||
snippet.functionName || null,
|
||||
snippet.inputParameters ? JSON.stringify(snippet.inputParameters) : null,
|
||||
snippet.updatedAt,
|
||||
snippet.id
|
||||
]
|
||||
)
|
||||
|
||||
await saveDB()
|
||||
}
|
||||
|
||||
export async function deleteSnippet(id: string): Promise<void> {
|
||||
const adapter = getFlaskAdapter()
|
||||
if (adapter) {
|
||||
return await adapter.deleteSnippet(id)
|
||||
}
|
||||
|
||||
const db = await initDB()
|
||||
|
||||
db.run('DELETE FROM snippets WHERE id = ?', [id])
|
||||
|
||||
await saveDB()
|
||||
}
|
||||
|
||||
export async function getSnippetsByNamespace(namespaceId: string): Promise<Snippet[]> {
|
||||
const db = await initDB()
|
||||
const results = db.exec('SELECT * FROM snippets WHERE namespaceId = ? OR (namespaceId IS NULL AND ? = (SELECT id FROM namespaces WHERE isDefault = 1)) ORDER BY updatedAt DESC', [namespaceId, namespaceId])
|
||||
|
||||
return mapRowsToObjects<Snippet>(results)
|
||||
}
|
||||
|
||||
export async function moveSnippetToNamespace(snippetId: string, targetNamespaceId: string): Promise<void> {
|
||||
const adapter = getFlaskAdapter()
|
||||
if (adapter) {
|
||||
const snippet = await adapter.getSnippet(snippetId)
|
||||
if (snippet) {
|
||||
snippet.namespaceId = targetNamespaceId
|
||||
snippet.updatedAt = Date.now()
|
||||
await adapter.updateSnippet(snippet)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
const db = await initDB()
|
||||
|
||||
db.run(
|
||||
'UPDATE snippets SET namespaceId = ?, updatedAt = ? WHERE id = ?',
|
||||
[targetNamespaceId, Date.now(), snippetId]
|
||||
)
|
||||
|
||||
await saveDB()
|
||||
}
|
||||
|
||||
export async function bulkMoveSnippets(snippetIds: string[], targetNamespaceId: string): Promise<void> {
|
||||
const adapter = getFlaskAdapter()
|
||||
if (adapter) {
|
||||
await adapter.bulkMoveSnippets(snippetIds, targetNamespaceId)
|
||||
return
|
||||
}
|
||||
|
||||
const db = await initDB()
|
||||
const now = Date.now()
|
||||
|
||||
for (const snippetId of snippetIds) {
|
||||
db.run(
|
||||
'UPDATE snippets SET namespaceId = ?, updatedAt = ? WHERE id = ?',
|
||||
[targetNamespaceId, now, snippetId]
|
||||
)
|
||||
}
|
||||
|
||||
await saveDB()
|
||||
}
|
||||
|
||||
export async function getAllTemplates(): Promise<SnippetTemplate[]> {
|
||||
const db = await initDB()
|
||||
const results = db.exec('SELECT * FROM snippet_templates')
|
||||
|
||||
return mapRowsToObjects<SnippetTemplate>(results)
|
||||
}
|
||||
|
||||
export async function createTemplate(template: SnippetTemplate): Promise<void> {
|
||||
const db = await initDB()
|
||||
|
||||
db.run(
|
||||
`INSERT INTO snippet_templates (id, title, description, code, language, category, hasPreview, functionName, inputParameters)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||
[
|
||||
template.id,
|
||||
template.title,
|
||||
template.description,
|
||||
template.code,
|
||||
template.language,
|
||||
template.category,
|
||||
template.hasPreview ? 1 : 0,
|
||||
template.functionName || null,
|
||||
template.inputParameters ? JSON.stringify(template.inputParameters) : null
|
||||
]
|
||||
)
|
||||
|
||||
await saveDB()
|
||||
}
|
||||
|
||||
export async function syncTemplatesFromJSON(templates: SnippetTemplate[]): Promise<void> {
|
||||
const db = await initDB()
|
||||
|
||||
const existingTemplates = db.exec('SELECT id FROM snippet_templates')
|
||||
const existingIds = new Set(
|
||||
existingTemplates[0]?.values.map(row => row[0] as string) || []
|
||||
)
|
||||
|
||||
let addedCount = 0
|
||||
for (const template of templates) {
|
||||
if (!existingIds.has(template.id)) {
|
||||
await createTemplate(template)
|
||||
addedCount++
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export async function seedDatabase(): Promise<void> {
|
||||
const db = await initDB()
|
||||
|
||||
await ensureDefaultNamespace()
|
||||
|
||||
const checkSnippets = db.exec('SELECT COUNT(*) as count FROM snippets')
|
||||
const snippetCount = checkSnippets[0]?.values[0]?.[0] as number
|
||||
|
||||
if (snippetCount > 0) {
|
||||
return
|
||||
}
|
||||
|
||||
const now = Date.now()
|
||||
|
||||
const seedSnippets: Snippet[] = [
|
||||
{
|
||||
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)
|
||||
}
|
||||
}
|
||||
1122
src/lib/db.ts
1122
src/lib/db.ts
File diff suppressed because it is too large
Load Diff
209
src/lib/snippets/atoms.ts
Normal file
209
src/lib/snippets/atoms.ts
Normal file
@@ -0,0 +1,209 @@
|
||||
/**
|
||||
* Atoms - Basic UI building blocks code snippets
|
||||
*/
|
||||
|
||||
export const atomsCodeSnippets = {
|
||||
buttonDefault: `interface ButtonProps {
|
||||
children?: string
|
||||
onClick?: () => void
|
||||
}
|
||||
|
||||
function DefaultButton({ children = "Default", onClick }: ButtonProps) {
|
||||
return <Button onClick={onClick}>{children}</Button>
|
||||
}`,
|
||||
buttonSecondary: `interface ButtonProps {
|
||||
children?: string
|
||||
variant?: "default" | "secondary" | "destructive" | "outline" | "ghost" | "link"
|
||||
onClick?: () => void
|
||||
}
|
||||
|
||||
function CustomButton({ children = "Secondary", variant = "secondary", onClick }: ButtonProps) {
|
||||
return <Button variant={variant} onClick={onClick}>{children}</Button>
|
||||
}`,
|
||||
buttonDestructive: `interface DestructiveButtonProps {
|
||||
children?: string
|
||||
onClick?: () => void
|
||||
}
|
||||
|
||||
function DestructiveButton({ children = "Destructive", onClick }: DestructiveButtonProps) {
|
||||
return <Button variant="destructive" onClick={onClick}>{children}</Button>
|
||||
}`,
|
||||
buttonOutline: `interface OutlineButtonProps {
|
||||
children?: string
|
||||
onClick?: () => void
|
||||
}
|
||||
|
||||
function OutlineButton({ children = "Outline", onClick }: OutlineButtonProps) {
|
||||
return <Button variant="outline" onClick={onClick}>{children}</Button>
|
||||
}`,
|
||||
buttonGhost: `interface GhostButtonProps {
|
||||
children?: string
|
||||
onClick?: () => void
|
||||
}
|
||||
|
||||
function GhostButton({ children = "Ghost", onClick }: GhostButtonProps) {
|
||||
return <Button variant="ghost" onClick={onClick}>{children}</Button>
|
||||
}`,
|
||||
buttonLink: `interface LinkButtonProps {
|
||||
children?: string
|
||||
onClick?: () => void
|
||||
}
|
||||
|
||||
function LinkButton({ children = "Link", onClick }: LinkButtonProps) {
|
||||
return <Button variant="link" onClick={onClick}>{children}</Button>
|
||||
}`,
|
||||
buttonSizes: `interface ButtonSizesProps {
|
||||
smallText?: string
|
||||
defaultText?: string
|
||||
largeText?: string
|
||||
onSmallClick?: () => void
|
||||
onDefaultClick?: () => void
|
||||
onLargeClick?: () => void
|
||||
onIconClick?: () => void
|
||||
}
|
||||
|
||||
function ButtonSizes({
|
||||
smallText = "Small",
|
||||
defaultText = "Default",
|
||||
largeText = "Large",
|
||||
onSmallClick,
|
||||
onDefaultClick,
|
||||
onLargeClick,
|
||||
onIconClick
|
||||
}: ButtonSizesProps) {
|
||||
return (
|
||||
<>
|
||||
<Button size="sm" onClick={onSmallClick}>{smallText}</Button>
|
||||
<Button size="default" onClick={onDefaultClick}>{defaultText}</Button>
|
||||
<Button size="lg" onClick={onLargeClick}>{largeText}</Button>
|
||||
<Button size="icon" onClick={onIconClick}>
|
||||
<Heart weight="fill" />
|
||||
</Button>
|
||||
</>
|
||||
)
|
||||
}`,
|
||||
buttonWithIcons: `interface IconButtonProps {
|
||||
primaryText?: string
|
||||
secondaryText?: string
|
||||
onPrimaryClick?: () => void
|
||||
onSecondaryClick?: () => void
|
||||
}
|
||||
|
||||
function IconButtons({
|
||||
primaryText = "Favorite",
|
||||
secondaryText = "Add Item",
|
||||
onPrimaryClick,
|
||||
onSecondaryClick
|
||||
}: IconButtonProps) {
|
||||
return (
|
||||
<>
|
||||
<Button onClick={onPrimaryClick}>
|
||||
<Star weight="fill" />
|
||||
{primaryText}
|
||||
</Button>
|
||||
<Button variant="outline" onClick={onSecondaryClick}>
|
||||
<Plus weight="bold" />
|
||||
{secondaryText}
|
||||
</Button>
|
||||
</>
|
||||
)
|
||||
}`,
|
||||
badgeVariants: `interface BadgeVariantsProps {
|
||||
defaultText?: string
|
||||
secondaryText?: string
|
||||
destructiveText?: string
|
||||
outlineText?: string
|
||||
}
|
||||
|
||||
function BadgeVariants({
|
||||
defaultText = "Default",
|
||||
secondaryText = "Secondary",
|
||||
destructiveText = "Destructive",
|
||||
outlineText = "Outline"
|
||||
}: BadgeVariantsProps) {
|
||||
return (
|
||||
<>
|
||||
<Badge>{defaultText}</Badge>
|
||||
<Badge variant="secondary">{secondaryText}</Badge>
|
||||
<Badge variant="destructive">{destructiveText}</Badge>
|
||||
<Badge variant="outline">{outlineText}</Badge>
|
||||
</>
|
||||
)
|
||||
}`,
|
||||
badgeWithIcons: `interface IconBadgeProps {
|
||||
completedText?: string
|
||||
failedText?: string
|
||||
}
|
||||
|
||||
function IconBadges({
|
||||
completedText = "Completed",
|
||||
failedText = "Failed"
|
||||
}: IconBadgeProps) {
|
||||
return (
|
||||
<>
|
||||
<Badge>
|
||||
<Check weight="bold" className="mr-1" />
|
||||
{completedText}
|
||||
</Badge>
|
||||
<Badge variant="destructive">
|
||||
<X weight="bold" className="mr-1" />
|
||||
{failedText}
|
||||
</Badge>
|
||||
</>
|
||||
)
|
||||
}`,
|
||||
inputBasic: `interface InputProps {
|
||||
placeholder?: string
|
||||
value?: string
|
||||
onChange?: (e: React.ChangeEvent<HTMLInputElement>) => void
|
||||
}
|
||||
|
||||
function BasicInput({ placeholder = "Default input", value, onChange }: InputProps) {
|
||||
return <Input placeholder={placeholder} value={value} onChange={onChange} />
|
||||
}`,
|
||||
inputWithIcon: `interface SearchInputProps {
|
||||
placeholder?: string
|
||||
value?: string
|
||||
onChange?: (e: React.ChangeEvent<HTMLInputElement>) => void
|
||||
}
|
||||
|
||||
function SearchInput({ placeholder = "Search...", value, onChange }: SearchInputProps) {
|
||||
return (
|
||||
<div className="relative">
|
||||
<MagnifyingGlass className="absolute left-3 top-1/2 -translate-y-1/2 h-5 w-5 text-muted-foreground" />
|
||||
<Input placeholder={placeholder} className="pl-10" value={value} onChange={onChange} />
|
||||
</div>
|
||||
)
|
||||
}`,
|
||||
inputTypes: `interface TypedInputsProps {
|
||||
textPlaceholder?: string
|
||||
emailPlaceholder?: string
|
||||
passwordPlaceholder?: string
|
||||
numberPlaceholder?: string
|
||||
onTextChange?: (e: React.ChangeEvent<HTMLInputElement>) => void
|
||||
onEmailChange?: (e: React.ChangeEvent<HTMLInputElement>) => void
|
||||
onPasswordChange?: (e: React.ChangeEvent<HTMLInputElement>) => void
|
||||
onNumberChange?: (e: React.ChangeEvent<HTMLInputElement>) => void
|
||||
}
|
||||
|
||||
function TypedInputs({
|
||||
textPlaceholder = "Text input",
|
||||
emailPlaceholder = "email@example.com",
|
||||
passwordPlaceholder = "Password",
|
||||
numberPlaceholder = "123",
|
||||
onTextChange,
|
||||
onEmailChange,
|
||||
onPasswordChange,
|
||||
onNumberChange
|
||||
}: TypedInputsProps) {
|
||||
return (
|
||||
<>
|
||||
<Input type="text" placeholder={textPlaceholder} onChange={onTextChange} />
|
||||
<Input type="email" placeholder={emailPlaceholder} onChange={onEmailChange} />
|
||||
<Input type="password" placeholder={passwordPlaceholder} onChange={onPasswordChange} />
|
||||
<Input type="number" placeholder={numberPlaceholder} onChange={onNumberChange} />
|
||||
</>
|
||||
)
|
||||
}`,
|
||||
}
|
||||
|
||||
8
src/lib/snippets/index.ts
Normal file
8
src/lib/snippets/index.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
/**
|
||||
* Component code snippets - Re-exports from focused modules
|
||||
*/
|
||||
|
||||
export { atomsCodeSnippets } from './atoms'
|
||||
export { moleculesCodeSnippets } from './molecules'
|
||||
export { organismsCodeSnippets } from './organisms'
|
||||
export { templatesCodeSnippets } from './templates'
|
||||
219
src/lib/snippets/molecules.ts
Normal file
219
src/lib/snippets/molecules.ts
Normal file
@@ -0,0 +1,219 @@
|
||||
/**
|
||||
* Molecules - Composite UI components code snippets
|
||||
*/
|
||||
|
||||
export const moleculesCodeSnippets = {
|
||||
formField: `interface FormFieldProps {
|
||||
label?: string
|
||||
placeholder?: string
|
||||
helperText?: string
|
||||
id?: string
|
||||
value?: string
|
||||
onChange?: (e: React.ChangeEvent<HTMLInputElement>) => void
|
||||
}
|
||||
|
||||
function EmailFormField({
|
||||
label = "Email Address",
|
||||
placeholder = "john@example.com",
|
||||
helperText = "We'll never share your email with anyone else.",
|
||||
id = "email",
|
||||
value,
|
||||
onChange
|
||||
}: FormFieldProps) {
|
||||
return (
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor={id}>{label}</Label>
|
||||
<div className="relative">
|
||||
<Envelope className="absolute left-3 top-1/2 -translate-y-1/2 h-5 w-5 text-muted-foreground" />
|
||||
<Input id={id} type="email" placeholder={placeholder} className="pl-10" value={value} onChange={onChange} />
|
||||
</div>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{helperText}
|
||||
</p>
|
||||
</div>
|
||||
)
|
||||
}`,
|
||||
searchBar: `interface SearchBarProps {
|
||||
placeholder?: string
|
||||
value?: string
|
||||
onChange?: (e: React.ChangeEvent<HTMLInputElement>) => void
|
||||
}
|
||||
|
||||
function SearchBar({ placeholder = "Search...", value, onChange }: SearchBarProps) {
|
||||
return (
|
||||
<div className="relative">
|
||||
<MagnifyingGlass className="absolute left-3 top-1/2 -translate-y-1/2 h-5 w-5 text-muted-foreground" />
|
||||
<Input placeholder={placeholder} className="pl-10" value={value} onChange={onChange} />
|
||||
</div>
|
||||
)
|
||||
}`,
|
||||
searchBarWithButton: `interface SearchBarWithButtonProps {
|
||||
placeholder?: string
|
||||
buttonText?: string
|
||||
value?: string
|
||||
onChange?: (e: React.ChangeEvent<HTMLInputElement>) => void
|
||||
onSearch?: () => void
|
||||
}
|
||||
|
||||
function SearchBarWithButton({
|
||||
placeholder = "Search...",
|
||||
buttonText = "Search",
|
||||
value,
|
||||
onChange,
|
||||
onSearch
|
||||
}: SearchBarWithButtonProps) {
|
||||
return (
|
||||
<div className="flex gap-2">
|
||||
<div className="relative flex-1">
|
||||
<MagnifyingGlass className="absolute left-3 top-1/2 -translate-y-1/2 h-5 w-5 text-muted-foreground" />
|
||||
<Input placeholder={placeholder} className="pl-10" value={value} onChange={onChange} />
|
||||
</div>
|
||||
<Button onClick={onSearch}>{buttonText}</Button>
|
||||
</div>
|
||||
)
|
||||
}`,
|
||||
userCard: `interface UserCardProps {
|
||||
name?: string
|
||||
username?: string
|
||||
bio?: string
|
||||
avatarUrl?: string
|
||||
avatarFallback?: string
|
||||
buttonText?: string
|
||||
onButtonClick?: () => void
|
||||
}
|
||||
|
||||
function UserCard({
|
||||
name = "Alex Morgan",
|
||||
username = "@alexmorgan",
|
||||
bio = "Product designer passionate about creating delightful user experiences.",
|
||||
avatarUrl = "https://i.pravatar.cc/150?img=1",
|
||||
avatarFallback = "AM",
|
||||
buttonText = "Follow",
|
||||
onButtonClick
|
||||
}: UserCardProps) {
|
||||
return (
|
||||
<Card className="p-6">
|
||||
<div className="flex items-start gap-4">
|
||||
<Avatar className="h-12 w-12">
|
||||
<AvatarImage src={avatarUrl} />
|
||||
<AvatarFallback>{avatarFallback}</AvatarFallback>
|
||||
</Avatar>
|
||||
<div className="flex-1 min-w-0">
|
||||
<h3 className="font-semibold text-lg">{name}</h3>
|
||||
<p className="text-sm text-muted-foreground">{username}</p>
|
||||
<p className="text-sm mt-2">
|
||||
{bio}
|
||||
</p>
|
||||
</div>
|
||||
<Button size="sm" variant="outline" onClick={onButtonClick}>
|
||||
{buttonText}
|
||||
</Button>
|
||||
</div>
|
||||
</Card>
|
||||
)
|
||||
}`,
|
||||
socialActions: `interface SocialActionsProps {
|
||||
likeText?: string
|
||||
commentText?: string
|
||||
shareText?: string
|
||||
onLike?: () => void
|
||||
onComment?: () => void
|
||||
onShare?: () => void
|
||||
}
|
||||
|
||||
function SocialActions({
|
||||
likeText = "Like",
|
||||
commentText = "Comment",
|
||||
shareText = "Share",
|
||||
onLike,
|
||||
onComment,
|
||||
onShare
|
||||
}: SocialActionsProps) {
|
||||
return (
|
||||
<div className="flex items-center gap-2">
|
||||
<Button variant="ghost" size="sm" onClick={onLike}>
|
||||
<Heart className="mr-2" />
|
||||
{likeText}
|
||||
</Button>
|
||||
<Button variant="ghost" size="sm" onClick={onComment}>
|
||||
<ChatCircle className="mr-2" />
|
||||
{commentText}
|
||||
</Button>
|
||||
<Button variant="ghost" size="sm" onClick={onShare}>
|
||||
<Share className="mr-2" />
|
||||
{shareText}
|
||||
</Button>
|
||||
</div>
|
||||
)
|
||||
}`,
|
||||
statusIndicator: `interface StatusIndicatorProps {
|
||||
statusText?: string
|
||||
badgeText?: string
|
||||
isActive?: boolean
|
||||
}
|
||||
|
||||
function StatusIndicator({
|
||||
statusText = "System Online",
|
||||
badgeText = "Active",
|
||||
isActive = true
|
||||
}: StatusIndicatorProps) {
|
||||
return (
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className={cn(
|
||||
"h-3 w-3 rounded-full bg-accent",
|
||||
isActive && "animate-pulse"
|
||||
)} />
|
||||
<span className="font-medium">{statusText}</span>
|
||||
</div>
|
||||
<Badge>{badgeText}</Badge>
|
||||
</div>
|
||||
)
|
||||
}`,
|
||||
contentCard: `interface ContentCardProps {
|
||||
title?: string
|
||||
description?: string
|
||||
date?: string
|
||||
readTime?: string
|
||||
tags?: string[]
|
||||
}
|
||||
|
||||
function ContentCard({
|
||||
title = "Building Scalable Design Systems",
|
||||
description = "Learn how to create and maintain design systems that grow with your team.",
|
||||
date = "Mar 15, 2024",
|
||||
readTime = "5 min read",
|
||||
tags = ["Design", "System"]
|
||||
}: ContentCardProps) {
|
||||
return (
|
||||
<Card className="p-6 hover:shadow-lg transition-shadow">
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-start justify-between gap-4">
|
||||
<div className="space-y-1">
|
||||
<h3 className="font-semibold text-lg line-clamp-2">
|
||||
{title}
|
||||
</h3>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{description}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-4 text-sm text-muted-foreground">
|
||||
<div className="flex items-center gap-1">
|
||||
<Calendar className="h-4 w-4" />
|
||||
<span>{date}</span>
|
||||
</div>
|
||||
<span>•</span>
|
||||
<span>{readTime}</span>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
{tags.map((tag) => (
|
||||
<Badge key={tag} variant="outline">{tag}</Badge>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
)
|
||||
}`,
|
||||
}
|
||||
|
||||
338
src/lib/snippets/organisms.ts
Normal file
338
src/lib/snippets/organisms.ts
Normal file
@@ -0,0 +1,338 @@
|
||||
/**
|
||||
* Organisms - Complex UI patterns code snippets
|
||||
*/
|
||||
|
||||
export const organismsCodeSnippets = {
|
||||
navigationBar: `interface NavigationBarProps {
|
||||
brandName?: string
|
||||
navItems?: Array<{ label: string; icon: React.ReactNode; onClick?: () => void }>
|
||||
avatarUrl?: string
|
||||
avatarFallback?: string
|
||||
onNotificationClick?: () => void
|
||||
onSettingsClick?: () => void
|
||||
}
|
||||
|
||||
function NavigationBar({
|
||||
brandName = "BrandName",
|
||||
navItems = [
|
||||
{ label: "Home", icon: <House className="mr-2" /> },
|
||||
{ label: "Analytics", icon: <ChartBar className="mr-2" /> },
|
||||
{ label: "Projects", icon: <Folder className="mr-2" /> }
|
||||
],
|
||||
avatarUrl = "https://i.pravatar.cc/150?img=3",
|
||||
avatarFallback = "U",
|
||||
onNotificationClick,
|
||||
onSettingsClick
|
||||
}: NavigationBarProps) {
|
||||
return (
|
||||
<div className="border-b border-border bg-card p-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-6">
|
||||
<h3 className="text-xl font-bold">{brandName}</h3>
|
||||
<nav className="hidden md:flex items-center gap-1">
|
||||
{navItems.map((item) => (
|
||||
<Button key={item.label} variant="ghost" size="sm" onClick={item.onClick}>
|
||||
{item.icon}
|
||||
{item.label}
|
||||
</Button>
|
||||
))}
|
||||
</nav>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Button variant="ghost" size="icon" onClick={onNotificationClick}>
|
||||
<Bell />
|
||||
</Button>
|
||||
<Button variant="ghost" size="icon" onClick={onSettingsClick}>
|
||||
<Gear />
|
||||
</Button>
|
||||
<Avatar className="h-8 w-8">
|
||||
<AvatarImage src={avatarUrl} />
|
||||
<AvatarFallback>{avatarFallback}</AvatarFallback>
|
||||
</Avatar>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}`,
|
||||
dataTable: `interface Transaction {
|
||||
id: string
|
||||
status: string
|
||||
statusVariant?: "default" | "secondary" | "destructive" | "outline"
|
||||
description: string
|
||||
date: string
|
||||
amount: string
|
||||
isNegative?: boolean
|
||||
}
|
||||
|
||||
interface DataTableProps {
|
||||
title?: string
|
||||
exportButtonText?: string
|
||||
transactions?: Transaction[]
|
||||
onExport?: () => void
|
||||
}
|
||||
|
||||
function DataTable({
|
||||
title = "Recent Transactions",
|
||||
exportButtonText = "Export",
|
||||
transactions = [
|
||||
{ id: "1", status: "Completed", description: "Payment received", date: "Mar 15, 2024", amount: "$250.00" }
|
||||
],
|
||||
onExport
|
||||
}: DataTableProps) {
|
||||
return (
|
||||
<Card>
|
||||
<div className="p-4 border-b border-border">
|
||||
<div className="flex items-center justify-between">
|
||||
<h3 className="font-semibold text-lg">{title}</h3>
|
||||
<Button variant="outline" size="sm" onClick={onExport}>
|
||||
{exportButtonText}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>Status</TableHead>
|
||||
<TableHead>Transaction</TableHead>
|
||||
<TableHead>Date</TableHead>
|
||||
<TableHead className="text-right">Amount</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{transactions.map((transaction) => (
|
||||
<TableRow key={transaction.id}>
|
||||
<TableCell>
|
||||
<Badge variant={transaction.statusVariant}>{transaction.status}</Badge>
|
||||
</TableCell>
|
||||
<TableCell className="font-medium">{transaction.description}</TableCell>
|
||||
<TableCell>{transaction.date}</TableCell>
|
||||
<TableCell className={cn(
|
||||
"text-right",
|
||||
transaction.isNegative && "text-destructive"
|
||||
)}>{transaction.amount}</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</Card>
|
||||
)
|
||||
}`,
|
||||
form: `interface CreateAccountFormProps {
|
||||
title?: string
|
||||
description?: string
|
||||
firstNameLabel?: string
|
||||
lastNameLabel?: string
|
||||
emailLabel?: string
|
||||
firstNamePlaceholder?: string
|
||||
lastNamePlaceholder?: string
|
||||
emailPlaceholder?: string
|
||||
cancelButtonText?: string
|
||||
submitButtonText?: string
|
||||
onCancel?: () => void
|
||||
onSubmit?: (data: { firstName: string; lastName: string; email: string }) => void
|
||||
}
|
||||
|
||||
function CreateAccountForm({
|
||||
title = "Create Account",
|
||||
description = "Fill in your details to get started",
|
||||
firstNameLabel = "First Name",
|
||||
lastNameLabel = "Last Name",
|
||||
emailLabel = "Email",
|
||||
firstNamePlaceholder = "John",
|
||||
lastNamePlaceholder = "Doe",
|
||||
emailPlaceholder = "john@example.com",
|
||||
cancelButtonText = "Cancel",
|
||||
submitButtonText = "Create Account",
|
||||
onCancel,
|
||||
onSubmit
|
||||
}: CreateAccountFormProps) {
|
||||
const [firstName, setFirstName] = useState("")
|
||||
const [lastName, setLastName] = useState("")
|
||||
const [email, setEmail] = useState("")
|
||||
|
||||
const handleSubmit = (e: React.FormEvent) => {
|
||||
e.preventDefault()
|
||||
onSubmit?.({ firstName, lastName, email })
|
||||
}
|
||||
|
||||
return (
|
||||
<Card className="p-6">
|
||||
<form className="space-y-6" onSubmit={handleSubmit}>
|
||||
<div>
|
||||
<h3 className="text-xl font-semibold mb-4">{title}</h3>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{description}
|
||||
</p>
|
||||
</div>
|
||||
<Separator />
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="firstName">{firstNameLabel}</Label>
|
||||
<Input
|
||||
id="firstName"
|
||||
placeholder={firstNamePlaceholder}
|
||||
value={firstName}
|
||||
onChange={(e) => setFirstName(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="lastName">{lastNameLabel}</Label>
|
||||
<Input
|
||||
id="lastName"
|
||||
placeholder={lastNamePlaceholder}
|
||||
value={lastName}
|
||||
onChange={(e) => setLastName(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="email">{emailLabel}</Label>
|
||||
<div className="relative">
|
||||
<Envelope className="absolute left-3 top-1/2 -translate-y-1/2 h-5 w-5 text-muted-foreground" />
|
||||
<Input
|
||||
id="email"
|
||||
type="email"
|
||||
placeholder={emailPlaceholder}
|
||||
className="pl-10"
|
||||
value={email}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<Separator />
|
||||
<div className="flex items-center justify-between gap-4">
|
||||
<Button variant="outline" type="button" onClick={onCancel}>
|
||||
{cancelButtonText}
|
||||
</Button>
|
||||
<Button type="submit">
|
||||
{submitButtonText}
|
||||
<ArrowRight className="ml-2" />
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</Card>
|
||||
)
|
||||
}`,
|
||||
taskList: `interface Task {
|
||||
id: string
|
||||
title: string
|
||||
description: string
|
||||
status: string
|
||||
statusColor?: "accent" | "destructive"
|
||||
badgeText: string
|
||||
badgeVariant?: "default" | "secondary" | "destructive" | "outline"
|
||||
icon: React.ReactNode
|
||||
}
|
||||
|
||||
interface TaskListProps {
|
||||
title?: string
|
||||
addButtonText?: string
|
||||
tasks?: Task[]
|
||||
onAddTask?: () => void
|
||||
}
|
||||
|
||||
function TaskList({
|
||||
title = "Project Tasks",
|
||||
addButtonText = "Add Task",
|
||||
tasks = [
|
||||
{
|
||||
id: "1",
|
||||
title: "Design system documentation",
|
||||
description: "Complete the component library documentation",
|
||||
status: "Completed",
|
||||
statusColor: "accent" as const,
|
||||
badgeText: "Design",
|
||||
badgeVariant: "secondary" as const,
|
||||
icon: <CheckCircle weight="fill" className="h-6 w-6 text-accent mt-0.5" />
|
||||
}
|
||||
],
|
||||
onAddTask
|
||||
}: TaskListProps) {
|
||||
return (
|
||||
<Card>
|
||||
<div className="p-4 border-b border-border">
|
||||
<div className="flex items-center justify-between">
|
||||
<h3 className="font-semibold text-lg">{title}</h3>
|
||||
<Button size="sm" onClick={onAddTask}>
|
||||
<Plus className="mr-2" />
|
||||
{addButtonText}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="divide-y divide-border">
|
||||
{tasks.map((task) => (
|
||||
<div key={task.id} className="p-4 hover:bg-muted/50 transition-colors">
|
||||
<div className="flex items-start gap-4">
|
||||
{task.icon}
|
||||
<div className="flex-1 min-w-0">
|
||||
<h4 className="font-medium">{task.title}</h4>
|
||||
<p className="text-sm text-muted-foreground mt-1">
|
||||
{task.description}
|
||||
</p>
|
||||
<div className="flex items-center gap-4 mt-3">
|
||||
<Badge variant={task.badgeVariant}>{task.badgeText}</Badge>
|
||||
<span className="text-xs text-muted-foreground">{task.status}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</Card>
|
||||
)
|
||||
}`,
|
||||
sidebarNavigation: `interface SidebarNavItem {
|
||||
label: string
|
||||
icon: React.ReactNode
|
||||
variant?: "default" | "ghost"
|
||||
onClick?: () => void
|
||||
}
|
||||
|
||||
interface SidebarNavigationProps {
|
||||
title?: string
|
||||
navItems?: SidebarNavItem[]
|
||||
contentText?: string
|
||||
}
|
||||
|
||||
function SidebarNavigation({
|
||||
title = "Dashboard",
|
||||
navItems = [
|
||||
{ label: "Home", icon: <House className="mr-2" />, variant: "ghost" as const },
|
||||
{ label: "Analytics", icon: <ChartBar className="mr-2" />, variant: "default" as const },
|
||||
{ label: "Projects", icon: <Folder className="mr-2" />, variant: "ghost" as const }
|
||||
],
|
||||
contentText = "Main content area"
|
||||
}: SidebarNavigationProps) {
|
||||
return (
|
||||
<div className="flex">
|
||||
<aside className="w-64 border-r border-border bg-card/50 p-4">
|
||||
<div className="space-y-6">
|
||||
<div className="flex items-center gap-2 px-2">
|
||||
<div className="h-8 w-8 rounded-lg bg-accent" />
|
||||
<span className="font-bold">{title}</span>
|
||||
</div>
|
||||
<nav className="space-y-1">
|
||||
{navItems.map((item) => (
|
||||
<Button
|
||||
key={item.label}
|
||||
variant={item.variant || "ghost"}
|
||||
className="w-full justify-start"
|
||||
onClick={item.onClick}
|
||||
>
|
||||
{item.icon}
|
||||
{item.label}
|
||||
</Button>
|
||||
))}
|
||||
</nav>
|
||||
</div>
|
||||
</aside>
|
||||
<div className="flex-1 p-6">
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{contentText}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}`,
|
||||
}
|
||||
|
||||
256
src/lib/snippets/templates.ts
Normal file
256
src/lib/snippets/templates.ts
Normal file
@@ -0,0 +1,256 @@
|
||||
/**
|
||||
* Templates - Complete page layouts code snippets
|
||||
*/
|
||||
|
||||
export const templatesCodeSnippets = {
|
||||
dashboardLayout: `interface StatCard {
|
||||
label: string
|
||||
value: string
|
||||
trend?: string
|
||||
}
|
||||
|
||||
interface DashboardLayoutProps {
|
||||
title?: string
|
||||
navItems?: Array<{ label: string; icon: React.ReactNode; variant?: "default" | "ghost" }>
|
||||
avatarUrl?: string
|
||||
avatarFallback?: string
|
||||
stats?: StatCard[]
|
||||
onNotificationClick?: () => void
|
||||
}
|
||||
|
||||
function DashboardLayout({
|
||||
title = "Dashboard",
|
||||
navItems = [
|
||||
{ label: "Overview", icon: <House className="mr-2" />, variant: "default" as const },
|
||||
{ label: "Analytics", icon: <ChartBar className="mr-2" />, variant: "ghost" as const }
|
||||
],
|
||||
avatarUrl,
|
||||
avatarFallback = "U",
|
||||
stats = [
|
||||
{ label: "Total Revenue", value: "$45,231", trend: "+20.1% from last month" }
|
||||
],
|
||||
onNotificationClick
|
||||
}: DashboardLayoutProps) {
|
||||
return (
|
||||
<div className="min-h-screen bg-background">
|
||||
<div className="border-b border-border bg-card p-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<h3 className="text-xl font-bold">{title}</h3>
|
||||
<div className="flex items-center gap-2">
|
||||
<Button variant="ghost" size="icon" onClick={onNotificationClick}>
|
||||
<Bell />
|
||||
</Button>
|
||||
<Avatar className="h-8 w-8">
|
||||
{avatarUrl && <AvatarImage src={avatarUrl} />}
|
||||
<AvatarFallback>{avatarFallback}</AvatarFallback>
|
||||
</Avatar>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex">
|
||||
<aside className="w-64 border-r border-border bg-card/30 p-4">
|
||||
<nav className="space-y-1">
|
||||
{navItems.map((item) => (
|
||||
<Button
|
||||
key={item.label}
|
||||
variant={item.variant || "ghost"}
|
||||
className="w-full justify-start"
|
||||
>
|
||||
{item.icon}
|
||||
{item.label}
|
||||
</Button>
|
||||
))}
|
||||
</nav>
|
||||
</aside>
|
||||
<main className="flex-1 p-6">
|
||||
<h1 className="text-3xl font-bold">Overview</h1>
|
||||
<div className="grid grid-cols-3 gap-6 mt-6">
|
||||
{stats.map((stat) => (
|
||||
<Card key={stat.label} className="p-6">
|
||||
<p className="text-sm text-muted-foreground">{stat.label}</p>
|
||||
<p className="text-3xl font-bold mt-2">{stat.value}</p>
|
||||
{stat.trend && (
|
||||
<p className="text-sm text-accent mt-2">{stat.trend}</p>
|
||||
)}
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}`,
|
||||
landingPage: `interface LandingPageProps {
|
||||
brandName?: string
|
||||
navItems?: string[]
|
||||
badge?: string
|
||||
headline?: string
|
||||
description?: string
|
||||
primaryCta?: string
|
||||
secondaryCta?: string
|
||||
onPrimaryCta?: () => void
|
||||
onSecondaryCta?: () => void
|
||||
}
|
||||
|
||||
function LandingPage({
|
||||
brandName = "ProductName",
|
||||
navItems = ["Features", "Sign Up"],
|
||||
badge = "New Release",
|
||||
headline = "Build Amazing Products Faster",
|
||||
description = "The complete toolkit for modern product development.",
|
||||
primaryCta = "Get Started",
|
||||
secondaryCta,
|
||||
onPrimaryCta,
|
||||
onSecondaryCta
|
||||
}: LandingPageProps) {
|
||||
return (
|
||||
<div className="min-h-screen">
|
||||
<div className="border-b border-border bg-card p-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="h-8 w-8 rounded-lg bg-accent" />
|
||||
<h3 className="text-xl font-bold">{brandName}</h3>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
{navItems.map((item) => (
|
||||
<Button key={item} variant="ghost" size="sm">{item}</Button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="p-12 text-center bg-gradient-to-br from-primary/20 to-accent/20">
|
||||
<Badge className="mb-4">{badge}</Badge>
|
||||
<h1 className="text-5xl font-bold mb-6">
|
||||
{headline}
|
||||
</h1>
|
||||
<p className="text-xl text-muted-foreground mb-8 max-w-2xl mx-auto">
|
||||
{description}
|
||||
</p>
|
||||
<div className="flex items-center justify-center gap-4">
|
||||
<Button size="lg" onClick={onPrimaryCta}>
|
||||
{primaryCta}
|
||||
<ArrowRight className="ml-2" />
|
||||
</Button>
|
||||
{secondaryCta && (
|
||||
<Button size="lg" variant="outline" onClick={onSecondaryCta}>
|
||||
{secondaryCta}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}`,
|
||||
ecommercePage: `interface EcommercePageProps {
|
||||
storeName?: string
|
||||
productBadge?: string
|
||||
productName?: string
|
||||
productPrice?: string
|
||||
originalPrice?: string
|
||||
onAddToCart?: () => void
|
||||
}
|
||||
|
||||
function EcommercePage({
|
||||
storeName = "Store",
|
||||
productBadge = "New Arrival",
|
||||
productName = "Premium Product",
|
||||
productPrice = "$299.00",
|
||||
originalPrice,
|
||||
onAddToCart
|
||||
}: EcommercePageProps) {
|
||||
return (
|
||||
<div className="min-h-screen">
|
||||
<div className="border-b border-border bg-card p-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<h3 className="text-xl font-bold">{storeName}</h3>
|
||||
<Button variant="ghost" size="icon">
|
||||
<ShoppingCart />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="p-8">
|
||||
<div className="grid grid-cols-2 gap-12">
|
||||
<div className="aspect-square rounded-lg bg-gradient-to-br from-primary to-accent" />
|
||||
<div className="space-y-6">
|
||||
<Badge>{productBadge}</Badge>
|
||||
<h1 className="text-4xl font-bold">{productName}</h1>
|
||||
<div className="flex items-baseline gap-3">
|
||||
<span className="text-3xl font-bold">{productPrice}</span>
|
||||
{originalPrice && (
|
||||
<span className="text-lg text-muted-foreground line-through">
|
||||
{originalPrice}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<Button size="lg" className="w-full" onClick={onAddToCart}>
|
||||
<ShoppingCart className="mr-2" />
|
||||
Add to Cart
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}`,
|
||||
blogArticle: `interface BlogArticleProps {
|
||||
blogName?: string
|
||||
tags?: string[]
|
||||
title?: string
|
||||
authorName?: string
|
||||
authorAvatar?: string
|
||||
authorFallback?: string
|
||||
date?: string
|
||||
readTime?: string
|
||||
excerpt?: string
|
||||
}
|
||||
|
||||
function BlogArticle({
|
||||
blogName = "Blog",
|
||||
tags = ["Design", "Tutorial"],
|
||||
title = "Building a Comprehensive Component Library",
|
||||
authorName = "Alex Writer",
|
||||
authorAvatar,
|
||||
authorFallback = "AW",
|
||||
date = "March 15, 2024",
|
||||
readTime = "10 min read",
|
||||
excerpt = "Design systems have become an essential part of modern product development."
|
||||
}: BlogArticleProps) {
|
||||
return (
|
||||
<div className="min-h-screen">
|
||||
<div className="border-b border-border bg-card p-4">
|
||||
<h3 className="text-xl font-bold">{blogName}</h3>
|
||||
</div>
|
||||
<div className="p-8">
|
||||
<div className="max-w-4xl mx-auto space-y-6">
|
||||
<div className="flex gap-2">
|
||||
{tags.map((tag, idx) => (
|
||||
<Badge key={tag} variant={idx === 0 ? "default" : "secondary"}>
|
||||
{tag}
|
||||
</Badge>
|
||||
))}
|
||||
</div>
|
||||
<h1 className="text-5xl font-bold">
|
||||
{title}
|
||||
</h1>
|
||||
<div className="flex items-center gap-4">
|
||||
<Avatar className="h-12 w-12">
|
||||
{authorAvatar && <AvatarImage src={authorAvatar} />}
|
||||
<AvatarFallback>{authorFallback}</AvatarFallback>
|
||||
</Avatar>
|
||||
<div>
|
||||
<p className="font-medium">{authorName}</p>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{date} · {readTime}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="aspect-video rounded-lg bg-gradient-to-br from-primary to-accent" />
|
||||
<p className="text-lg text-muted-foreground leading-relaxed">
|
||||
{excerpt}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}`,
|
||||
}
|
||||
Reference in New Issue
Block a user