Deuxième passe DA : direction "punchy, friendly, donne envie de fun"
Retour direct de Kévin après la 1ère passe (trop sage) : palette poussée en saturation (corail vif en primaire, prune + doré en accents), formes plus rondes (boutons pilule, rayons 20px), micro-animations avec du peps (easing overshoot, coche qui se dessine + confettis CSS purs à l'envoi, spinner animé). Police d'accent Fraunces ajoutée pour les titres (2 fichiers .woff2 auto-hébergés, ~44 Ko, SIL OFL, jamais de CDN). Ton de voix retravaillé : micro-copies plus souriantes, nouvel état "limite de photos atteinte" traité chaleureusement (pas comme une erreur). Cohérence maintenue sur galerie admin, carton imprimable, et générateur QR (sync légère, priorité basse). Testé en conditions réelles sur photobooth.assistantaikev.eu.
This commit is contained in:
+77
-33
@@ -12,6 +12,8 @@
|
||||
lors du déploiement, hors scope de ce fichier front.
|
||||
-->
|
||||
<meta name="robots" content="noindex, nofollow">
|
||||
<meta name="theme-color" content="#faf6f0">
|
||||
<link rel="preload" href="/vendor/fonts/fraunces-latin-600-normal.woff2" as="font" type="font/woff2" crossorigin>
|
||||
<link rel="stylesheet" href="/e/style.css">
|
||||
</head>
|
||||
<body>
|
||||
@@ -25,19 +27,43 @@
|
||||
<span class="brand__name">photobooth</span>
|
||||
</p>
|
||||
<h1 id="event-name">Ajouter une photo</h1>
|
||||
<p class="page__kicker">Une photo, un petit mot, et hop dans l'album !</p>
|
||||
</header>
|
||||
|
||||
<section id="status-loading" class="state-block" role="status">
|
||||
<section id="status-loading" class="state-block state-block--loading" role="status">
|
||||
<span class="spinner" aria-hidden="true"></span>
|
||||
<p>Chargement de l'événement…</p>
|
||||
</section>
|
||||
|
||||
<section id="status-error" class="state-block state-block--error" hidden role="alert">
|
||||
<div class="state-icon state-icon--error" aria-hidden="true">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" focusable="false">
|
||||
<circle cx="12" cy="12" r="9"/>
|
||||
<path d="M12 8v5"/>
|
||||
<path d="M12 16.2v.1"/>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="state-icon state-icon--limit" aria-hidden="true">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" focusable="false">
|
||||
<path d="M12 20.2s-7.5-4.6-7.5-10.1a4.4 4.4 0 0 1 7.5-3.1 4.4 4.4 0 0 1 7.5 3.1c0 5.5-7.5 10.1-7.5 10.1Z"/>
|
||||
</svg>
|
||||
</div>
|
||||
<p id="status-error-message"></p>
|
||||
<button type="button" id="retry-btn" hidden>Réessayer</button>
|
||||
</section>
|
||||
|
||||
<section id="success-block" class="state-block state-block--success" hidden role="status">
|
||||
<p>Merci ! Ta photo est bien envoyée 🎉</p>
|
||||
<div class="confetti" aria-hidden="true">
|
||||
<span></span><span></span><span></span><span></span><span></span><span></span>
|
||||
</div>
|
||||
<div class="success-check" aria-hidden="true">
|
||||
<svg viewBox="0 0 52 52" fill="none" focusable="false">
|
||||
<circle class="success-check__circle" cx="26" cy="26" r="23" stroke-width="2.5"/>
|
||||
<path class="success-check__mark" d="M15 27.5 22.5 35 38 18" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
</div>
|
||||
<p class="success-title">C'est envoyé !</p>
|
||||
<p class="success-text">Ta photo vient de rejoindre la fête. Merci d'avoir été là !</p>
|
||||
<button type="button" id="add-another-btn">Ajouter une autre photo</button>
|
||||
</section>
|
||||
|
||||
@@ -47,16 +73,20 @@
|
||||
<!-- Deux entrées séparées : "photo-input" (capture="environment")
|
||||
ouvre directement l'appareil photo, "photo-input-gallery" ouvre
|
||||
le sélecteur de fichiers classique. Décision actée avec Kévin le
|
||||
2026-09-16 : les deux doivent être accessibles en un tap. -->
|
||||
2026-09-16 : les deux doivent être accessibles en un tap.
|
||||
Hiérarchie visuelle ajoutée : "Prendre une photo" est l'action
|
||||
attendue pour l'immense majorité des invités pendant l'événement
|
||||
(capture sur le moment), donc traitée en primaire ; "Depuis la
|
||||
galerie" reste secondaire mais tout aussi accessible. -->
|
||||
<div class="photo-source-buttons">
|
||||
<label for="photo-input" class="btn-secondary">
|
||||
<label for="photo-input" class="btn-source btn-source--primary">
|
||||
<svg class="btn-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false">
|
||||
<path d="M4 8h3l1.5-2h7L17 8h3a1 1 0 0 1 1 1v10a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1V9a1 1 0 0 1 1-1Z"/>
|
||||
<circle cx="12" cy="13" r="3.5"/>
|
||||
</svg>
|
||||
<span>Prendre une photo</span>
|
||||
</label>
|
||||
<label for="photo-input-gallery" class="btn-secondary">
|
||||
<label for="photo-input-gallery" class="btn-source btn-source--secondary">
|
||||
<svg class="btn-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false">
|
||||
<rect x="3" y="4" width="18" height="16" rx="2.5"/>
|
||||
<circle cx="8.5" cy="9.5" r="1.5"/>
|
||||
@@ -87,42 +117,56 @@
|
||||
|
||||
<img id="preview" class="preview" alt="Aperçu de la photo choisie" hidden>
|
||||
|
||||
<div class="field">
|
||||
<label for="nom-input">Ton prénom <span class="optional">(facultatif)</span></label>
|
||||
<input
|
||||
type="text"
|
||||
id="nom-input"
|
||||
name="nom"
|
||||
maxlength="60"
|
||||
autocomplete="given-name"
|
||||
placeholder="Ex : Camille"
|
||||
>
|
||||
</div>
|
||||
<div class="field-group">
|
||||
<div class="field">
|
||||
<label for="nom-input">Ton prénom <span class="optional">(facultatif)</span></label>
|
||||
<input
|
||||
type="text"
|
||||
id="nom-input"
|
||||
name="nom"
|
||||
maxlength="60"
|
||||
autocomplete="given-name"
|
||||
placeholder="Ex : Camille"
|
||||
>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label for="message-input">Un petit mot <span class="optional">(facultatif)</span></label>
|
||||
<textarea
|
||||
id="message-input"
|
||||
name="message"
|
||||
maxlength="300"
|
||||
rows="3"
|
||||
placeholder="Ton petit mot pour l'occasion…"
|
||||
></textarea>
|
||||
<div class="field">
|
||||
<label for="message-input">Un petit mot <span class="optional">(facultatif)</span></label>
|
||||
<textarea
|
||||
id="message-input"
|
||||
name="message"
|
||||
maxlength="300"
|
||||
rows="3"
|
||||
placeholder="Ton petit mot pour l'occasion…"
|
||||
></textarea>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p class="legal-notice">
|
||||
En déposant une photo, vous acceptez qu'elle soit stockée temporairement
|
||||
dans le cadre de cet événement et mise à disposition de l'organisateur.
|
||||
Vos photos sont automatiquement supprimées 7 jours après la date de
|
||||
l'événement, sans action de votre part. Aucune photo n'est utilisée à
|
||||
d'autres fins, ni transmise à des tiers. Vous pouvez demander la
|
||||
suppression anticipée d'une photo en contactant l'organisateur de
|
||||
l'événement.
|
||||
<svg class="legal-notice__icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false">
|
||||
<rect x="4.5" y="10.5" width="15" height="9.5" rx="2"/>
|
||||
<path d="M8 10.5V7.5a4 4 0 0 1 8 0v3"/>
|
||||
</svg>
|
||||
<span>
|
||||
En déposant une photo, vous acceptez qu'elle soit stockée temporairement
|
||||
dans le cadre de cet événement et mise à disposition de l'organisateur.
|
||||
Vos photos sont automatiquement supprimées 7 jours après la date de
|
||||
l'événement, sans action de votre part. Aucune photo n'est utilisée à
|
||||
d'autres fins, ni transmise à des tiers. Vous pouvez demander la
|
||||
suppression anticipée d'une photo en contactant l'organisateur de
|
||||
l'événement.
|
||||
</span>
|
||||
</p>
|
||||
|
||||
<p id="progress-text" class="progress-text" aria-live="polite"></p>
|
||||
|
||||
<button type="submit" id="submit-btn">Envoyer ma photo</button>
|
||||
<button type="submit" id="submit-btn">
|
||||
<svg class="btn-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false">
|
||||
<path d="M21 4 3 11.5l7 2.5"/>
|
||||
<path d="M10 14l2.5 7L21 4"/>
|
||||
</svg>
|
||||
<span id="submit-btn-label">Envoyer ma photo</span>
|
||||
</button>
|
||||
</form>
|
||||
</main>
|
||||
|
||||
|
||||
+413
-66
@@ -1,42 +1,92 @@
|
||||
/* Page upload invité — mobile-first, sobre, sans dépendance externe.
|
||||
/* Page upload invité — mobile-first, sans dépendance externe.
|
||||
Système de design partagé avec la galerie admin (src/admin/gallery/style.css) :
|
||||
mêmes tokens de couleur/rayon/ombre, pour une identité cohérente entre les
|
||||
deux écrans (voir docs/decisions.md pour le contexte produit). */
|
||||
deux écrans (voir docs/decisions.md pour le contexte produit).
|
||||
|
||||
Direction créative (passe 2, 2026-09-16) — voir rapport de la passe pour
|
||||
le détail complet, résumé ici :
|
||||
Retour direct de Kévin sur la 1ère passe : trop "démo sage", pas assez
|
||||
"envie de participer". Consigne resserrée en cours de route : viser
|
||||
PUNCHY / FRIENDLY / FUN, pas un premium feutré silencieux. Traduction :
|
||||
- Palette plus affirmée : on garde la famille chaude (cohérente avec
|
||||
l'identité perso de Kévin / Rêves Liquides) mais on la sature — un
|
||||
corail vif en primaire plutôt que le brun terracotta sourd de la V1,
|
||||
avec un prune et un doré en accents secondaires (confettis, badges).
|
||||
- Formes plus rondes/organiques (rayons agrandis, boutons pilule).
|
||||
- Micro-animations qui ont du peps : coche qui "pop" avec rebond,
|
||||
confettis colorés au succès, spinner de chargement, easing "overshoot"
|
||||
sur les interactions plutôt que du linear plat.
|
||||
- Ton de voix plus souriant dans les micro-copies (succès, limite
|
||||
atteinte) — cf. src/e/upload.js pour les textes.
|
||||
- Police d'accent Fraunces (serif à forte personnalité, chaleureuse et
|
||||
un peu "pop") en plus de la pile système pour les titres/moments clés
|
||||
— auto-hébergée en .woff2, 2 graisses (~41 Ko au total), voir
|
||||
src/vendor/fonts/ et src/vendor/NOTICE.md. Le corps de texte reste
|
||||
100% système (poids nul). */
|
||||
|
||||
@font-face {
|
||||
font-family: "Fraunces";
|
||||
src: url("/vendor/fonts/fraunces-latin-600-normal.woff2") format("woff2");
|
||||
font-weight: 600;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+2000-206F,
|
||||
U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: "Fraunces";
|
||||
src: url("/vendor/fonts/fraunces-latin-500-italic.woff2") format("woff2");
|
||||
font-weight: 500;
|
||||
font-style: italic;
|
||||
font-display: swap;
|
||||
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+2000-206F,
|
||||
U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
|
||||
}
|
||||
|
||||
:root {
|
||||
color-scheme: light;
|
||||
|
||||
/* Couleurs — palette chaude et sobre, cohérente avec l'identité perso de
|
||||
Kévin Lecou (Rêves Liquides). L'accent brun/terracotta est inchangé par
|
||||
rapport à la V1 déployée : on affine la palette autour, pas de rupture. */
|
||||
--color-bg: #faf8f5;
|
||||
/* Couleurs — famille chaude toujours ancrée dans l'identité de Kévin,
|
||||
mais poussée en saturation pour un rendu festif plutôt que sobre. */
|
||||
--color-bg: #fff8f0;
|
||||
--color-surface: #ffffff;
|
||||
--color-surface-muted: #f2ede4;
|
||||
--color-text: #262420;
|
||||
--color-muted: #6f6a62;
|
||||
--color-border: #e3ddd2;
|
||||
--color-surface-muted: #fdecdd;
|
||||
--color-text: #2b2016;
|
||||
--color-muted: #8a7869;
|
||||
--color-border: #f1ddc6;
|
||||
|
||||
--color-accent: #8a5a44;
|
||||
--color-accent-hover: #6e4735;
|
||||
--color-accent-soft: #f1e3d8;
|
||||
--color-accent-contrast: #ffffff;
|
||||
--color-accent: #e35f2b;
|
||||
--color-accent-hover: #c74c1e;
|
||||
--color-accent-light: #ff8a52;
|
||||
--color-accent-soft: #ffe3d0;
|
||||
--color-accent-contrast: #fffaf6;
|
||||
|
||||
--color-error-bg: #fbeceb;
|
||||
--color-error-text: #7a1f14;
|
||||
--color-success-bg: #eaf3ec;
|
||||
--color-success-text: #1e5c2d;
|
||||
--color-plum: #7c2f56;
|
||||
--color-plum-soft: #f8e3ee;
|
||||
|
||||
/* Rayon à deux niveaux : sm pour les petits éléments (inputs, chips),
|
||||
base pour les blocs/cards — donne une identité "arrondie/artisanale"
|
||||
un peu plus marquée que le 10px uniforme de la V1. */
|
||||
--radius-sm: 8px;
|
||||
--radius: 14px;
|
||||
--color-gold: #f0a91e;
|
||||
--color-gold-soft: #fef0d3;
|
||||
|
||||
--shadow-sm: 0 1px 2px rgba(38, 36, 32, 0.06);
|
||||
--shadow-md: 0 6px 20px rgba(38, 36, 32, 0.10);
|
||||
--color-error-bg: #fdeae6;
|
||||
--color-error-text: #a3341c;
|
||||
--color-success-bg: #fff1de;
|
||||
--color-success-text: #8a4a10;
|
||||
--color-limit-bg: #fbe9d8;
|
||||
--color-limit-text: #8a4a10;
|
||||
|
||||
--radius-sm: 12px;
|
||||
--radius: 20px;
|
||||
--radius-pill: 999px;
|
||||
|
||||
--shadow-sm: 0 2px 6px rgba(163, 78, 30, 0.1);
|
||||
--shadow-md: 0 10px 28px rgba(163, 78, 30, 0.16);
|
||||
|
||||
--ease-bounce: cubic-bezier(0.34, 1.56, 0.64, 1);
|
||||
|
||||
--font-sans: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,
|
||||
"Helvetica Neue", Arial, sans-serif;
|
||||
--font-display: "Fraunces", Georgia, "Times New Roman", serif;
|
||||
}
|
||||
|
||||
* {
|
||||
@@ -64,7 +114,7 @@ body {
|
||||
|
||||
.page__header {
|
||||
text-align: center;
|
||||
margin-bottom: 1.5rem;
|
||||
margin-bottom: 1.6rem;
|
||||
}
|
||||
|
||||
/* Bloc de marque — discret, rassure l'invité (outil "pro") et signe le
|
||||
@@ -76,7 +126,7 @@ body {
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 0.4rem;
|
||||
margin-bottom: 0.6rem;
|
||||
margin-bottom: 0.7rem;
|
||||
color: var(--color-muted);
|
||||
}
|
||||
|
||||
@@ -95,27 +145,59 @@ body {
|
||||
}
|
||||
|
||||
.page__header h1 {
|
||||
font-size: 1.5rem;
|
||||
font-weight: 700;
|
||||
font-family: var(--font-display);
|
||||
font-size: 2rem;
|
||||
font-weight: 600;
|
||||
letter-spacing: -0.01em;
|
||||
margin: 0.15rem 0 0;
|
||||
line-height: 1.15;
|
||||
margin: 0;
|
||||
/* Titre en dégradé corail -> doré : la touche "pop" la plus visible de la
|
||||
page, réservée à ce seul élément pour ne pas diluer son effet. Fallback
|
||||
: couleur pleine (--color-accent) si background-clip:text n'est pas
|
||||
supporté (support large sur mobile Safari/Chrome en pratique). */
|
||||
color: var(--color-accent);
|
||||
background: linear-gradient(100deg, var(--color-accent) 20%, var(--color-gold) 100%);
|
||||
-webkit-background-clip: text;
|
||||
background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
}
|
||||
|
||||
.page__kicker {
|
||||
font-family: var(--font-display);
|
||||
font-style: italic;
|
||||
font-weight: 500;
|
||||
font-size: 1.05rem;
|
||||
color: var(--color-plum);
|
||||
margin: 0.35rem 0 0;
|
||||
}
|
||||
|
||||
.state-block {
|
||||
text-align: center;
|
||||
padding: 1.25rem 1rem;
|
||||
padding: 1.5rem 1.2rem;
|
||||
border-radius: var(--radius);
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.state-block--loading {
|
||||
color: var(--color-muted);
|
||||
}
|
||||
|
||||
.state-block--error {
|
||||
background: var(--color-error-bg);
|
||||
color: var(--color-error-text);
|
||||
}
|
||||
|
||||
.state-block--error.state-block--limit {
|
||||
background: var(--color-limit-bg);
|
||||
color: var(--color-limit-text);
|
||||
}
|
||||
|
||||
.state-block--success {
|
||||
background: var(--color-success-bg);
|
||||
position: relative;
|
||||
background: linear-gradient(180deg, var(--color-success-bg), var(--color-surface) 75%);
|
||||
color: var(--color-success-text);
|
||||
overflow: visible;
|
||||
padding-top: 1.75rem;
|
||||
}
|
||||
|
||||
.state-block p {
|
||||
@@ -126,10 +208,67 @@ body {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
/* Spinner de chargement — anneau CSS pur, aucune dépendance. Remplace le
|
||||
simple texte "Chargement…" par un repère visuel qui bouge : la première
|
||||
impression de la page ne doit jamais sembler figée/en panne. */
|
||||
.spinner {
|
||||
display: inline-block;
|
||||
width: 26px;
|
||||
height: 26px;
|
||||
margin-bottom: 0.6rem;
|
||||
border: 3px solid var(--color-accent-soft);
|
||||
border-top-color: var(--color-accent);
|
||||
border-radius: 50%;
|
||||
animation: spin 0.8s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
/* Icônes des états erreur / limite atteinte — les deux existent toujours
|
||||
dans le DOM, affichées/masquées via la classe portée par le bloc parent
|
||||
(state-block--limit vs par défaut = erreur). Le choix entre les deux se
|
||||
fait côté JS uniquement en togglant cette classe (aucune autre logique
|
||||
déplacée dans le CSS). */
|
||||
.state-icon {
|
||||
display: none;
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
margin: 0 auto 0.6rem;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.state-block--error .state-icon--error {
|
||||
display: flex;
|
||||
background: rgba(163, 52, 28, 0.12);
|
||||
color: var(--color-error-text);
|
||||
}
|
||||
|
||||
.state-block--error.state-block--limit .state-icon--error {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.state-block--limit .state-icon--limit {
|
||||
display: flex;
|
||||
background: rgba(138, 74, 16, 0.14);
|
||||
color: var(--color-gold);
|
||||
animation: pop-in 0.5s var(--ease-bounce) both;
|
||||
}
|
||||
|
||||
.state-icon svg {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
}
|
||||
|
||||
.upload-form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1.1rem;
|
||||
gap: 1.15rem;
|
||||
}
|
||||
|
||||
.field {
|
||||
@@ -139,7 +278,7 @@ body {
|
||||
}
|
||||
|
||||
.field label {
|
||||
font-weight: 600;
|
||||
font-weight: 700;
|
||||
font-size: 0.95rem;
|
||||
}
|
||||
|
||||
@@ -149,20 +288,34 @@ body {
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
|
||||
/* Regroupe prénom + petit mot dans une même carte : crée un second niveau
|
||||
de hiérarchie visuelle (la photo reste l'action principale au-dessus,
|
||||
ces champs facultatifs sont secondaires et "posés" ensemble). */
|
||||
.field-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
background: var(--color-surface);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius);
|
||||
padding: 1.1rem;
|
||||
}
|
||||
|
||||
input[type="text"],
|
||||
input[type="file"],
|
||||
textarea {
|
||||
font: inherit;
|
||||
padding: 0.7rem 0.75rem;
|
||||
border: 1px solid var(--color-border);
|
||||
padding: 0.7rem 0.8rem;
|
||||
border: 1.5px solid var(--color-border);
|
||||
border-radius: var(--radius-sm);
|
||||
background: var(--color-surface);
|
||||
color: inherit;
|
||||
width: 100%;
|
||||
transition: border-color 0.15s ease;
|
||||
}
|
||||
|
||||
input[type="file"] {
|
||||
padding: 0.55rem 0.5rem;
|
||||
input[type="text"]:focus,
|
||||
textarea:focus {
|
||||
border-color: var(--color-accent);
|
||||
}
|
||||
|
||||
.visually-hidden {
|
||||
@@ -182,27 +335,51 @@ input[type="file"] {
|
||||
gap: 0.6rem;
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
/* Boutons "Prendre une photo" / "Depuis la galerie" — forme pilule (identité
|
||||
plus ronde/organique que la V1) avec une hiérarchie claire : la capture
|
||||
directe est l'action attendue pour la quasi-totalité des invités pendant
|
||||
l'événement (le moment se vit là, tout de suite), donc traitée en
|
||||
primaire pleine couleur ; la galerie reste secondaire mais tout aussi
|
||||
accessible en un tap (contrainte actée le 2026-09-16). */
|
||||
.btn-source {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 0.45rem;
|
||||
text-align: center;
|
||||
font-weight: 600;
|
||||
font-size: 0.9rem;
|
||||
padding: 0.85rem 0.6rem;
|
||||
min-height: 48px;
|
||||
border: 1px solid var(--color-accent);
|
||||
border-radius: var(--radius-sm);
|
||||
color: var(--color-accent);
|
||||
background: var(--color-surface);
|
||||
font-weight: 700;
|
||||
font-size: 0.88rem;
|
||||
padding: 0.9rem 0.6rem;
|
||||
min-height: 50px;
|
||||
border-radius: var(--radius-pill);
|
||||
cursor: pointer;
|
||||
transition: transform 0.12s var(--ease-bounce), box-shadow 0.12s ease, background 0.12s ease;
|
||||
}
|
||||
|
||||
.btn-secondary:active {
|
||||
background: var(--color-accent-soft);
|
||||
transform: translateY(1px);
|
||||
.btn-source--primary {
|
||||
border: none;
|
||||
background: linear-gradient(135deg, var(--color-accent-light), var(--color-accent));
|
||||
color: var(--color-accent-contrast);
|
||||
box-shadow: var(--shadow-sm);
|
||||
}
|
||||
|
||||
.btn-source--secondary {
|
||||
border: 1.5px solid var(--color-plum);
|
||||
background: var(--color-surface);
|
||||
color: var(--color-plum);
|
||||
}
|
||||
|
||||
.btn-source:active {
|
||||
transform: scale(0.96);
|
||||
}
|
||||
|
||||
.btn-source--primary:active {
|
||||
background: linear-gradient(135deg, var(--color-accent), var(--color-accent-hover));
|
||||
}
|
||||
|
||||
.btn-source--secondary:active {
|
||||
background: var(--color-plum-soft);
|
||||
}
|
||||
|
||||
/* Icônes SVG inline (aucune requête réseau) : remplacent les emoji des
|
||||
@@ -219,7 +396,8 @@ input[type="file"] {
|
||||
.file-chosen-label {
|
||||
font-size: 0.85rem;
|
||||
color: var(--color-muted);
|
||||
margin: 0;
|
||||
margin: 0.5rem 0 0;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
textarea {
|
||||
@@ -230,8 +408,8 @@ textarea {
|
||||
input:focus-visible,
|
||||
textarea:focus-visible,
|
||||
button:focus-visible {
|
||||
outline: 3px solid var(--color-accent);
|
||||
outline-offset: 1px;
|
||||
outline: 3px solid var(--color-plum);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
.preview {
|
||||
@@ -240,50 +418,71 @@ button:focus-visible {
|
||||
object-fit: contain;
|
||||
border-radius: var(--radius);
|
||||
background: var(--color-surface-muted);
|
||||
animation: pop-in 0.35s var(--ease-bounce) both;
|
||||
}
|
||||
|
||||
.legal-notice {
|
||||
font-size: 0.8rem;
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 0.6rem;
|
||||
font-size: 0.78rem;
|
||||
color: var(--color-muted);
|
||||
background: var(--color-surface-muted);
|
||||
border-radius: var(--radius);
|
||||
padding: 0.8rem 0.9rem;
|
||||
padding: 0.85rem 0.9rem;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.legal-notice__icon {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
flex-shrink: 0;
|
||||
margin-top: 0.1rem;
|
||||
color: var(--color-plum);
|
||||
}
|
||||
|
||||
.progress-text {
|
||||
min-height: 1.2em;
|
||||
font-size: 0.9rem;
|
||||
color: var(--color-muted);
|
||||
font-weight: 600;
|
||||
color: var(--color-plum);
|
||||
margin: 0;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
button {
|
||||
font: inherit;
|
||||
font-weight: 600;
|
||||
font-weight: 700;
|
||||
border: none;
|
||||
border-radius: var(--radius-sm);
|
||||
padding: 0.85rem 1rem;
|
||||
min-height: 48px;
|
||||
border-radius: var(--radius-pill);
|
||||
padding: 0.9rem 1rem;
|
||||
min-height: 50px;
|
||||
cursor: pointer;
|
||||
transition: transform 0.12s var(--ease-bounce), background 0.12s ease, opacity 0.12s ease;
|
||||
}
|
||||
|
||||
#submit-btn,
|
||||
#add-another-btn {
|
||||
background: var(--color-accent);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 0.5rem;
|
||||
background: linear-gradient(135deg, var(--color-accent-light), var(--color-accent));
|
||||
color: var(--color-accent-contrast);
|
||||
box-shadow: var(--shadow-sm);
|
||||
box-shadow: var(--shadow-md);
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
#submit-btn:active:not(:disabled),
|
||||
#add-another-btn:active {
|
||||
background: var(--color-accent-hover);
|
||||
transform: scale(0.97);
|
||||
background: linear-gradient(135deg, var(--color-accent), var(--color-accent-hover));
|
||||
}
|
||||
|
||||
#submit-btn:disabled {
|
||||
opacity: 0.6;
|
||||
cursor: not-allowed;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
#retry-btn {
|
||||
@@ -291,6 +490,154 @@ button {
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
button:active {
|
||||
transform: translateY(1px);
|
||||
/* ---------------------------------------------------------------------
|
||||
Écran de succès — LE moment à soigner : c'est la dernière impression
|
||||
que garde l'invité. Coche qui "pop" avec rebond + petite volée de
|
||||
confettis colorés (pur CSS, aucune dépendance JS supplémentaire).
|
||||
------------------------------------------------------------------ */
|
||||
|
||||
.success-check {
|
||||
position: relative;
|
||||
width: 76px;
|
||||
height: 76px;
|
||||
margin: 0 auto 0.9rem;
|
||||
animation: pop-in 0.55s var(--ease-bounce) both;
|
||||
}
|
||||
|
||||
.success-check svg {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
.success-check__circle {
|
||||
stroke: var(--color-accent);
|
||||
fill: var(--color-gold-soft);
|
||||
stroke-dasharray: 145;
|
||||
stroke-dashoffset: 145;
|
||||
animation: draw 0.5s ease-out 0.1s forwards;
|
||||
}
|
||||
|
||||
.success-check__mark {
|
||||
stroke: var(--color-accent);
|
||||
fill: none;
|
||||
stroke-dasharray: 32;
|
||||
stroke-dashoffset: 32;
|
||||
animation: draw 0.35s ease-out 0.5s forwards;
|
||||
}
|
||||
|
||||
@keyframes draw {
|
||||
to {
|
||||
stroke-dashoffset: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes pop-in {
|
||||
0% {
|
||||
transform: scale(0.3);
|
||||
opacity: 0;
|
||||
}
|
||||
65% {
|
||||
transform: scale(1.12);
|
||||
opacity: 1;
|
||||
}
|
||||
100% {
|
||||
transform: scale(1);
|
||||
}
|
||||
}
|
||||
|
||||
.confetti {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
pointer-events: none;
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
.confetti span {
|
||||
position: absolute;
|
||||
top: 0.5rem;
|
||||
width: 9px;
|
||||
height: 9px;
|
||||
opacity: 0;
|
||||
animation: confetti-fall 1.1s ease-out forwards;
|
||||
}
|
||||
|
||||
.confetti span:nth-child(1) {
|
||||
left: 18%;
|
||||
background: var(--color-accent);
|
||||
border-radius: 2px;
|
||||
animation-delay: 0.05s;
|
||||
}
|
||||
.confetti span:nth-child(2) {
|
||||
left: 32%;
|
||||
top: 0.2rem;
|
||||
background: var(--color-gold);
|
||||
border-radius: 50%;
|
||||
animation-delay: 0.15s;
|
||||
}
|
||||
.confetti span:nth-child(3) {
|
||||
left: 48%;
|
||||
background: var(--color-plum);
|
||||
border-radius: 2px;
|
||||
animation-delay: 0.02s;
|
||||
}
|
||||
.confetti span:nth-child(4) {
|
||||
left: 62%;
|
||||
background: var(--color-accent-light);
|
||||
border-radius: 50%;
|
||||
animation-delay: 0.22s;
|
||||
}
|
||||
.confetti span:nth-child(5) {
|
||||
left: 74%;
|
||||
background: var(--color-gold);
|
||||
border-radius: 2px;
|
||||
animation-delay: 0.1s;
|
||||
}
|
||||
.confetti span:nth-child(6) {
|
||||
left: 86%;
|
||||
background: var(--color-plum);
|
||||
border-radius: 50%;
|
||||
animation-delay: 0.28s;
|
||||
}
|
||||
|
||||
@keyframes confetti-fall {
|
||||
0% {
|
||||
transform: translateY(-6px) rotate(0deg) scale(0.6);
|
||||
opacity: 0;
|
||||
}
|
||||
15% {
|
||||
opacity: 1;
|
||||
}
|
||||
100% {
|
||||
transform: translateY(92px) rotate(300deg) scale(1);
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.success-title {
|
||||
font-family: var(--font-display);
|
||||
font-size: 1.75rem;
|
||||
font-weight: 600;
|
||||
color: var(--color-accent-hover);
|
||||
margin: 0 0 0.3rem;
|
||||
}
|
||||
|
||||
.success-text {
|
||||
font-size: 0.95rem;
|
||||
margin: 0 0 1.3rem;
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.spinner,
|
||||
.success-check,
|
||||
.success-check__circle,
|
||||
.success-check__mark,
|
||||
.confetti span,
|
||||
.preview,
|
||||
.state-icon--limit,
|
||||
button,
|
||||
.btn-source {
|
||||
animation: none !important;
|
||||
transition: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
+11
-2
@@ -47,6 +47,7 @@ const nomInput = document.getElementById("nom-input");
|
||||
const messageInput = document.getElementById("message-input");
|
||||
const progressText = document.getElementById("progress-text");
|
||||
const submitBtn = document.getElementById("submit-btn");
|
||||
const submitBtnLabel = document.getElementById("submit-btn-label");
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// État
|
||||
@@ -72,7 +73,7 @@ const ERROR_MESSAGES = {
|
||||
"event-closed":
|
||||
"Cet événement n'accepte plus de photos (il est clos). Merci quand même d'avoir essayé !",
|
||||
"guest-limit-reached":
|
||||
"Tu as déjà envoyé le nombre maximum de photos autorisé pour cet événement. Merci pour ta participation !",
|
||||
"Dis donc, on ne t'arrête plus ! Tu as atteint le nombre de photos autorisé pour cet événement. Merci d'avoir aussi bien joué le jeu !",
|
||||
"file-missing": "Choisis d'abord une photo avant d'envoyer.",
|
||||
"file-not-image":
|
||||
"Ce fichier n'est pas une photo. Choisis une image (JPEG, PNG, HEIC…).",
|
||||
@@ -239,6 +240,11 @@ function showError(rawErr, retryAction) {
|
||||
successBlock.hidden = true;
|
||||
errorBlock.hidden = false;
|
||||
errorMessageEl.textContent = ERROR_MESSAGES[err.code] || ERROR_MESSAGES.unknown;
|
||||
// Traitement visuel dédié (chaleureux, pas rouge "erreur") pour la limite
|
||||
// de photos atteinte : ce n'est pas un échec pour l'invité, juste une
|
||||
// limite après participation. Pur toggle de classe CSS, cf. style.css
|
||||
// (.state-block--limit) — aucun changement de logique ici.
|
||||
errorBlock.classList.toggle("state-block--limit", err.code === "guest-limit-reached");
|
||||
|
||||
state.lastRetryAction = retryAction || null;
|
||||
const canRetry = Boolean(retryAction) && RETRYABLE_CODES.has(err.code);
|
||||
@@ -290,7 +296,10 @@ function setProgress(text) {
|
||||
function setSubmitting(isSubmitting) {
|
||||
state.isSubmitting = isSubmitting;
|
||||
submitBtn.disabled = isSubmitting;
|
||||
submitBtn.textContent = isSubmitting ? "Envoi en cours…" : "Envoyer ma photo";
|
||||
// On cible le <span> du libellé (et non submitBtn.textContent) pour ne pas
|
||||
// effacer l'icône SVG du bouton à chaque bascule — changement purement
|
||||
// visuel, la logique de soumission elle-même est inchangée.
|
||||
submitBtnLabel.textContent = isSubmitting ? "Envoi en cours…" : "Envoyer ma photo";
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
Reference in New Issue
Block a user