founders-break/views/index.ejs

353 lines
18 KiB
Text

<%
/* ---- Next occurrence -------------------------------------------------
All arithmetic happens on a UTC Date that carries the *Eastern calendar
date*, never a wall-clock instant. That keeps day-of-week and day-adding
correct across DST without needing a timezone library. public/js/main.js
runs the same rule so the live countdown agrees with this render and can
roll itself over without a reload. */
const TZ = site.timezone || "America/New_York";
const now = new Date();
const V = site.venue;
const todayISO = new Intl.DateTimeFormat("en-CA", {
timeZone: TZ, year: "numeric", month: "2-digit", day: "2-digit",
}).format(now);
const hourET = Number(
new Intl.DateTimeFormat("en-US", { timeZone: TZ, hour: "2-digit", hourCycle: "h23" }).format(now)
) % 24;
const toDate = (iso) => { const [y, m, d] = iso.split("-").map(Number); return new Date(Date.UTC(y, m - 1, d)); };
const isoOf = (dt) => dt.toISOString().slice(0, 10);
let next = toDate(todayISO);
const first = toDate(site.firstEvent);
if (next < first) next = first;
while (next.getUTCDay() !== site.weekday) next.setUTCDate(next.getUTCDate() + 1);
if (isoOf(next) === todayISO && hourET >= (site.endHour24 || 21)) next.setUTCDate(next.getUTCDate() + 7);
// Keep the FIRST skipped date: with two cancellations in a row the notice
// should name the soonest one people were expecting, not the last one.
const cancelled = site.cancelled || [];
let skippedWeek = null;
while (cancelled.includes(isoOf(next))) {
if (!skippedWeek) skippedWeek = isoOf(next);
next.setUTCDate(next.getUTCDate() + 7);
}
const nextISO = isoOf(next);
const isTonight = nextISO === todayISO;
const fmtDate = (opts) => new Intl.DateTimeFormat("en-US", Object.assign({ timeZone: "UTC" }, opts)).format(next);
const nextLong = fmtDate({ weekday: "long", month: "long", day: "numeric" });
const nextShort = fmtDate({ weekday: "short", month: "short", day: "numeric" });
const weekdayName = new Intl.DateTimeFormat("en-US", { timeZone: "UTC", weekday: "long" }).format(next);
// Eastern UTC offset on the event date, so the countdown target is a real instant.
const offLabel = new Intl.DateTimeFormat("en-US", { timeZone: TZ, timeZoneName: "shortOffset" })
.format(new Date(`${nextISO}T12:00:00Z`));
const offHours = Number((offLabel.match(/GMT([+-]?\d{1,2})/) || [, "-5"])[1]);
const pad = (n) => String(n).padStart(2, "0");
const offStr = `${offHours < 0 ? "-" : "+"}${pad(Math.abs(offHours))}:00`;
const startISO = `${nextISO}T${pad(site.startHour24)}:00:00${offStr}`;
const endISO = `${nextISO}T${pad(site.endHour24)}:00:00${offStr}`;
const heroLine = isTonight
? `Tonight at ${V.name}, ${site.startTime} to ${site.endTime}. Grab a cue and meet the people building things in ${V.city}.`
: `Every ${weekdayName} at ${V.name}, ${site.startTime} to ${site.endTime}. The next one is ${nextLong}.`;
const details = [
{ label: "When", value: `Every ${weekdayName}` },
{ label: "Time", value: `${site.startTime} to ${site.endTime}` },
{ label: "Where", value: `${V.name}, ${V.street}`, href: V.mapUrl },
{ label: "Entry", value: site.entry },
];
%><!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title><%= site.name %> | <%= site.tagline %></title>
<meta name="description" content="<%= site.description %>">
<meta name="theme-color" content="#06060b">
<meta property="og:type" content="website">
<meta property="og:title" content="<%= site.name %>">
<meta property="og:description" content="<%= site.description %>">
<meta property="og:image" content="<%= site.images.hero %>">
<meta property="og:url" content="<%= site.url %>">
<meta name="twitter:card" content="summary_large_image">
<link rel="icon" href="/images/favicon.svg" type="image/svg+xml">
<link rel="apple-touch-icon" href="/images/favicon.svg">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=Space+Grotesk:wght@500;600;700&display=swap" rel="stylesheet">
<link rel="stylesheet" href="/css/style.css">
<link rel="preload" as="image" href="<%= site.images.hero %>">
<!-- Gates the scroll-reveal styles on JS being alive, so a blocked or failed
main.js leaves the page fully readable instead of blank. -->
<script>document.documentElement.className += " js";</script>
<script type="application/ld+json">
<%- JSON.stringify({
"@context": "https://schema.org",
"@type": "Event",
name: site.name,
description: site.description,
startDate: startISO,
endDate: endISO,
eventSchedule: { "@type": "Schedule", repeatFrequency: "P1W", byDay: "https://schema.org/Thursday", startTime: "19:00", endTime: "21:00" },
eventStatus: "https://schema.org/EventScheduled",
eventAttendanceMode: "https://schema.org/OfflineEventAttendanceMode",
location: { "@type": "Place", name: V.fullName, address: { "@type": "PostalAddress", streetAddress: V.street, addressLocality: V.city, addressRegion: V.state, postalCode: V.zip, addressCountry: "US" } },
organizer: { "@type": "Organization", name: site.host.name, url: site.host.url },
isAccessibleForFree: true,
image: [site.images.hero],
url: site.url,
}) %>
</script>
</head>
<body>
<a class="skip" href="#main">Skip to content</a>
<!-- Decorative blueprint layer. Everything here is driven by CSS scroll-driven
animation, so it costs no JavaScript and no scroll listener; where the
browser has no support it simply sits still. Hidden from assistive tech and
frozen under prefers-reduced-motion. -->
<div class="bp" aria-hidden="true">
<svg class="bp-table" viewBox="0 0 1200 640" fill="none" preserveAspectRatio="xMidYMid meet">
<rect class="bp-draw" x="24" y="24" width="1152" height="592" rx="40" pathLength="1" stroke="currentColor" stroke-width="3"/>
<rect class="bp-draw bp-d2" x="86" y="86" width="1028" height="468" rx="14" pathLength="1" stroke="currentColor" stroke-width="2"/>
<circle class="bp-draw bp-d3" cx="86" cy="86" r="30" pathLength="1" stroke="currentColor" stroke-width="2"/>
<circle class="bp-draw bp-d3" cx="600" cy="70" r="26" pathLength="1" stroke="currentColor" stroke-width="2"/>
<circle class="bp-draw bp-d3" cx="1114" cy="86" r="30" pathLength="1" stroke="currentColor" stroke-width="2"/>
<circle class="bp-draw bp-d3" cx="86" cy="554" r="30" pathLength="1" stroke="currentColor" stroke-width="2"/>
<circle class="bp-draw bp-d3" cx="600" cy="570" r="26" pathLength="1" stroke="currentColor" stroke-width="2"/>
<circle class="bp-draw bp-d3" cx="1114" cy="554" r="30" pathLength="1" stroke="currentColor" stroke-width="2"/>
<line class="bp-draw bp-d4" x1="330" y1="86" x2="330" y2="554" pathLength="1" stroke="currentColor" stroke-width="1.5" stroke-dasharray="6 10"/>
<path class="bp-draw bp-d4" d="M330 320 L980 320" pathLength="1" stroke="currentColor" stroke-width="1.5" stroke-dasharray="6 10"/>
<g stroke="currentColor" stroke-width="2">
<circle class="bp-draw bp-d5" pathLength="1" cx="900" cy="320" r="18"/>
<circle class="bp-draw bp-d5" pathLength="1" cx="936" cy="299" r="18"/><circle class="bp-draw bp-d5" pathLength="1" cx="936" cy="341" r="18"/>
<circle class="bp-draw bp-d5" pathLength="1" cx="972" cy="278" r="18"/><circle class="bp-draw bp-d5" pathLength="1" cx="972" cy="320" r="18"/><circle class="bp-draw bp-d5" pathLength="1" cx="972" cy="362" r="18"/>
</g>
</svg>
<svg class="bp-ball" viewBox="0 0 64 64" fill="none">
<circle cx="32" cy="32" r="29" stroke="currentColor" stroke-width="3"/>
<ellipse cx="32" cy="32" rx="12" ry="29" stroke="currentColor" stroke-width="2"/>
<path d="M6 22 h52 M6 42 h52" stroke="currentColor" stroke-width="1.6"/>
</svg>
<svg class="bp-cue" viewBox="0 0 600 40" fill="none">
<path d="M4 20 L596 20" stroke="currentColor" stroke-width="3" stroke-linecap="round"/>
<path d="M560 14 L596 14 M560 26 L596 26" stroke="currentColor" stroke-width="2"/>
<circle cx="14" cy="20" r="7" stroke="currentColor" stroke-width="2"/>
</svg>
</div>
<header class="nav" id="nav">
<a class="brand" href="/">
<img class="brand-mark" src="/images/onetouch-studio.svg" alt="" width="26" height="26">
<span class="brand-name"><%= site.name %></span>
</a>
<nav class="nav-right" aria-label="Primary">
<a class="nav-link" href="#details">Details</a>
<a class="nav-link" href="#expect">What to expect</a>
<a class="nav-link" href="#venue">The venue</a>
<a class="nav-cta" href="#rsvp" data-track="rsvp-nav">RSVP</a>
</nav>
</header>
<main id="main">
<!-- Hero: asymmetric split, copy left / photograph right -->
<section class="hero">
<div class="hero-copy">
<p class="eyebrow"><%= site.tagline %></p>
<h1><%= site.name %></h1>
<p class="hero-sub"><%= heroLine %></p>
<div class="hero-actions">
<a class="btn btn-primary" href="#rsvp" data-track="rsvp-hero">RSVP</a>
<a class="btn btn-ghost" href="#expect" data-track="hero-what-to-expect">What to expect</a>
</div>
</div>
<figure class="hero-figure">
<img src="<%= site.images.hero %>" width="1536" height="1024" fetchpriority="high"
alt="Four people talking beside a lit pool table in a dark bar.">
</figure>
</section>
<!-- Live countdown. Server-rendered values are the no-JS fallback; main.js
takes over, ticks, and recomputes the following Thursday on its own. -->
<section class="countdown-band">
<div class="countdown"
id="countdown"
data-start="<%= startISO %>"
data-end="<%= endISO %>"
data-weekday="<%= site.weekday %>"
data-start-hour="<%= site.startHour24 %>"
data-end-hour="<%= site.endHour24 %>"
data-first="<%= site.firstEvent %>"
data-cancelled="<%= (site.cancelled || []).join(',') %>">
<p class="countdown-label" id="countdown-label">
<%= isTonight ? "The Break is tonight" : "Until the next Break" %>
</p>
<div class="countdown-clock" id="countdown-clock" role="timer" aria-live="off">
<div class="unit"><span class="num" data-unit="days">-</span><span class="cap">days</span></div>
<div class="unit"><span class="num" data-unit="hours">-</span><span class="cap">hours</span></div>
<div class="unit"><span class="num" data-unit="minutes">-</span><span class="cap">min</span></div>
<div class="unit"><span class="num" data-unit="seconds">-</span><span class="cap">sec</span></div>
</div>
<p class="countdown-when" id="countdown-when"><%= nextLong %>, <%= site.startTime %> at <%= V.name %></p>
</div>
</section>
<!-- Details: display-value strip, vertical hairlines only -->
<section class="details" id="details">
<% if (skippedWeek) { %>
<p class="notice" role="status">No Break on <%= new Intl.DateTimeFormat("en-US", { timeZone: "UTC", month: "long", day: "numeric" }).format(toDate(skippedWeek)) %>. Back on <%= nextLong %>.</p>
<% } %>
<dl class="detail-grid reveal">
<% details.forEach(function (d) { %>
<div class="detail">
<dt><%= d.label %></dt>
<dd><% if (d.href) { %><a href="<%= d.href %>" target="_blank" rel="noopener" data-track="details-map"><%= d.value %></a><% } else { %><%= d.value %><% } %></dd>
</div>
<% }) %>
</dl>
<p class="detail-foot">
<span<% if (isTonight) { %> class="is-live"<% } %>><%= isTonight ? "Happening tonight" : `Next: ${nextShort}` %></span>
<a class="ics" href="/assets/founders-break.ics" data-track="add-to-calendar">Add the whole series to your calendar</a>
</p>
</section>
<!-- Pitch: full-bleed editorial band over the break shot -->
<section class="pitch" style="--pitch-image: url('<%= site.images.breakShot %>')">
<div class="pitch-inner reveal">
<h2>We are building Gainesville's startup culture, one terrible pool shot at a time.</h2>
<p>Whether you are an engineer, a designer, a business strategist, or just an ambitious person driven to build things from the ground up, this is your crowd.</p>
<p class="pitch-kicker">Zero pool skills required. Seriously. We are here to build startups, not run the table.</p>
</div>
</section>
<!-- What to expect: bento, four cells, one photographic -->
<section class="expect" id="expect">
<h2 class="section-title reveal">What to expect</h2>
<div class="bento">
<figure class="cell cell-photo reveal">
<img src="<%= site.images.napkin %>" width="1024" height="1024" loading="lazy"
alt="Three students laughing over a napkin sketch at the rail of a pool table.">
</figure>
<% site.expect.forEach(function (item, i) { %>
<div class="cell cell-text<%= i === 2 ? " cell-wide" : "" %><%= i === 1 ? " cell-tint" : "" %> reveal">
<h3><%= item.title %></h3>
<p><%= item.body %></p>
</div>
<% }) %>
</div>
</section>
<!-- Venue -->
<section class="venue" id="venue">
<div class="venue-copy reveal">
<img class="venue-logo" src="<%= site.images.venueLogo %>" width="395" height="736"
alt="<%= V.fullName %>" loading="lazy">
<h2>We play at <%= V.name %>.</h2>
<p><%= V.note %></p>
<address>
<a href="<%= V.mapUrl %>" target="_blank" rel="noopener" data-track="venue-map"><%= V.street %><br><%= V.city %>, <%= V.state %> <%= V.zip %></a>
</address>
<p class="venue-meta">
<a href="<%= V.instagramUrl %>" target="_blank" rel="noopener" data-track="venue-instagram">@<%= V.instagram %></a>
<span>Pouring since <%= V.established %></span>
</p>
</div>
<figure class="venue-figure reveal">
<img src="<%= site.images.venue %>" width="1400" height="991" loading="lazy"
alt="The Silver Q storefront on West University Avenue at dusk.">
<figcaption>Look for the sign on West University Avenue.</figcaption>
</figure>
</section>
<!-- RSVP -->
<section class="rsvp" id="rsvp">
<div class="rsvp-panel reveal">
<!-- Everything the visitor interacts with lives in one wrapper so the
confirmation can replace the whole block. Hiding only the form left
the heading and the QR stranded above the confirmation. -->
<div id="rsvp-live">
<h2>RSVP for <%= isTonight ? "tonight" : nextLong %></h2>
<p class="rsvp-sub">Not required, but it helps us know how many tables to hold. We will email you a reminder the day before.</p>
<form id="rsvp-form" method="POST" action="/forms/rsvp" novalidate>
<input type="hidden" name="_redirect" value="/?rsvp=ok#rsvp">
<input type="hidden" name="event_date" value="<%= nextISO %>">
<p class="hp" aria-hidden="true"><label>Leave this empty<input type="text" name="company_website" tabindex="-1" autocomplete="off"></label></p>
<div class="field">
<label for="f-name">Name</label>
<input id="f-name" name="name" type="text" autocomplete="name" required>
<p class="field-error" id="e-name"></p>
</div>
<div class="field">
<label for="f-email">Email</label>
<input id="f-email" name="email" type="email" autocomplete="email" inputmode="email" required>
<p class="field-help">For the weekly reminder. Nothing else.</p>
<p class="field-error" id="e-email"></p>
</div>
<div class="field">
<label for="f-building">What are you building? <span class="opt">Optional</span></label>
<input id="f-building" name="building" type="text" autocomplete="off">
<p class="field-help">A few words is plenty. It helps us introduce you to the right people.</p>
</div>
<button class="btn btn-primary btn-block" type="submit" id="rsvp-submit" data-track="rsvp-submit">RSVP</button>
<p class="form-status" id="form-status" role="status" aria-live="polite"></p>
<p class="fine">One email a week at most. Unsubscribe any time.</p>
</form>
<!-- Deliberately dark-on-white: inverted QR codes are a coin flip on older
scanners, and this is going on flyers. -->
<div class="qr">
<a class="qr-card" href="#rsvp" aria-hidden="true" tabindex="-1">
<img src="<%= site.images.qr %>" width="1024" height="1024" loading="lazy"
alt="QR code linking to the RSVP form on this page.">
</a>
<div class="qr-copy">
<p class="qr-title">Scan to RSVP</p>
<p>Opens this page at the form. Screenshot it to share.</p>
</div>
</div>
</div>
<div class="rsvp-done" id="rsvp-done" hidden>
<h3>You are on the list.</h3>
<p><%= isTonight ? "Tonight" : nextLong %> at <%= V.name %>, <%= V.street %>, <%= site.startTime %>. A confirmation is on its way to your inbox.</p>
<a class="ics" href="/assets/founders-break.ics" data-track="add-to-calendar">Add the whole series to your calendar</a>
</div>
</div>
</section>
</main>
<footer class="foot">
<p class="foot-line">Bring your ideas and a valid ID.</p>
<div class="foot-meta">
<a class="foot-host" href="<%= site.host.url %>" target="_blank" rel="noopener">
<img src="/images/onetouch-studio.svg" alt="" width="20" height="20">
<span>Hosted by <%= site.host.name %></span>
</a>
<a href="mailto:<%= site.host.email %>"><%= site.host.email %></a>
<a href="<%= V.mapUrl %>" target="_blank" rel="noopener" data-track="venue-map"><%= V.name %>, <%= V.street %></a>
</div>
</footer>
<script src="/js/main.js" data-site="<%= tenant.id %>" defer></script>
</body>
</html>