diff --git a/src/app/releases/page.tsx b/src/app/releases/page.tsx index 2ada437..c439396 100644 --- a/src/app/releases/page.tsx +++ b/src/app/releases/page.tsx @@ -2,7 +2,7 @@ import Link from 'next/link'; import { Plus } from 'lucide-react'; import { Button } from '@/components/ui/button'; import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'; -import { ReleaseCard } from '@/components/releases/release-card'; +import { ReleaseList } from '@/components/releases/release-list'; import { listReleases } from '@/lib/actions/releases'; export default async function ReleasesPage() { @@ -43,45 +43,15 @@ export default async function ReleasesPage() { - {active.length === 0 ? ( -
-

No active releases.

-
- ) : ( -
- {active.map((release) => ( - - ))} -
- )} +
- {drafts.length === 0 ? ( -
-

No draft releases.

-
- ) : ( -
- {drafts.map((release) => ( - - ))} -
- )} +
- {archived.length === 0 ? ( -
-

No archived releases.

-
- ) : ( -
- {archived.map((release) => ( - - ))} -
- )} +
diff --git a/src/components/releases/release-list.tsx b/src/components/releases/release-list.tsx new file mode 100644 index 0000000..8970153 --- /dev/null +++ b/src/components/releases/release-list.tsx @@ -0,0 +1,279 @@ +'use client'; + +import Link from 'next/link'; +import { useState } from 'react'; +import { Package, Play, Archive, Copy, Edit, Trash2, ChevronDown, ChevronUp, ArrowUpDown } from 'lucide-react'; +import { Button } from '@/components/ui/button'; +import { Badge } from '@/components/ui/badge'; +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from '@/components/ui/table'; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, +} from '@/components/ui/dropdown-menu'; +import type { Release } from '@/lib/db/schema'; + +interface ReleaseListProps { + releases: Release[]; + showActions?: boolean; +} + +type SortField = 'name' | 'type' | 'status' | 'createdAt' | 'releaseDate'; +type SortDirection = 'asc' | 'desc'; + +const typeColors: Record = { + onboarding: 'bg-purple-100 text-purple-800', + release: 'bg-blue-100 text-blue-800', + hotfix: 'bg-red-100 text-red-800', +}; + +const statusColors: Record = { + draft: 'bg-slate-100 text-slate-800', + active: 'bg-green-100 text-green-800', + archived: 'bg-gray-100 text-gray-800', +}; + +function getTypeColor(type: string) { + return typeColors[type] || 'bg-slate-100 text-slate-800'; +} + +function getStatusColor(status: string) { + return statusColors[status] || 'bg-slate-100 text-slate-800'; +} + +export function ReleaseList({ releases, showActions = true }: ReleaseListProps) { + const [sortField, setSortField] = useState('createdAt'); + const [sortDirection, setSortDirection] = useState('desc'); + + const handleSort = (field: SortField) => { + if (sortField === field) { + setSortDirection(sortDirection === 'asc' ? 'desc' : 'asc'); + } else { + setSortField(field); + setSortDirection('asc'); + } + }; + + const sortedReleases = [...releases].sort((a, b) => { + let comparison = 0; + + switch (sortField) { + case 'name': + comparison = a.name.localeCompare(b.name); + break; + case 'type': + comparison = (a.type || '').localeCompare(b.type || ''); + break; + case 'status': + comparison = (a.status || '').localeCompare(b.status || ''); + break; + case 'createdAt': + comparison = new Date(a.createdAt || 0).getTime() - new Date(b.createdAt || 0).getTime(); + break; + case 'releaseDate': + const dateA = a.releaseDate ? new Date(a.releaseDate).getTime() : 0; + const dateB = b.releaseDate ? new Date(b.releaseDate).getTime() : 0; + comparison = dateA - dateB; + break; + } + + return sortDirection === 'asc' ? comparison : -comparison; + }); + + const SortIcon = ({ field }: { field: SortField }) => { + if (sortField !== field) { + return ; + } + return sortDirection === 'asc' + ? + : ; + }; + + async function handleActivate(id: number) { + try { + const { activateRelease } = await import('@/lib/actions/releases'); + await activateRelease(id); + window.location.reload(); + } catch (error) { + alert(error instanceof Error ? error.message : 'Failed to activate release'); + } + } + + async function handleArchive(id: number) { + try { + const { archiveRelease } = await import('@/lib/actions/releases'); + await archiveRelease(id); + window.location.reload(); + } catch (error) { + alert(error instanceof Error ? error.message : 'Failed to archive release'); + } + } + + async function handleClone(release: Release) { + try { + const { cloneRelease } = await import('@/lib/actions/releases'); + const newName = `${release.name} (Copy)`; + await cloneRelease(release.id, newName); + window.location.reload(); + } catch (error) { + alert(error instanceof Error ? error.message : 'Failed to clone release'); + } + } + + if (releases.length === 0) { + return ( +
+

No releases found.

+
+ ); + } + + return ( +
+ + + + + handleSort('name')} + > +
+ Name + +
+
+ handleSort('type')} + > +
+ Type + +
+
+ handleSort('status')} + > +
+ Status + +
+
+ Version + handleSort('releaseDate')} + > +
+ Release Date + +
+
+ handleSort('createdAt')} + > +
+ Created + +
+
+ {showActions && Actions} +
+
+ + {sortedReleases.map((release) => ( + + +
+ +
+
+ + + {release.name} + + {release.description && ( +

+ {release.description} +

+ )} +
+ + + {release.type} + + + + + {release.status} + + + + {release.versionNumber || '-'} + + + {release.releaseDate + ? new Date(release.releaseDate).toLocaleDateString() + : '-'} + + + {release.createdAt + ? new Date(release.createdAt).toLocaleDateString() + : '-'} + + {showActions && ( + + + + + + + {release.status === 'draft' && ( + handleActivate(release.id)}> + + Activate + + )} + handleClone(release)}> + + Clone + + {release.status === 'active' && ( + handleArchive(release.id)}> + + Archive + + )} + + + + Edit + + + + + + )} +
+ ))} +
+
+
+ ); +}