TableauDuMariage / index.html
MatteoScript's picture
Update index.html
979fb0e verified
<!doctype html>
<html lang="it">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
<meta name="theme-color" content="#F3EFE9" />
<title>Trova la tua Cala</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link rel="preconnect" href="https://db.onlinewebfonts.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Montserrat:wght@400;500;600;700;800;900&family=Cormorant+Garamond:ital,wght@0,400;0,500;0,700;1,400;1,500;1,700&family=Great+Vibes&display=swap" rel="stylesheet">
<style>
@font-face {
font-family: "Amsterdam 1";
src: url("https://db.onlinewebfonts.com/t/3aab0c222119b30542df27260dad0ebd.eot");
src:
url("https://db.onlinewebfonts.com/t/3aab0c222119b30542df27260dad0ebd.eot?#iefix") format("embedded-opentype"),
url("https://db.onlinewebfonts.com/t/3aab0c222119b30542df27260dad0ebd.woff2") format("woff2"),
url("https://db.onlinewebfonts.com/t/3aab0c222119b30542df27260dad0ebd.woff") format("woff"),
url("https://db.onlinewebfonts.com/t/3aab0c222119b30542df27260dad0ebd.ttf") format("truetype"),
url("https://db.onlinewebfonts.com/t/3aab0c222119b30542df27260dad0ebd.svg#Amsterdam 1") format("svg");
font-weight: 400;
font-style: normal;
font-display: swap;
}
:root {
--blue: #254B6B;
--blue-deep: #1B3A55;
--blue-soft: #8FA3B3;
--olive: #939675;
--olive-deep: #6F7257;
--sage: #C2C5B2;
--coral: #D08A7A;
--coral-deep: #B8705F;
--rose: #EDD3CD;
--cream: #F3EFE9;
--paper: #FFFDF8;
--ink: rgba(37, 75, 107, .82);
--ink-soft: rgba(37, 75, 107, .58);
--ink-faint: rgba(37, 75, 107, .34);
--hair: rgba(147, 150, 117, .24);
--font-main: "Montserrat", system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
--font-serif: "Cormorant Garamond", "Cormorant", Georgia, serif;
--font-script: "Amsterdam 1", "Great Vibes", "Snell Roundhand", cursive;
--pine-bg: url("./assets/pine-bg-transparent.png");
--grain:
url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='220' height='220'><filter id='n'><feTurbulence type='fractalNoise' baseFrequency='0.86' numOctaves='2' stitchTiles='stitch'/><feColorMatrix values='0 0 0 0 0.145 0 0 0 0 0.294 0 0 0 0 0.420 0 0 0 0.04 0'/></filter><rect width='100%25' height='100%25' filter='url(%23n)'/></svg>");
}
* { box-sizing: border-box; }
html,
body {
width: 100%;
height: 100%;
margin: 0;
overflow: hidden;
font-family: var(--font-main);
color: var(--blue);
background: var(--cream);
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
body {
min-height: 100dvh;
background-color: var(--cream);
background-image: none;
position: relative;
}
body::before {
content: "";
position: fixed;
inset: 0;
background-image: var(--grain);
background-size: 220px 220px;
opacity: .18;
mix-blend-mode: multiply;
pointer-events: none;
z-index: 0;
}
body::after {
content: "";
position: fixed;
inset: 0;
background-image: var(--pine-bg);
background-repeat: no-repeat;
background-position: center 50%;
background-size: min(112vw, 620px) auto;
opacity: .55;
mix-blend-mode: multiply;
pointer-events: none;
z-index: 1;
}
body.reveal-open { overflow: hidden; }
.frame,
.reveal-frame {
position: fixed;
inset: 18px;
border: 1px solid var(--hair);
border-radius: 30px;
pointer-events: none;
z-index: 2;
}
.screen {
position: relative;
width: 100%;
height: 100dvh;
min-height: 100dvh;
display: flex;
justify-content: center;
overflow: hidden;
z-index: 3;
}
.page {
position: relative;
width: min(100%, 500px);
height: 100dvh;
padding: max(22px, env(safe-area-inset-top)) 30px max(24px, env(safe-area-inset-bottom));
display: grid;
grid-template-rows: auto 1fr;
gap: 0;
overflow: hidden;
}
@keyframes rise {
from { opacity: 0; transform: translateY(14px); }
to { opacity: 1; transform: translateY(0); }
}
.anim { opacity: 0; animation: rise .7s cubic-bezier(.2,.7,.2,1) forwards; }
.anim-1 { animation-delay: .05s; }
.anim-2 { animation-delay: .18s; }
.anim-3 { animation-delay: .32s; }
.anim-4 { animation-delay: .46s; }
.anim-5 { animation-delay: .60s; }
.top-mark {
display: flex;
align-items: center;
justify-content: center;
padding: 0;
}
.logo {
width: 82px;
height: 82px;
display: block;
object-fit: contain;
background: transparent;
}
.hero {
min-height: 0;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
text-align: center;
gap: clamp(30px, 5.5dvh, 54px);
padding: 0 0 clamp(10px, 2dvh, 18px);
}
.title-frame {
position: relative;
display: inline-block;
margin: 0 auto;
padding: 0;
}
.script-title {
margin: 0 auto;
color: var(--blue);
font-family: var(--font-script);
font-size: clamp(28px, 6.9vw, 42px);
font-weight: 400;
line-height: 2.45;
letter-spacing: -.012em;
text-wrap: balance;
max-width: 360px;
}
.script-title span {
display: block;
}
.script-title span + span {
margin-top: .02em;
}
.script-title span:first-child { transform: translateX(-.02em); }
.script-title span:last-child { transform: translateX(.04em); }
.form-block {
width: 100%;
max-width: 380px;
}
.field-label {
display: flex;
align-items: center;
justify-content: center;
gap: 14px;
margin: 0 0 16px;
color: var(--coral);
font-size: 10px;
font-weight: 700;
letter-spacing: .34em;
text-transform: uppercase;
}
.field-label::before,
.field-label::after {
content: "";
flex: 0 0 24px;
height: 1px;
background: rgba(208, 138, 122, .58);
}
.input-wrap { position: relative; }
input {
width: 100%;
height: 56px;
border: 1px solid rgba(147, 150, 117, .30);
outline: 0;
border-radius: 999px;
padding: 0 24px;
color: var(--blue);
background: rgba(255, 253, 248, .78);
font-family: var(--font-main);
font-size: 15px;
font-weight: 500;
letter-spacing: .005em;
text-align: center;
box-shadow: 0 4px 14px rgba(37, 75, 107, .04), inset 0 1px 0 rgba(255, 255, 255, .5);
transition: border-color .22s ease, box-shadow .22s ease, background .22s ease;
}
input::placeholder {
color: rgba(143, 163, 179, .82);
font-weight: 400;
font-style: italic;
}
input:focus {
border-color: rgba(147, 150, 117, .55);
background: var(--paper);
box-shadow:
0 0 0 4px rgba(194, 197, 178, .22),
0 8px 22px rgba(37, 75, 107, .06),
inset 0 1px 0 rgba(255, 255, 255, .6);
}
.discover {
position: relative;
width: 100%;
height: 58px;
margin-top: 14px;
border: 0;
border-radius: 999px;
cursor: pointer;
overflow: hidden;
color: var(--paper);
background: var(--blue);
font-family: var(--font-main);
font-size: 11.5px;
font-weight: 700;
letter-spacing: .32em;
text-transform: uppercase;
box-shadow: 0 8px 20px rgba(37, 75, 107, .17);
transition: transform .18s ease, box-shadow .18s ease, background .18s ease;
}
.discover:hover {
transform: translateY(-1px);
background: var(--blue-deep);
box-shadow: 0 10px 24px rgba(37, 75, 107, .18);
}
.discover:active {
transform: translateY(1px);
box-shadow: 0 4px 12px rgba(37, 75, 107, .12);
}
.message {
min-height: 0;
margin-top: 14px;
opacity: 0;
transform: translateY(8px);
transition: opacity .26s ease, transform .26s ease;
}
.message.show { opacity: 1; transform: translateY(0); }
.message-card {
border-radius: 14px;
padding: 12px 14px;
color: var(--ink);
background: rgba(255, 253, 248, .88);
border: 1px solid var(--hair);
text-align: center;
font-size: 12.5px;
line-height: 1.42;
font-weight: 500;
letter-spacing: .005em;
}
.reveal {
position: fixed;
inset: 0;
z-index: 20;
display: grid;
place-items: center;
padding: max(28px, env(safe-area-inset-top)) 28px max(24px, env(safe-area-inset-bottom));
color: var(--blue);
background-color: var(--cream);
background-image: none;
opacity: 0;
visibility: hidden;
pointer-events: none;
transform: scale(1.015);
transition: opacity .36s ease, transform .36s ease, visibility 0s linear .36s;
overflow: hidden;
}
.reveal::before {
content: "";
position: absolute;
inset: 0;
background-image: var(--pine-bg);
background-repeat: no-repeat;
background-position: center 50%;
background-size: min(112vw, 620px) auto;
opacity: .55;
mix-blend-mode: multiply;
pointer-events: none;
z-index: 0;
}
.reveal::after {
content: "";
position: absolute;
inset: 0;
background-image: var(--grain);
background-size: 220px 220px;
opacity: .18;
mix-blend-mode: multiply;
pointer-events: none;
z-index: 0;
}
.reveal.show {
opacity: 1;
visibility: visible;
pointer-events: auto;
transform: scale(1);
transition: opacity .36s ease, transform .36s ease, visibility 0s;
}
.reveal-frame { z-index: 1; }
.reveal-page {
position: relative;
z-index: 2;
width: min(100%, 560px);
height: 100%;
display: flex;
flex-direction: column;
justify-content: center;
text-align: center;
overflow: hidden;
}
.reveal.show .reveal-step { animation: rise .8s cubic-bezier(.2,.7,.2,1) both; }
.reveal.show .step-1 { animation-delay: .08s; }
.reveal.show .step-2 { animation-delay: .20s; }
.reveal.show .step-3 { animation-delay: .32s; }
.reveal.show .step-4 { animation-delay: .46s; }
.reveal.show .step-5 { animation-delay: .60s; }
.reveal.show .step-6 { animation-delay: .74s; }
.hello {
margin: 0 0 6px;
color: var(--ink-soft);
font-family: var(--font-serif);
font-style: italic;
font-size: clamp(20px, 5vw, 28px);
font-weight: 900;
letter-spacing: .005em;
}
.hello-divider {
width: 38px;
height: 1px;
margin: 0 auto clamp(20px, 3.7dvh, 30px);
background: rgba(208, 138, 122, .62);
position: relative;
}
.hello-divider::before {
content: "";
position: absolute;
left: 50%;
top: 50%;
width: 7px;
height: 7px;
border-radius: 999px;
background: var(--coral);
transform: translate(-50%, -50%);
}
.table-kicker {
margin: 0 0 14px;
color: var(--coral-deep);
font-size: 12px;
font-weight: 800;
letter-spacing: .42em;
text-transform: uppercase;
}
.table-name-wrap {
position: relative;
display: inline-block;
margin: 0 auto;
padding: 0;
}
.table-name {
margin: 0 auto;
max-width: 12ch;
color: var(--blue);
font-family: var(--font-script);
font-size: clamp(34px, 9.5vw, 58px);
font-weight: 400;
line-height: 2.45;
letter-spacing: -.02em;
text-wrap: balance;
}
.table-name[data-long="true"] {
max-width: 13ch;
font-size: clamp(30px, 8.2vw, 50px);
}
.table-art {
display: block;
max-width: min(86vw, 430px);
max-height: 230px;
margin: 0 auto;
object-fit: contain;
}
.table-art[hidden],
.table-name[hidden] {
display: none !important;
}
.reveal-divider {
display: flex;
align-items: center;
justify-content: center;
gap: 10px;
margin: clamp(18px, 3.5dvh, 28px) auto 0;
}
.reveal-divider::before,
.reveal-divider::after {
content: "";
width: 42px;
height: 1px;
background: var(--olive);
opacity: .55;
}
.reveal-divider .dot {
width: 8px;
height: 8px;
border-radius: 999px;
background: var(--coral);
box-shadow: 0 0 0 5px rgba(208, 138, 122, .16);
}
.companions {
margin: 16px auto 0;
max-width: 34ch;
color: var(--ink-soft);
font-family: var(--font-serif);
font-style: italic;
text-align: center;
}
.companions-title {
margin: 0 0 9px;
color: var(--coral);
font-size: clamp(20px, 5vw, 28px);
font-weight: 1000;
line-height: 1.35;
}
.companions-list {
list-style: none;
padding: 0;
margin: 0;
}
.companions-list li {
margin: 5px 0;
font-size: clamp(20px, 5vw, 28px);
font-weight: 1000;
line-height: 1.35;
letter-spacing: .003em;
}
.cheers {
margin: 15px auto 0;
color: var(--coral);
font-family: var(--font-serif);
font-style: italic;
font-size: clamp(20px, 5vw, 28px);
font-weight: 1000;
line-height: 1.35;
}
.again {
align-self: center;
height: 52px;
margin-top: clamp(24px, 4.6dvh, 36px);
border: 1px solid rgba(37, 75, 107, .14);
border-radius: 999px;
padding: 0 28px;
color: var(--blue);
background: rgba(255, 253, 248, .92);
box-shadow: 0 12px 26px rgba(37, 75, 107, .07);
font-family: var(--font-main);
font-size: 11px;
font-weight: 800;
letter-spacing: .26em;
text-transform: uppercase;
cursor: pointer;
transition: transform .18s ease, box-shadow .18s ease, background .18s ease;
}
.again:hover {
transform: translateY(-1px);
background: var(--paper);
box-shadow: 0 16px 30px rgba(37, 75, 107, .10);
}
.status {
position: absolute;
width: 1px;
height: 1px;
overflow: hidden;
clip: rect(0 0 0 0);
white-space: nowrap;
}
#confetti {
position: fixed;
inset: 0;
z-index: 40;
width: 100%;
height: 100%;
pointer-events: none;
}
@media (max-height: 760px) {
.logo { width: 120px; height: 120px; }
.script-title { font-size: clamp(27px, 6.6vw, 40px); }
.hero { gap: clamp(28px, 5dvh, 44px); }
.hello { font-size: clamp(18px, 4.4vw, 24px); }
.table-kicker { margin-bottom: 10px; }
.table-name { font-size: clamp(32px, 8.6vw, 52px); }
.table-name[data-long="true"] { font-size: clamp(28px, 7.5vw, 46px); }
.reveal-divider { margin-top: 16px; }
.companions { margin-top: 12px; }
.companions-title {
margin-bottom: 6px;
font-size: clamp(18px, 4.4vw, 24px);
}
.companions-list li {
margin: 3px 0;
font-size: clamp(18px, 4.4vw, 24px);
line-height: 1.35;
}
.cheers {
margin-top: 10px;
font-size: clamp(18px, 4.4vw, 24px);
}
.again { height: 48px; margin-top: 20px; }
}
@media (max-width: 380px) {
.frame,
.reveal-frame { inset: 12px; border-radius: 24px; }
.page { padding-left: 22px; padding-right: 22px; }
.logo { width: 68px; height: 68px; }
.script-title { font-size: clamp(26px, 7.8vw, 38px); }
input,
.discover { height: 54px; }
.field-label { letter-spacing: .26em; }
.field-label::before,
.field-label::after { flex-basis: 18px; }
.table-name { font-size: clamp(30px, 9vw, 50px); }
.table-name[data-long="true"] { font-size: clamp(26px, 7.8vw, 44px); }
.again { padding-inline: 22px; letter-spacing: .20em; }
}
@media (prefers-reduced-motion: reduce) {
.anim,
.reveal.show .reveal-step {
animation: none;
opacity: 1;
transform: none;
}
}
</style>
</head>
<body>
<canvas id="confetti"></canvas>
<div class="frame" aria-hidden="true"></div>
<main class="screen">
<div class="page">
<div class="top-mark anim anim-1">
<img
class="logo"
src="./logo.png"
alt="Logo matrimonio"
onerror="this.onerror=null;this.src='./assets/logo.png';"
/>
</div>
<section class="hero" aria-label="Trova la tua cala">
<div class="title-frame anim anim-3">
<h1 class="script-title" aria-label="Trova la tua Cala">
<span>Trova</span>
<span>la tua Cala</span>
</h1>
</div>
<div class="form-block anim anim-4">
<label class="field-label" for="guestName">NOME E COGNOME</label>
<div class="input-wrap">
<input id="guestName" autocomplete="name" inputmode="text" placeholder="Es. Gaia Pollastrini" />
</div>
<button class="discover" id="discover" type="button">Scopri il tavolo</button>
<div id="message" class="message" aria-live="polite"></div>
<div id="status" class="status" aria-live="polite"></div>
</div>
</section>
</div>
</main>
<section id="reveal" class="reveal" aria-live="assertive" aria-hidden="true">
<div class="reveal-frame" aria-hidden="true"></div>
<div class="reveal-page">
<p class="hello reveal-step step-1" id="hello"></p>
<div class="hello-divider reveal-step step-2" aria-hidden="true"></div>
<p class="table-kicker reveal-step step-3">IL TUO TAVOLO È</p>
<div class="table-name-wrap reveal-step step-4">
<h2 class="table-name" id="tableName"></h2>
<img class="table-art" id="tableArt" alt="Nome tavolo" hidden />
</div>
<div class="reveal-divider reveal-step step-5" aria-hidden="true">
<span class="dot"></span>
</div>
<div class="companions reveal-step step-5" id="companions">
<p class="companions-title">Sei al tavolo con:</p>
<ul class="companions-list" id="companionsList"></ul>
<p class="cheers">Brinda, sorridi e goditi la serata!</p>
</div>
<button class="again reveal-step step-6" id="again" type="button">Cerca un altro nome</button>
</div>
</section>
<script>
const DATA_URL = "https://docs.google.com/spreadsheets/d/e/2PACX-1vSlb-0cSIFaGN_BWrn_S9tQoQsdqGH7qYRYQGC3_3wRB-rGzwZoPoxIBNr9l6NmxW9Ont82YKIdGKIR/pub?output=csv";
/*
OPZIONALE, se poi vuoi usare immagini Canva per alcuni nomi dei tavoli:
1. crea la cartella ./assets/tables/
2. carica i PNG
3. aggiungi qui una riga con chiave normalizzata.
Esempio:
const TABLE_IMAGE_BY_NAME = {
"cala degli inglesi": "./assets/tables/cala-degli-inglesi.png"
};
Se lasci l'oggetto vuoto, il nome tavolo viene scritto col font Amsterdam 1.
*/
const TABLE_IMAGE_BY_NAME = {};
// false = nella lista mostra solo le altre persone del tavolo.
// true = include anche la persona cercata.
const INCLUDE_SELF_IN_TABLE_LIST = false;
const els = {
input: document.getElementById("guestName"),
discover: document.getElementById("discover"),
message: document.getElementById("message"),
status: document.getElementById("status"),
reveal: document.getElementById("reveal"),
hello: document.getElementById("hello"),
tableName: document.getElementById("tableName"),
tableArt: document.getElementById("tableArt"),
companionsList: document.getElementById("companionsList"),
again: document.getElementById("again"),
canvas: document.getElementById("confetti")
};
let guests = [];
let guestsLoaded = false;
let guestsLoadingPromise = null;
let hideTimer = null;
function normalizeName(value) {
return String(value || "")
.normalize("NFD")
.replace(/[\u0300-\u036f]/g, "")
.toLowerCase()
.replace(/[^\p{L}\p{N}\s'-]/gu, " ")
.replace(/\s+/g, " ")
.trim();
}
function firstName(fullName) {
return String(fullName || "").trim().split(/\s+/)[0] || "ospite";
}
function escapeHTML(value) {
return String(value)
.replaceAll("&", "&amp;")
.replaceAll("<", "&lt;")
.replaceAll(">", "&gt;")
.replaceAll('"', "&quot;")
.replaceAll("'", "&#039;");
}
function parseCSV(text) {
const rows = [];
let row = [];
let cell = "";
let quoted = false;
for (let i = 0; i < text.length; i++) {
const char = text[i];
const next = text[i + 1];
if (char === '"' && quoted && next === '"') {
cell += '"';
i++;
} else if (char === '"') {
quoted = !quoted;
} else if (char === "," && !quoted) {
row.push(cell.trim());
cell = "";
} else if ((char === "\n" || char === "\r") && !quoted) {
if (char === "\r" && next === "\n") i++;
row.push(cell.trim());
if (row.some(Boolean)) rows.push(row);
row = [];
cell = "";
} else {
cell += char;
}
}
row.push(cell.trim());
if (row.some(Boolean)) rows.push(row);
return rows;
}
function tableFromCSV(csvText) {
const rows = parseCSV(csvText);
const headerIndex = rows.findIndex(row =>
normalizeName(row[0]) === "nome cognome" &&
normalizeName(row[1]) === "nome del tavolo"
);
const start = headerIndex >= 0 ? headerIndex + 1 : 1;
return rows
.slice(start)
.map(row => ({
fullName: row[0] || "",
tableName: row[1] || "",
key: normalizeName(row[0] || "")
}))
.filter(item => item.fullName && item.tableName);
}
async function loadGuests() {
try {
const response = await fetch(DATA_URL, { cache: "no-store" });
if (!response.ok) throw new Error("Lista non disponibile");
guests = tableFromCSV(await response.text());
els.status.textContent = "Lista invitati pronta.";
} catch (error) {
guests = [];
els.status.textContent = "Lista invitati non disponibile.";
} finally {
guestsLoaded = true;
}
}
function findGuest(query) {
const key = normalizeName(query);
if (!key) return null;
const exact = guests.find(guest => guest.key === key);
if (exact) return exact;
const partials = guests.filter(guest => guest.key.includes(key) || key.includes(guest.key));
return partials.length === 1 ? partials[0] : null;
}
function tableMatesFor(guest) {
const tableKey = normalizeName(guest.tableName);
return guests
.filter(person => normalizeName(person.tableName) === tableKey)
.filter(person => INCLUDE_SELF_IN_TABLE_LIST || person.key !== guest.key)
.map(person => person.fullName)
.filter(Boolean);
}
function renderCompanions(guest) {
const mates = tableMatesFor(guest);
if (!mates.length) {
els.companionsList.innerHTML = `<li>${escapeHTML("Tavolo riservato solo per te.")}</li>`;
return;
}
els.companionsList.innerHTML = mates
.map(name => `<li>${escapeHTML(name)}</li>`)
.join("");
}
function renderTableName(guest) {
const tableKey = normalizeName(guest.tableName);
const imagePath = TABLE_IMAGE_BY_NAME[tableKey];
if (imagePath) {
els.tableName.hidden = true;
els.tableArt.hidden = false;
els.tableArt.src = imagePath;
els.tableArt.alt = guest.tableName;
return;
}
els.tableArt.hidden = true;
els.tableArt.removeAttribute("src");
els.tableName.hidden = false;
els.tableName.textContent = guest.tableName;
els.tableName.dataset.long = guest.tableName.length > 14 ? "true" : "false";
}
function showMessage(text) {
els.message.classList.remove("show");
window.setTimeout(() => {
els.message.innerHTML = `<div class="message-card">${escapeHTML(text)}</div>`;
els.message.classList.add("show");
}, 60);
}
function showReveal(guest) {
clearTimeout(hideTimer);
els.hello.textContent = `Ciao, ${firstName(guest.fullName)}`;
renderTableName(guest);
renderCompanions(guest);
document.body.classList.add("reveal-open");
els.reveal.classList.add("show");
els.reveal.setAttribute("aria-hidden", "false");
celebrate();
}
function hideReveal() {
els.reveal.classList.remove("show");
els.reveal.setAttribute("aria-hidden", "true");
document.body.classList.remove("reveal-open");
stopConfetti();
hideTimer = window.setTimeout(() => {
els.input.value = "";
els.message.classList.remove("show");
els.input.focus({ preventScroll: true });
}, 360);
}
async function discoverTable() {
const typed = els.input.value;
const normalized = normalizeName(typed);
if (!normalized) {
showMessage("Scrivi nome e cognome per scoprire il tuo tavolo.");
return;
}
if (!guestsLoaded) {
showMessage("Sto caricando la lista invitati... riprova tra un secondo.");
if (!guestsLoadingPromise) guestsLoadingPromise = loadGuests();
await guestsLoadingPromise;
}
if (!guests.length) {
showMessage("Lista invitati non ancora disponibile. Riprova tra qualche secondo.");
return;
}
const guest = findGuest(typed);
if (!guest) {
showMessage("Nome non trovato. Controlla di aver scritto nome e cognome corretti.");
return;
}
showReveal(guest);
}
els.discover.addEventListener("click", discoverTable);
els.again.addEventListener("click", hideReveal);
els.input.addEventListener("keydown", event => {
if (event.key === "Enter") discoverTable();
});
window.addEventListener("keydown", event => {
if (event.key === "Escape" && els.reveal.classList.contains("show")) hideReveal();
});
/* ============= petali ============= */
const ctx = els.canvas.getContext("2d");
let petals = [];
let rafId = null;
function resizeCanvas() {
const ratio = Math.min(window.devicePixelRatio || 1, 2);
els.canvas.width = Math.floor(window.innerWidth * ratio);
els.canvas.height = Math.floor(window.innerHeight * ratio);
ctx.setTransform(ratio, 0, 0, ratio, 0, 0);
}
function celebrate() {
resizeCanvas();
const colors = ["#EDD3CD", "#D08A7A", "#C2C5B2", "#8FA3B3", "#F3EFE9", "#EDD3CD", "#D08A7A"];
const w = window.innerWidth;
petals = Array.from({ length: 56 }, () => ({
x: Math.random() * w,
y: -20 - Math.random() * 200,
rx: 5 + Math.random() * 5,
ry: 2.4 + Math.random() * 1.6,
vy: 1.0 + Math.random() * 1.6,
vx: (Math.random() - .5) * .6,
rotation: Math.random() * Math.PI,
spin: (Math.random() - .5) * .04,
sway: Math.random() * Math.PI * 2,
swaySpeed: .015 + Math.random() * .02,
swayAmp: .4 + Math.random() * .9,
color: colors[Math.floor(Math.random() * colors.length)],
alpha: .75 + Math.random() * .25,
life: 360 + Math.random() * 200
}));
if (rafId) cancelAnimationFrame(rafId);
animatePetals();
}
function stopConfetti() {
if (rafId) cancelAnimationFrame(rafId);
rafId = null;
petals = [];
ctx.clearRect(0, 0, window.innerWidth, window.innerHeight);
}
function animatePetals() {
ctx.clearRect(0, 0, window.innerWidth, window.innerHeight);
const h = window.innerHeight;
petals.forEach(p => {
p.sway += p.swaySpeed;
p.x += p.vx + Math.sin(p.sway) * p.swayAmp;
p.y += p.vy;
p.rotation += p.spin;
p.life -= 1;
ctx.save();
ctx.translate(p.x, p.y);
ctx.rotate(p.rotation);
ctx.globalAlpha = Math.min(p.alpha, Math.max(p.life / 100, 0));
ctx.fillStyle = p.color;
ctx.beginPath();
ctx.ellipse(0, 0, p.rx, p.ry, 0, 0, Math.PI * 2);
ctx.fill();
ctx.restore();
});
petals = petals.filter(p => p.life > 0 && p.y < h + 40);
if (petals.length) {
rafId = requestAnimationFrame(animatePetals);
} else {
rafId = null;
ctx.clearRect(0, 0, window.innerWidth, window.innerHeight);
}
}
window.addEventListener("resize", resizeCanvas);
guestsLoadingPromise = loadGuests();
</script>
</body>
</html>