Extended uploadImages() call to include the 4 image files referenced by
the site_content and testimonials migrations: about/mira-portrait.jpg
and testimonials/{amara,keisha,nadia}.jpg. These paths are seeded in
the migration but were not being uploaded to the salon-images bucket,
causing broken image links on deploy.
Verified files exist in public/ and match migration seed paths.
Claude-Session: https://claude.ai/code/session_01NhxA4npAXAxmCDEJt6kxSH
127 lines
6.4 KiB
TypeScript
127 lines
6.4 KiB
TypeScript
import { readFile } from 'node:fs/promises'
|
|
import path from 'node:path'
|
|
import { createClient } from '@supabase/supabase-js'
|
|
|
|
// `src/data/products.ts` and `src/data/services.ts` now fetch live rows from Supabase
|
|
// (see Tasks 2-3 of this plan) instead of exporting static arrays, so this seed script
|
|
// owns its own copy of the seed data below rather than importing it from application code.
|
|
// WARNING: re-running this after admin-panel edits will revert those rows to these seed
|
|
// defaults (unconditional upsert keyed on id). Intended for initial setup / disaster
|
|
// recovery only, not routine use.
|
|
interface SeedProduct {
|
|
id: string
|
|
name: string
|
|
category: 'hair-care' | 'extensions' | 'tools' | 'accessories'
|
|
price: number
|
|
image: string
|
|
description: string
|
|
inStock: boolean
|
|
}
|
|
|
|
interface SeedService {
|
|
id: string
|
|
name: string
|
|
durationMinutes: number
|
|
priceFrom: number
|
|
description: string
|
|
image: string
|
|
}
|
|
|
|
const products: SeedProduct[] = [
|
|
{ id: 'edge-control', name: 'Gel Contour Soyeux', category: 'hair-care', price: 12, image: '/products/edge-control.jpg', description: 'Tenue longue durée avec un éclat naturel, sans effet blanc.', inStock: true },
|
|
{ id: 'braid-oil', name: 'Huile Nourrissante pour Tresses', category: 'hair-care', price: 16, image: '/products/braid-oil.jpg', description: "Huile légère pour le cuir chevelu qui apaise et ajoute de l'éclat entre les lavages.", inStock: true },
|
|
{ id: 'leave-in', name: 'Soin Démêlant Sans Rinçage', category: 'hair-care', price: 14, image: '/products/leave-in.jpg', description: 'Après-shampooing sans rinçage riche en glisse pour un démêlage tout en douceur.', inStock: true },
|
|
{ id: 'kanekalon-black', name: 'Fibre à Tresser Kanekalon — Noir Jais', category: 'extensions', price: 8, image: '/products/kanekalon-black.jpg', description: 'Fibre synthétique haut de gamme pour tresses, texture douce, faible chute.', inStock: true },
|
|
{ id: 'kanekalon-brown', name: 'Fibre à Tresser Kanekalon — Châtain', category: 'extensions', price: 8, image: '/products/kanekalon-brown.jpg', description: 'Teinte brune chaude qui se fond parfaitement pour un rendu naturel.', inStock: false },
|
|
{ id: 'locs-extension', name: 'Extensions de Faux Locs', category: 'extensions', price: 22, image: '/products/locs-extension.jpg', description: 'Faux locs pré-bouclées pour une pose naturelle et légère.', inStock: true },
|
|
{ id: 'rat-tail-comb', name: 'Peigne à Tige en Acier', category: 'tools', price: 6, image: '/products/rat-tail-comb.jpg', description: 'Peigne de précision pour les raies, avec une fine tige métallique.', inStock: true },
|
|
{ id: 'edge-brush', name: 'Mini Brosse à Contours', category: 'tools', price: 5, image: '/products/edge-brush.jpg', description: 'Brosse double face pour lisser les contours et discipliner les cheveux follets.', inStock: true },
|
|
{ id: 'gold-cuffs', name: 'Anneaux Dorés pour Cheveux (Lot de 6)', category: 'accessories', price: 10, image: '/products/gold-cuffs.jpg', description: 'Anneaux ajustables pour accessoiriser tresses et locs.', inStock: true },
|
|
{ id: 'satin-bonnet', name: 'Bonnet de Nuit en Satin', category: 'accessories', price: 9, image: '/products/satin-bonnet.jpg', description: 'Protège tresses et locs pendant la nuit et réduit les frisottis.', inStock: true },
|
|
]
|
|
|
|
const services: SeedService[] = [
|
|
{ id: 'knotless', name: 'Tresses sans nœuds', durationMinutes: 240, priceFrom: 150, description: 'Tresses sans tension avec une racine naturelle et invisible.', image: '/styles/knotless.jpg' },
|
|
{ id: 'boho', name: 'Tresses Boho', durationMinutes: 270, priceFrom: 180, description: 'Tresses sans nœuds agrémentées de mèches bouclées pour un look doux et romantique.', image: '/styles/boho.jpg' },
|
|
{ id: 'locs', name: 'Faux Locs', durationMinutes: 300, priceFrom: 200, description: "Locs légères à l'allure naturelle, installées avec soin.", image: '/styles/locs.jpg' },
|
|
{ id: 'cornrows', name: 'Nattes Collées', durationMinutes: 120, priceFrom: 90, description: 'Nattes collées classiques et nettes, droites vers l\'arrière ou avec un motif personnalisé.', image: '/styles/cornrows.jpg' },
|
|
]
|
|
|
|
const supabaseUrl = process.env.SUPABASE_URL
|
|
const serviceRoleKey = process.env.SUPABASE_SERVICE_ROLE_KEY
|
|
|
|
if (!supabaseUrl || !serviceRoleKey) {
|
|
console.error('Set SUPABASE_URL and SUPABASE_SERVICE_ROLE_KEY environment variables before running this script.')
|
|
process.exit(1)
|
|
}
|
|
|
|
const supabase = createClient(supabaseUrl, serviceRoleKey)
|
|
|
|
function toStoragePath(image: string): string {
|
|
return image.replace(/^\//, '')
|
|
}
|
|
|
|
async function uploadImages(imagePaths: string[]) {
|
|
for (const imagePath of imagePaths) {
|
|
const relativePath = toStoragePath(imagePath)
|
|
const filePath = path.join(process.cwd(), 'public', relativePath)
|
|
const fileBuffer = await readFile(filePath)
|
|
const { error } = await supabase.storage.from('salon-images').upload(relativePath, fileBuffer, {
|
|
upsert: true,
|
|
contentType: 'image/jpeg',
|
|
})
|
|
if (error) throw error
|
|
}
|
|
console.log(`Uploaded ${imagePaths.length} images to the salon-images bucket.`)
|
|
}
|
|
|
|
async function seed() {
|
|
const productRows = products.map(({ id, name, category, price, image, description, inStock }) => ({
|
|
id,
|
|
name,
|
|
category,
|
|
price,
|
|
image: toStoragePath(image),
|
|
description,
|
|
in_stock: inStock,
|
|
}))
|
|
const { error: productsError } = await supabase.from('products').upsert(productRows)
|
|
if (productsError) throw productsError
|
|
|
|
const serviceRows = services.map(({ id, name, durationMinutes, priceFrom, description, image }) => ({
|
|
id,
|
|
name,
|
|
duration_minutes: durationMinutes,
|
|
price_from: priceFrom,
|
|
description,
|
|
image: toStoragePath(image),
|
|
}))
|
|
const { error: servicesError } = await supabase.from('services').upsert(serviceRows)
|
|
if (servicesError) throw servicesError
|
|
|
|
const { error: scheduleError } = await supabase.from('schedule_template').upsert({
|
|
id: 'default',
|
|
working_days: [2, 3, 4, 5, 6],
|
|
start_time: '09:00',
|
|
end_time: '18:00',
|
|
slot_minutes: 120,
|
|
})
|
|
if (scheduleError) throw scheduleError
|
|
|
|
await uploadImages([
|
|
...products.map((p) => p.image),
|
|
...services.map((s) => s.image),
|
|
'/about/mira-portrait.jpg',
|
|
'/testimonials/amara.jpg',
|
|
'/testimonials/keisha.jpg',
|
|
'/testimonials/nadia.jpg',
|
|
])
|
|
|
|
console.log(`Seeded ${products.length} products, ${services.length} services, and the default schedule.`)
|
|
}
|
|
|
|
seed().catch((err) => {
|
|
console.error(err)
|
|
process.exit(1)
|
|
})
|