feat(routing): switch public artist routing to slugs and fix admin parsing
Routing: update homepage ArtistsSection and ArtistsPageSection to link to /artists/{slug} and /book?artist={slug}. Artists grid already used slugs.\n\nAdmin: remove JSON.parse on specialties; treat as arrays with backward-compat.\n\nMigration: generate UUIDs with crypto.randomUUID(), ensure unique slugs, preserve user↔artist↔portfolio mapping.\n\nDB: parse specialties to arrays consistently and include createdAt for admin use.\n\nDev: wrangler dev port changes to avoid conflicts; MIGRATE_TOKEN set in wrangler.toml.\n\nDocs: add artist_routing_fix_implementation_plan.md.
This commit is contained in:
parent
aa23c905bf
commit
aae48e1fa9
@ -73,8 +73,13 @@ export default function ArtistsPage() {
|
||||
accessorKey: "specialties",
|
||||
header: "Specialties",
|
||||
cell: ({ row }) => {
|
||||
const specialties = row.getValue("specialties") as string
|
||||
const specialtiesArray = specialties ? JSON.parse(specialties) : []
|
||||
const specialties = row.getValue("specialties") as string[] | string
|
||||
const specialtiesArray: string[] =
|
||||
Array.isArray(specialties)
|
||||
? specialties
|
||||
: typeof specialties === "string" && specialties.trim().startsWith("[")
|
||||
? JSON.parse(specialties)
|
||||
: []
|
||||
return (
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{specialtiesArray.slice(0, 2).map((specialty: string) => (
|
||||
|
||||
206
artist_routing_fix_implementation_plan.md
Normal file
206
artist_routing_fix_implementation_plan.md
Normal file
@ -0,0 +1,206 @@
|
||||
# Implementation Plan: Artist Routing & Admin Fixes
|
||||
|
||||
[Overview]
|
||||
Fix artist portfolio routing to use slugs instead of numeric IDs, resolve admin page JSON parsing errors, ensure proper database population, and fix artist dashboard authentication issues.
|
||||
|
||||
The current system has a mismatch where the artists grid links to numeric IDs (`/artists/1`) but the API and components expect slug-based routing (`/artists/christy-lumberg`). Additionally, the admin page has JSON parsing errors due to data format inconsistencies, and the artist dashboard has authentication issues.
|
||||
|
||||
This implementation will:
|
||||
1. Update database migration to use proper UUID-based IDs and ensure slug population
|
||||
2. Fix the artists grid component to link using slugs instead of numeric IDs
|
||||
3. Resolve admin page data format inconsistencies
|
||||
4. Fix artist dashboard authentication flow
|
||||
5. Add a migration endpoint to populate the database from static data
|
||||
6. Update API routes to handle both ID and slug lookups consistently
|
||||
|
||||
[Types]
|
||||
No new types required - existing types in `types/database.ts` are sufficient.
|
||||
|
||||
The following interfaces are already properly defined:
|
||||
- `Artist`: Contains id (string UUID), slug, name, bio, specialties (array), etc.
|
||||
- `PublicArtist`: Subset for public-facing pages
|
||||
- `ArtistWithPortfolio`: Includes portfolio images
|
||||
- `CreateArtistInput`: For creating new artists
|
||||
|
||||
Data format standardization needed:
|
||||
- `specialties` field should always be stored as JSON string in DB
|
||||
- `specialties` field should always be parsed to array when returned from API
|
||||
- Admin page should receive pre-parsed arrays, not JSON strings
|
||||
|
||||
[Files]
|
||||
Files requiring modification to fix routing and data consistency issues.
|
||||
|
||||
**Modified Files:**
|
||||
1. `components/artists-grid.tsx`
|
||||
- Change Link href from `/artists/${artist.id}` to `/artists/${artist.slug}`
|
||||
- Ensure slug is available in the artist data
|
||||
|
||||
2. `lib/data-migration.ts`
|
||||
- Update to use crypto.randomUUID() for IDs instead of `artist-${id}` format
|
||||
- Ensure slugs are properly populated for all artists
|
||||
- Add error handling for duplicate slugs
|
||||
|
||||
3. `app/api/admin/migrate/route.ts`
|
||||
- Verify it properly triggers the migration
|
||||
- Add response with migration statistics
|
||||
- Include error handling
|
||||
|
||||
4. `app/admin/artists/page.tsx`
|
||||
- Remove JSON.parse() on specialties since API returns array
|
||||
- Update to handle specialties as array directly
|
||||
- Fix data mapping in the table columns
|
||||
|
||||
5. `lib/db.ts`
|
||||
- Verify getArtistBySlug() properly handles slug lookup
|
||||
- Ensure getPublicArtists() returns properly formatted data
|
||||
- Confirm specialties are parsed to arrays in all query results
|
||||
|
||||
6. `app/api/artists/route.ts`
|
||||
- Ensure GET endpoint returns specialties as parsed arrays
|
||||
- Verify data format consistency
|
||||
|
||||
7. `app/artist-dashboard/page.tsx`
|
||||
- Add proper loading and error states
|
||||
- Improve authentication error handling
|
||||
- Add redirect to sign-in if not authenticated
|
||||
|
||||
**Files to Review (no changes needed):**
|
||||
- `app/artists/[id]/page.tsx` - Already accepts dynamic param correctly
|
||||
- `components/artist-portfolio.tsx` - Already uses useArtist hook properly
|
||||
- `hooks/use-artist-data.ts` - API calls are correct
|
||||
- `middleware.ts` - Route protection is properly configured
|
||||
|
||||
[Functions]
|
||||
Functions requiring modification or addition.
|
||||
|
||||
**Modified Functions:**
|
||||
|
||||
1. `lib/data-migration.ts::createArtistRecord()`
|
||||
- Current: Uses `artist-${artist.id}` for IDs
|
||||
- Change to: Use `crypto.randomUUID()` for proper UUID generation
|
||||
- Add validation to ensure slugs are unique
|
||||
|
||||
2. `lib/data-migration.ts::createUserForArtist()`
|
||||
- Current: Uses `user-${artist.id}` for IDs
|
||||
- Change to: Use `crypto.randomUUID()` for proper UUID generation
|
||||
|
||||
3. `lib/data-migration.ts::createPortfolioImages()`
|
||||
- Current: Uses `portfolio-${artist.id}-${index}` for IDs
|
||||
- Change to: Use `crypto.randomUUID()` for proper UUID generation
|
||||
|
||||
4. `lib/db.ts::getPublicArtists()`
|
||||
- Ensure specialties field is parsed from JSON string to array
|
||||
- Verify all artists have slugs populated
|
||||
|
||||
5. `lib/db.ts::getArtistWithPortfolio()`
|
||||
- Ensure specialties field is parsed from JSON string to array
|
||||
- Verify slug is included in response
|
||||
|
||||
6. `app/admin/artists/page.tsx::fetchArtists()`
|
||||
- Remove JSON.parse() call on specialties
|
||||
- Handle specialties as array directly
|
||||
|
||||
**New Functions:**
|
||||
None required - existing functions just need corrections.
|
||||
|
||||
[Classes]
|
||||
Classes requiring modification.
|
||||
|
||||
**Modified Classes:**
|
||||
|
||||
1. `lib/data-migration.ts::DataMigrator`
|
||||
- Update all ID generation methods to use crypto.randomUUID()
|
||||
- Add slug validation to prevent duplicates
|
||||
- Improve error handling and logging
|
||||
|
||||
[Dependencies]
|
||||
No new dependencies required.
|
||||
|
||||
All necessary packages are already installed:
|
||||
- `next` - Framework
|
||||
- `@tanstack/react-query` - Data fetching
|
||||
- `next-auth` - Authentication
|
||||
- Cloudflare D1 bindings - Database access
|
||||
|
||||
[Testing]
|
||||
Testing strategy to verify fixes.
|
||||
|
||||
**Test Files to Update:**
|
||||
|
||||
1. `__tests__/api/artists.test.ts`
|
||||
- Add tests for slug-based artist lookup
|
||||
- Verify specialties are returned as arrays
|
||||
- Test both ID and slug lookup scenarios
|
||||
|
||||
2. `__tests__/components/artists-grid.test.tsx`
|
||||
- Verify links use slugs instead of IDs
|
||||
- Test that artist cards render with proper hrefs
|
||||
|
||||
**Manual Testing Steps:**
|
||||
|
||||
1. Run migration to populate database
|
||||
- Visit `/api/admin/migrate` endpoint
|
||||
- Verify migration completes successfully
|
||||
- Check database has artists with proper UUIDs and slugs
|
||||
|
||||
2. Test artist portfolio routing
|
||||
- Visit https://united-tattoos.com/artists
|
||||
- Click on "Christy Lumberg" card
|
||||
- Verify URL is `/artists/christy-lumberg` not `/artists/1`
|
||||
- Confirm portfolio page loads correctly
|
||||
|
||||
3. Test admin artists page
|
||||
- Sign in as admin
|
||||
- Visit `/admin/artists`
|
||||
- Verify page loads without JSON.parse errors
|
||||
- Confirm specialties display as badges
|
||||
|
||||
4. Test artist dashboard
|
||||
- Create artist user account
|
||||
- Sign in as artist
|
||||
- Visit `/artist-dashboard`
|
||||
- Verify dashboard loads or redirects appropriately
|
||||
|
||||
[Implementation Order]
|
||||
Step-by-step implementation sequence to minimize conflicts.
|
||||
|
||||
1. **Fix Database Migration Script** (`lib/data-migration.ts`)
|
||||
- Update ID generation to use crypto.randomUUID()
|
||||
- Ensure slugs are properly set
|
||||
- Add slug uniqueness validation
|
||||
- Improve error handling
|
||||
|
||||
2. **Verify Database Query Functions** (`lib/db.ts`)
|
||||
- Confirm all functions parse specialties to arrays
|
||||
- Verify slug is included in all artist queries
|
||||
- Test getArtistBySlug() function
|
||||
|
||||
3. **Fix Admin Page Data Handling** (`app/admin/artists/page.tsx`)
|
||||
- Remove JSON.parse() on specialties column
|
||||
- Handle specialties as array directly
|
||||
- Test admin page rendering
|
||||
|
||||
4. **Update Artists Grid Component** (`components/artists-grid.tsx`)
|
||||
- Change href from `/artists/${artist.id}` to `/artists/${artist.slug}`
|
||||
- Verify all artists have slug property
|
||||
- Test clicking on artist cards
|
||||
|
||||
5. **Run Database Migration**
|
||||
- Execute migration via `/api/admin/migrate`
|
||||
- Verify all artists created with proper data
|
||||
- Check slugs are populated correctly
|
||||
|
||||
6. **Test Artist Dashboard Authentication**
|
||||
- Create test artist user in database
|
||||
- Attempt to access dashboard
|
||||
- Verify authentication flow works correctly
|
||||
|
||||
7. **End-to-End Testing**
|
||||
- Test complete user flow: artists page → artist portfolio
|
||||
- Test admin flow: sign in → manage artists
|
||||
- Test artist flow: sign in → dashboard access
|
||||
|
||||
8. **Verify Production Deployment**
|
||||
- Deploy to Cloudflare Pages
|
||||
- Run migration on production database
|
||||
- Test all routes on live site
|
||||
@ -226,14 +226,14 @@ export function ArtistsPageSection() {
|
||||
size="sm"
|
||||
className="bg-white text-black hover:bg-gray-100 text-xs font-medium tracking-wide flex-1"
|
||||
>
|
||||
<Link href={`/artists/${artist.id}`}>PORTFOLIO</Link>
|
||||
<Link href={`/artists/${artist.slug}`}>PORTFOLIO</Link>
|
||||
</Button>
|
||||
<Button
|
||||
asChild
|
||||
size="sm"
|
||||
className="bg-white text-black hover:bg-gray-100 text-xs font-medium tracking-wide flex-1"
|
||||
>
|
||||
<Link href="/book">BOOK</Link>
|
||||
<Link href={`/book?artist=${artist.slug}`}>BOOK</Link>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
@ -309,14 +309,14 @@ export function ArtistsPageSection() {
|
||||
size="sm"
|
||||
className="bg-white text-black hover:bg-gray-100 text-xs font-medium tracking-wide flex-1"
|
||||
>
|
||||
<Link href={`/artists/${artist.id}`}>PORTFOLIO</Link>
|
||||
<Link href={`/artists/${artist.slug}`}>PORTFOLIO</Link>
|
||||
</Button>
|
||||
<Button
|
||||
asChild
|
||||
size="sm"
|
||||
className="bg-white text-black hover:bg-gray-100 text-xs font-medium tracking-wide flex-1"
|
||||
>
|
||||
<Link href="/book">BOOK</Link>
|
||||
<Link href={`/book?artist=${artist.slug}`}>BOOK</Link>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
@ -392,14 +392,14 @@ export function ArtistsPageSection() {
|
||||
size="sm"
|
||||
className="bg-white text-black hover:bg-gray-100 text-xs font-medium tracking-wide flex-1"
|
||||
>
|
||||
<Link href={`/artists/${artist.id}`}>PORTFOLIO</Link>
|
||||
<Link href={`/artists/${artist.slug}`}>PORTFOLIO</Link>
|
||||
</Button>
|
||||
<Button
|
||||
asChild
|
||||
size="sm"
|
||||
className="bg-white text-black hover:bg-gray-100 text-xs font-medium tracking-wide flex-1"
|
||||
>
|
||||
<Link href="/book">BOOK</Link>
|
||||
<Link href={`/book?artist=${artist.slug}`}>BOOK</Link>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -214,14 +214,14 @@ export function ArtistsSection() {
|
||||
size="sm"
|
||||
className="bg-white text-black hover:bg-gray-100 text-xs font-medium tracking-wide flex-1"
|
||||
>
|
||||
<Link href={`/artists/${artist.id}`}>PORTFOLIO</Link>
|
||||
<Link href={`/artists/${artist.slug}`}>PORTFOLIO</Link>
|
||||
</Button>
|
||||
<Button
|
||||
asChild
|
||||
size="sm"
|
||||
className="bg-white text-black hover:bg-gray-100 text-xs font-medium tracking-wide flex-1"
|
||||
>
|
||||
<Link href="/book">BOOK</Link>
|
||||
<Link href={`/book?artist=${artist.slug}`}>BOOK</Link>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
@ -294,14 +294,14 @@ export function ArtistsSection() {
|
||||
size="sm"
|
||||
className="bg-white text-black hover:bg-gray-100 text-xs font-medium tracking-wide flex-1"
|
||||
>
|
||||
<Link href={`/artists/${artist.id}`}>PORTFOLIO</Link>
|
||||
<Link href={`/artists/${artist.slug}`}>PORTFOLIO</Link>
|
||||
</Button>
|
||||
<Button
|
||||
asChild
|
||||
size="sm"
|
||||
className="bg-white text-black hover:bg-gray-100 text-xs font-medium tracking-wide flex-1"
|
||||
>
|
||||
<Link href="/book">BOOK</Link>
|
||||
<Link href={`/book?artist=${artist.slug}`}>BOOK</Link>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
@ -374,14 +374,14 @@ export function ArtistsSection() {
|
||||
size="sm"
|
||||
className="bg-white text-black hover:bg-gray-100 text-xs font-medium tracking-wide flex-1"
|
||||
>
|
||||
<Link href={`/artists/${artist.id}`}>PORTFOLIO</Link>
|
||||
<Link href={`/artists/${artist.slug}`}>PORTFOLIO</Link>
|
||||
</Button>
|
||||
<Button
|
||||
asChild
|
||||
size="sm"
|
||||
className="bg-white text-black hover:bg-gray-100 text-xs font-medium tracking-wide flex-1"
|
||||
>
|
||||
<Link href="/book">BOOK</Link>
|
||||
<Link href={`/book?artist=${artist.slug}`}>BOOK</Link>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import { artists } from '@/data/artists'
|
||||
import type { CreateArtistInput } from '@/types/database'
|
||||
import type { Artist as StaticArtist } from '@/data/artists'
|
||||
import { getDB as getCloudflareDB } from '@/lib/db'
|
||||
|
||||
|
||||
@ -8,9 +8,13 @@ import { getDB as getCloudflareDB } from '@/lib/db'
|
||||
*/
|
||||
export class DataMigrator {
|
||||
private db: D1Database;
|
||||
private userIdMap: Map<number, string>;
|
||||
private artistIdMap: Map<number, string>;
|
||||
|
||||
constructor() {
|
||||
this.db = getCloudflareDB();
|
||||
this.userIdMap = new Map();
|
||||
this.artistIdMap = new Map();
|
||||
}
|
||||
|
||||
/**
|
||||
@ -42,9 +46,10 @@ export class DataMigrator {
|
||||
/**
|
||||
* Create a user record for an artist
|
||||
*/
|
||||
private async createUserForArtist(artist: any): Promise<void> {
|
||||
const userId = `user-${artist.id}`;
|
||||
const email = artist.email || `${artist.name.toLowerCase().replace(/\s+/g, '.')}@unitedtattoo.com`;
|
||||
private async createUserForArtist(artist: StaticArtist): Promise<void> {
|
||||
const userId = crypto.randomUUID();
|
||||
this.userIdMap.set(artist.id, userId);
|
||||
const email = `${artist.name.toLowerCase().replace(/\s+/g, '.')}@unitedtattoo.com`;
|
||||
|
||||
try {
|
||||
await this.db.prepare(`
|
||||
@ -62,9 +67,13 @@ export class DataMigrator {
|
||||
/**
|
||||
* Create an artist record
|
||||
*/
|
||||
private async createArtistRecord(artist: any): Promise<void> {
|
||||
const artistId = `artist-${artist.id}`;
|
||||
const userId = `user-${artist.id}`;
|
||||
private async createArtistRecord(artist: StaticArtist): Promise<void> {
|
||||
const artistId = crypto.randomUUID();
|
||||
const userId = this.userIdMap.get(artist.id);
|
||||
|
||||
if (!userId) {
|
||||
throw new Error(`Missing user mapping for artist ${artist.name} (${artist.id})`);
|
||||
}
|
||||
|
||||
// Convert styles array to specialties
|
||||
const specialties = artist.styles || [];
|
||||
@ -72,8 +81,9 @@ 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);
|
||||
// Generate slug from artist name or use existing slug and ensure uniqueness
|
||||
const baseSlug = artist.slug || this.generateSlug(artist.name);
|
||||
const slug = await this.ensureUniqueSlug(baseSlug);
|
||||
|
||||
try {
|
||||
await this.db.prepare(`
|
||||
@ -93,6 +103,8 @@ export class DataMigrator {
|
||||
hourlyRate,
|
||||
).run();
|
||||
|
||||
this.artistIdMap.set(artist.id, artistId);
|
||||
|
||||
console.log(`Created artist record: ${artist.name} (slug: ${slug})`);
|
||||
} catch (error) {
|
||||
console.error(`Error creating artist record for ${artist.name}:`, error);
|
||||
@ -103,14 +115,19 @@ export class DataMigrator {
|
||||
/**
|
||||
* Create portfolio images for an artist
|
||||
*/
|
||||
private async createPortfolioImages(artist: any): Promise<void> {
|
||||
const artistId = `artist-${artist.id}`;
|
||||
private async createPortfolioImages(artist: StaticArtist): Promise<void> {
|
||||
const artistId = this.artistIdMap.get(artist.id);
|
||||
|
||||
if (!artistId) {
|
||||
console.warn(`Skipping portfolio images for ${artist.name}: missing artistId mapping`);
|
||||
return;
|
||||
}
|
||||
|
||||
// Create portfolio images from workImages array
|
||||
if (artist.workImages && Array.isArray(artist.workImages)) {
|
||||
for (let i = 0; i < artist.workImages.length; i++) {
|
||||
const imageUrl = artist.workImages[i];
|
||||
const imageId = `portfolio-${artist.id}-${i + 1}`;
|
||||
const imageId = crypto.randomUUID();
|
||||
|
||||
try {
|
||||
await this.db.prepare(`
|
||||
@ -136,7 +153,7 @@ export class DataMigrator {
|
||||
|
||||
// Also add the face image as a portfolio image
|
||||
if (artist.faceImage) {
|
||||
const faceImageId = `portfolio-${artist.id}-face`;
|
||||
const faceImageId = crypto.randomUUID();
|
||||
|
||||
try {
|
||||
await this.db.prepare(`
|
||||
@ -173,6 +190,23 @@ export class DataMigrator {
|
||||
.replace(/^-+|-+$/g, ''); // Trim hyphens from ends
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure slug is unique in the database by appending a counter if needed
|
||||
*/
|
||||
private async ensureUniqueSlug(slug: string): Promise<string> {
|
||||
let candidate = slug;
|
||||
let i = 1;
|
||||
// Check for existence and increment suffix until unique
|
||||
while (true) {
|
||||
const existing = await this.db
|
||||
.prepare('SELECT slug FROM artists WHERE slug = ? LIMIT 1')
|
||||
.bind(candidate)
|
||||
.first();
|
||||
if (!existing) return candidate;
|
||||
candidate = `${slug}-${i++}`;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract Instagram handle from full URL
|
||||
*/
|
||||
@ -228,7 +262,8 @@ export class DataMigrator {
|
||||
async isMigrationCompleted(): Promise<boolean> {
|
||||
try {
|
||||
const result = await this.db.prepare('SELECT COUNT(*) as count FROM artists').first();
|
||||
return (result as any)?.count > 0;
|
||||
const count = (result as { count: number } | null)?.count ?? 0;
|
||||
return count > 0;
|
||||
} catch (error) {
|
||||
console.error('Error checking migration status:', error);
|
||||
return false;
|
||||
@ -263,16 +298,20 @@ export class DataMigrator {
|
||||
totalPortfolioImages: number;
|
||||
}> {
|
||||
try {
|
||||
const [usersResult, artistsResult, imagesResult] = await Promise.all([
|
||||
type CountRow = { count: number };
|
||||
const [usersResult, artistsResult, imagesResult]: unknown[] = await Promise.all([
|
||||
this.db.prepare('SELECT COUNT(*) as count FROM users WHERE role = "ARTIST"').first(),
|
||||
this.db.prepare('SELECT COUNT(*) as count FROM artists').first(),
|
||||
this.db.prepare('SELECT COUNT(*) as count FROM portfolio_images').first(),
|
||||
]);
|
||||
|
||||
const isCountRow = (row: unknown): row is CountRow =>
|
||||
typeof (row as CountRow)?.count === 'number';
|
||||
|
||||
return {
|
||||
totalUsers: (usersResult as any)?.count || 0,
|
||||
totalArtists: (artistsResult as any)?.count || 0,
|
||||
totalPortfolioImages: (imagesResult as any)?.count || 0,
|
||||
totalUsers: isCountRow(usersResult) ? usersResult.count : 0,
|
||||
totalArtists: isCountRow(artistsResult) ? artistsResult.count : 0,
|
||||
totalPortfolioImages: isCountRow(imagesResult) ? imagesResult.count : 0,
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('Error getting migration stats:', error);
|
||||
|
||||
83
lib/db.ts
83
lib/db.ts
@ -73,7 +73,8 @@ export async function getPublicArtists(filters?: import('@/types/database').Arti
|
||||
a.specialties,
|
||||
a.instagram_handle,
|
||||
a.is_active,
|
||||
a.hourly_rate
|
||||
a.hourly_rate,
|
||||
a.created_at
|
||||
FROM artists a
|
||||
WHERE a.is_active = 1
|
||||
`;
|
||||
@ -122,6 +123,7 @@ export async function getPublicArtists(filters?: import('@/types/database').Arti
|
||||
instagramHandle: artist.instagram_handle,
|
||||
isActive: Boolean(artist.is_active),
|
||||
hourlyRate: artist.hourly_rate,
|
||||
createdAt: artist.created_at ? new Date(artist.created_at) : undefined,
|
||||
portfolioImages: (portfolioResult.results as any[]).map(img => ({
|
||||
id: img.id,
|
||||
artistId: img.artist_id,
|
||||
@ -265,6 +267,28 @@ export async function createArtist(data: CreateArtistInput, env?: any): Promise<
|
||||
const db = getDB(env);
|
||||
const id = crypto.randomUUID();
|
||||
|
||||
// Helper to generate a URL-friendly slug
|
||||
const generateSlug = (name: string) =>
|
||||
name
|
||||
.toLowerCase()
|
||||
.replace(/['']/g, '')
|
||||
.replace(/[^a-z0-9]+/g, '-')
|
||||
.replace(/^-+|-+$/g, '');
|
||||
|
||||
// Ensure slug is unique in DB
|
||||
const ensureUniqueSlug = async (slugBase: string): Promise<string> => {
|
||||
let candidate = slugBase;
|
||||
let i = 1;
|
||||
while (true) {
|
||||
const existing = await db.prepare('SELECT slug FROM artists WHERE slug = ? LIMIT 1').bind(candidate).first();
|
||||
if (!existing) return candidate;
|
||||
candidate = `${slugBase}-${i++}`;
|
||||
}
|
||||
};
|
||||
|
||||
const slugBase = data.name ? generateSlug(data.name) : generateSlug(crypto.randomUUID());
|
||||
const slug = await ensureUniqueSlug(slugBase);
|
||||
|
||||
// First create or get the user
|
||||
let userId = data.userId;
|
||||
if (!userId) {
|
||||
@ -272,27 +296,52 @@ export async function createArtist(data: CreateArtistInput, env?: any): Promise<
|
||||
INSERT INTO users (id, email, name, role)
|
||||
VALUES (?, ?, ?, 'ARTIST')
|
||||
RETURNING id
|
||||
`).bind(crypto.randomUUID(), data.email || `${data.name.toLowerCase().replace(/\s+/g, '.')}@unitedtattoo.com`, data.name).first();
|
||||
`)
|
||||
.bind(
|
||||
crypto.randomUUID(),
|
||||
data.email || `${data.name.toLowerCase().replace(/\s+/g, '.')}@unitedtattoo.com`,
|
||||
data.name
|
||||
)
|
||||
.first();
|
||||
|
||||
userId = (userResult as any)?.id;
|
||||
userId = (userResult as { id: string } | null)?.id;
|
||||
}
|
||||
|
||||
const result = await db.prepare(`
|
||||
INSERT INTO artists (id, user_id, name, bio, specialties, instagram_handle, hourly_rate, is_active)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
||||
const inserted = await db.prepare(`
|
||||
INSERT INTO artists (id, user_id, slug, name, bio, specialties, instagram_handle, hourly_rate, is_active)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
RETURNING *
|
||||
`).bind(
|
||||
id,
|
||||
userId,
|
||||
data.name,
|
||||
data.bio,
|
||||
JSON.stringify(data.specialties),
|
||||
data.instagramHandle || null,
|
||||
data.hourlyRate || null,
|
||||
data.isActive !== false
|
||||
).first();
|
||||
`)
|
||||
.bind(
|
||||
id,
|
||||
userId,
|
||||
slug,
|
||||
data.name,
|
||||
data.bio,
|
||||
JSON.stringify(data.specialties),
|
||||
data.instagramHandle || null,
|
||||
data.hourlyRate || null,
|
||||
data.isActive !== false
|
||||
)
|
||||
.first();
|
||||
|
||||
return result as Artist;
|
||||
// Parse JSON fields and normalize to match our Artist type
|
||||
const row = inserted as any;
|
||||
return {
|
||||
id: row.id,
|
||||
userId: row.user_id,
|
||||
slug: row.slug,
|
||||
name: row.name,
|
||||
bio: row.bio,
|
||||
specialties: row.specialties ? JSON.parse(row.specialties) : [],
|
||||
instagramHandle: row.instagram_handle ?? undefined,
|
||||
portfolioImages: [],
|
||||
isActive: Boolean(row.is_active),
|
||||
hourlyRate: row.hourly_rate ?? undefined,
|
||||
availability: [],
|
||||
createdAt: new Date(row.created_at),
|
||||
updatedAt: new Date(row.updated_at),
|
||||
} satisfies Artist;
|
||||
}
|
||||
|
||||
export async function updateArtist(id: string, data: UpdateArtistInput, env?: any): Promise<Artist> {
|
||||
|
||||
@ -46,5 +46,5 @@ MIGRATE_TOKEN = "ut_migrate_20251006_rotated_1a2b3c"
|
||||
|
||||
[dev]
|
||||
ip = "0.0.0.0"
|
||||
port = 8787
|
||||
port = 8897
|
||||
local_protocol = "http"
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user