mirror of
https://github.com/johndoe6345789/snippet-pastebin.git
synced 2026-04-24 13:34:55 +00:00
Generated by Spark: Have option to have a split screen snippet. Code + Preview. This could allow rendering a React app.
This commit is contained in:
42
PRD.md
42
PRD.md
@@ -13,18 +13,25 @@ This is a CRUD application with search, filtering, and organization features but
|
||||
## Essential Features
|
||||
|
||||
### Create Snippet
|
||||
- **Functionality**: Users can create a new code snippet with title, description, language selection, and code content using Monaco Editor for enhanced code editing
|
||||
- **Purpose**: Core value proposition - storing reusable code for later retrieval with professional IDE-like experience
|
||||
- **Functionality**: Users can create a new code snippet with title, description, language selection, code content using Monaco Editor, and optional preview mode for React components
|
||||
- **Purpose**: Core value proposition - storing reusable code for later retrieval with professional IDE-like experience and live preview capability for React snippets
|
||||
- **Trigger**: Click "New Snippet" button or keyboard shortcut
|
||||
- **Progression**: Click New Snippet → Fill in title field → Select language from dropdown → Write/paste code in Monaco Editor with syntax highlighting → Add optional description → Click Save
|
||||
- **Success criteria**: Snippet appears in the list immediately, persists across page refreshes, Monaco Editor loads lazily without blocking UI, and code is searchable
|
||||
- **Progression**: Click New Snippet → Fill in title field → Select language from dropdown (including JSX/TSX options) → Toggle preview mode checkbox if creating React component → Write/paste code in Monaco Editor with syntax highlighting → Add optional description → Click Save
|
||||
- **Success criteria**: Snippet appears in the list immediately with preview badge if enabled, persists across page refreshes, Monaco Editor loads lazily without blocking UI, and code is searchable
|
||||
|
||||
### View & Organize Snippets
|
||||
- **Functionality**: Display all snippets in a filterable list with preview cards showing title, language, and truncated code; click to open full-screen Monaco viewer
|
||||
- **Purpose**: Quick scanning and navigation through saved snippets with professional code viewing
|
||||
- **Functionality**: Display all snippets in a filterable list with preview cards showing title, language, preview badge, and truncated code; click to open full-screen Monaco viewer with optional split-screen preview
|
||||
- **Purpose**: Quick scanning and navigation through saved snippets with professional code viewing and live React component preview
|
||||
- **Trigger**: Default view on app load, click card to view full code
|
||||
- **Progression**: View list → Scan titles and languages → Click card to open full-screen Monaco viewer with syntax highlighting → Copy or edit from viewer
|
||||
- **Success criteria**: All snippets visible, sorted by recent first, viewer opens instantly with lazy-loaded Monaco Editor
|
||||
- **Progression**: View list → Scan titles, languages, and preview badges → Click card to open full-screen viewer with syntax highlighting → Toggle split-screen preview for React components → Copy or edit from viewer
|
||||
- **Success criteria**: All snippets visible, sorted by recent first, viewer opens instantly with lazy-loaded Monaco Editor, preview renders React components in real-time
|
||||
|
||||
### Split-Screen Preview
|
||||
- **Functionality**: For React-compatible snippets (JSX/TSX/JavaScript/TypeScript), render live preview alongside code editor in split-screen layout
|
||||
- **Purpose**: Enable developers to see React components rendered in real-time, test UI snippets instantly, and save complete working component examples
|
||||
- **Trigger**: Enable preview mode checkbox when creating/editing snippet, toggle preview button in viewer
|
||||
- **Progression**: Create snippet with JSX/TSX → Check "Enable preview" → Save → Open viewer → See code on left, live preview on right → Toggle preview on/off as needed
|
||||
- **Success criteria**: React code compiles and renders safely, errors display helpful messages, preview updates reflect code changes, supports React hooks and JSX syntax
|
||||
|
||||
### Search & Filter
|
||||
- **Functionality**: Real-time search across snippet titles, descriptions, and code content; filter by programming language
|
||||
@@ -54,6 +61,8 @@ This is a CRUD application with search, filtering, and organization features but
|
||||
- **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)
|
||||
- **Preview Rendering Errors**: Display detailed error messages when React code fails to compile or render, show warnings for non-React language previews
|
||||
- **Preview Not Available**: Show informative message for snippets without preview enabled or non-React languages
|
||||
|
||||
## 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.
|
||||
@@ -91,20 +100,25 @@ Animations should feel responsive and technical - quick, purposeful movements th
|
||||
- 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
|
||||
- Badge for language tags with color coding, preview indicator badge with split-screen icon
|
||||
- ScrollArea for long code blocks within cards
|
||||
- Toast (Sonner) for copy confirmations and success messages
|
||||
- AlertDialog for delete confirmations
|
||||
- Checkbox for enabling split-screen preview mode
|
||||
- Alert for displaying preview rendering errors
|
||||
|
||||
- **Customizations**:
|
||||
- Custom syntax language badges with predefined color mapping (JavaScript=yellow, Python=blue, etc.)
|
||||
- Custom syntax language badges with predefined color mapping (JavaScript=yellow, Python=blue, JSX/TSX=cyan/sky, 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
|
||||
- Split-screen preview renderer with safe React code execution
|
||||
- Preview badge with split-screen icon to indicate preview-enabled snippets
|
||||
|
||||
- **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
|
||||
- Buttons: Default with subtle gradient, hover with brightness increase and shadow, active with slight scale-down (0.98), disabled with 50% opacity, toggle state for preview on/off
|
||||
- Cards: Default flat, hover with shadow-lg and border glow, selected/expanded with accent border, preview badge visible when enabled
|
||||
- Inputs: Default with muted border, focus with accent ring and border color shift, error with red ring
|
||||
- Preview pane: Loading state, error state with helpful message, rendered state with scrolling
|
||||
|
||||
- **Icon Selection**:
|
||||
- Plus (create new snippet)
|
||||
@@ -114,6 +128,8 @@ Animations should feel responsive and technical - quick, purposeful movements th
|
||||
- MagnifyingGlass (search)
|
||||
- Code (app logo/branding)
|
||||
- Check (confirmation feedback)
|
||||
- SplitVertical (preview mode indicator and toggle)
|
||||
- WarningCircle (preview errors)
|
||||
|
||||
- **Spacing**:
|
||||
- Container padding: p-6 (desktop) / p-4 (mobile)
|
||||
@@ -121,6 +137,7 @@ Animations should feel responsive and technical - quick, purposeful movements th
|
||||
- Form fields: space-y-4 for vertical stacking
|
||||
- Section margins: mb-8 for major sections
|
||||
- Inline elements: gap-2 for icon+text combinations
|
||||
- Split-screen: Equal 50/50 width distribution with border separator
|
||||
|
||||
- **Mobile**:
|
||||
- Single column card layout with full-width cards
|
||||
@@ -129,3 +146,4 @@ Animations should feel responsive and technical - quick, purposeful movements th
|
||||
- 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
|
||||
- Preview stacks vertically below code editor on small screens
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
{
|
||||
"dbType": null
|
||||
"dbType": null
|
||||
{
|
||||
"templateVersion": 0,
|
||||
"dbType": null
|
||||
}
|
||||
107
src/components/ReactPreview.tsx
Normal file
107
src/components/ReactPreview.tsx
Normal file
@@ -0,0 +1,107 @@
|
||||
import { useEffect, useState, useRef } from 'react'
|
||||
import * as React from 'react'
|
||||
import * as ReactDOM from 'react-dom'
|
||||
import { Alert, AlertDescription } from '@/components/ui/alert'
|
||||
import { WarningCircle } from '@phosphor-icons/react'
|
||||
|
||||
interface ReactPreviewProps {
|
||||
code: string
|
||||
language: string
|
||||
}
|
||||
|
||||
export function ReactPreview({ code, language }: ReactPreviewProps) {
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
const [Component, setComponent] = useState<React.ComponentType | null>(null)
|
||||
const mountRef = useRef<HTMLDivElement>(null)
|
||||
|
||||
useEffect(() => {
|
||||
setError(null)
|
||||
setComponent(null)
|
||||
|
||||
const isReactCode = ['JSX', 'TSX', 'JavaScript', 'TypeScript'].includes(language)
|
||||
|
||||
if (!isReactCode) {
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
const transformedCode = code
|
||||
.replace(/^import\s+.*from\s+['"]react['"];?\s*/gm, '')
|
||||
.replace(/^import\s+.*from\s+['"].*['"];?\s*/gm, '')
|
||||
.replace(/export\s+default\s+/g, '')
|
||||
.replace(/export\s+/g, '')
|
||||
|
||||
const wrappedCode = `
|
||||
(function() {
|
||||
const React = arguments[0];
|
||||
const useState = React.useState;
|
||||
const useEffect = React.useEffect;
|
||||
const useRef = React.useRef;
|
||||
const useMemo = React.useMemo;
|
||||
const useCallback = React.useCallback;
|
||||
|
||||
${transformedCode}
|
||||
|
||||
const lastStatement = (${transformedCode.trim().split('\n').pop()});
|
||||
return lastStatement;
|
||||
})
|
||||
`
|
||||
|
||||
const componentFactory = eval(wrappedCode)
|
||||
const CreatedComponent = componentFactory(React)
|
||||
|
||||
if (typeof CreatedComponent === 'function') {
|
||||
setComponent(() => CreatedComponent)
|
||||
} else if (React.isValidElement(CreatedComponent)) {
|
||||
setComponent(() => () => CreatedComponent)
|
||||
} else {
|
||||
setError('Code must export a React component or JSX element')
|
||||
}
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : 'Failed to render preview')
|
||||
}
|
||||
}, [code, language])
|
||||
|
||||
if (!['JSX', 'TSX', 'JavaScript', 'TypeScript'].includes(language)) {
|
||||
return (
|
||||
<div className="h-full flex items-center justify-center p-6 bg-muted/30">
|
||||
<div className="text-center text-muted-foreground">
|
||||
<WarningCircle className="h-12 w-12 mx-auto mb-3 opacity-50" />
|
||||
<p className="text-sm">Preview not available for {language}</p>
|
||||
<p className="text-xs mt-1">Use JSX, TSX, JavaScript, or TypeScript</p>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<div className="h-full overflow-auto p-6 bg-destructive/5">
|
||||
<Alert variant="destructive">
|
||||
<WarningCircle className="h-4 w-4" />
|
||||
<AlertDescription className="font-mono text-xs whitespace-pre-wrap">
|
||||
{error}
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
if (!Component) {
|
||||
return (
|
||||
<div className="h-full flex items-center justify-center p-6 bg-muted/30">
|
||||
<div className="text-center text-muted-foreground">
|
||||
<p className="text-sm">Loading preview...</p>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="h-full overflow-auto bg-background">
|
||||
<div className="p-6" ref={mountRef}>
|
||||
<Component />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -2,7 +2,7 @@ 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, ArrowsOut } from '@phosphor-icons/react'
|
||||
import { Copy, Pencil, Trash, Check, ArrowsOut, SplitVertical } from '@phosphor-icons/react'
|
||||
import { Snippet, LANGUAGE_COLORS } from '@/lib/types'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
@@ -46,15 +46,26 @@ export function SnippetCard({ snippet, onEdit, onDelete, onCopy, onView }: Snipp
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
<Badge
|
||||
variant="outline"
|
||||
className={cn(
|
||||
"shrink-0 border font-medium text-xs px-2 py-1",
|
||||
LANGUAGE_COLORS[snippet.language] || LANGUAGE_COLORS['Other']
|
||||
<div className="flex gap-2 shrink-0 items-start">
|
||||
{snippet.hasPreview && (
|
||||
<Badge
|
||||
variant="outline"
|
||||
className="border-accent/30 bg-accent/10 text-accent text-xs px-2 py-1 gap-1"
|
||||
>
|
||||
<SplitVertical className="h-3 w-3" weight="bold" />
|
||||
Preview
|
||||
</Badge>
|
||||
)}
|
||||
>
|
||||
{snippet.language}
|
||||
</Badge>
|
||||
<Badge
|
||||
variant="outline"
|
||||
className={cn(
|
||||
"border font-medium text-xs px-2 py-1",
|
||||
LANGUAGE_COLORS[snippet.language] || LANGUAGE_COLORS['Other']
|
||||
)}
|
||||
>
|
||||
{snippet.language}
|
||||
</Badge>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
|
||||
@@ -11,6 +11,7 @@ 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 { Checkbox } from '@/components/ui/checkbox'
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
@@ -33,6 +34,7 @@ export function SnippetDialog({ open, onOpenChange, onSave, editingSnippet }: Sn
|
||||
const [description, setDescription] = useState('')
|
||||
const [language, setLanguage] = useState('JavaScript')
|
||||
const [code, setCode] = useState('')
|
||||
const [hasPreview, setHasPreview] = useState(false)
|
||||
const [errors, setErrors] = useState<{ title?: string; code?: string }>({})
|
||||
|
||||
useEffect(() => {
|
||||
@@ -41,11 +43,13 @@ export function SnippetDialog({ open, onOpenChange, onSave, editingSnippet }: Sn
|
||||
setDescription(editingSnippet.description)
|
||||
setLanguage(editingSnippet.language)
|
||||
setCode(editingSnippet.code)
|
||||
setHasPreview(editingSnippet.hasPreview || false)
|
||||
} else {
|
||||
setTitle('')
|
||||
setDescription('')
|
||||
setLanguage('JavaScript')
|
||||
setCode('')
|
||||
setHasPreview(false)
|
||||
}
|
||||
setErrors({})
|
||||
}, [editingSnippet, open])
|
||||
@@ -70,12 +74,14 @@ export function SnippetDialog({ open, onOpenChange, onSave, editingSnippet }: Sn
|
||||
description: description.trim(),
|
||||
language,
|
||||
code: code.trim(),
|
||||
hasPreview,
|
||||
})
|
||||
|
||||
setTitle('')
|
||||
setDescription('')
|
||||
setLanguage('JavaScript')
|
||||
setCode('')
|
||||
setHasPreview(false)
|
||||
setErrors({})
|
||||
onOpenChange(false)
|
||||
}
|
||||
@@ -125,6 +131,22 @@ export function SnippetDialog({ open, onOpenChange, onSave, editingSnippet }: Sn
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
{['JSX', 'TSX', 'JavaScript', 'TypeScript'].includes(language) && (
|
||||
<div className="flex items-center space-x-2 py-2">
|
||||
<Checkbox
|
||||
id="hasPreview"
|
||||
checked={hasPreview}
|
||||
onCheckedChange={(checked) => setHasPreview(checked as boolean)}
|
||||
/>
|
||||
<Label
|
||||
htmlFor="hasPreview"
|
||||
className="text-sm font-normal cursor-pointer"
|
||||
>
|
||||
Enable split-screen preview for this snippet
|
||||
</Label>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="description">Description</Label>
|
||||
<Textarea
|
||||
|
||||
@@ -6,9 +6,10 @@ import {
|
||||
} from '@/components/ui/dialog'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
import { Copy, Pencil, X, Check } from '@phosphor-icons/react'
|
||||
import { Copy, Pencil, X, Check, SplitVertical } from '@phosphor-icons/react'
|
||||
import { Snippet, LANGUAGE_COLORS } from '@/lib/types'
|
||||
import { MonacoEditor } from '@/components/MonacoEditor'
|
||||
import { ReactPreview } from '@/components/ReactPreview'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { useState } from 'react'
|
||||
|
||||
@@ -22,6 +23,7 @@ interface SnippetViewerProps {
|
||||
|
||||
export function SnippetViewer({ snippet, open, onOpenChange, onEdit, onCopy }: SnippetViewerProps) {
|
||||
const [isCopied, setIsCopied] = useState(false)
|
||||
const [showPreview, setShowPreview] = useState(true)
|
||||
|
||||
if (!snippet) return null
|
||||
|
||||
@@ -35,6 +37,8 @@ export function SnippetViewer({ snippet, open, onOpenChange, onEdit, onCopy }: S
|
||||
onOpenChange(false)
|
||||
onEdit(snippet)
|
||||
}
|
||||
|
||||
const canPreview = snippet.hasPreview && ['JSX', 'TSX', 'JavaScript', 'TypeScript'].includes(snippet.language)
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
@@ -66,6 +70,17 @@ export function SnippetViewer({ snippet, open, onOpenChange, onEdit, onCopy }: S
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex gap-2 shrink-0">
|
||||
{canPreview && (
|
||||
<Button
|
||||
variant={showPreview ? "default" : "outline"}
|
||||
size="sm"
|
||||
onClick={() => setShowPreview(!showPreview)}
|
||||
className="gap-2"
|
||||
>
|
||||
<SplitVertical className="h-4 w-4" />
|
||||
{showPreview ? 'Hide' : 'Show'} Preview
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
@@ -105,14 +120,33 @@ export function SnippetViewer({ snippet, open, onOpenChange, onEdit, onCopy }: S
|
||||
</div>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="flex-1 overflow-hidden">
|
||||
<MonacoEditor
|
||||
value={snippet.code}
|
||||
onChange={() => {}}
|
||||
language={snippet.language}
|
||||
height="100%"
|
||||
readOnly={true}
|
||||
/>
|
||||
<div className="flex-1 overflow-hidden flex">
|
||||
{canPreview && showPreview ? (
|
||||
<>
|
||||
<div className="flex-1 overflow-hidden border-r border-border">
|
||||
<MonacoEditor
|
||||
value={snippet.code}
|
||||
onChange={() => {}}
|
||||
language={snippet.language}
|
||||
height="100%"
|
||||
readOnly={true}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex-1 overflow-hidden">
|
||||
<ReactPreview code={snippet.code} language={snippet.language} />
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<div className="flex-1 overflow-hidden">
|
||||
<MonacoEditor
|
||||
value={snippet.code}
|
||||
onChange={() => {}}
|
||||
language={snippet.language}
|
||||
height="100%"
|
||||
readOnly={true}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
@@ -6,11 +6,14 @@ export interface Snippet {
|
||||
code: string
|
||||
createdAt: number
|
||||
updatedAt: number
|
||||
hasPreview?: boolean
|
||||
}
|
||||
|
||||
export const LANGUAGES = [
|
||||
'JavaScript',
|
||||
'TypeScript',
|
||||
'JSX',
|
||||
'TSX',
|
||||
'Python',
|
||||
'Java',
|
||||
'C++',
|
||||
@@ -33,6 +36,8 @@ export type Language = typeof LANGUAGES[number]
|
||||
export const LANGUAGE_COLORS: Record<string, string> = {
|
||||
'JavaScript': 'bg-yellow-500/20 text-yellow-300 border-yellow-500/30',
|
||||
'TypeScript': 'bg-blue-500/20 text-blue-300 border-blue-500/30',
|
||||
'JSX': 'bg-cyan-500/20 text-cyan-300 border-cyan-500/30',
|
||||
'TSX': 'bg-sky-500/20 text-sky-300 border-sky-500/30',
|
||||
'Python': 'bg-blue-400/20 text-blue-200 border-blue-400/30',
|
||||
'Java': 'bg-red-500/20 text-red-300 border-red-500/30',
|
||||
'C++': 'bg-pink-500/20 text-pink-300 border-pink-500/30',
|
||||
|
||||
Reference in New Issue
Block a user