Split SnippetCard.tsx (285 LOC) into 3 focused components

Co-authored-by: johndoe6345789 <224850594+johndoe6345789@users.noreply.github.com>
This commit is contained in:
copilot-swe-agent[bot]
2026-01-17 22:00:16 +00:00
parent 9328bf3102
commit 5a419e764a
4 changed files with 216 additions and 133 deletions

View File

@@ -1,23 +1,12 @@
import { useState, useMemo, useEffect } from 'react'
import { Badge } from '@/components/ui/badge'
import { Button } from '@/components/ui/button'
import { Card } from '@/components/ui/card'
import { Checkbox } from '@/components/ui/checkbox'
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuSeparator,
DropdownMenuSub,
DropdownMenuSubContent,
DropdownMenuSubTrigger,
DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu'
import { Copy, Pencil, Trash, Eye, DotsThree, FolderOpen } from '@phosphor-icons/react'
import { Snippet, Namespace } from '@/lib/types'
import { strings, appConfig, LANGUAGE_COLORS } from '@/lib/config'
import { strings, appConfig } from '@/lib/config'
import { getAllNamespaces, moveSnippetToNamespace } from '@/lib/db'
import { toast } from 'sonner'
import { SnippetCardHeader } from './SnippetCardHeader'
import { SnippetCodePreview } from './SnippetCodePreview'
import { SnippetCardActions } from './SnippetCardActions'
interface SnippetCardProps {
snippet: Snippet
@@ -157,127 +146,30 @@ export function SnippetCard({
onClick={handleView}
>
<div className="p-6 space-y-4">
<div className="flex items-start justify-between gap-3">
<div className="flex items-start gap-3 flex-1 min-w-0">
{selectionMode && (
<Checkbox
checked={isSelected}
onCheckedChange={handleToggleSelect}
onClick={(e) => e.stopPropagation()}
className="mt-1"
/>
)}
<div className="flex-1 min-w-0">
<h3 className="text-lg font-semibold text-foreground mb-1 truncate">
{snippet.title}
</h3>
{snippetData.description && (
<p className="text-sm text-muted-foreground line-clamp-2">
{snippetData.description}
</p>
)}
</div>
</div>
<Badge
className={`shrink-0 ${LANGUAGE_COLORS[snippet.language] || LANGUAGE_COLORS['Other']}`}
>
{snippet.language}
</Badge>
</div>
<SnippetCardHeader
snippet={snippet}
description={snippetData.description}
selectionMode={selectionMode}
isSelected={isSelected}
onToggleSelect={handleToggleSelect}
/>
<div className="rounded-md bg-secondary/30 p-3 border border-border">
<pre className="text-xs text-muted-foreground overflow-x-auto whitespace-pre-wrap break-words font-mono">
{snippetData.displayCode}
</pre>
{snippetData.isTruncated && (
<p className="text-xs text-accent mt-2">
{strings.snippetCard.viewFullCode}
</p>
)}
</div>
<SnippetCodePreview
displayCode={snippetData.displayCode}
isTruncated={snippetData.isTruncated}
/>
{!selectionMode && (
<div className="flex items-center justify-between gap-2 pt-2">
<div className="flex-1 flex items-center gap-2">
<Button
variant="ghost"
size="sm"
onClick={handleView}
className="gap-2"
>
<Eye className="h-4 w-4" />
{strings.snippetCard.viewButton}
</Button>
</div>
<div className="flex items-center gap-2">
<Button
variant="ghost"
size="sm"
onClick={handleCopy}
className="gap-2"
aria-label={strings.snippetCard.ariaLabels.copy}
>
<Copy className="h-4 w-4" />
{isCopied ? strings.snippetCard.copiedButton : strings.snippetCard.copyButton}
</Button>
<Button
variant="ghost"
size="sm"
onClick={handleEdit}
aria-label={strings.snippetCard.ariaLabels.edit}
>
<Pencil className="h-4 w-4" />
</Button>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
variant="ghost"
size="sm"
onClick={(e) => e.stopPropagation()}
aria-label="More options"
>
<DotsThree className="h-4 w-4" weight="bold" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" onClick={(e) => e.stopPropagation()}>
<DropdownMenuSub>
<DropdownMenuSubTrigger disabled={isMoving || availableNamespaces.length === 0}>
<FolderOpen className="h-4 w-4 mr-2" />
<span>Move to...</span>
</DropdownMenuSubTrigger>
<DropdownMenuSubContent>
{availableNamespaces.length === 0 ? (
<DropdownMenuItem disabled>
No other namespaces
</DropdownMenuItem>
) : (
availableNamespaces.map((namespace) => (
<DropdownMenuItem
key={namespace.id}
onClick={() => handleMoveToNamespace(namespace.id)}
>
{namespace.name}
{namespace.isDefault && (
<span className="ml-2 text-xs text-muted-foreground">(Default)</span>
)}
</DropdownMenuItem>
))
)}
</DropdownMenuSubContent>
</DropdownMenuSub>
<DropdownMenuSeparator />
<DropdownMenuItem
onClick={handleDelete}
className="text-destructive focus:text-destructive"
>
<Trash className="h-4 w-4 mr-2" />
Delete
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
</div>
<SnippetCardActions
isCopied={isCopied}
isMoving={isMoving}
availableNamespaces={availableNamespaces}
onView={handleView}
onCopy={handleCopy}
onEdit={handleEdit}
onDelete={handleDelete}
onMoveToNamespace={handleMoveToNamespace}
/>
)}
</div>
</Card>

View File

@@ -0,0 +1,120 @@
import { Button } from '@/components/ui/button'
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuSeparator,
DropdownMenuSub,
DropdownMenuSubContent,
DropdownMenuSubTrigger,
DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu'
import { Copy, Pencil, Trash, Eye, DotsThree, FolderOpen } from '@phosphor-icons/react'
import { Namespace } from '@/lib/types'
import { strings } from '@/lib/config'
interface SnippetCardActionsProps {
isCopied: boolean
isMoving: boolean
availableNamespaces: Namespace[]
onView: (e: React.MouseEvent) => void
onCopy: (e: React.MouseEvent) => void
onEdit: (e: React.MouseEvent) => void
onDelete: (e: React.MouseEvent) => void
onMoveToNamespace: (namespaceId: string) => void
}
export function SnippetCardActions({
isCopied,
isMoving,
availableNamespaces,
onView,
onCopy,
onEdit,
onDelete,
onMoveToNamespace,
}: SnippetCardActionsProps) {
return (
<div className="flex items-center justify-between gap-2 pt-2">
<div className="flex-1 flex items-center gap-2">
<Button
variant="ghost"
size="sm"
onClick={onView}
className="gap-2"
>
<Eye className="h-4 w-4" />
{strings.snippetCard.viewButton}
</Button>
</div>
<div className="flex items-center gap-2">
<Button
variant="ghost"
size="sm"
onClick={onCopy}
className="gap-2"
aria-label={strings.snippetCard.ariaLabels.copy}
>
<Copy className="h-4 w-4" />
{isCopied ? strings.snippetCard.copiedButton : strings.snippetCard.copyButton}
</Button>
<Button
variant="ghost"
size="sm"
onClick={onEdit}
aria-label={strings.snippetCard.ariaLabels.edit}
>
<Pencil className="h-4 w-4" />
</Button>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
variant="ghost"
size="sm"
onClick={(e) => e.stopPropagation()}
aria-label="More options"
>
<DotsThree className="h-4 w-4" weight="bold" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" onClick={(e) => e.stopPropagation()}>
<DropdownMenuSub>
<DropdownMenuSubTrigger disabled={isMoving || availableNamespaces.length === 0}>
<FolderOpen className="h-4 w-4 mr-2" />
<span>Move to...</span>
</DropdownMenuSubTrigger>
<DropdownMenuSubContent>
{availableNamespaces.length === 0 ? (
<DropdownMenuItem disabled>
No other namespaces
</DropdownMenuItem>
) : (
availableNamespaces.map((namespace) => (
<DropdownMenuItem
key={namespace.id}
onClick={() => onMoveToNamespace(namespace.id)}
>
{namespace.name}
{namespace.isDefault && (
<span className="ml-2 text-xs text-muted-foreground">(Default)</span>
)}
</DropdownMenuItem>
))
)}
</DropdownMenuSubContent>
</DropdownMenuSub>
<DropdownMenuSeparator />
<DropdownMenuItem
onClick={onDelete}
className="text-destructive focus:text-destructive"
>
<Trash className="h-4 w-4 mr-2" />
Delete
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
</div>
)
}

View File

@@ -0,0 +1,50 @@
import { Badge } from '@/components/ui/badge'
import { Checkbox } from '@/components/ui/checkbox'
import { Snippet } from '@/lib/types'
import { LANGUAGE_COLORS } from '@/lib/config'
interface SnippetCardHeaderProps {
snippet: Snippet
description: string
selectionMode: boolean
isSelected: boolean
onToggleSelect: () => void
}
export function SnippetCardHeader({
snippet,
description,
selectionMode,
isSelected,
onToggleSelect
}: SnippetCardHeaderProps) {
return (
<div className="flex items-start justify-between gap-3">
<div className="flex items-start gap-3 flex-1 min-w-0">
{selectionMode && (
<Checkbox
checked={isSelected}
onCheckedChange={onToggleSelect}
onClick={(e) => e.stopPropagation()}
className="mt-1"
/>
)}
<div className="flex-1 min-w-0">
<h3 className="text-lg font-semibold text-foreground mb-1 truncate">
{snippet.title}
</h3>
{description && (
<p className="text-sm text-muted-foreground line-clamp-2">
{description}
</p>
)}
</div>
</div>
<Badge
className={`shrink-0 ${LANGUAGE_COLORS[snippet.language] || LANGUAGE_COLORS['Other']}`}
>
{snippet.language}
</Badge>
</div>
)
}

View File

@@ -0,0 +1,21 @@
import { strings } from '@/lib/config'
interface SnippetCodePreviewProps {
displayCode: string
isTruncated: boolean
}
export function SnippetCodePreview({ displayCode, isTruncated }: SnippetCodePreviewProps) {
return (
<div className="rounded-md bg-secondary/30 p-3 border border-border">
<pre className="text-xs text-muted-foreground overflow-x-auto whitespace-pre-wrap break-words font-mono">
{displayCode}
</pre>
{isTruncated && (
<p className="text-xs text-accent mt-2">
{strings.snippetCard.viewFullCode}
</p>
)}
</div>
)
}