founders-break/views/index.ejs

238 lines
11 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. */
const TZ = site.timezone || "America/New_York";
const now = new Date();
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);
const cancelled = site.cancelled || [];
let skippedWeek = null;
// 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.
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);
const heroLine = isTonight
? `Tonight at ${site.venue}, ${site.startTime} to ${site.endTime}. Grab a cue and meet the people building things in ${site.city}.`
: `Every ${weekdayName} at ${site.venue}, ${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: `${site.venue}, ${site.city}`, href: site.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="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: `${nextISO}T19:00:00-04:00`,
endDate: `${nextISO}T21:00:00-04:00`,
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: site.venue, address: { "@type": "PostalAddress", addressLocality: site.city, addressRegion: "FL", 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>
<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-cta" href="#rsvp">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">RSVP</a>
<a class="btn btn-ghost" href="#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>
<!-- 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"><%= 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">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>
<!-- RSVP -->
<section class="rsvp" id="rsvp">
<div class="rsvp-panel reveal">
<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">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>
<div class="rsvp-done" id="rsvp-done" hidden>
<h3>You are on the list.</h3>
<p>See you <%= isTonight ? "tonight" : nextLong %> at <%= site.venue %>, <%= site.startTime %>. Bring your ideas and a valid ID.</p>
<a class="ics" href="/assets/founders-break.ics">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="<%= site.mapUrl %>" target="_blank" rel="noopener"><%= site.venue %>, <%= site.city %></a>
</div>
</footer>
<script src="/js/main.js" defer></script>
</body>
</html>