398 lines
12 KiB
TypeScript
398 lines
12 KiB
TypeScript
"use client"
|
|
|
|
import { useState, useEffect } from 'react'
|
|
import { useRouter } from 'next/navigation'
|
|
import { Plus, MoreHorizontal, ArrowUpDown, ChevronDown } from 'lucide-react'
|
|
import {
|
|
ColumnDef,
|
|
ColumnFiltersState,
|
|
flexRender,
|
|
getCoreRowModel,
|
|
getFilteredRowModel,
|
|
getPaginationRowModel,
|
|
getSortedRowModel,
|
|
SortingState,
|
|
useReactTable,
|
|
VisibilityState,
|
|
} from "@tanstack/react-table"
|
|
|
|
import { Button } from '@/components/ui/button'
|
|
import { Input } from '@/components/ui/input'
|
|
import { Badge } from '@/components/ui/badge'
|
|
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
|
|
import {
|
|
Table,
|
|
TableBody,
|
|
TableCell,
|
|
TableHead,
|
|
TableHeader,
|
|
TableRow,
|
|
} from '@/components/ui/table'
|
|
import {
|
|
DropdownMenu,
|
|
DropdownMenuCheckboxItem,
|
|
DropdownMenuContent,
|
|
DropdownMenuItem,
|
|
DropdownMenuLabel,
|
|
DropdownMenuSeparator,
|
|
DropdownMenuTrigger,
|
|
} from '@/components/ui/dropdown-menu'
|
|
import { useToast } from '@/hooks/use-toast'
|
|
import type { Artist } from '@/types/database'
|
|
|
|
export default function ArtistsPage() {
|
|
const router = useRouter()
|
|
const { toast } = useToast()
|
|
const [artists, setArtists] = useState<Artist[]>([])
|
|
const [loading, setLoading] = useState(true)
|
|
const [sorting, setSorting] = useState<SortingState>([])
|
|
const [columnFilters, setColumnFilters] = useState<ColumnFiltersState>([])
|
|
const [columnVisibility, setColumnVisibility] = useState<VisibilityState>({})
|
|
const [rowSelection, setRowSelection] = useState({})
|
|
|
|
// Define columns for the data table
|
|
const columns: ColumnDef<Artist>[] = [
|
|
{
|
|
accessorKey: "name",
|
|
header: ({ column }) => {
|
|
return (
|
|
<Button
|
|
variant="ghost"
|
|
onClick={() => column.toggleSorting(column.getIsSorted() === "asc")}
|
|
>
|
|
Name
|
|
<ArrowUpDown className="ml-2 h-4 w-4" />
|
|
</Button>
|
|
)
|
|
},
|
|
cell: ({ row }) => (
|
|
<div className="font-medium">{row.getValue("name")}</div>
|
|
),
|
|
},
|
|
{
|
|
accessorKey: "specialties",
|
|
header: "Specialties",
|
|
cell: ({ row }) => {
|
|
const specialties = row.getValue("specialties") as string
|
|
const specialtiesArray = specialties ? JSON.parse(specialties) : []
|
|
return (
|
|
<div className="flex flex-wrap gap-1">
|
|
{specialtiesArray.slice(0, 2).map((specialty: string) => (
|
|
<Badge key={specialty} variant="secondary" className="text-xs">
|
|
{specialty}
|
|
</Badge>
|
|
))}
|
|
{specialtiesArray.length > 2 && (
|
|
<Badge variant="outline" className="text-xs">
|
|
+{specialtiesArray.length - 2}
|
|
</Badge>
|
|
)}
|
|
</div>
|
|
)
|
|
},
|
|
},
|
|
{
|
|
accessorKey: "hourlyRate",
|
|
header: ({ column }) => {
|
|
return (
|
|
<Button
|
|
variant="ghost"
|
|
onClick={() => column.toggleSorting(column.getIsSorted() === "asc")}
|
|
>
|
|
Rate
|
|
<ArrowUpDown className="ml-2 h-4 w-4" />
|
|
</Button>
|
|
)
|
|
},
|
|
cell: ({ row }) => {
|
|
const rate = row.getValue("hourlyRate") as number
|
|
return rate ? `$${rate}/hr` : 'Not set'
|
|
},
|
|
},
|
|
{
|
|
accessorKey: "isActive",
|
|
header: "Status",
|
|
cell: ({ row }) => {
|
|
const isActive = row.getValue("isActive") as boolean
|
|
return (
|
|
<Badge variant={isActive ? "default" : "secondary"}>
|
|
{isActive ? "Active" : "Inactive"}
|
|
</Badge>
|
|
)
|
|
},
|
|
},
|
|
{
|
|
accessorKey: "createdAt",
|
|
header: "Created",
|
|
cell: ({ row }) => {
|
|
const date = new Date(row.getValue("createdAt"))
|
|
return date.toLocaleDateString()
|
|
},
|
|
},
|
|
{
|
|
id: "actions",
|
|
enableHiding: false,
|
|
cell: ({ row }) => {
|
|
const artist = row.original
|
|
|
|
return (
|
|
<DropdownMenu>
|
|
<DropdownMenuTrigger asChild>
|
|
<Button variant="ghost" className="h-8 w-8 p-0">
|
|
<span className="sr-only">Open menu</span>
|
|
<MoreHorizontal className="h-4 w-4" />
|
|
</Button>
|
|
</DropdownMenuTrigger>
|
|
<DropdownMenuContent align="end">
|
|
<DropdownMenuLabel>Actions</DropdownMenuLabel>
|
|
<DropdownMenuItem
|
|
onClick={() => router.push(`/admin/artists/${artist.id}`)}
|
|
>
|
|
Edit artist
|
|
</DropdownMenuItem>
|
|
<DropdownMenuItem
|
|
onClick={() => router.push(`/admin/artists/${artist.id}/portfolio`)}
|
|
>
|
|
Manage portfolio
|
|
</DropdownMenuItem>
|
|
<DropdownMenuSeparator />
|
|
<DropdownMenuItem
|
|
onClick={() => handleToggleStatus(artist)}
|
|
className={artist.isActive ? "text-red-600" : "text-green-600"}
|
|
>
|
|
{artist.isActive ? "Deactivate" : "Activate"}
|
|
</DropdownMenuItem>
|
|
</DropdownMenuContent>
|
|
</DropdownMenu>
|
|
)
|
|
},
|
|
},
|
|
]
|
|
|
|
const table = useReactTable({
|
|
data: artists,
|
|
columns,
|
|
onSortingChange: setSorting,
|
|
onColumnFiltersChange: setColumnFilters,
|
|
getCoreRowModel: getCoreRowModel(),
|
|
getPaginationRowModel: getPaginationRowModel(),
|
|
getSortedRowModel: getSortedRowModel(),
|
|
getFilteredRowModel: getFilteredRowModel(),
|
|
onColumnVisibilityChange: setColumnVisibility,
|
|
onRowSelectionChange: setRowSelection,
|
|
state: {
|
|
sorting,
|
|
columnFilters,
|
|
columnVisibility,
|
|
rowSelection,
|
|
},
|
|
})
|
|
|
|
const fetchArtists = async () => {
|
|
try {
|
|
const response = await fetch('/api/artists')
|
|
if (!response.ok) throw new Error('Failed to fetch artists')
|
|
const data = await response.json()
|
|
setArtists(data.artists || [])
|
|
} catch (error) {
|
|
console.error('Error fetching artists:', error)
|
|
toast({
|
|
title: 'Error',
|
|
description: 'Failed to load artists',
|
|
variant: 'destructive',
|
|
})
|
|
} finally {
|
|
setLoading(false)
|
|
}
|
|
}
|
|
|
|
const handleToggleStatus = async (artist: Artist) => {
|
|
try {
|
|
const response = await fetch(`/api/artists/${artist.id}`, {
|
|
method: 'PUT',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
},
|
|
body: JSON.stringify({
|
|
isActive: !artist.isActive,
|
|
}),
|
|
})
|
|
|
|
if (!response.ok) throw new Error('Failed to update artist')
|
|
|
|
toast({
|
|
title: 'Success',
|
|
description: `Artist ${artist.isActive ? 'deactivated' : 'activated'} successfully`,
|
|
})
|
|
|
|
// Refresh the list
|
|
fetchArtists()
|
|
} catch (error) {
|
|
console.error('Error updating artist:', error)
|
|
toast({
|
|
title: 'Error',
|
|
description: 'Failed to update artist status',
|
|
variant: 'destructive',
|
|
})
|
|
}
|
|
}
|
|
|
|
useEffect(() => {
|
|
fetchArtists()
|
|
}, [])
|
|
|
|
if (loading) {
|
|
return (
|
|
<div className="flex items-center justify-center h-64">
|
|
<div className="text-lg">Loading artists...</div>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
return (
|
|
<div className="space-y-6">
|
|
<div className="flex items-center justify-between">
|
|
<div>
|
|
<h1 className="text-3xl font-bold tracking-tight">Artists</h1>
|
|
<p className="text-muted-foreground">
|
|
Manage your tattoo artists and their information
|
|
</p>
|
|
</div>
|
|
<Button onClick={() => router.push('/admin/artists/new')}>
|
|
<Plus className="mr-2 h-4 w-4" />
|
|
Add Artist
|
|
</Button>
|
|
</div>
|
|
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle>All Artists</CardTitle>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<div className="space-y-4">
|
|
{/* Filters and Controls */}
|
|
<div className="flex items-center justify-between">
|
|
<div className="flex items-center space-x-2">
|
|
<Input
|
|
placeholder="Filter artists..."
|
|
value={(table.getColumn("name")?.getFilterValue() as string) ?? ""}
|
|
onChange={(event) =>
|
|
table.getColumn("name")?.setFilterValue(event.target.value)
|
|
}
|
|
className="max-w-sm"
|
|
/>
|
|
</div>
|
|
<DropdownMenu>
|
|
<DropdownMenuTrigger asChild>
|
|
<Button variant="outline">
|
|
Columns <ChevronDown className="ml-2 h-4 w-4" />
|
|
</Button>
|
|
</DropdownMenuTrigger>
|
|
<DropdownMenuContent align="end">
|
|
{table
|
|
.getAllColumns()
|
|
.filter((column) => column.getCanHide())
|
|
.map((column) => {
|
|
return (
|
|
<DropdownMenuCheckboxItem
|
|
key={column.id}
|
|
className="capitalize"
|
|
checked={column.getIsVisible()}
|
|
onCheckedChange={(value) =>
|
|
column.toggleVisibility(!!value)
|
|
}
|
|
>
|
|
{column.id}
|
|
</DropdownMenuCheckboxItem>
|
|
)
|
|
})}
|
|
</DropdownMenuContent>
|
|
</DropdownMenu>
|
|
</div>
|
|
|
|
{/* Data Table */}
|
|
<div className="rounded-md border">
|
|
<Table>
|
|
<TableHeader>
|
|
{table.getHeaderGroups().map((headerGroup) => (
|
|
<TableRow key={headerGroup.id}>
|
|
{headerGroup.headers.map((header) => {
|
|
return (
|
|
<TableHead key={header.id}>
|
|
{header.isPlaceholder
|
|
? null
|
|
: flexRender(
|
|
header.column.columnDef.header,
|
|
header.getContext()
|
|
)}
|
|
</TableHead>
|
|
)
|
|
})}
|
|
</TableRow>
|
|
))}
|
|
</TableHeader>
|
|
<TableBody>
|
|
{table.getRowModel().rows?.length ? (
|
|
table.getRowModel().rows.map((row) => (
|
|
<TableRow
|
|
key={row.id}
|
|
data-state={row.getIsSelected() && "selected"}
|
|
className="cursor-pointer"
|
|
onClick={() => router.push(`/admin/artists/${row.original.id}`)}
|
|
>
|
|
{row.getVisibleCells().map((cell) => (
|
|
<TableCell key={cell.id}>
|
|
{flexRender(
|
|
cell.column.columnDef.cell,
|
|
cell.getContext()
|
|
)}
|
|
</TableCell>
|
|
))}
|
|
</TableRow>
|
|
))
|
|
) : (
|
|
<TableRow>
|
|
<TableCell
|
|
colSpan={columns.length}
|
|
className="h-24 text-center"
|
|
>
|
|
No artists found.
|
|
</TableCell>
|
|
</TableRow>
|
|
)}
|
|
</TableBody>
|
|
</Table>
|
|
</div>
|
|
|
|
{/* Pagination */}
|
|
<div className="flex items-center justify-end space-x-2">
|
|
<div className="text-muted-foreground flex-1 text-sm">
|
|
{table.getFilteredSelectedRowModel().rows.length} of{" "}
|
|
{table.getFilteredRowModel().rows.length} row(s) selected.
|
|
</div>
|
|
<div className="space-x-2">
|
|
<Button
|
|
variant="outline"
|
|
size="sm"
|
|
onClick={() => table.previousPage()}
|
|
disabled={!table.getCanPreviousPage()}
|
|
>
|
|
Previous
|
|
</Button>
|
|
<Button
|
|
variant="outline"
|
|
size="sm"
|
|
onClick={() => table.nextPage()}
|
|
disabled={!table.getCanNextPage()}
|
|
>
|
|
Next
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
</div>
|
|
)
|
|
}
|