|
1 | 1 | import { useState, useRef, useMemo } from 'react' |
2 | 2 | import { motion } from 'framer-motion' |
3 | | -import { FolderGit2, Plus, Files, FunctionSquare, Clock } from 'lucide-react' |
| 3 | +import { FolderGit2, Plus, Files, FunctionSquare, Clock, MoreVertical, Trash2 } from 'lucide-react' |
4 | 4 | import { cn } from '@/lib/utils' |
| 5 | +import { |
| 6 | + DropdownMenu, |
| 7 | + DropdownMenuContent, |
| 8 | + DropdownMenuItem, |
| 9 | + DropdownMenuTrigger, |
| 10 | +} from './ui/dropdown-menu' |
| 11 | +import { |
| 12 | + Dialog, |
| 13 | + DialogContent, |
| 14 | + DialogDescription, |
| 15 | + DialogFooter, |
| 16 | + DialogHeader, |
| 17 | + DialogTitle, |
| 18 | +} from './ui/dialog' |
| 19 | +import { Button } from './ui/button' |
5 | 20 | import type { Repository } from '../types' |
6 | 21 | import { RepoGridSkeleton } from './ui/Skeleton' |
7 | 22 |
|
8 | 23 | interface RepoListProps { |
9 | 24 | repos: Repository[] |
10 | 25 | selectedRepo: string | null |
11 | 26 | onSelect: (repoId: string) => void |
| 27 | + onDelete?: (repoId: string) => void |
12 | 28 | onAddClick?: () => void |
13 | 29 | loading?: boolean |
14 | 30 | } |
@@ -60,10 +76,11 @@ const StatusDot = ({ status }: { status: string }) => { |
60 | 76 | ) |
61 | 77 | } |
62 | 78 |
|
63 | | -const RepoCard = ({ repo, index, onSelect }: { |
| 79 | +const RepoCard = ({ repo, index, onSelect, onDeleteClick }: { |
64 | 80 | repo: Repository |
65 | 81 | index: number |
66 | 82 | onSelect: () => void |
| 83 | + onDeleteClick?: () => void |
67 | 84 | }) => { |
68 | 85 | const cardRef = useRef<HTMLButtonElement>(null) |
69 | 86 | const [mousePos, setMousePos] = useState({ x: 0, y: 0 }) |
@@ -100,12 +117,35 @@ const RepoCard = ({ repo, index, onSelect }: { |
100 | 117 | )} |
101 | 118 |
|
102 | 119 | <div className="relative"> |
103 | | - {/* Top row: icon + status */} |
| 120 | + {/* Top row: icon + status + menu */} |
104 | 121 | <div className="flex items-start justify-between mb-3"> |
105 | 122 | <div className="w-10 h-10 rounded-xl bg-primary/10 border border-primary/20 flex items-center justify-center group-hover:bg-primary/15 transition-colors"> |
106 | 123 | <FolderGit2 className="w-5 h-5 text-primary" /> |
107 | 124 | </div> |
108 | | - <StatusDot status={repo.status} /> |
| 125 | + <div className="flex items-center gap-1"> |
| 126 | + <StatusDot status={repo.status} /> |
| 127 | + {onDeleteClick && ( |
| 128 | + <DropdownMenu> |
| 129 | + <DropdownMenuTrigger asChild> |
| 130 | + <button |
| 131 | + onClick={(e) => e.stopPropagation()} |
| 132 | + className="w-6 h-6 flex items-center justify-center rounded text-muted-foreground hover:text-foreground hover:bg-muted transition-colors opacity-0 group-hover:opacity-100" |
| 133 | + > |
| 134 | + <MoreVertical className="w-3.5 h-3.5" /> |
| 135 | + </button> |
| 136 | + </DropdownMenuTrigger> |
| 137 | + <DropdownMenuContent align="end"> |
| 138 | + <DropdownMenuItem |
| 139 | + onClick={(e) => { e.stopPropagation(); onDeleteClick() }} |
| 140 | + className="text-destructive focus:text-destructive" |
| 141 | + > |
| 142 | + <Trash2 className="w-3.5 h-3.5 mr-2" /> |
| 143 | + Delete repository |
| 144 | + </DropdownMenuItem> |
| 145 | + </DropdownMenuContent> |
| 146 | + </DropdownMenu> |
| 147 | + )} |
| 148 | + </div> |
109 | 149 | </div> |
110 | 150 |
|
111 | 151 | {/* Repo name + slug */} |
@@ -158,8 +198,9 @@ const SortTab = ({ label, active, onClick }: { |
158 | 198 | </button> |
159 | 199 | ) |
160 | 200 |
|
161 | | -export function RepoList({ repos, selectedRepo, onSelect, onAddClick, loading }: RepoListProps) { |
| 201 | +export function RepoList({ repos, selectedRepo, onSelect, onDelete, onAddClick, loading }: RepoListProps) { |
162 | 202 | const [sortMode, setSortMode] = useState<SortMode>('recent') |
| 203 | + const [deleteTarget, setDeleteTarget] = useState<Repository | null>(null) |
163 | 204 |
|
164 | 205 | const sortedRepos = useMemo(() => { |
165 | 206 | const sorted = [...repos] |
@@ -222,9 +263,36 @@ export function RepoList({ repos, selectedRepo, onSelect, onAddClick, loading }: |
222 | 263 | repo={repo} |
223 | 264 | index={index} |
224 | 265 | onSelect={() => onSelect(repo.id)} |
| 266 | + onDeleteClick={onDelete ? () => setDeleteTarget(repo) : undefined} |
225 | 267 | /> |
226 | 268 | ))} |
227 | 269 | </div> |
| 270 | + |
| 271 | + {/* Delete confirmation dialog */} |
| 272 | + <Dialog open={!!deleteTarget} onOpenChange={(open) => !open && setDeleteTarget(null)}> |
| 273 | + <DialogContent> |
| 274 | + <DialogHeader> |
| 275 | + <DialogTitle>Delete repository</DialogTitle> |
| 276 | + <DialogDescription> |
| 277 | + This will permanently remove <strong>{deleteTarget?.name}</strong> and all its indexed data. This action cannot be undone. |
| 278 | + </DialogDescription> |
| 279 | + </DialogHeader> |
| 280 | + <DialogFooter className="gap-2"> |
| 281 | + <Button variant="outline" onClick={() => setDeleteTarget(null)}>Cancel</Button> |
| 282 | + <Button |
| 283 | + variant="destructive" |
| 284 | + onClick={() => { |
| 285 | + if (deleteTarget && onDelete) { |
| 286 | + onDelete(deleteTarget.id) |
| 287 | + setDeleteTarget(null) |
| 288 | + } |
| 289 | + }} |
| 290 | + > |
| 291 | + Delete |
| 292 | + </Button> |
| 293 | + </DialogFooter> |
| 294 | + </DialogContent> |
| 295 | + </Dialog> |
228 | 296 | </div> |
229 | 297 | ) |
230 | 298 | } |
0 commit comments