Generated by Spark: Implement monaco editor with lazy loading

This commit is contained in:
2026-01-17 14:39:28 +00:00
committed by GitHub
parent 0efee389d3
commit ea56ab7754
9 changed files with 348 additions and 51 deletions
+15
View File
@@ -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
View 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'
}
+24 -24
View File
@@ -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>
+11 -10
View File
@@ -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
View 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>
)
}