From 43b336acf9c67286e29bc15962eb34c3a96120be Mon Sep 17 00:00:00 2001 From: Nicholai Date: Mon, 6 Oct 2025 03:53:28 -0600 Subject: [PATCH] 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 --- app/api/artists/[id]/route.ts | 142 ++--- app/api/artists/me/route.ts | 52 ++ app/api/artists/route.ts | 54 +- ...st_profile_refactor_implementation_plan.md | 512 ++++++++++++++++++ components/artists-grid.tsx | 292 +++++----- dist/README.md | 2 +- hooks/use-artist-data.ts | 170 ++++++ lib/data-migration.ts | 21 +- lib/db.ts | 192 ++++++- sql/migrations/0001_add_artist_slug.sql | 10 + tsconfig.json | 2 +- types/database.ts | 37 ++ wrangler.toml | 8 +- 13 files changed, 1196 insertions(+), 298 deletions(-) create mode 100644 app/api/artists/me/route.ts create mode 100644 artist_profile_refactor_implementation_plan.md create mode 100644 hooks/use-artist-data.ts create mode 100644 sql/migrations/0001_add_artist_slug.sql diff --git a/app/api/artists/[id]/route.ts b/app/api/artists/[id]/route.ts index 3e659aa99..f64e5b0a3 100644 --- a/app/api/artists/[id]/route.ts +++ b/app/api/artists/[id]/route.ts @@ -2,55 +2,34 @@ import { NextRequest, NextResponse } from "next/server" import { requireAuth } from "@/lib/auth" import { UserRole } from "@/types/database" import { updateArtistSchema } from "@/lib/validations" -import { db } from "@/lib/db" -import { Flags } from "@/lib/flags" +import { getArtistWithPortfolio, getArtistBySlug, updateArtist, deleteArtist } from "@/lib/db" -// GET /api/artists/[id] - Fetch a specific artist +export const dynamic = "force-dynamic"; + +// GET /api/artists/[id] - Fetch single artist with portfolio export async function GET( request: NextRequest, - { params }: { params: { id: string } } + { params }: { params: { id: string } }, + context?: any ) { try { const { id } = params - - // TODO: Implement via Supabase MCP - // const artist = await db.artists.findUnique(id) - // Mock response for now - const mockArtist = { - id, - userId: "user-1", - name: "Alex Rivera", - bio: "Specializing in traditional and neo-traditional tattoos with over 8 years of experience.", - specialties: ["Traditional", "Neo-Traditional", "Color Work"], - instagramHandle: "alexrivera_tattoo", - isActive: true, - hourlyRate: 150, - portfolioImages: [ - { - id: "img-1", - artistId: id, - url: "/artists/alex-rivera-traditional-rose.jpg", - caption: "Traditional rose tattoo", - tags: ["traditional", "rose", "color"], - order: 1, - isPublic: true, - createdAt: new Date(), - }, - ], - availability: [], - createdAt: new Date(), - updatedAt: new Date(), + // Try to fetch by ID first, then by slug + let artist = await getArtistWithPortfolio(id, context?.env) + + if (!artist) { + artist = await getArtistBySlug(id, context?.env) } - - if (!mockArtist) { + + if (!artist) { return NextResponse.json( { error: "Artist not found" }, { status: 404 } ) } - return NextResponse.json(mockArtist) + return NextResponse.json(artist) } catch (error) { console.error("Error fetching artist:", error) return NextResponse.json( @@ -60,42 +39,50 @@ export async function GET( } } -// PUT /api/artists/[id] - Update a specific artist (Admin only) +// PUT /api/artists/[id] - Update artist (admin or artist themselves) export async function PUT( request: NextRequest, - { params }: { params: { id: string } } + { params }: { params: { id: string } }, + context?: any ) { try { - if (!Flags.ARTISTS_MODULE_ENABLED) { - return NextResponse.json({ error: 'Artists module disabled' }, { status: 503 }) - } - // Require admin authentication - const session = await requireAuth(UserRole.SHOP_ADMIN) - const { id } = params - const body = await request.json() - const validatedData = updateArtistSchema.parse({ ...body, id }) - - // TODO: Implement via Supabase MCP - // const updatedArtist = await db.artists.update(id, validatedData) + const session = await requireAuth() - // Mock response for now - const mockUpdatedArtist = { - id, - userId: "user-1", - name: validatedData.name || "Alex Rivera", - bio: validatedData.bio || "Updated bio", - specialties: validatedData.specialties || ["Traditional"], - instagramHandle: validatedData.instagramHandle, - isActive: validatedData.isActive ?? true, - hourlyRate: validatedData.hourlyRate, - portfolioImages: [], - availability: [], - createdAt: new Date("2024-01-01"), - updatedAt: new Date(), + // Get the artist to check ownership + const artist = await getArtistWithPortfolio(id, context?.env) + if (!artist) { + return NextResponse.json( + { error: "Artist not found" }, + { status: 404 } + ) + } + + // Check authorization: must be the artist themselves or an admin + const isOwner = artist.userId === session.user.id + const isAdmin = [UserRole.SUPER_ADMIN, UserRole.SHOP_ADMIN].includes(session.user.role) + + if (!isOwner && !isAdmin) { + return NextResponse.json( + { error: "Insufficient permissions" }, + { status: 403 } + ) + } + + const body = await request.json() + const validatedData = updateArtistSchema.parse(body) + + // If artist is updating themselves (not admin), restrict what they can change + let updateData = validatedData + if (isOwner && !isAdmin) { + // Artists can only update: bio, specialties, instagramHandle, hourlyRate + const { bio, specialties, instagramHandle, hourlyRate } = validatedData + updateData = { bio, specialties, instagramHandle, hourlyRate } } - return NextResponse.json(mockUpdatedArtist) + const updatedArtist = await updateArtist(id, updateData, context?.env) + + return NextResponse.json(updatedArtist) } catch (error) { console.error("Error updating artist:", error) @@ -106,12 +93,6 @@ export async function PUT( { status: 401 } ) } - if (error.message.includes("Insufficient permissions")) { - return NextResponse.json( - { error: "Insufficient permissions" }, - { status: 403 } - ) - } } return NextResponse.json( @@ -121,30 +102,21 @@ export async function PUT( } } -// DELETE /api/artists/[id] - Delete a specific artist (Admin only) +// DELETE /api/artists/[id] - Soft delete artist (admin only) export async function DELETE( request: NextRequest, - { params }: { params: { id: string } } + { params }: { params: { id: string } }, + context?: any ) { try { - if (!Flags.ARTISTS_MODULE_ENABLED) { - return NextResponse.json({ error: 'Artists module disabled' }, { status: 503 }) - } + const { id } = params + // Require admin authentication await requireAuth(UserRole.SHOP_ADMIN) - const { id } = params + await deleteArtist(id, context?.env) - // TODO: Implement via Supabase MCP - // await db.artists.delete(id) - - // Mock response for now - console.log(`Artist ${id} would be deleted`) - - return NextResponse.json( - { message: "Artist deleted successfully" }, - { status: 200 } - ) + return NextResponse.json({ success: true }) } catch (error) { console.error("Error deleting artist:", error) diff --git a/app/api/artists/me/route.ts b/app/api/artists/me/route.ts new file mode 100644 index 000000000..20197eb1c --- /dev/null +++ b/app/api/artists/me/route.ts @@ -0,0 +1,52 @@ +import { NextRequest, NextResponse } from "next/server" +import { requireAuth } from "@/lib/auth" +import { UserRole } from "@/types/database" +import { getArtistByUserId } from "@/lib/db" + +export const dynamic = "force-dynamic"; + +// GET /api/artists/me - Get current logged-in artist's profile +export async function GET( + request: NextRequest, + { params }: { params?: any } = {}, + context?: any +) { + try { + // Require artist authentication + const session = await requireAuth(UserRole.ARTIST) + + // Fetch artist data by user ID + const artist = await getArtistByUserId(session.user.id, context?.env) + + if (!artist) { + return NextResponse.json( + { error: "Artist profile not found" }, + { status: 404 } + ) + } + + return NextResponse.json(artist) + } catch (error) { + console.error("Error fetching artist profile:", error) + + if (error instanceof Error) { + if (error.message.includes("Authentication required")) { + return NextResponse.json( + { error: "Authentication required" }, + { status: 401 } + ) + } + if (error.message.includes("Insufficient permissions")) { + return NextResponse.json( + { error: "You must be an artist to access this endpoint" }, + { status: 403 } + ) + } + } + + return NextResponse.json( + { error: "Failed to fetch artist profile" }, + { status: 500 } + ) + } +} diff --git a/app/api/artists/route.ts b/app/api/artists/route.ts index 3ef0ffbbf..d3b87db21 100644 --- a/app/api/artists/route.ts +++ b/app/api/artists/route.ts @@ -1,8 +1,8 @@ import { NextRequest, NextResponse } from "next/server" import { requireAuth } from "@/lib/auth" -import { UserRole } from "@/types/database" +import { UserRole, ArtistFilters } from "@/types/database" import { createArtistSchema, paginationSchema, artistFiltersSchema } from "@/lib/validations" -import { getArtists, createArtist } from "@/lib/db" +import { getPublicArtists, createArtist } from "@/lib/db" import { Flags } from "@/lib/flags" export const dynamic = "force-dynamic"; @@ -15,7 +15,7 @@ export async function GET(request: NextRequest, { params }: { params?: any } = { // Parse and validate query parameters const pagination = paginationSchema.parse({ page: searchParams.get("page") || "1", - limit: searchParams.get("limit") || "10", + limit: searchParams.get("limit") || "50", // Increased default for artists grid }) const filters = artistFiltersSchema.parse({ @@ -24,46 +24,28 @@ export async function GET(request: NextRequest, { params }: { params?: any } = { search: searchParams.get("search"), }) - // Fetch artists from database with environment context - const artists = await getArtists(context?.env) - - // Apply filters - let filteredArtists = artists - - if (filters.isActive !== undefined) { - filteredArtists = filteredArtists.filter(artist => - artist.isActive === filters.isActive - ) + // Build filters for database query + const dbFilters: ArtistFilters = { + specialty: filters.specialty || undefined, + search: filters.search || undefined, + isActive: filters.isActive !== undefined ? filters.isActive : true, + limit: pagination.limit, + offset: (pagination.page - 1) * pagination.limit, } + + // Fetch artists from database with portfolio images + const artists = await getPublicArtists(dbFilters, context?.env) - if (filters.specialty) { - filteredArtists = filteredArtists.filter(artist => - artist.specialties.some(specialty => - specialty.toLowerCase().includes(filters.specialty!.toLowerCase()) - ) - ) - } - - if (filters.search) { - const searchTerm = filters.search.toLowerCase() - filteredArtists = filteredArtists.filter(artist => - artist.name.toLowerCase().includes(searchTerm) || - artist.bio.toLowerCase().includes(searchTerm) - ) - } - - // Apply pagination - const startIndex = (pagination.page - 1) * pagination.limit - const endIndex = startIndex + pagination.limit - const paginatedArtists = filteredArtists.slice(startIndex, endIndex) + // Get total count for pagination (this is a simplified approach) + // In production, you'd want a separate count query + const hasMore = artists.length === pagination.limit return NextResponse.json({ - artists: paginatedArtists, + artists, pagination: { page: pagination.page, limit: pagination.limit, - total: filteredArtists.length, - totalPages: Math.ceil(filteredArtists.length / pagination.limit), + hasMore, }, filters, }) diff --git a/artist_profile_refactor_implementation_plan.md b/artist_profile_refactor_implementation_plan.md new file mode 100644 index 000000000..e73aa102b --- /dev/null +++ b/artist_profile_refactor_implementation_plan.md @@ -0,0 +1,512 @@ +# Artist Profile Refactor Implementation Plan + +## Overview +Refactor the artists grid and individual artist profile pages to load content from a Cloudflare D1 database instead of static data. Enable artists to log into backend profiles where they can manage their portfolio images, bio, and other information. The existing `data/artists.ts` will serve as seed data for the database. + +This implementation connects the existing database schema, API routes, and authentication system with the public-facing components, while also creating an artist dashboard for self-service portfolio management. + +## Types +Database and API type definitions for artist profile management. + +**Key Type Updates:** +- Ensure `Artist` interface in `types/database.ts` matches D1 schema columns +- Add `ArtistWithPortfolio` type that includes populated `portfolioImages` array +- Create `PublicArtist` type for sanitized public API responses +- Add `ArtistDashboardStats` type for artist-specific analytics + +**New Type Definitions:** +```typescript +// types/database.ts additions +export interface ArtistWithPortfolio extends Artist { + portfolioImages: PortfolioImage[] + user?: { + name: string + email: string + avatar?: string + } +} + +export interface PublicArtist { + id: string + name: string + bio: string + specialties: string[] + instagramHandle?: string + portfolioImages: PortfolioImage[] + isActive: boolean + hourlyRate?: number +} + +export interface ArtistDashboardStats { + totalImages: number + activeImages: number + profileViews?: number + lastUpdated: Date +} +``` + +## Files + +### Files to Modify + +**1. `lib/db.ts`** +- Add `getArtistWithPortfolio(id: string)` function that joins artists with portfolio_images +- Add `getPublicArtists()` function that returns only active artists with public portfolio images +- Add `getArtistByUserId(userId: string)` function for artist dashboard access +- Update `getArtists()` to parse JSON fields properly (specialties) + +**2. `app/api/artists/route.ts`** +- Update GET handler to return artists with portfolio images +- Add pagination support to prevent loading all artists at once +- Add filtering by specialty and search query +- Ensure responses match `PublicArtist` type for public endpoints + +**3. `app/api/artists/[id]/route.ts`** +- Create this file (currently missing) +- Add GET endpoint to fetch single artist with portfolio +- Add PUT endpoint for updating artist (admin or artist themselves) +- Add authorization check (admin or owner) + +**4. `components/artists-grid.tsx`** +- Remove hardcoded `artists` array +- Add `useEffect` to fetch artists from `/api/artists` +- Add loading and error states +- Update to use API response data structure +- Keep existing filtering UI but apply to fetched data +- Update routing to use database IDs instead of hardcoded IDs + +**5. `components/artist-portfolio.tsx`** +- Remove hardcoded `artistsData` object +- Add `useEffect` to fetch artist data from `/api/artists/${artistId}` +- Add loading and error states +- Update image galleries to use portfolio images from database +- Handle missing artist gracefully +- Update data structure to match API response + +**6. `components/artists-page-section.tsx`** +- Remove import from `@/data/artists` +- Fetch artists from API in component +- Add loading state + +**7. `components/artists-section.tsx`** +- Remove import from `@/data/artists` +- Fetch artists from API +- Add loading skeleton + +**8. `components/booking-form.tsx`** +- Update artist selection to fetch from API +- Remove import from `@/data/artists` + +**9. `app/artists/[id]/page.tsx`** +- Update to use database-friendly slug or ID +- May need to support both slug-based and ID-based routing during migration + +**10. `lib/auth.ts`** +- Add `getArtistSession()` helper that checks if logged-in user is an artist +- Add `requireArtistAuth()` helper for artist-only routes + +**11. `sql/schema.sql`** +- Add missing columns if needed (review against `data/artists.ts` structure) +- Add indexes for performance (slug, user_id lookups) +- Consider adding `slug` column to artists table for SEO-friendly URLs + +**12. `lib/data-migration.ts`** +- Update migration to use all artists from `data/artists.ts` +- Fix portfolio image creation to handle all work images +- Add proper slug generation from artist names +- Ensure idempotency (can be run multiple times safely) + +### Files to Create + +**1. `app/artist-dashboard/page.tsx`** +- New artist dashboard home page +- Show artist's own profile overview +- Display stats (total images, views, etc.) +- Quick links to edit profile and portfolio + +**2. `app/artist-dashboard/layout.tsx`** +- Layout wrapper for artist dashboard +- Navigation sidebar for dashboard sections +- Artist profile header +- Requires ARTIST or SHOP_ADMIN role + +**3. `app/artist-dashboard/profile/page.tsx`** +- Artist profile edit page +- Reuse `` component but filtered for artist-editable fields +- Artists can edit: bio, specialties, instagram, hourly rate +- Cannot edit: name, email, isActive (admin only) + +**4. `app/artist-dashboard/portfolio/page.tsx`** +- Portfolio image management page +- Upload new images +- Edit captions and tags +- Reorder images (drag and drop) +- Delete images +- Set visibility (public/private) + +**5. `app/api/artists/me/route.ts`** +- GET endpoint to fetch current logged-in artist's data +- Requires authentication +- Returns artist profile for logged-in user + +**6. `app/api/portfolio/route.ts`** (if not exists) +- POST endpoint to add portfolio images +- Requires artist or admin auth +- Handles R2 upload integration + +**7. `app/api/portfolio/[id]/route.ts`** +- GET single portfolio image +- PUT to update (caption, tags, order, visibility) +- DELETE to remove image +- Authorization: admin or image owner + +**8. `components/admin/portfolio-manager.tsx`** +- Reusable component for managing portfolio images +- Used in both admin and artist dashboard +- Image upload, grid display, edit modals +- Drag-drop reordering + +**9. `hooks/use-artist-data.ts`** +- Custom hook for fetching artist data +- Handles loading, error states +- Caching with SWR or React Query pattern +- Reusable across components + +**10. `middleware.ts` (update)** +- Add route protection for `/artist-dashboard/*` +- Verify user has ARTIST role +- Redirect to signin if not authenticated + +### Files to Delete (after migration) + +**None immediately** - Keep `data/artists.ts` as seed data reference + +## Functions + +### New Functions in `lib/db.ts` + +**1. `getArtistWithPortfolio(id: string, env?: any): Promise`** +- Fetch artist by ID +- Join with portfolio_images table +- Parse JSON fields (specialties, tags) +- Return combined object with images array + +**2. `getPublicArtists(filters?: ArtistFilters, env?: any): Promise`** +- Fetch only active artists +- Include only public portfolio images +- Apply filters (specialty, search) +- Sanitize data for public consumption + +**3. `getArtistByUserId(userId: string, env?: any): Promise`** +- Fetch artist record by user_id +- Used for artist dashboard access +- Returns full artist data for owner + +**4. `getArtistBySlug(slug: string, env?: any): Promise`** +- Fetch artist by URL slug +- Join with portfolio images +- For SEO-friendly URLs + +**5. `updatePortfolioImageOrder(artistId: string, imageOrders: Array<{id: string, orderIndex: number}>, env?: any): Promise`** +- Batch update order indices +- Used for drag-drop reordering +- Transaction support if available + +### New Functions in `lib/auth.ts` + +**1. `getArtistSession(): Promise<{ artist: Artist, user: User } | null>`** +- Get current session +- Check if user has ARTIST role +- Fetch associated artist record +- Return combined data or null + +**2. `requireArtistAuth(): Promise<{ artist: Artist, user: User }>`** +- Like requireAuth but specifically for artists +- Throws error if not an artist +- Returns artist and user data + +**3. `canEditArtist(userId: string, artistId: string): Promise`** +- Check if user can edit specific artist +- True if: user is the artist, SHOP_ADMIN, or SUPER_ADMIN +- Used for authorization checks + +### Modified Functions + +**1. `lib/db.ts:getArtists()`** +- Add optional `includePortfolio` parameter +- Parse JSON fields properly +- Add error handling + +**2. `lib/db.ts:createArtist()`** +- Add slug generation +- Ensure user creation if needed +- Return artist with portfolio array (empty initially) + +**3. `lib/data-migration.ts:migrateArtistData()`** +- Update to migrate all fields from `data/artists.ts` +- Add slug generation +- Handle all portfolio images +- Add progress logging + +## Classes + +### New Classes + +**1. `ArtistDashboardManager` (optional)** +- Class to encapsulate artist dashboard operations +- Methods: getStats(), updateProfile(), getPortfolio() +- Centralizes artist-specific business logic +- Located in `lib/artist-dashboard.ts` + +**No other new classes required** - Functional approach with helper functions is sufficient + +## Dependencies + +### Current Dependencies (verify versions) +- `next`: 15.x +- `next-auth`: Latest compatible with Next.js 15 +- `@tanstack/react-table`: For admin tables +- `react-hook-form`: Form management +- `zod`: Schema validation +- `@hookform/resolvers`: Zod integration with react-hook-form + +### New Dependencies to Install + +**1. `swr` or `@tanstack/react-query`** +- For client-side data fetching and caching +- Recommended: `swr` (lighter weight) +- Install: `npm install swr` + +**2. `@dnd-kit/core`, `@dnd-kit/sortable` (optional)** +- For drag-drop portfolio reordering +- Only if implementing drag-drop UI +- Install: `npm install @dnd-kit/core @dnd-kit/sortable @dnd-kit/utilities` + +**3. `sharp` (dev dependency)** +- Image optimization during migration +- May already be included with Next.js + +### Configuration Updates + +**1. `next.config.mjs`** +- Ensure image optimization configured for R2 URLs +- Add remote patterns for portfolio image domains + +**2. `wrangler.toml`** +- Verify D1 database binding name matches code +- Verify R2 bucket binding for portfolio uploads + +**3. `.env.local`** +- No new env vars needed (using Cloudflare bindings) + +## Testing + +### Unit Tests to Create + +**1. `__tests__/lib/db.test.ts`** +- Test artist CRUD operations +- Test portfolio image operations +- Mock D1 database +- Verify JSON parsing + +**2. `__tests__/lib/auth.test.ts`** +- Test artist authentication helpers +- Test authorization checks +- Mock NextAuth session + +**3. `__tests__/hooks/use-artist-data.test.ts`** +- Test hook with mock data +- Test loading and error states +- Test cache behavior + +### Integration Tests to Create + +**1. `__tests__/api/artists.test.ts`** +- Test GET /api/artists endpoint +- Test filtering and pagination +- Test error handling + +**2. `__tests__/api/artists/[id].test.ts`** +- Test GET single artist +- Test PUT artist update +- Test authorization + +**3. `__tests__/api/portfolio.test.ts`** +- Test portfolio CRUD operations +- Test file uploads +- Test authorization + +### Component Tests to Update + +**1. `__tests__/components/artists-grid.test.tsx`** +- Update to mock API fetch +- Test loading states +- Test error states +- Test filtering + +**2. `__tests__/components/artist-portfolio.test.tsx`** +- Update to mock API fetch +- Test portfolio image display +- Test modal interactions + +### E2E Test Scenarios + +**1. Artist Login and Profile Edit** +- Artist signs in +- Navigates to dashboard +- Edits bio and specialties +- Saves successfully + +**2. Portfolio Image Upload** +- Artist uploads images +- Images appear in portfolio +- Reorders images +- Deletes an image + +**3. Public Artist Profile View** +- Anonymous user visits artist page +- Sees artist info and portfolio +- Images load correctly +- Filtering works + +**4. Admin Artist Management** +- Admin creates new artist +- Edits artist information +- Uploads portfolio images for artist +- Deactivates artist + +## Implementation Order + +### Phase 1: Database & API Foundation (Steps 1-5) + +**1. Update Database Schema and Migration** +- Review and update `sql/schema.sql` if needed (add slug column, indexes) +- Update `lib/data-migration.ts` to properly migrate all artist data from `data/artists.ts` +- Test migration script locally +- Verify all artists and portfolio images are created correctly + +**2. Enhance Database Functions** +- Update `lib/db.ts` with new functions: `getArtistWithPortfolio`, `getPublicArtists`, `getArtistByUserId`, `getArtistBySlug` +- Update existing functions to properly parse JSON fields +- Add error handling and logging +- Write unit tests for new functions + +**3. Create/Update API Endpoints** +- Create `app/api/artists/[id]/route.ts` with GET and PUT handlers +- Update `app/api/artists/route.ts` to support filtering and return portfolio images +- Create `app/api/artists/me/route.ts` for artist self-access +- Add authorization checks to all endpoints +- Test endpoints with Postman or similar + +**4. Update Authentication Helpers** +- Add `getArtistSession()` and `requireArtistAuth()` to `lib/auth.ts` +- Add `canEditArtist()` authorization helper +- Test with mock sessions + +**5. Create Data Fetching Hook** +- Create `hooks/use-artist-data.ts` with SWR +- Implement loading and error states +- Add caching and revalidation +- Test hook in isolation + +### Phase 2: Public-Facing Components (Steps 6-8) + +**6. Refactor Artists Grid** +- Update `components/artists-grid.tsx` to fetch from API +- Remove hardcoded data +- Add loading skeleton UI +- Add error handling UI +- Test filtering and pagination +- Verify routing to individual artists works + +**7. Refactor Artist Portfolio Page** +- Update `components/artist-portfolio.tsx` to fetch from API +- Remove hardcoded `artistsData` +- Add loading states +- Update image galleries to use database images +- Test with various artist IDs +- Verify modal/lightbox still works + +**8. Update Supporting Components** +- Update `components/artists-section.tsx` to use API +- Update `components/artists-page-section.tsx` to use API +- Update `components/booking-form.tsx` artist selection +- Add loading states to all +- Test home page and booking flow + +### Phase 3: Artist Dashboard (Steps 9-12) + +**9. Create Artist Dashboard Layout** +- Create `app/artist-dashboard/layout.tsx` with navigation +- Add role protection in middleware +- Create dashboard home page `app/artist-dashboard/page.tsx` +- Display artist stats and quick links +- Test authentication and routing + +**10. Build Profile Editor** +- Create `app/artist-dashboard/profile/page.tsx` +- Reuse and adapt `` component +- Filter fields for artist-editable only +- Connect to PUT `/api/artists/me` endpoint +- Test profile updates + +**11. Build Portfolio Manager** +- Create `app/artist-dashboard/portfolio/page.tsx` +- Create `components/admin/portfolio-manager.tsx` component +- Implement image upload UI +- Add image editing (captions, tags, visibility) +- Test upload and management + +**12. Implement Portfolio API** +- Create `app/api/portfolio/route.ts` for creating images +- Create `app/api/portfolio/[id]/route.ts` for update/delete +- Integrate with R2 upload system +- Add authorization checks +- Test full upload flow + +### Phase 4: Admin Enhancements (Steps 13-14) + +**13. Update Admin Artist Management** +- Update `app/admin/artists/[id]/page.tsx` to use API +- Enhance `` with portfolio management +- Test admin CRUD operations +- Verify authorization works + +**14. Add Portfolio Management to Admin** +- Integrate `` into admin artist edit page +- Allow admins to manage any artist's portfolio +- Test admin portfolio operations + +### Phase 5: Testing & Refinement (Steps 15-17) + +**15. Write and Run Tests** +- Create all unit tests for new functions +- Create integration tests for API endpoints +- Create component tests +- Run full test suite +- Fix any failing tests + +**16. Performance Optimization** +- Add database indexes for common queries +- Implement image lazy loading +- Add proper caching headers to API +- Optimize large portfolio image displays +- Test with large datasets + +**17. Migration & Deployment** +- Run migration on development D1 database +- Test full application flow +- Create migration checklist for production +- Deploy to staging environment +- Final testing in staging +- Production deployment + +### Phase 6: Documentation & Cleanup (Step 18) + +**18. Documentation and Cleanup** +- Document new API endpoints +- Update README with setup instructions +- Add inline code comments +- Create artist onboarding guide +- Remove deprecated code/comments +- Archive old implementation if needed diff --git a/components/artists-grid.tsx b/components/artists-grid.tsx index 9c45f9946..e3d77bead 100644 --- a/components/artists-grid.tsx +++ b/components/artists-grid.tsx @@ -1,102 +1,35 @@ "use client" -import { useState } from "react" +import { useState, useMemo } from "react" import { Button } from "@/components/ui/button" import { Card, CardContent } from "@/components/ui/card" import { Badge } from "@/components/ui/badge" import Link from "next/link" -import { Star, MapPin, Calendar } from "lucide-react" +import { Star, Loader2 } from "lucide-react" +import { useArtists } from "@/hooks/use-artist-data" -const artists = [ - { - id: "1", - name: "Sarah Chen", - specialty: "Traditional & Neo-Traditional", - image: "/professional-female-tattoo-artist-with-traditional.jpg", - bio: "Specializing in bold traditional designs with a modern twist. Sarah brings 8 years of experience creating vibrant, timeless tattoos.", - experience: "8 years", - rating: 4.9, - reviews: 127, - location: "Studio A", - availability: "Available", - styles: ["Traditional", "Neo-Traditional", "American Traditional", "Color Work"], - portfolio: [ - "/traditional-rose-tattoo-with-bold-colors.jpg", - "/neo-traditional-wolf-tattoo-design.jpg", - "/american-traditional-anchor-tattoo.jpg", - "/colorful-traditional-bird-tattoo.jpg", - ], - }, - { - id: "2", - name: "Marcus Rodriguez", - specialty: "Realism & Portraits", - image: "/professional-male-tattoo-artist-specializing-in-re.jpg", - bio: "Master of photorealistic tattoos and detailed portrait work. Marcus has perfected the art of bringing photographs to life on skin.", - experience: "12 years", - rating: 5.0, - reviews: 89, - location: "Studio B", - availability: "Booked until March", - styles: ["Realism", "Portraits", "Black & Grey", "Photorealism"], - portfolio: [ - "/photorealistic-portrait-tattoo-black-and-grey.jpg", - "/realistic-animal-tattoo-detailed-shading.jpg", - "/black-and-grey-portrait-tattoo-masterpiece.jpg", - "/hyperrealistic-eye-tattoo-design.jpg", - ], - }, - { - id: "3", - name: "Luna Kim", - specialty: "Fine Line & Minimalist", - image: "/professional-female-tattoo-artist-with-delicate-fi.jpg", - bio: "Creating elegant, minimalist designs with precision and grace. Luna's delicate touch brings subtle beauty to every piece.", - experience: "6 years", - rating: 4.8, - reviews: 156, - location: "Studio C", - availability: "Available", - styles: ["Fine Line", "Minimalist", "Geometric", "Botanical"], - portfolio: [ - "/delicate-fine-line-flower-tattoo.jpg", - "/minimalist-geometric-tattoo-design.jpg", - "/fine-line-botanical-tattoo-elegant.jpg", - "/simple-line-work-tattoo-artistic.jpg", - ], - }, - { - id: "4", - name: "Jake Thompson", - specialty: "Japanese & Oriental", - image: "/professional-male-tattoo-artist-with-japanese-styl.jpg", - bio: "Traditional Japanese tattooing with authentic techniques passed down through generations. Jake honors the ancient art form.", - experience: "15 years", - rating: 4.9, - reviews: 203, - location: "Studio D", - availability: "Limited slots", - styles: ["Japanese", "Oriental", "Irezumi", "Traditional Japanese"], - portfolio: [ - "/traditional-japanese-dragon-tattoo-sleeve.jpg", - "/japanese-koi-fish-tattoo-colorful.jpg", - "/oriental-cherry-blossom-tattoo-design.jpg", - "/japanese-samurai-tattoo-traditional.jpg", - ], - }, -] - -const specialties = ["All", "Traditional", "Realism", "Fine Line", "Japanese", "Portraits", "Minimalist"] +const specialties = ["All", "Traditional", "Realism", "Fine Line", "Japanese", "Portraits", "Minimalist", "Black & Grey"] export function ArtistsGrid() { const [selectedSpecialty, setSelectedSpecialty] = useState("All") + + // Fetch artists from API + const { data: artists, isLoading, error } = useArtists({ limit: 50 }) - const filteredArtists = - selectedSpecialty === "All" - ? artists - : artists.filter((artist) => - artist.styles.some((style) => style.toLowerCase().includes(selectedSpecialty.toLowerCase())), - ) + // Filter artists client-side + const filteredArtists = useMemo(() => { + if (!artists) return [] + + if (selectedSpecialty === "All") { + return artists + } + + return artists.filter((artist) => + artist.specialties.some((style) => + style.toLowerCase().includes(selectedSpecialty.toLowerCase()) + ) + ) + }, [artists, selectedSpecialty]) return (
@@ -123,87 +56,112 @@ export function ArtistsGrid() { ))} + {/* Loading State */} + {isLoading && ( +
+ +
+ )} + + {/* Error State */} + {error && ( +
+

Failed to load artists. Please try again later.

+ +
+ )} + {/* Artists Grid */} -
- {filteredArtists.map((artist) => ( - -
- {/* Artist Image */} -
- {artist.name} -
- - {artist.availability} - -
-
- - {/* Artist Info */} - -
-
-

{artist.name}

-

{artist.specialty}

-
-
- - {artist.rating} - ({artist.reviews}) -
-
- -

{artist.bio}

- -
-
- - {artist.experience} experience -
-
- - {artist.location} -
-
- - {/* Styles */} -
-

Specializes in:

-
- {artist.styles.slice(0, 3).map((style) => ( - - {style} - - ))} - {artist.styles.length > 3 && ( - - +{artist.styles.length - 3} more - - )} -
-
- - {/* Action Buttons */} -
- - -
-
+ {!isLoading && !error && ( +
+ {filteredArtists.length === 0 ? ( +
+

No artists found matching your criteria.

- - ))} -
+ ) : ( + filteredArtists.map((artist) => { + // Get profile image (first portfolio image or placeholder) + const profileImage = artist.portfolioImages.find(img => img.tags.includes('profile'))?.url || + artist.portfolioImages[0]?.url || + "/placeholder.svg" + + return ( + +
+ {/* Artist Image */} +
+ {artist.name} +
+ + {artist.isActive ? "Available" : "Unavailable"} + +
+
+ + {/* Artist Info */} + +
+
+

{artist.name}

+

+ {artist.specialties.slice(0, 2).join(", ")} +

+
+
+ +

{artist.bio}

+ + {/* Hourly Rate */} + {artist.hourlyRate && ( +
+

+ Starting at ${artist.hourlyRate}/hr +

+
+ )} + + {/* Styles */} +
+

Specializes in:

+
+ {artist.specialties.slice(0, 3).map((style) => ( + + {style} + + ))} + {artist.specialties.length > 3 && ( + + +{artist.specialties.length - 3} more + + )} +
+
+ + {/* Action Buttons */} +
+ + +
+
+
+
+ ) + }) + )} +
+ )}
) diff --git a/dist/README.md b/dist/README.md index d4825e8d9..f48eadcc2 100644 --- a/dist/README.md +++ b/dist/README.md @@ -1 +1 @@ -This folder contains the built output assets for the worker "united-tattoo" generated at 2025-09-26T06:30:46.424Z. \ No newline at end of file +This folder contains the built output assets for the worker "united-tattoo" generated at 2025-10-06T09:30:19.755Z. \ No newline at end of file diff --git a/hooks/use-artist-data.ts b/hooks/use-artist-data.ts new file mode 100644 index 000000000..cf24f9cd6 --- /dev/null +++ b/hooks/use-artist-data.ts @@ -0,0 +1,170 @@ +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) => [...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 + }, + 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 + }, + 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 }) => { + 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 + }, + 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 + }, + 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) }) + }, + }) +} diff --git a/lib/data-migration.ts b/lib/data-migration.ts index 7280c4867..8c16b38ea 100644 --- a/lib/data-migration.ts +++ b/lib/data-migration.ts @@ -81,16 +81,20 @@ export class DataMigrator { // Extract hourly rate from experience or set default const hourlyRate = this.extractHourlyRate(artist.experience); + // Generate slug from artist name or use existing slug + const slug = artist.slug || this.generateSlug(artist.name); + try { await this.db.prepare(` INSERT OR IGNORE INTO artists ( - id, user_id, name, bio, specialties, instagram_handle, + id, user_id, slug, name, bio, specialties, instagram_handle, hourly_rate, is_active, created_at, updated_at ) - VALUES (?, ?, ?, ?, ?, ?, ?, 1, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, 1, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP) `).bind( artistId, userId, + slug, artist.name, artist.bio, JSON.stringify(specialties), @@ -98,7 +102,7 @@ export class DataMigrator { hourlyRate, ).run(); - console.log(`Created artist record: ${artist.name}`); + console.log(`Created artist record: ${artist.name} (slug: ${slug})`); } catch (error) { console.error(`Error creating artist record for ${artist.name}:`, error); throw error; @@ -167,6 +171,17 @@ export class DataMigrator { console.log(`Created portfolio images for: ${artist.name}`); } + /** + * Generate URL-friendly slug from artist name + */ + private generateSlug(name: string): string { + return name + .toLowerCase() + .replace(/['']/g, '') // Remove apostrophes + .replace(/[^a-z0-9]+/g, '-') // Replace non-alphanumeric with hyphens + .replace(/^-+|-+$/g, ''); // Trim hyphens from ends + } + /** * Extract Instagram handle from full URL */ diff --git a/lib/db.ts b/lib/db.ts index ec0d1a3a5..35c0a9457 100644 --- a/lib/db.ts +++ b/lib/db.ts @@ -53,7 +53,197 @@ export async function getArtists(env?: any): Promise { ORDER BY a.created_at DESC `).all(); - return result.results as Artist[]; + // Parse JSON fields + return (result.results as any[]).map(artist => ({ + ...artist, + specialties: artist.specialties ? JSON.parse(artist.specialties) : [], + portfolioImages: [] + })); +} + +export async function getPublicArtists(filters?: import('@/types/database').ArtistFilters, env?: any): Promise { + const db = getDB(env); + + let query = ` + SELECT + a.id, + a.slug, + a.name, + a.bio, + a.specialties, + a.instagram_handle, + a.is_active, + a.hourly_rate + FROM artists a + WHERE a.is_active = 1 + `; + + const values: any[] = []; + + if (filters?.specialty) { + query += ` AND a.specialties LIKE ?`; + values.push(`%${filters.specialty}%`); + } + + if (filters?.search) { + query += ` AND (a.name LIKE ? OR a.bio LIKE ?)`; + values.push(`%${filters.search}%`, `%${filters.search}%`); + } + + query += ` ORDER BY a.created_at DESC`; + + if (filters?.limit) { + query += ` LIMIT ?`; + values.push(filters.limit); + } + + if (filters?.offset) { + query += ` OFFSET ?`; + values.push(filters.offset); + } + + const result = await db.prepare(query).bind(...values).all(); + + // Fetch portfolio images for each artist + const artistsWithPortfolio = await Promise.all( + (result.results as any[]).map(async (artist) => { + const portfolioResult = await db.prepare(` + SELECT * FROM portfolio_images + WHERE artist_id = ? AND is_public = 1 + ORDER BY order_index ASC, created_at DESC + `).bind(artist.id).all(); + + return { + id: artist.id, + slug: artist.slug, + name: artist.name, + bio: artist.bio, + specialties: artist.specialties ? JSON.parse(artist.specialties) : [], + instagramHandle: artist.instagram_handle, + isActive: Boolean(artist.is_active), + hourlyRate: artist.hourly_rate, + portfolioImages: (portfolioResult.results as any[]).map(img => ({ + id: img.id, + artistId: img.artist_id, + url: img.url, + caption: img.caption, + tags: img.tags ? JSON.parse(img.tags) : [], + orderIndex: img.order_index, + isPublic: Boolean(img.is_public), + createdAt: new Date(img.created_at) + })) + }; + }) + ); + + return artistsWithPortfolio; +} + +export async function getArtistWithPortfolio(id: string, env?: any): Promise { + const db = getDB(env); + + const artistResult = await db.prepare(` + SELECT + a.*, + u.name as user_name, + u.email as user_email, + u.avatar as user_avatar + FROM artists a + LEFT JOIN users u ON a.user_id = u.id + WHERE a.id = ? + `).bind(id).first(); + + if (!artistResult) return null; + + const portfolioResult = await db.prepare(` + SELECT * FROM portfolio_images + WHERE artist_id = ? + ORDER BY order_index ASC, created_at DESC + `).bind(id).all(); + + const artist = artistResult as any; + + return { + id: artist.id, + userId: artist.user_id, + slug: artist.slug, + name: artist.name, + bio: artist.bio, + specialties: artist.specialties ? JSON.parse(artist.specialties) : [], + instagramHandle: artist.instagram_handle, + isActive: Boolean(artist.is_active), + hourlyRate: artist.hourly_rate, + portfolioImages: (portfolioResult.results as any[]).map(img => ({ + id: img.id, + artistId: img.artist_id, + url: img.url, + caption: img.caption, + tags: img.tags ? JSON.parse(img.tags) : [], + orderIndex: img.order_index, + isPublic: Boolean(img.is_public), + createdAt: new Date(img.created_at) + })), + availability: [], + createdAt: new Date(artist.created_at), + updatedAt: new Date(artist.updated_at), + user: { + name: artist.user_name, + email: artist.user_email, + avatar: artist.user_avatar + } + }; +} + +export async function getArtistBySlug(slug: string, env?: any): Promise { + const db = getDB(env); + + const artistResult = await db.prepare(` + SELECT + a.*, + u.name as user_name, + u.email as user_email, + u.avatar as user_avatar + FROM artists a + LEFT JOIN users u ON a.user_id = u.id + WHERE a.slug = ? + `).bind(slug).first(); + + if (!artistResult) return null; + + const artist = artistResult as any; + return getArtistWithPortfolio(artist.id, env); +} + +export async function getArtistByUserId(userId: string, env?: any): Promise { + const db = getDB(env); + const result = await db.prepare(` + SELECT + a.*, + u.name as user_name, + u.email as user_email + FROM artists a + LEFT JOIN users u ON a.user_id = u.id + WHERE a.user_id = ? + `).bind(userId).first(); + + if (!result) return null; + + const artist = result as any; + return { + id: artist.id, + userId: artist.user_id, + slug: artist.slug, + name: artist.name, + bio: artist.bio, + specialties: artist.specialties ? JSON.parse(artist.specialties) : [], + instagramHandle: artist.instagram_handle, + isActive: Boolean(artist.is_active), + hourlyRate: artist.hourly_rate, + portfolioImages: [], + availability: [], + createdAt: new Date(artist.created_at), + updatedAt: new Date(artist.updated_at) + }; } export async function getArtist(id: string, env?: any): Promise { diff --git a/sql/migrations/0001_add_artist_slug.sql b/sql/migrations/0001_add_artist_slug.sql new file mode 100644 index 000000000..84cd49b2e --- /dev/null +++ b/sql/migrations/0001_add_artist_slug.sql @@ -0,0 +1,10 @@ +-- Add slug column to artists table for SEO-friendly URLs +-- Run this with: wrangler d1 execute united-tattoo-db --file=./sql/migrations/0001_add_artist_slug.sql + +-- Add slug column +ALTER TABLE artists ADD COLUMN slug TEXT; + +-- Create unique index on slug +CREATE UNIQUE INDEX IF NOT EXISTS idx_artists_slug ON artists(slug); + +-- Note: Existing artists will need slugs populated via migration script diff --git a/tsconfig.json b/tsconfig.json index 4b2dc7ba6..7898f961f 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -23,5 +23,5 @@ } }, "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], - "exclude": ["node_modules"] + "exclude": ["types/node_modules"] } diff --git a/types/database.ts b/types/database.ts index c3fb5afdd..447325e7a 100644 --- a/types/database.ts +++ b/types/database.ts @@ -134,6 +134,7 @@ export enum UserRole { export interface Artist { id: string userId: string + slug: string name: string bio: string specialties: string[] @@ -146,6 +147,42 @@ export interface Artist { updatedAt: Date } +export interface ArtistWithPortfolio extends Artist { + portfolioImages: PortfolioImage[] + user?: { + name: string + email: string + avatar?: string + } +} + +export interface PublicArtist { + id: string + slug: string + name: string + bio: string + specialties: string[] + instagramHandle?: string + portfolioImages: PortfolioImage[] + isActive: boolean + hourlyRate?: number +} + +export interface ArtistDashboardStats { + totalImages: number + activeImages: number + profileViews?: number + lastUpdated: Date +} + +export interface ArtistFilters { + specialty?: string + search?: string + isActive?: boolean + limit?: number + offset?: number +} + export interface PortfolioImage { id: string artistId: string diff --git a/wrangler.toml b/wrangler.toml index 8df044367..258d1d945 100644 --- a/wrangler.toml +++ b/wrangler.toml @@ -11,7 +11,7 @@ binding = "ASSETS" [[d1_databases]] binding = "DB" database_name = "united-tattoo" -database_id = "5d133d3b-680f-4772-b4ea-594c55cd1bd5" +database_id = "7191a4c4-e3b2-49c6-bd8d-9cc3394977ec" # R2 bucket binding [[r2_buckets]] @@ -29,13 +29,13 @@ service = "united-tattoo" # Environment variables for production [env.production.vars] -NEXTAUTH_URL = "https://your-domain.com" +NEXTAUTH_URL = "https://united-tattoos.com" NODE_ENV = "production" # Environment variables for preview [env.preview.vars] -NEXTAUTH_URL = "https://development.united-tattoos.com" -NODE_ENV = "development" +NEXTAUTH_URL = "https://united-tattoos.com" +NODE_ENV = "production" [dev] ip = "0.0.0.0"