ors-shooting
- Année
- 2025
- Rôle
- Site vitrine · Réservation · Motion design
- Stack
- Next.js 16, TypeScript, Tailwind 4, Framer Motion, Lenis, Drizzle ORM, Turso, Better-Auth, Vercel Blob, Resend
- Liens
- Voir le siteDépôt privé
Site d'un photographe indépendant, entièrement repensé en 2026 autour d'une direction « chambre noire ». Intro de développement, hero « révélateur » où une loupe qui suit le curseur révèle le tirage en couleur sous la lumière rouge du labo, pellicule épinglée qui défile à l'horizontale, planche contact annotée au crayon, visionneuse qui s'agrandit depuis la vignette, défilement fluide et transitions de page. Côté métier : réservation par créneaux à l'heure de Zurich, supplément automatique soirée et dimanche, validation et anti-spam côté serveur, back-office pour confirmer les demandes et gérer les photos.
Le contexte
Un photographe indépendant avait besoin d'un site qui montre son travail et d'une réservation en ligne, sans allers-retours de messages.
Mon rôle
Seul : site, réservation et back-office, puis refonte « chambre noire » en 2026.
Défis techniques
- 01
Une réservation par créneaux
Créneaux à l'heure de Zurich, supplément automatique le soir et le dimanche, validation et anti-spam côté serveur ; chaque demande reste en attente jusqu'à la confirmation du photographe.
- 02
Jamais deux fois le même créneau
Un index unique partiel, limité aux demandes en attente ou confirmées, tranche entre deux demandes simultanées ; un créneau refusé redevient libre.
- 03
Une révélation à la loupe
Une loupe qui suit le curseur révèle le tirage en couleur sous la lumière rouge du labo, avec une pellicule épinglée qui défile à l'horizontale.
Extrait de code
// db/schema.ts — partiel : seules les demandes en attente ou confirmées occupent la dateuniqueDate: uniqueIndex('idx_bookings_date').on(t.dateTime).where(sql`status in ('pending', 'confirmed')`), // actions/booking-actions.tstry { await db.insert(bookings).values({ /* … */ dateTime: date, status: "pending" });} catch (error) { // Deux demandes simultanées sur le même créneau : l'index unique tranche if (String((error as { cause?: unknown })?.cause ?? error).includes("UNIQUE")) { throw new ActionError(SLOT_TAKEN); } throw error;}Résultat
En ligne sur ors-shooting.ch ; le photographe confirme les demandes et gère les photos depuis le back-office.









