Spaces:
Running
Running
| <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 ; | |
| } | |
| .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("&", "&") | |
| .replaceAll("<", "<") | |
| .replaceAll(">", ">") | |
| .replaceAll('"', """) | |
| .replaceAll("'", "'"); | |
| } | |
| 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> |