Tous les projetsProjet 05 / 06

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.

Étude de cas

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

  1. 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.

  2. 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.

  3. 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 · actions/booking-actions.tsExtrait d'un dépôt privé
// 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;}
La base tranche la course : pas de verrou applicatif, un index unique partiel.

Résultat

En ligne sur ors-shooting.ch ; le photographe confirme les demandes et gère les photos depuis le back-office.

ors-shooting — Accueil « révélateur »
Fig. 01Accueil « révélateur »
ors-shooting — Planche contact
Fig. 02Planche contact
ors-shooting — Tirage développé
Fig. 03Tirage développé
ors-shooting — Pellicule épinglée
Fig. 04Pellicule épinglée
ors-shooting — Visionneuse
Fig. 05Visionneuse
ors-shooting — Réservation par créneaux
Fig. 06Réservation par créneaux
ors-shooting — Manifeste au scroll
Fig. 07Manifeste au scroll
ors-shooting — Le procédé
Fig. 08Le procédé
ors-shooting — Horaires du labo
Fig. 09Horaires du labo
© 2026 Giovanni Salcuni · Conçu & développé par moi-mêmeHeure locale (Suisse) ·
Mode présentationConfidentialité