mirror of
https://github.com/johndoe6345789/snippet-pastebin.git
synced 2026-05-04 10:24:50 +00:00
Generated by Spark: Implement monaco editor with lazy loading
This commit is contained in:
+15
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
)}
|
||||
|
||||
@@ -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