mirror of
https://github.com/johndoe6345789/snippet-pastebin.git
synced 2026-04-24 21:44:54 +00:00
Generated by Spark: Implement monaco editor with lazy loading
This commit is contained in:
28
PRD.md
28
PRD.md
@@ -13,18 +13,18 @@ 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
|
||||
- **Purpose**: Core value proposition - storing reusable code for later retrieval
|
||||
- **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
|
||||
- **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
|
||||
- **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
|
||||
|
||||
### 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
|
||||
- **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
|
||||
- **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
|
||||
|
||||
### Search & Filter
|
||||
- **Functionality**: Real-time search across snippet titles, descriptions, and code content; filter by programming language
|
||||
@@ -34,11 +34,11 @@ This is a CRUD application with search, filtering, and organization features but
|
||||
- **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
|
||||
- **Functionality**: Modify existing snippets using Monaco Editor or remove them entirely
|
||||
- **Purpose**: Keep snippet library current and relevant with professional editing experience
|
||||
- **Trigger**: Click edit icon on snippet card or from viewer, click delete with confirmation
|
||||
- **Progression**: Click Edit → Monaco Editor opens with existing code → Modify fields with syntax highlighting → Save changes → See updated snippet in list
|
||||
- **Success criteria**: Changes persist, Monaco Editor retains user edits, delete requires confirmation, no accidental data loss
|
||||
|
||||
### Copy to Clipboard
|
||||
- **Functionality**: One-click copy of code content to clipboard
|
||||
|
||||
72
package-lock.json
generated
72
package-lock.json
generated
@@ -11,6 +11,7 @@
|
||||
"@github/spark": ">=0.43.1 <1",
|
||||
"@heroicons/react": "^2.2.0",
|
||||
"@hookform/resolvers": "^4.1.3",
|
||||
"@monaco-editor/react": "^4.7.0",
|
||||
"@octokit/core": "^6.1.4",
|
||||
"@phosphor-icons/react": "^2.1.7",
|
||||
"@radix-ui/colors": "^3.0.0",
|
||||
@@ -1076,6 +1077,29 @@
|
||||
"@jridgewell/sourcemap-codec": "^1.4.14"
|
||||
}
|
||||
},
|
||||
"node_modules/@monaco-editor/loader": {
|
||||
"version": "1.7.0",
|
||||
"resolved": "https://registry.npmjs.org/@monaco-editor/loader/-/loader-1.7.0.tgz",
|
||||
"integrity": "sha512-gIwR1HrJrrx+vfyOhYmCZ0/JcWqG5kbfG7+d3f/C1LXk2EvzAbHSg3MQ5lO2sMlo9izoAZ04shohfKLVT6crVA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"state-local": "^1.0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/@monaco-editor/react": {
|
||||
"version": "4.7.0",
|
||||
"resolved": "https://registry.npmjs.org/@monaco-editor/react/-/react-4.7.0.tgz",
|
||||
"integrity": "sha512-cyzXQCtO47ydzxpQtCGSQGOC8Gk3ZUeBXFAxD+CWXYFo5OqZyZUonFl0DwUlTyAfRHntBfw2p3w4s9R6oe1eCA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@monaco-editor/loader": "^1.5.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"monaco-editor": ">= 0.25.0 < 1",
|
||||
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
|
||||
"react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@nodelib/fs.scandir": {
|
||||
"version": "2.1.5",
|
||||
"resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
|
||||
@@ -4404,6 +4428,14 @@
|
||||
"@types/node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/trusted-types": {
|
||||
"version": "2.0.7",
|
||||
"resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz",
|
||||
"integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/@typescript-eslint/eslint-plugin": {
|
||||
"version": "8.48.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.48.0.tgz",
|
||||
@@ -5992,6 +6024,16 @@
|
||||
"csstype": "^3.0.2"
|
||||
}
|
||||
},
|
||||
"node_modules/dompurify": {
|
||||
"version": "3.2.7",
|
||||
"resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.2.7.tgz",
|
||||
"integrity": "sha512-WhL/YuveyGXJaerVlMYGWhvQswa7myDG17P7Vu65EWC05o8vfeNbvNf4d/BOvH99+ZW+LlQsc1GDKMa1vNK6dw==",
|
||||
"license": "(MPL-2.0 OR Apache-2.0)",
|
||||
"peer": true,
|
||||
"optionalDependencies": {
|
||||
"@types/trusted-types": "^2.0.7"
|
||||
}
|
||||
},
|
||||
"node_modules/dunder-proto": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
|
||||
@@ -7953,6 +7995,30 @@
|
||||
"node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/monaco-editor": {
|
||||
"version": "0.55.1",
|
||||
"resolved": "https://registry.npmjs.org/monaco-editor/-/monaco-editor-0.55.1.tgz",
|
||||
"integrity": "sha512-jz4x+TJNFHwHtwuV9vA9rMujcZRb0CEilTEwG2rRSpe/A7Jdkuj8xPKttCgOh+v/lkHy7HsZ64oj+q3xoAFl9A==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"dompurify": "3.2.7",
|
||||
"marked": "14.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/monaco-editor/node_modules/marked": {
|
||||
"version": "14.0.0",
|
||||
"resolved": "https://registry.npmjs.org/marked/-/marked-14.0.0.tgz",
|
||||
"integrity": "sha512-uIj4+faQ+MgHgwUW1l2PsPglZLOLOT1uErt06dAPtx2kjteLAkbsd/0FiYg/MGS+i7ZKLb7w2WClxHkzOOuryQ==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"marked": "bin/marked.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 18"
|
||||
}
|
||||
},
|
||||
"node_modules/motion-dom": {
|
||||
"version": "12.23.23",
|
||||
"resolved": "https://registry.npmjs.org/motion-dom/-/motion-dom-12.23.23.tgz",
|
||||
@@ -9198,6 +9264,12 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/state-local": {
|
||||
"version": "1.0.7",
|
||||
"resolved": "https://registry.npmjs.org/state-local/-/state-local-1.0.7.tgz",
|
||||
"integrity": "sha512-HTEHMNieakEnoe33shBYcZ7NX83ACUjCu8c40iOGEZsngj9zRnkqS9j1pqQPXwobB0ZcVTk27REb7COQ0UR59w==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/statuses": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz",
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
"@github/spark": ">=0.43.1 <1",
|
||||
"@heroicons/react": "^2.2.0",
|
||||
"@hookform/resolvers": "^4.1.3",
|
||||
"@monaco-editor/react": "^4.7.0",
|
||||
"@octokit/core": "^6.1.4",
|
||||
"@phosphor-icons/react": "^2.1.7",
|
||||
"@radix-ui/colors": "^3.0.0",
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
{
|
||||
"dbType": null
|
||||
|
||||
{
|
||||
"templateVersion": 0,
|
||||
"dbType": null
|
||||
}
|
||||
15
src/App.tsx
15
src/App.tsx
@@ -23,6 +23,7 @@ import { Code, Plus, MagnifyingGlass, Funnel } from '@phosphor-icons/react'
|
||||
import { toast } from 'sonner'
|
||||
import { SnippetCard } from '@/components/SnippetCard'
|
||||
import { SnippetDialog } from '@/components/SnippetDialog'
|
||||
import { SnippetViewer } from '@/components/SnippetViewer'
|
||||
import { EmptyState } from '@/components/EmptyState'
|
||||
import { Snippet, LANGUAGES } from '@/lib/types'
|
||||
|
||||
@@ -30,6 +31,7 @@ function App() {
|
||||
const [snippets, setSnippets] = useKV<Snippet[]>('snippets', [])
|
||||
const [dialogOpen, setDialogOpen] = useState(false)
|
||||
const [editingSnippet, setEditingSnippet] = useState<Snippet | null>(null)
|
||||
const [viewingSnippet, setViewingSnippet] = useState<Snippet | null>(null)
|
||||
const [deleteId, setDeleteId] = useState<string | null>(null)
|
||||
const [searchQuery, setSearchQuery] = useState('')
|
||||
const [filterLanguage, setFilterLanguage] = useState<string>('all')
|
||||
@@ -105,6 +107,10 @@ function App() {
|
||||
setDialogOpen(true)
|
||||
}
|
||||
|
||||
const handleView = (snippet: Snippet) => {
|
||||
setViewingSnippet(snippet)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-background">
|
||||
<div className="border-b border-border bg-card/50 backdrop-blur-sm sticky top-0 z-10">
|
||||
@@ -189,6 +195,7 @@ function App() {
|
||||
onEdit={handleEdit}
|
||||
onDelete={handleDelete}
|
||||
onCopy={handleCopy}
|
||||
onView={handleView}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
@@ -213,6 +220,14 @@ function App() {
|
||||
editingSnippet={editingSnippet}
|
||||
/>
|
||||
|
||||
<SnippetViewer
|
||||
snippet={viewingSnippet}
|
||||
open={!!viewingSnippet}
|
||||
onOpenChange={(open) => !open && setViewingSnippet(null)}
|
||||
onEdit={handleEdit}
|
||||
onCopy={handleCopy}
|
||||
/>
|
||||
|
||||
<AlertDialog open={!!deleteId} onOpenChange={(open) => !open && setDeleteId(null)}>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
|
||||
87
src/components/MonacoEditor.tsx
Normal file
87
src/components/MonacoEditor.tsx
Normal file
@@ -0,0 +1,87 @@
|
||||
import { lazy, Suspense } from 'react'
|
||||
import { Skeleton } from '@/components/ui/skeleton'
|
||||
|
||||
const Editor = lazy(() => import('@monaco-editor/react'))
|
||||
|
||||
interface MonacoEditorProps {
|
||||
value: string
|
||||
onChange: (value: string) => void
|
||||
language: string
|
||||
height?: string
|
||||
readOnly?: boolean
|
||||
}
|
||||
|
||||
function EditorLoadingSkeleton({ height = '400px' }: { height?: string }) {
|
||||
return (
|
||||
<div className="space-y-2" style={{ height }}>
|
||||
<Skeleton className="h-full w-full rounded-md" />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export function MonacoEditor({
|
||||
value,
|
||||
onChange,
|
||||
language,
|
||||
height = '400px',
|
||||
readOnly = false
|
||||
}: MonacoEditorProps) {
|
||||
const monacoLanguage = getMonacoLanguage(language)
|
||||
|
||||
return (
|
||||
<Suspense fallback={<EditorLoadingSkeleton height={height} />}>
|
||||
<Editor
|
||||
height={height}
|
||||
language={monacoLanguage}
|
||||
value={value}
|
||||
onChange={(newValue) => onChange(newValue || '')}
|
||||
theme="vs-dark"
|
||||
options={{
|
||||
minimap: { enabled: false },
|
||||
fontSize: 14,
|
||||
lineNumbers: 'on',
|
||||
scrollBeyondLastLine: false,
|
||||
automaticLayout: true,
|
||||
tabSize: 2,
|
||||
wordWrap: 'on',
|
||||
readOnly,
|
||||
scrollbar: {
|
||||
vertical: 'auto',
|
||||
horizontal: 'auto',
|
||||
useShadows: false,
|
||||
},
|
||||
padding: {
|
||||
top: 12,
|
||||
bottom: 12,
|
||||
},
|
||||
fontFamily: 'JetBrains Mono, monospace',
|
||||
fontLigatures: true,
|
||||
}}
|
||||
/>
|
||||
</Suspense>
|
||||
)
|
||||
}
|
||||
|
||||
function getMonacoLanguage(language: string): string {
|
||||
const languageMap: Record<string, string> = {
|
||||
'JavaScript': 'javascript',
|
||||
'TypeScript': 'typescript',
|
||||
'Python': 'python',
|
||||
'Java': 'java',
|
||||
'C++': 'cpp',
|
||||
'C#': 'csharp',
|
||||
'Ruby': 'ruby',
|
||||
'Go': 'go',
|
||||
'Rust': 'rust',
|
||||
'PHP': 'php',
|
||||
'Swift': 'swift',
|
||||
'Kotlin': 'kotlin',
|
||||
'HTML': 'html',
|
||||
'CSS': 'css',
|
||||
'SQL': 'sql',
|
||||
'Bash': 'shell',
|
||||
'Other': 'plaintext',
|
||||
}
|
||||
|
||||
return languageMap[language] || 'plaintext'
|
||||
}
|
||||
@@ -2,20 +2,19 @@ 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 { Copy, Pencil, Trash, Check, ArrowsOut } 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
|
||||
onView: (snippet: Snippet) => void
|
||||
}
|
||||
|
||||
export function SnippetCard({ snippet, onEdit, onDelete, onCopy }: SnippetCardProps) {
|
||||
const [isExpanded, setIsExpanded] = useState(false)
|
||||
export function SnippetCard({ snippet, onEdit, onDelete, onCopy, onView }: SnippetCardProps) {
|
||||
const [isCopied, setIsCopied] = useState(false)
|
||||
|
||||
const handleCopy = () => {
|
||||
@@ -32,10 +31,8 @@ export function SnippetCard({ snippet, onEdit, onDelete, onCopy }: SnippetCardPr
|
||||
<Card
|
||||
className={cn(
|
||||
"group relative overflow-hidden transition-all duration-200",
|
||||
"hover:shadow-lg hover:shadow-accent/10 hover:border-accent/30",
|
||||
"cursor-pointer"
|
||||
"hover:shadow-lg hover:shadow-accent/10 hover:border-accent/30"
|
||||
)}
|
||||
onClick={() => setIsExpanded(!isExpanded)}
|
||||
>
|
||||
<div className="p-5 space-y-3">
|
||||
<div className="flex items-start justify-between gap-3">
|
||||
@@ -60,26 +57,29 @@ export function SnippetCard({ snippet, onEdit, onDelete, onCopy }: SnippetCardPr
|
||||
</Badge>
|
||||
</div>
|
||||
|
||||
<div className="relative">
|
||||
{isExpanded ? (
|
||||
<ScrollArea className="h-64 w-full rounded-md border border-border bg-secondary/30 p-3">
|
||||
<pre className="text-sm text-foreground/90">
|
||||
<code className="font-mono">{snippet.code}</code>
|
||||
</pre>
|
||||
</ScrollArea>
|
||||
) : (
|
||||
<div className="relative rounded-md border border-border bg-secondary/30 p-3 overflow-hidden">
|
||||
<pre className="text-sm text-foreground/90 line-clamp-4">
|
||||
<code className="font-mono">{truncatedCode}</code>
|
||||
</pre>
|
||||
{snippet.code.length > 200 && (
|
||||
<div className="absolute bottom-0 left-0 right-0 h-12 bg-gradient-to-t from-secondary/30 to-transparent" />
|
||||
)}
|
||||
</div>
|
||||
<div
|
||||
className="relative rounded-md border border-border bg-secondary/30 p-3 overflow-hidden cursor-pointer hover:border-accent/50 transition-colors"
|
||||
onClick={() => onView(snippet)}
|
||||
>
|
||||
<pre className="text-sm text-foreground/90 line-clamp-6">
|
||||
<code className="font-mono">{truncatedCode}</code>
|
||||
</pre>
|
||||
{snippet.code.length > 200 && (
|
||||
<div className="absolute bottom-0 left-0 right-0 h-12 bg-gradient-to-t from-secondary/30 to-transparent" />
|
||||
)}
|
||||
<div className="absolute top-2 right-2 opacity-0 group-hover:opacity-100 transition-opacity">
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
className="h-8 gap-2 bg-background/80 backdrop-blur-sm"
|
||||
>
|
||||
<ArrowsOut className="h-4 w-4" />
|
||||
View
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between pt-2" onClick={(e) => e.stopPropagation()}>
|
||||
<div className="flex items-center justify-between pt-2">
|
||||
<span className="text-xs text-muted-foreground">
|
||||
{new Date(snippet.updatedAt).toLocaleDateString()}
|
||||
</span>
|
||||
|
||||
@@ -19,6 +19,7 @@ import {
|
||||
SelectValue,
|
||||
} from '@/components/ui/select'
|
||||
import { Snippet, LANGUAGES } from '@/lib/types'
|
||||
import { MonacoEditor } from '@/components/MonacoEditor'
|
||||
|
||||
interface SnippetDialogProps {
|
||||
open: boolean
|
||||
@@ -81,7 +82,7 @@ export function SnippetDialog({ open, onOpenChange, onSave, editingSnippet }: Sn
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent className="sm:max-w-[600px] max-h-[90vh] overflow-y-auto">
|
||||
<DialogContent className="sm:max-w-[900px] max-h-[90vh] overflow-hidden flex flex-col">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="text-2xl">
|
||||
{editingSnippet ? 'Edit Snippet' : 'Create New Snippet'}
|
||||
@@ -93,7 +94,7 @@ export function SnippetDialog({ open, onOpenChange, onSave, editingSnippet }: Sn
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="space-y-4 py-4">
|
||||
<div className="space-y-4 py-4 overflow-y-auto flex-1">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="title">Title *</Label>
|
||||
<Input
|
||||
@@ -137,14 +138,14 @@ export function SnippetDialog({ open, onOpenChange, onSave, editingSnippet }: Sn
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="code">Code *</Label>
|
||||
<Textarea
|
||||
id="code"
|
||||
placeholder="Paste your code here..."
|
||||
value={code}
|
||||
onChange={(e) => setCode(e.target.value)}
|
||||
rows={12}
|
||||
className={`font-mono text-sm ${errors.code ? 'border-destructive ring-destructive' : ''}`}
|
||||
/>
|
||||
<div className={`rounded-md border overflow-hidden ${errors.code ? 'border-destructive ring-2 ring-destructive/20' : 'border-border'}`}>
|
||||
<MonacoEditor
|
||||
value={code}
|
||||
onChange={setCode}
|
||||
language={language}
|
||||
height="400px"
|
||||
/>
|
||||
</div>
|
||||
{errors.code && (
|
||||
<p className="text-sm text-destructive">{errors.code}</p>
|
||||
)}
|
||||
|
||||
120
src/components/SnippetViewer.tsx
Normal file
120
src/components/SnippetViewer.tsx
Normal file
@@ -0,0 +1,120 @@
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} 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 { Snippet, LANGUAGE_COLORS } from '@/lib/types'
|
||||
import { MonacoEditor } from '@/components/MonacoEditor'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { useState } from 'react'
|
||||
|
||||
interface SnippetViewerProps {
|
||||
snippet: Snippet | null
|
||||
open: boolean
|
||||
onOpenChange: (open: boolean) => void
|
||||
onEdit: (snippet: Snippet) => void
|
||||
onCopy: (code: string) => void
|
||||
}
|
||||
|
||||
export function SnippetViewer({ snippet, open, onOpenChange, onEdit, onCopy }: SnippetViewerProps) {
|
||||
const [isCopied, setIsCopied] = useState(false)
|
||||
|
||||
if (!snippet) return null
|
||||
|
||||
const handleCopy = () => {
|
||||
onCopy(snippet.code)
|
||||
setIsCopied(true)
|
||||
setTimeout(() => setIsCopied(false), 2000)
|
||||
}
|
||||
|
||||
const handleEdit = () => {
|
||||
onOpenChange(false)
|
||||
onEdit(snippet)
|
||||
}
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent className="sm:max-w-[95vw] sm:max-h-[95vh] h-[95vh] overflow-hidden flex flex-col p-0">
|
||||
<DialogHeader className="px-6 pt-6 pb-4 border-b border-border">
|
||||
<div className="flex items-start justify-between gap-4">
|
||||
<div className="flex-1 min-w-0 space-y-2">
|
||||
<div className="flex items-center gap-3">
|
||||
<DialogTitle className="text-2xl font-bold truncate">
|
||||
{snippet.title}
|
||||
</DialogTitle>
|
||||
<Badge
|
||||
variant="outline"
|
||||
className={cn(
|
||||
"shrink-0 border font-medium text-xs px-2 py-1",
|
||||
LANGUAGE_COLORS[snippet.language] || LANGUAGE_COLORS['Other']
|
||||
)}
|
||||
>
|
||||
{snippet.language}
|
||||
</Badge>
|
||||
</div>
|
||||
{snippet.description && (
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{snippet.description}
|
||||
</p>
|
||||
)}
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Last updated: {new Date(snippet.updatedAt).toLocaleString()}
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex gap-2 shrink-0">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={handleCopy}
|
||||
className="gap-2"
|
||||
>
|
||||
{isCopied ? (
|
||||
<>
|
||||
<Check className="h-4 w-4" weight="bold" />
|
||||
Copied
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Copy className="h-4 w-4" />
|
||||
Copy
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={handleEdit}
|
||||
className="gap-2"
|
||||
>
|
||||
<Pencil className="h-4 w-4" />
|
||||
Edit
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => onOpenChange(false)}
|
||||
className="h-9 w-9 p-0"
|
||||
>
|
||||
<X className="h-5 w-5" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="flex-1 overflow-hidden">
|
||||
<MonacoEditor
|
||||
value={snippet.code}
|
||||
onChange={() => {}}
|
||||
language={snippet.language}
|
||||
height="100%"
|
||||
readOnly={true}
|
||||
/>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user