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
171 lines
4.9 KiB
TypeScript
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) })
|
|
},
|
|
})
|
|
}
|