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 ? (
-
- ) : (
-
- {active.map((release) => (
-
- ))}
-
- )}
+
- {drafts.length === 0 ? (
-
- ) : (
-
- {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 (
+
+ );
+ }
+
+ 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
+
+
+
+
+
+ )}
+
+ ))}
+
+
+
+ );
+}