From 857c900714393cf6665a1b46daa2b9ee2cf1eca6 Mon Sep 17 00:00:00 2001 From: johndoe6345789 Date: Sat, 17 Jan 2026 14:32:11 +0000 Subject: [PATCH] Generated by Spark: implement program --- PRD.md | 131 +++++++++++++++++ index.html | 3 +- spark.meta.json | 5 +- src/App.tsx | 233 ++++++++++++++++++++++++++++++- src/components/EmptyState.tsx | 24 ++++ src/components/SnippetCard.tsx | 120 ++++++++++++++++ src/components/SnippetDialog.tsx | 165 ++++++++++++++++++++++ src/index.css | 8 +- src/lib/types.ts | 51 +++++++ src/main.css | 53 +++---- src/main.tsx | 2 + 11 files changed, 764 insertions(+), 31 deletions(-) create mode 100644 PRD.md create mode 100644 src/components/EmptyState.tsx create mode 100644 src/components/SnippetCard.tsx create mode 100644 src/components/SnippetDialog.tsx create mode 100644 src/lib/types.ts diff --git a/PRD.md b/PRD.md new file mode 100644 index 0000000..3a463cf --- /dev/null +++ b/PRD.md @@ -0,0 +1,131 @@ +# Planning Guide + +A developer-focused code snippet manager that allows users to save, organize, search, and quickly access reusable code snippets across multiple programming languages. + +**Experience Qualities**: +1. **Efficient** - Users should be able to save and retrieve snippets in seconds with minimal friction +2. **Professional** - The interface should feel polished and trustworthy, suitable for daily developer workflow +3. **Intuitive** - Navigation and organization should be self-evident without requiring documentation + +**Complexity Level**: Light Application (multiple features with basic state) +This is a CRUD application with search, filtering, and organization features but doesn't require complex routing or advanced state management beyond persisted storage. + +## Essential Features + +### Create Snippet +- **Functionality**: Users can create a new code snippet with title, description, language selection, and code content +- **Purpose**: Core value proposition - storing reusable code for later retrieval +- **Trigger**: Click "New Snippet" button or keyboard shortcut +- **Progression**: Click New Snippet → Fill in title field → Select language from dropdown → Paste/type code → Add optional description → Click Save +- **Success criteria**: Snippet appears in the list immediately, persists across page refreshes, and is searchable + +### View & Organize Snippets +- **Functionality**: Display all snippets in a filterable list with preview cards showing title, language, and truncated code +- **Purpose**: Quick scanning and navigation through saved snippets +- **Trigger**: Default view on app load +- **Progression**: View list → Scan titles and languages → Click card to expand/view full details +- **Success criteria**: All snippets visible, sorted by recent first, with clear visual hierarchy + +### Search & Filter +- **Functionality**: Real-time search across snippet titles, descriptions, and code content; filter by programming language +- **Purpose**: Quick retrieval when snippet library grows large +- **Trigger**: Type in search bar or select language filter +- **Progression**: Type search query → Results filter in real-time → Clear search to return to full list +- **Success criteria**: Results appear instantly (<100ms), search is case-insensitive, highlights matched terms + +### Edit & Delete +- **Functionality**: Modify existing snippets or remove them entirely +- **Purpose**: Keep snippet library current and relevant +- **Trigger**: Click edit icon on snippet card or click delete with confirmation +- **Progression**: Click Edit → Modify fields in modal → Save changes → See updated snippet in list +- **Success criteria**: Changes persist, delete requires confirmation, no accidental data loss + +### Copy to Clipboard +- **Functionality**: One-click copy of code content to clipboard +- **Purpose**: Primary use case - quickly use snippets in other projects +- **Trigger**: Click copy icon on snippet card +- **Progression**: Click copy button → Visual feedback (toast notification) → Paste code elsewhere +- **Success criteria**: Code copies exactly as stored, toast confirms action, works across browsers + +## Edge Case Handling +- **Empty State**: Show welcoming illustration and "Create your first snippet" CTA when no snippets exist +- **Long Code Blocks**: Truncate preview with "Show more" expansion, scroll within card for full view +- **Duplicate Titles**: Allow duplicates but show language badge to differentiate +- **Invalid Input**: Require title and code content minimum, show inline validation errors +- **Search No Results**: Display "No snippets found" message with suggestion to adjust filters +- **Network/Storage Errors**: Graceful error messages with retry options (though KV storage is local) + +## Design Direction +The design should evoke a premium developer tool - clean, focused, and sophisticated with subtle tech-forward aesthetics. Think VS Code meets Notion: professional minimalism with purposeful color accents and smooth micro-interactions that make frequent use satisfying. + +## Color Selection +A developer-focused dark-leaning palette with vibrant accent colors for actions and syntax language tags. + +- **Primary Color**: Deep indigo `oklch(0.35 0.15 265)` - Communicates technical sophistication and trust, used for primary actions and headers +- **Secondary Colors**: Charcoal gray `oklch(0.25 0.01 265)` for cards/surfaces; Soft gray `oklch(0.65 0.01 265)` for secondary text +- **Accent Color**: Electric cyan `oklch(0.75 0.15 195)` - Attention-grabbing highlight for CTAs, copy actions, and success states +- **Foreground/Background Pairings**: + - Background (Off-black #0F0F14 / oklch(0.08 0.01 265)): Light gray text (oklch(0.95 0.01 265) #F0F0F2) - Ratio 11.2:1 ✓ + - Card surface (oklch(0.15 0.01 265)): White text (oklch(0.98 0 0)) - Ratio 13.5:1 ✓ + - Primary (Deep indigo oklch(0.35 0.15 265)): White text (oklch(0.98 0 0)) - Ratio 7.8:1 ✓ + - Accent (Cyan oklch(0.75 0.15 195)): Deep gray text (oklch(0.15 0.01 265)) - Ratio 8.5:1 ✓ + +## Font Selection +Typography should balance technical precision with readability - a clean geometric sans for UI elements and a monospace font for code display that developers recognize and trust. + +- **Typographic Hierarchy**: + - H1 (App Title): Space Grotesk Bold/32px/tight letter spacing (-0.02em) + - H2 (Section Headers): Space Grotesk SemiBold/20px/normal spacing + - Body (UI Text): Space Grotesk Regular/15px/1.5 line height + - Code Display: JetBrains Mono Regular/14px/1.6 line height + - Labels & Captions: Space Grotesk Medium/13px/uppercase with increased tracking (0.05em) + +## Animations +Animations should feel responsive and technical - quick, purposeful movements that provide feedback without delay. Focus on micro-interactions: button states that respond instantly, smooth card expansions when viewing details, and satisfying confirmation animations when copying code (scale pulse + checkmark). Keep transitions under 200ms for interactions, 300ms max for layout changes. Use ease-out curves for most interactions to feel snappy. + +## Component Selection +- **Components**: + - Dialog for create/edit snippet forms with full-screen modal feel on mobile + - Card for snippet list items with hover elevation and click interaction + - Input for search bar with clear button and icon + - Button for all actions (primary solid style for Save/Create, ghost style for secondary actions) + - Select for language dropdown with custom styling to match theme + - Textarea for code input with monospace font + - Badge for language tags with color coding + - ScrollArea for long code blocks within cards + - Toast (Sonner) for copy confirmations and success messages + - AlertDialog for delete confirmations + +- **Customizations**: + - Custom syntax language badges with predefined color mapping (JavaScript=yellow, Python=blue, etc.) + - Floating action button for "New Snippet" with plus icon, fixed bottom-right on mobile + - Custom empty state component with illustration or icon and encouraging copy + +- **States**: + - Buttons: Default with subtle gradient, hover with brightness increase and shadow, active with slight scale-down (0.98), disabled with 50% opacity + - Cards: Default flat, hover with shadow-lg and border glow, selected/expanded with accent border + - Inputs: Default with muted border, focus with accent ring and border color shift, error with red ring + +- **Icon Selection**: + - Plus (create new snippet) + - Copy (clipboard action) + - Pencil (edit action) + - Trash (delete action) + - MagnifyingGlass (search) + - Code (app logo/branding) + - Check (confirmation feedback) + +- **Spacing**: + - Container padding: p-6 (desktop) / p-4 (mobile) + - Card gaps: gap-4 for grid layout + - Form fields: space-y-4 for vertical stacking + - Section margins: mb-8 for major sections + - Inline elements: gap-2 for icon+text combinations + +- **Mobile**: + - Single column card layout with full-width cards + - Dialog becomes full-screen sheet on mobile + - Search bar remains sticky at top + - Floating action button (FAB) for create action instead of header button + - Touch-friendly button sizes (min 44px tap targets) + - Bottom sheet for language filter on mobile vs. dropdown on desktop diff --git a/index.html b/index.html index f62002d..fe38ae7 100644 --- a/index.html +++ b/index.html @@ -4,9 +4,10 @@ - + SnippetVault - Code Snippet Manager + diff --git a/spark.meta.json b/spark.meta.json index 706a311..a277bc5 100644 --- a/spark.meta.json +++ b/spark.meta.json @@ -1,3 +1,4 @@ { - "templateVersion": 1 -} + "templateVersion": 1, + "dbType": "kv" +} \ No newline at end of file diff --git a/src/App.tsx b/src/App.tsx index 98ef973..386ad5a 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,5 +1,236 @@ +import { useState, useMemo } from 'react' +import { useKV } from '@github/spark/hooks' +import { Button } from '@/components/ui/button' +import { Input } from '@/components/ui/input' +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from '@/components/ui/select' +import { + AlertDialog, + AlertDialogAction, + AlertDialogCancel, + AlertDialogContent, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogTitle, +} from '@/components/ui/alert-dialog' +import { Code, Plus, MagnifyingGlass, Funnel } from '@phosphor-icons/react' +import { toast } from 'sonner' +import { SnippetCard } from '@/components/SnippetCard' +import { SnippetDialog } from '@/components/SnippetDialog' +import { EmptyState } from '@/components/EmptyState' +import { Snippet, LANGUAGES } from '@/lib/types' + function App() { - return
+ const [snippets, setSnippets] = useKV('snippets', []) + const [dialogOpen, setDialogOpen] = useState(false) + const [editingSnippet, setEditingSnippet] = useState(null) + const [deleteId, setDeleteId] = useState(null) + const [searchQuery, setSearchQuery] = useState('') + const [filterLanguage, setFilterLanguage] = useState('all') + + const filteredSnippets = useMemo(() => { + let filtered = snippets || [] + + if (searchQuery.trim()) { + const query = searchQuery.toLowerCase() + filtered = filtered.filter( + (snippet) => + snippet.title.toLowerCase().includes(query) || + snippet.description.toLowerCase().includes(query) || + snippet.code.toLowerCase().includes(query) || + snippet.language.toLowerCase().includes(query) + ) + } + + if (filterLanguage !== 'all') { + filtered = filtered.filter((snippet) => snippet.language === filterLanguage) + } + + return filtered.sort((a, b) => b.updatedAt - a.updatedAt) + }, [snippets, searchQuery, filterLanguage]) + + const handleSave = (snippetData: Omit) => { + if (editingSnippet) { + setSnippets((current) => + (current || []).map((s) => + s.id === editingSnippet.id + ? { ...s, ...snippetData, updatedAt: Date.now() } + : s + ) + ) + toast.success('Snippet updated successfully') + } else { + const newSnippet: Snippet = { + ...snippetData, + id: Date.now().toString(), + createdAt: Date.now(), + updatedAt: Date.now(), + } + setSnippets((current) => [newSnippet, ...(current || [])]) + toast.success('Snippet created successfully') + } + setEditingSnippet(null) + } + + const handleEdit = (snippet: Snippet) => { + setEditingSnippet(snippet) + setDialogOpen(true) + } + + const handleDelete = (id: string) => { + setDeleteId(id) + } + + const confirmDelete = () => { + if (deleteId) { + setSnippets((current) => (current || []).filter((s) => s.id !== deleteId)) + toast.success('Snippet deleted') + setDeleteId(null) + } + } + + const handleCopy = (code: string) => { + navigator.clipboard.writeText(code) + toast.success('Code copied to clipboard') + } + + const handleNewSnippet = () => { + setEditingSnippet(null) + setDialogOpen(true) + } + + return ( +
+
+
+
+
+
+ +
+
+

+ SnippetVault +

+

+ Your personal code snippet library +

+
+
+ +
+ +
+
+ + setSearchQuery(e.target.value)} + className="pl-10" + /> +
+
+
+ + +
+
+
+
+
+ +
+ {!snippets || snippets.length === 0 ? ( + + ) : filteredSnippets.length === 0 ? ( +
+

+ No snippets match your search. +

+ +
+ ) : ( +
+ {filteredSnippets.map((snippet) => ( + + ))} +
+ )} +
+ + + + { + setDialogOpen(open) + if (!open) setEditingSnippet(null) + }} + onSave={handleSave} + editingSnippet={editingSnippet} + /> + + !open && setDeleteId(null)}> + + + Delete snippet? + + This action cannot be undone. This will permanently delete this code snippet. + + + + Cancel + + Delete + + + + +
+ ) } export default App \ No newline at end of file diff --git a/src/components/EmptyState.tsx b/src/components/EmptyState.tsx new file mode 100644 index 0000000..ecd9107 --- /dev/null +++ b/src/components/EmptyState.tsx @@ -0,0 +1,24 @@ +import { Code } from '@phosphor-icons/react' +import { Button } from '@/components/ui/button' + +interface EmptyStateProps { + onCreateClick: () => void +} + +export function EmptyState({ onCreateClick }: EmptyStateProps) { + return ( +
+
+ +
+

No snippets yet

+

+ Start building your code snippet library. Save reusable code for quick access anytime. +

+ +
+ ) +} diff --git a/src/components/SnippetCard.tsx b/src/components/SnippetCard.tsx new file mode 100644 index 0000000..afc853a --- /dev/null +++ b/src/components/SnippetCard.tsx @@ -0,0 +1,120 @@ +import { useState } from 'react' +import { Card } from '@/components/ui/card' +import { Button } from '@/components/ui/button' +import { Badge } from '@/components/ui/badge' +import { Copy, Pencil, Trash, Check } from '@phosphor-icons/react' +import { Snippet, LANGUAGE_COLORS } from '@/lib/types' +import { cn } from '@/lib/utils' +import { ScrollArea } from '@/components/ui/scroll-area' + +interface SnippetCardProps { + snippet: Snippet + onEdit: (snippet: Snippet) => void + onDelete: (id: string) => void + onCopy: (code: string) => void +} + +export function SnippetCard({ snippet, onEdit, onDelete, onCopy }: SnippetCardProps) { + const [isExpanded, setIsExpanded] = useState(false) + const [isCopied, setIsCopied] = useState(false) + + const handleCopy = () => { + onCopy(snippet.code) + setIsCopied(true) + setTimeout(() => setIsCopied(false), 2000) + } + + const truncatedCode = snippet.code.length > 200 + ? snippet.code.slice(0, 200) + '...' + : snippet.code + + return ( + setIsExpanded(!isExpanded)} + > +
+
+
+

+ {snippet.title} +

+ {snippet.description && ( +

+ {snippet.description} +

+ )} +
+ + {snippet.language} + +
+ +
+ {isExpanded ? ( + +
+                {snippet.code}
+              
+
+ ) : ( +
+
+                {truncatedCode}
+              
+ {snippet.code.length > 200 && ( +
+ )} +
+ )} +
+ +
e.stopPropagation()}> + + {new Date(snippet.updatedAt).toLocaleDateString()} + +
+ + + +
+
+
+ + ) +} diff --git a/src/components/SnippetDialog.tsx b/src/components/SnippetDialog.tsx new file mode 100644 index 0000000..e3e86a6 --- /dev/null +++ b/src/components/SnippetDialog.tsx @@ -0,0 +1,165 @@ +import { useEffect, useState } from 'react' +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, +} from '@/components/ui/dialog' +import { Button } from '@/components/ui/button' +import { Input } from '@/components/ui/input' +import { Label } from '@/components/ui/label' +import { Textarea } from '@/components/ui/textarea' +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from '@/components/ui/select' +import { Snippet, LANGUAGES } from '@/lib/types' + +interface SnippetDialogProps { + open: boolean + onOpenChange: (open: boolean) => void + onSave: (snippet: Omit) => void + editingSnippet?: Snippet | null +} + +export function SnippetDialog({ open, onOpenChange, onSave, editingSnippet }: SnippetDialogProps) { + const [title, setTitle] = useState('') + const [description, setDescription] = useState('') + const [language, setLanguage] = useState('JavaScript') + const [code, setCode] = useState('') + const [errors, setErrors] = useState<{ title?: string; code?: string }>({}) + + useEffect(() => { + if (editingSnippet) { + setTitle(editingSnippet.title) + setDescription(editingSnippet.description) + setLanguage(editingSnippet.language) + setCode(editingSnippet.code) + } else { + setTitle('') + setDescription('') + setLanguage('JavaScript') + setCode('') + } + setErrors({}) + }, [editingSnippet, open]) + + const handleSave = () => { + const newErrors: { title?: string; code?: string } = {} + + if (!title.trim()) { + newErrors.title = 'Title is required' + } + if (!code.trim()) { + newErrors.code = 'Code is required' + } + + if (Object.keys(newErrors).length > 0) { + setErrors(newErrors) + return + } + + onSave({ + title: title.trim(), + description: description.trim(), + language, + code: code.trim(), + }) + + setTitle('') + setDescription('') + setLanguage('JavaScript') + setCode('') + setErrors({}) + onOpenChange(false) + } + + return ( + + + + + {editingSnippet ? 'Edit Snippet' : 'Create New Snippet'} + + + {editingSnippet + ? 'Update your code snippet details below.' + : 'Save a reusable code snippet for quick access later.'} + + + +
+
+ + setTitle(e.target.value)} + className={errors.title ? 'border-destructive ring-destructive' : ''} + /> + {errors.title && ( +

{errors.title}

+ )} +
+ +
+ + +
+ +
+ +