united-tattoo/hooks/use-artist-data.ts
Nicholai 43b336acf9 feat: Phase 1 - Artist profile database refactor with API foundation
Implements backend infrastructure for loading artist profiles from Cloudflare D1 database instead of static data.

Database Changes:
- Add slug column migration for SEO-friendly URLs (0001_add_artist_slug.sql)
- Enhanced data migration script with slug generation
- Support for all artist fields from data/artists.ts

Type Definitions:
- Add slug field to Artist interface
- Create ArtistWithPortfolio type for full artist data
- Create PublicArtist type for sanitized API responses
- Add ArtistFilters type for query parameters
- Add ArtistDashboardStats for analytics

Database Functions (lib/db.ts):
- getPublicArtists() - fetch active artists with portfolio and filtering
- getArtistWithPortfolio() - fetch single artist with full portfolio
- getArtistBySlug() - fetch by URL-friendly slug
- getArtistByUserId() - fetch by user ID for dashboard
- Enhanced getArtists() with JSON parsing

API Endpoints:
- Updated GET /api/artists - filtering, pagination, portfolio images
- Created GET /api/artists/[id] - fetch by ID or slug
- Created PUT /api/artists/[id] - update with authorization
- Created DELETE /api/artists/[id] - soft delete (admin only)
- Created GET /api/artists/me - current artist profile

React Hooks (hooks/use-artist-data.ts):
- useArtists() - fetch with filtering
- useArtist() - fetch single artist
- useCurrentArtist() - logged-in artist
- useUpdateArtist(), useCreateArtist(), useDeleteArtist() - mutations

Frontend Components:
- Refactored artists-grid.tsx to use API with loading/error states
- Use database field names (slug, specialties, portfolioImages)
- Display profile images from portfolio
- Client-side filtering by specialty

Files Modified:
- sql/migrations/0001_add_artist_slug.sql (new)
- types/database.ts (enhanced)
- lib/data-migration.ts (enhanced)
- lib/db.ts (enhanced)
- app/api/artists/route.ts (updated)
- app/api/artists/[id]/route.ts (new)
- app/api/artists/me/route.ts (new)
- hooks/use-artist-data.ts (new)
- components/artists-grid.tsx (refactored)

Remaining work: Artist portfolio page, artist dashboard, admin enhancements

Ref: artist_profile_refactor_implementation_plan.md
2025-10-06 03:53:28 -06:00

171 lines
4.9 KiB
TypeScript

import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
import type { PublicArtist, ArtistWithPortfolio, Artist } from '@/types/database'
// Query keys for cache management
export const artistKeys = {
all: ['artists'] as const,
lists: () => [...artistKeys.all, 'list'] as const,
list: (filters?: Record<string, any>) => [...artistKeys.lists(), filters] as const,
details: () => [...artistKeys.all, 'detail'] as const,
detail: (id: string) => [...artistKeys.details(), id] as const,
me: () => [...artistKeys.all, 'me'] as const,
}
// Fetch all artists
export function useArtists(filters?: {
specialty?: string
search?: string
limit?: number
page?: number
}) {
return useQuery({
queryKey: artistKeys.list(filters),
queryFn: async () => {
const params = new URLSearchParams()
if (filters?.specialty) params.append('specialty', filters.specialty)
if (filters?.search) params.append('search', filters.search)
if (filters?.limit) params.append('limit', filters.limit.toString())
if (filters?.page) params.append('page', filters.page.toString())
const response = await fetch(`/api/artists?${params.toString()}`)
if (!response.ok) {
throw new Error('Failed to fetch artists')
}
const data = await response.json()
return data.artists as PublicArtist[]
},
staleTime: 1000 * 60 * 5, // 5 minutes
})
}
// Fetch single artist by ID or slug
export function useArtist(id: string | undefined) {
return useQuery({
queryKey: artistKeys.detail(id || ''),
queryFn: async () => {
if (!id) return null
const response = await fetch(`/api/artists/${id}`)
if (!response.ok) {
if (response.status === 404) return null
throw new Error('Failed to fetch artist')
}
return response.json() as Promise<ArtistWithPortfolio>
},
enabled: !!id,
staleTime: 1000 * 60 * 5, // 5 minutes
})
}
// Fetch current artist (for artist dashboard)
export function useCurrentArtist() {
return useQuery({
queryKey: artistKeys.me(),
queryFn: async () => {
const response = await fetch('/api/artists/me')
if (!response.ok) {
if (response.status === 401 || response.status === 403) {
return null
}
throw new Error('Failed to fetch artist profile')
}
return response.json() as Promise<Artist>
},
staleTime: 1000 * 60 * 5, // 5 minutes
retry: false, // Don't retry on auth errors
})
}
// Update artist mutation
export function useUpdateArtist() {
const queryClient = useQueryClient()
return useMutation({
mutationFn: async ({ id, data }: { id: string; data: Partial<Artist> }) => {
const response = await fetch(`/api/artists/${id}`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(data),
})
if (!response.ok) {
const error = await response.json()
throw new Error(error.error || 'Failed to update artist')
}
return response.json() as Promise<Artist>
},
onSuccess: (data, variables) => {
// Invalidate and refetch
queryClient.invalidateQueries({ queryKey: artistKeys.detail(variables.id) })
queryClient.invalidateQueries({ queryKey: artistKeys.lists() })
queryClient.invalidateQueries({ queryKey: artistKeys.me() })
},
})
}
// Create artist mutation (admin only)
export function useCreateArtist() {
const queryClient = useQueryClient()
return useMutation({
mutationFn: async (data: {
name: string
bio: string
specialties: string[]
instagramHandle?: string
hourlyRate?: number
email?: string
}) => {
const response = await fetch('/api/artists', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(data),
})
if (!response.ok) {
const error = await response.json()
throw new Error(error.error || 'Failed to create artist')
}
return response.json() as Promise<Artist>
},
onSuccess: () => {
// Invalidate artists list
queryClient.invalidateQueries({ queryKey: artistKeys.lists() })
},
})
}
// Delete artist mutation (admin only)
export function useDeleteArtist() {
const queryClient = useQueryClient()
return useMutation({
mutationFn: async (id: string) => {
const response = await fetch(`/api/artists/${id}`, {
method: 'DELETE',
})
if (!response.ok) {
const error = await response.json()
throw new Error(error.error || 'Failed to delete artist')
}
return response.json()
},
onSuccess: (_, id) => {
// Invalidate queries
queryClient.invalidateQueries({ queryKey: artistKeys.lists() })
queryClient.removeQueries({ queryKey: artistKeys.detail(id) })
},
})
}