Add live countdown, Silver Q venue section, real address, OneTouch favicon
This commit is contained in:
parent
cd91fbbb95
commit
7a331d241a
6 changed files with 329 additions and 30 deletions
|
|
@ -28,9 +28,9 @@ DTSTART;TZID=America/New_York:20260827T190000
|
||||||
DTEND;TZID=America/New_York:20260827T210000
|
DTEND;TZID=America/New_York:20260827T210000
|
||||||
RRULE:FREQ=WEEKLY;BYDAY=TH
|
RRULE:FREQ=WEEKLY;BYDAY=TH
|
||||||
SUMMARY:The Founder's Break
|
SUMMARY:The Founder's Break
|
||||||
LOCATION:Silver Q\, Gainesville\, FL
|
LOCATION:Silver Q Billiards & Sports Bar\, 225 W University Ave\, Gainesville\, FL 32601
|
||||||
URL:https://foundersbreak.onetouch.studio
|
URL:https://foundersbreak.onetouch.studio
|
||||||
DESCRIPTION:UF startup pool social. Every Thursday\, 7 to 9 PM at Silver Q.
|
DESCRIPTION:UF startup pool social. Every Thursday\, 7 to 9 PM at Silver Q\, 225 W University Ave.
|
||||||
Zero pool skills required. Free\, 18+. Bring your ideas and a valid ID.\n\n
|
Zero pool skills required. Free\, 18+. Bring your ideas and a valid ID.\n\n
|
||||||
Hosted by OneTouch Studio. Details and RSVP: https://foundersbreak.onetouch.
|
Hosted by OneTouch Studio. Details and RSVP: https://foundersbreak.onetouch.
|
||||||
studio
|
studio
|
||||||
|
|
|
||||||
|
|
@ -161,6 +161,53 @@ a { color: var(--accent2); }
|
||||||
background: linear-gradient(190deg, transparent 55%, rgba(6, 6, 11, 0.5));
|
background: linear-gradient(190deg, transparent 55%, rgba(6, 6, 11, 0.5));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ── Countdown ────────────────────────────────────────────── */
|
||||||
|
|
||||||
|
.countdown-band { max-width: var(--shell); margin: 0 auto; padding: 0 var(--gut) 4.5rem; }
|
||||||
|
|
||||||
|
.countdown {
|
||||||
|
position: relative; isolation: isolate; overflow: hidden;
|
||||||
|
padding: 2.6rem 2rem;
|
||||||
|
border: 1px solid var(--border); border-radius: var(--r-surface);
|
||||||
|
background:
|
||||||
|
radial-gradient(90% 140% at 50% 0%, rgba(16, 185, 129, 0.12), transparent 68%),
|
||||||
|
var(--bg2);
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.countdown-label {
|
||||||
|
font-size: 0.72rem; font-weight: 700; letter-spacing: 0.16em;
|
||||||
|
text-transform: uppercase; color: var(--accent2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.countdown-clock {
|
||||||
|
display: flex; justify-content: center; align-items: flex-start;
|
||||||
|
gap: clamp(1.2rem, 5vw, 3.4rem);
|
||||||
|
margin: 1.5rem 0 1.2rem;
|
||||||
|
}
|
||||||
|
.countdown-clock .unit { display: flex; flex-direction: column; align-items: center; gap: 0.35rem; min-width: 3.4ch; }
|
||||||
|
.countdown-clock .num {
|
||||||
|
font-family: "Space Grotesk", sans-serif; font-weight: 700;
|
||||||
|
font-size: clamp(2.4rem, 7vw, 4rem); line-height: 1;
|
||||||
|
font-variant-numeric: tabular-nums; letter-spacing: -0.03em;
|
||||||
|
}
|
||||||
|
.countdown-clock .cap {
|
||||||
|
font-size: 0.7rem; font-weight: 600; letter-spacing: 0.13em;
|
||||||
|
text-transform: uppercase; color: var(--muted);
|
||||||
|
}
|
||||||
|
/* One-frame lift as a digit changes, so the clock reads as live rather than static. */
|
||||||
|
.countdown-clock .num.tick { animation: tick 0.42s var(--ease); }
|
||||||
|
@keyframes tick {
|
||||||
|
0% { transform: translateY(-0.14em); opacity: 0.45; }
|
||||||
|
100% { transform: none; opacity: 1; }
|
||||||
|
}
|
||||||
|
|
||||||
|
.countdown-when { color: var(--muted); font-size: 0.95rem; }
|
||||||
|
|
||||||
|
.countdown.is-live { border-color: var(--border-hover); }
|
||||||
|
.countdown.is-live .countdown-label { color: var(--accent2); }
|
||||||
|
.countdown.is-live .countdown-when { color: var(--text); font-weight: 600; }
|
||||||
|
|
||||||
/* ── Details: display-value strip ─────────────────────────── */
|
/* ── Details: display-value strip ─────────────────────────── */
|
||||||
|
|
||||||
.details { max-width: var(--shell); margin: 0 auto; padding: 0 var(--gut) 6rem; }
|
.details { max-width: var(--shell); margin: 0 auto; padding: 0 var(--gut) 6rem; }
|
||||||
|
|
@ -248,6 +295,39 @@ a { color: var(--accent2); }
|
||||||
.cell-text h3 { font-size: 1.16rem; font-weight: 600; margin-bottom: 0.65rem; }
|
.cell-text h3 { font-size: 1.16rem; font-weight: 600; margin-bottom: 0.65rem; }
|
||||||
.cell-text p { color: var(--muted); font-size: 0.96rem; max-width: 52ch; }
|
.cell-text p { color: var(--muted); font-size: 0.96rem; max-width: 52ch; }
|
||||||
|
|
||||||
|
/* ── Venue ────────────────────────────────────────────────── */
|
||||||
|
|
||||||
|
.venue {
|
||||||
|
max-width: var(--shell); margin: 0 auto;
|
||||||
|
padding: 0 var(--gut) 7rem;
|
||||||
|
display: grid; grid-template-columns: 0.85fr 1.15fr; gap: 4rem; align-items: center;
|
||||||
|
}
|
||||||
|
.venue-logo { width: auto; height: clamp(96px, 13vw, 132px); margin-bottom: 1.8rem; opacity: 0.95; }
|
||||||
|
.venue h2 { font-size: clamp(1.6rem, 3.2vw, 2.2rem); font-weight: 700; }
|
||||||
|
.venue p { margin-top: 1rem; color: var(--muted); max-width: 46ch; }
|
||||||
|
|
||||||
|
.venue address { margin-top: 1.4rem; font-style: normal; }
|
||||||
|
.venue address a {
|
||||||
|
font-family: "Space Grotesk", sans-serif; font-weight: 600;
|
||||||
|
font-size: 1.12rem; line-height: 1.45; color: var(--text); text-decoration: none;
|
||||||
|
border-bottom: 1px solid var(--border-hover);
|
||||||
|
}
|
||||||
|
.venue address a:hover { color: var(--accent2); }
|
||||||
|
|
||||||
|
.venue-meta {
|
||||||
|
display: flex; flex-wrap: wrap; gap: 0.5rem 1.5rem;
|
||||||
|
margin-top: 1.4rem; font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
.venue-meta a { color: var(--accent2); text-decoration: none; border-bottom: 1px solid var(--border-hover); }
|
||||||
|
.venue-meta a:hover { border-bottom-color: var(--accent2); }
|
||||||
|
.venue-meta span { color: var(--muted); }
|
||||||
|
|
||||||
|
.venue-figure img {
|
||||||
|
width: 100%; aspect-ratio: 3 / 2; object-fit: cover;
|
||||||
|
border-radius: var(--r-surface); border: 1px solid var(--hairline);
|
||||||
|
}
|
||||||
|
.venue-figure figcaption { margin-top: 0.8rem; font-size: 0.82rem; color: var(--fine); }
|
||||||
|
|
||||||
/* ── RSVP ─────────────────────────────────────────────────── */
|
/* ── RSVP ─────────────────────────────────────────────────── */
|
||||||
|
|
||||||
.rsvp { padding: 0 var(--gut) 7rem; }
|
.rsvp { padding: 0 var(--gut) 7rem; }
|
||||||
|
|
@ -321,9 +401,17 @@ form { margin-top: 2rem; }
|
||||||
.cell-wide { grid-column: span 2; }
|
.cell-wide { grid-column: span 2; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@media (max-width: 1000px) {
|
||||||
|
.venue { grid-template-columns: 1fr; gap: 2.4rem; }
|
||||||
|
.venue-figure { order: -1; }
|
||||||
|
}
|
||||||
|
|
||||||
@media (max-width: 760px) {
|
@media (max-width: 760px) {
|
||||||
:root { --gut: 1.25rem; }
|
:root { --gut: 1.25rem; }
|
||||||
.nav-link { display: none; }
|
.nav-link { display: none; }
|
||||||
|
.countdown { padding: 2rem 1rem; }
|
||||||
|
.countdown-clock { gap: 0.9rem; }
|
||||||
|
.venue { padding-bottom: 5rem; }
|
||||||
.detail-grid { grid-template-columns: 1fr 1fr; }
|
.detail-grid { grid-template-columns: 1fr 1fr; }
|
||||||
.detail { padding: 1.4rem 1.1rem; }
|
.detail { padding: 1.4rem 1.1rem; }
|
||||||
.detail:nth-child(odd) { border-left: 0; }
|
.detail:nth-child(odd) { border-left: 0; }
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,13 @@
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" width="64" height="64">
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 672 672" width="672" height="672">
|
||||||
<!-- OneTouch Studio's mark, cropped to the cube and ring so it still reads at 16px. -->
|
<!-- The OneTouch Studio mark exactly as it ships on onetouch.studio, sat on a
|
||||||
<rect width="64" height="64" rx="14" fill="#06060b"/>
|
dark rounded square so it stays visible in a light browser tab. -->
|
||||||
<g transform="translate(32 32)" fill="none" stroke="#10b981" stroke-linecap="round" stroke-linejoin="round">
|
<rect width="672" height="672" rx="148" fill="#06060b"/>
|
||||||
<circle cx="0" cy="0" r="23" stroke-width="2.5"/>
|
<g transform="translate(336 336)">
|
||||||
<g transform="scale(0.62)" stroke-width="4.5">
|
<circle cx="0" cy="0" r="160" fill="none" stroke="#FFFFFF" stroke-width="5.5"/>
|
||||||
|
<circle cx="0" cy="0" r="223" fill="none" stroke="#FFFFFF" stroke-width="7.5"/>
|
||||||
|
<circle cx="0" cy="0" r="279" fill="none" stroke="#FFFFFF" stroke-width="5.5"/>
|
||||||
|
|
||||||
|
<g transform="scale(0.72)" fill="none" stroke="#FFFFFF" stroke-width="8" stroke-linecap="round" stroke-linejoin="round">
|
||||||
<polygon points="0,-34 30,-14 0,6 -30,-14"/>
|
<polygon points="0,-34 30,-14 0,6 -30,-14"/>
|
||||||
<polygon points="0,6 30,-14 30,20 0,40 -30,20 -30,-14"/>
|
<polygon points="0,6 30,-14 30,20 0,40 -30,20 -30,-14"/>
|
||||||
<line x1="0" y1="-34" x2="0" y2="6"/>
|
<line x1="0" y1="-34" x2="0" y2="6"/>
|
||||||
|
|
@ -11,6 +15,17 @@
|
||||||
<line x1="-30" y1="-14" x2="-30" y2="20"/>
|
<line x1="-30" y1="-14" x2="-30" y2="20"/>
|
||||||
<line x1="30" y1="-14" x2="30" y2="20"/>
|
<line x1="30" y1="-14" x2="30" y2="20"/>
|
||||||
</g>
|
</g>
|
||||||
<circle cx="0" cy="-23" r="3.4" fill="#10b981" stroke="none"/>
|
|
||||||
|
<circle cx="0" cy="-279" r="9" fill="#FFFFFF"/>
|
||||||
|
<circle cx="223" cy="0" r="9" fill="#FFFFFF"/>
|
||||||
|
<circle cx="0" cy="223" r="9" fill="#FFFFFF"/>
|
||||||
|
<circle cx="-197.28" cy="-197.28" r="6.5" fill="#FFFFFF"/>
|
||||||
|
<circle cx="113.14" cy="113.14" r="5.5" fill="#FFFFFF"/>
|
||||||
|
|
||||||
|
<line x1="-12" y1="-279" x2="12" y2="-279" stroke="#FFFFFF" stroke-width="3" stroke-linecap="round"/>
|
||||||
|
<line x1="223" y1="-12" x2="223" y2="12" stroke="#FFFFFF" stroke-width="3" stroke-linecap="round"/>
|
||||||
|
<line x1="-12" y1="223" x2="12" y2="223" stroke="#FFFFFF" stroke-width="3" stroke-linecap="round"/>
|
||||||
|
<line x1="-205.76" y1="-188.80" x2="-188.80" y2="-205.76" stroke="#FFFFFF" stroke-width="2.5" stroke-linecap="round"/>
|
||||||
|
<line x1="108.19" y1="118.09" x2="118.09" y2="108.19" stroke="#FFFFFF" stroke-width="2.5" stroke-linecap="round"/>
|
||||||
</g>
|
</g>
|
||||||
</svg>
|
</svg>
|
||||||
|
|
|
||||||
|
Before Width: | Height: | Size: 834 B After Width: | Height: | Size: 1.8 KiB |
|
|
@ -1,9 +1,10 @@
|
||||||
/* The Founder's Break - nav state, scroll reveal, RSVP submit.
|
/* The Founder's Break - nav state, scroll reveal, live countdown, RSVP submit.
|
||||||
No scroll listeners: nav state and reveal both ride IntersectionObserver. */
|
No scroll listeners: nav state and reveal both ride IntersectionObserver. */
|
||||||
|
|
||||||
(function () {
|
(function () {
|
||||||
"use strict";
|
"use strict";
|
||||||
|
|
||||||
|
var TZ = "America/New_York";
|
||||||
var reduceMotion = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
|
var reduceMotion = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
|
||||||
|
|
||||||
/* ── Nav background once the page has moved off the top ── */
|
/* ── Nav background once the page has moved off the top ── */
|
||||||
|
|
@ -23,14 +24,130 @@
|
||||||
var io = new IntersectionObserver(function (entries, obs) {
|
var io = new IntersectionObserver(function (entries, obs) {
|
||||||
entries.forEach(function (entry, i) {
|
entries.forEach(function (entry, i) {
|
||||||
if (!entry.isIntersecting) return;
|
if (!entry.isIntersecting) return;
|
||||||
var delay = Math.min(i, 4) * 70;
|
setTimeout(function () { entry.target.classList.add("in"); }, Math.min(i, 4) * 70);
|
||||||
setTimeout(function () { entry.target.classList.add("in"); }, delay);
|
|
||||||
obs.unobserve(entry.target);
|
obs.unobserve(entry.target);
|
||||||
});
|
});
|
||||||
}, { threshold: 0.15, rootMargin: "0px 0px -8% 0px" });
|
}, { threshold: 0.15, rootMargin: "0px 0px -8% 0px" });
|
||||||
reveals.forEach(function (el) { io.observe(el); });
|
reveals.forEach(function (el) { io.observe(el); });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ── Live countdown ──────────────────────────────────────
|
||||||
|
Mirrors the rule in views/index.ejs so the clock keeps itself honest
|
||||||
|
without a reload: once an event ends, it rolls to the following week
|
||||||
|
on its own, skipping any cancelled date. */
|
||||||
|
|
||||||
|
var el = document.getElementById("countdown");
|
||||||
|
if (el) startCountdown(el);
|
||||||
|
|
||||||
|
function etParts(date) {
|
||||||
|
// "2026-08-27" plus the weekday index, both in Eastern time.
|
||||||
|
var ymd = new Intl.DateTimeFormat("en-CA", {
|
||||||
|
timeZone: TZ, year: "numeric", month: "2-digit", day: "2-digit",
|
||||||
|
}).format(date);
|
||||||
|
var wd = new Intl.DateTimeFormat("en-US", { timeZone: TZ, weekday: "short" }).format(date);
|
||||||
|
var order = { Sun: 0, Mon: 1, Tue: 2, Wed: 3, Thu: 4, Fri: 5, Sat: 6 };
|
||||||
|
return { iso: ymd, day: order[wd] };
|
||||||
|
}
|
||||||
|
|
||||||
|
function etOffsetHours(iso) {
|
||||||
|
var label = new Intl.DateTimeFormat("en-US", { timeZone: TZ, timeZoneName: "shortOffset" })
|
||||||
|
.format(new Date(iso + "T12:00:00Z"));
|
||||||
|
var m = label.match(/GMT([+-]?\d{1,2})/);
|
||||||
|
return m ? parseInt(m[1], 10) : -5;
|
||||||
|
}
|
||||||
|
|
||||||
|
function instantFor(iso, hour) {
|
||||||
|
var p = iso.split("-").map(Number);
|
||||||
|
return new Date(Date.UTC(p[0], p[1] - 1, p[2], hour - etOffsetHours(iso), 0, 0));
|
||||||
|
}
|
||||||
|
|
||||||
|
function startCountdown(root) {
|
||||||
|
var weekday = Number(root.dataset.weekday);
|
||||||
|
var startHour = Number(root.dataset.startHour);
|
||||||
|
var endHour = Number(root.dataset.endHour);
|
||||||
|
var first = root.dataset.first;
|
||||||
|
var cancelled = (root.dataset.cancelled || "").split(",").filter(Boolean);
|
||||||
|
|
||||||
|
var nums = {};
|
||||||
|
root.querySelectorAll("[data-unit]").forEach(function (n) { nums[n.dataset.unit] = n; });
|
||||||
|
var label = document.getElementById("countdown-label");
|
||||||
|
var when = document.getElementById("countdown-when");
|
||||||
|
var clock = document.getElementById("countdown-clock");
|
||||||
|
|
||||||
|
function nextOccurrence(now) {
|
||||||
|
var today = etParts(now);
|
||||||
|
var parts = today.iso.split("-").map(Number);
|
||||||
|
var cur = new Date(Date.UTC(parts[0], parts[1] - 1, parts[2]));
|
||||||
|
var firstParts = first.split("-").map(Number);
|
||||||
|
var firstDate = new Date(Date.UTC(firstParts[0], firstParts[1] - 1, firstParts[2]));
|
||||||
|
if (cur < firstDate) cur = firstDate;
|
||||||
|
|
||||||
|
var iso = function (d) { return d.toISOString().slice(0, 10); };
|
||||||
|
while (cur.getUTCDay() !== weekday) cur.setUTCDate(cur.getUTCDate() + 1);
|
||||||
|
// Only roll past today once the night is actually over.
|
||||||
|
if (iso(cur) === today.iso && now >= instantFor(today.iso, endHour)) {
|
||||||
|
cur.setUTCDate(cur.getUTCDate() + 7);
|
||||||
|
}
|
||||||
|
while (cancelled.indexOf(iso(cur)) !== -1) cur.setUTCDate(cur.getUTCDate() + 7);
|
||||||
|
return iso(cur);
|
||||||
|
}
|
||||||
|
|
||||||
|
function render() {
|
||||||
|
var now = new Date();
|
||||||
|
var iso = nextOccurrence(now);
|
||||||
|
var start = instantFor(iso, startHour);
|
||||||
|
var end = instantFor(iso, endHour);
|
||||||
|
|
||||||
|
var pretty = new Intl.DateTimeFormat("en-US", {
|
||||||
|
timeZone: TZ, weekday: "long", month: "long", day: "numeric",
|
||||||
|
}).format(start);
|
||||||
|
|
||||||
|
if (now >= start && now < end) {
|
||||||
|
root.classList.add("is-live");
|
||||||
|
label.textContent = "Happening right now";
|
||||||
|
clock.hidden = true;
|
||||||
|
when.textContent = "Until " + fmtTime(end) + " tonight. Come find us at the tables.";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
root.classList.remove("is-live");
|
||||||
|
clock.hidden = false;
|
||||||
|
var diff = Math.max(0, start - now);
|
||||||
|
var total = Math.floor(diff / 1000);
|
||||||
|
set("days", Math.floor(total / 86400));
|
||||||
|
set("hours", Math.floor(total / 3600) % 24);
|
||||||
|
set("minutes", Math.floor(total / 60) % 60);
|
||||||
|
set("seconds", total % 60);
|
||||||
|
|
||||||
|
label.textContent = total < 86400 ? "The Break is tonight" : "Until the next Break";
|
||||||
|
when.textContent = pretty + ", " + fmtTime(start) + " at Silver Q";
|
||||||
|
}
|
||||||
|
|
||||||
|
function fmtTime(d) {
|
||||||
|
return new Intl.DateTimeFormat("en-US", { timeZone: TZ, hour: "numeric", minute: "2-digit" }).format(d);
|
||||||
|
}
|
||||||
|
|
||||||
|
function set(unit, value) {
|
||||||
|
var node = nums[unit];
|
||||||
|
if (!node) return;
|
||||||
|
var text = unit === "days" ? String(value) : String(value).padStart(2, "0");
|
||||||
|
if (node.textContent === text) return;
|
||||||
|
node.textContent = text;
|
||||||
|
if (reduceMotion) return;
|
||||||
|
node.classList.remove("tick");
|
||||||
|
void node.offsetWidth; // restart the animation
|
||||||
|
node.classList.add("tick");
|
||||||
|
}
|
||||||
|
|
||||||
|
render();
|
||||||
|
var timer = setInterval(render, 1000);
|
||||||
|
// A backgrounded tab throttles the interval; catch up the moment it returns.
|
||||||
|
document.addEventListener("visibilitychange", function () {
|
||||||
|
if (!document.hidden) render();
|
||||||
|
});
|
||||||
|
window.addEventListener("pagehide", function () { clearInterval(timer); });
|
||||||
|
}
|
||||||
|
|
||||||
/* ── RSVP ── */
|
/* ── RSVP ── */
|
||||||
var form = document.getElementById("rsvp-form");
|
var form = document.getElementById("rsvp-form");
|
||||||
if (!form) return;
|
if (!form) return;
|
||||||
|
|
|
||||||
|
|
@ -2,14 +2,15 @@
|
||||||
//
|
//
|
||||||
// The event recurs weekly, so nothing here is a one-off date. index.ejs computes
|
// The event recurs weekly, so nothing here is a one-off date. index.ejs computes
|
||||||
// the next occurrence at render time from `weekday` + `firstEvent`, minus anything
|
// the next occurrence at render time from `weekday` + `firstEvent`, minus anything
|
||||||
// listed in `cancelled`. To skip a week (holiday, venue closed, whatever), add the
|
// listed in `cancelled`, and public/js/main.js runs the same rule client-side so a
|
||||||
// ISO date to `cancelled` and redeploy. Do not hand-edit dates anywhere else.
|
// page left open overnight still rolls itself forward. To skip a week, add the ISO
|
||||||
|
// date to `cancelled` and redeploy. Do not hand-edit dates anywhere else.
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
name: "The Founder's Break",
|
name: "The Founder's Break",
|
||||||
tagline: "UF Startup Pool Social",
|
tagline: "UF Startup Pool Social",
|
||||||
description:
|
description:
|
||||||
"A weekly pool social for the people building things in Gainesville. Every Thursday at Silver Q.",
|
"A weekly pool social for the people building things in Gainesville. Every Thursday at Silver Q, 225 W University Ave.",
|
||||||
|
|
||||||
// Schedule
|
// Schedule
|
||||||
timezone: "America/New_York",
|
timezone: "America/New_York",
|
||||||
|
|
@ -17,15 +18,28 @@ module.exports = {
|
||||||
firstEvent: "2026-08-27",
|
firstEvent: "2026-08-27",
|
||||||
startTime: "7:00 PM",
|
startTime: "7:00 PM",
|
||||||
endTime: "9:00 PM",
|
endTime: "9:00 PM",
|
||||||
|
startHour24: 19,
|
||||||
endHour24: 21, // after this hour on event day, the page rolls to next week
|
endHour24: 21, // after this hour on event day, the page rolls to next week
|
||||||
cancelled: [
|
cancelled: [
|
||||||
// "2026-11-26", // example: Thanksgiving
|
// "2026-11-26", // example: Thanksgiving
|
||||||
],
|
],
|
||||||
|
|
||||||
// Venue
|
// Venue. Every field here is sourced, not guessed. Silver Q Billiards & Sports
|
||||||
venue: "Silver Q",
|
// Bar, established 1992, is the host venue; it is not run by OneTouch Studio.
|
||||||
city: "Gainesville",
|
venue: {
|
||||||
mapUrl: "https://www.google.com/maps/search/?api=1&query=Silver+Q+Gainesville+FL",
|
name: "Silver Q",
|
||||||
|
fullName: "Silver Q Billiards & Sports Bar",
|
||||||
|
street: "225 W University Ave",
|
||||||
|
city: "Gainesville",
|
||||||
|
state: "FL",
|
||||||
|
zip: "32601",
|
||||||
|
note: "Downtown, about a mile from campus. Doors from 4:00 PM, we take over the tables at 7:00.",
|
||||||
|
established: "1992",
|
||||||
|
instagram: "silverqgnv",
|
||||||
|
instagramUrl: "https://www.instagram.com/silverqgnv/",
|
||||||
|
mapUrl:
|
||||||
|
"https://www.google.com/maps/search/?api=1&query=Silver+Q+Billiards+225+W+University+Ave+Gainesville+FL+32601",
|
||||||
|
},
|
||||||
entry: "Free, 18+",
|
entry: "Free, 18+",
|
||||||
|
|
||||||
// Host
|
// Host
|
||||||
|
|
@ -45,6 +59,10 @@ module.exports = {
|
||||||
"https://nyc3.digitaloceanspaces.com/corben-sites/images/c23ea202-5442-41b4-9a1b-1586e81c50eb/0b5cd98c-b31a-4d09-980f-d31da5873a53/break-shot.jpg",
|
"https://nyc3.digitaloceanspaces.com/corben-sites/images/c23ea202-5442-41b4-9a1b-1586e81c50eb/0b5cd98c-b31a-4d09-980f-d31da5873a53/break-shot.jpg",
|
||||||
napkin:
|
napkin:
|
||||||
"https://nyc3.digitaloceanspaces.com/corben-sites/images/c23ea202-5442-41b4-9a1b-1586e81c50eb/eb54a5f7-a30e-4a7a-b3cb-0beb5eff43ff/napkin.jpg",
|
"https://nyc3.digitaloceanspaces.com/corben-sites/images/c23ea202-5442-41b4-9a1b-1586e81c50eb/eb54a5f7-a30e-4a7a-b3cb-0beb5eff43ff/napkin.jpg",
|
||||||
|
venue:
|
||||||
|
"https://nyc3.digitaloceanspaces.com/corben-sites/images/c23ea202-5442-41b4-9a1b-1586e81c50eb/458dab49-e942-4016-a744-127da6f9a754/silverq-venue.jpg",
|
||||||
|
venueLogo:
|
||||||
|
"https://nyc3.digitaloceanspaces.com/corben-sites/images/c23ea202-5442-41b4-9a1b-1586e81c50eb/5adbc5b1-ed83-48ae-b067-8ca01d0fde44/silverq-logo.png",
|
||||||
},
|
},
|
||||||
|
|
||||||
expect: [
|
expect: [
|
||||||
|
|
|
||||||
|
|
@ -2,9 +2,12 @@
|
||||||
/* ---- Next occurrence -------------------------------------------------
|
/* ---- Next occurrence -------------------------------------------------
|
||||||
All arithmetic happens on a UTC Date that carries the *Eastern calendar
|
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
|
date*, never a wall-clock instant. That keeps day-of-week and day-adding
|
||||||
correct across DST without needing a timezone library. */
|
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 TZ = site.timezone || "America/New_York";
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
|
const V = site.venue;
|
||||||
|
|
||||||
const todayISO = new Intl.DateTimeFormat("en-CA", {
|
const todayISO = new Intl.DateTimeFormat("en-CA", {
|
||||||
timeZone: TZ, year: "numeric", month: "2-digit", day: "2-digit",
|
timeZone: TZ, year: "numeric", month: "2-digit", day: "2-digit",
|
||||||
|
|
@ -24,10 +27,10 @@
|
||||||
while (next.getUTCDay() !== site.weekday) next.setUTCDate(next.getUTCDate() + 1);
|
while (next.getUTCDay() !== site.weekday) next.setUTCDate(next.getUTCDate() + 1);
|
||||||
if (isoOf(next) === todayISO && hourET >= (site.endHour24 || 21)) next.setUTCDate(next.getUTCDate() + 7);
|
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
|
// 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.
|
// should name the soonest one people were expecting, not the last one.
|
||||||
|
const cancelled = site.cancelled || [];
|
||||||
|
let skippedWeek = null;
|
||||||
while (cancelled.includes(isoOf(next))) {
|
while (cancelled.includes(isoOf(next))) {
|
||||||
if (!skippedWeek) skippedWeek = isoOf(next);
|
if (!skippedWeek) skippedWeek = isoOf(next);
|
||||||
next.setUTCDate(next.getUTCDate() + 7);
|
next.setUTCDate(next.getUTCDate() + 7);
|
||||||
|
|
@ -40,14 +43,23 @@
|
||||||
const nextShort = fmtDate({ weekday: "short", month: "short", 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 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
|
const heroLine = isTonight
|
||||||
? `Tonight at ${site.venue}, ${site.startTime} to ${site.endTime}. Grab a cue and meet the people building things in ${site.city}.`
|
? `Tonight at ${V.name}, ${site.startTime} to ${site.endTime}. Grab a cue and meet the people building things in ${V.city}.`
|
||||||
: `Every ${weekdayName} at ${site.venue}, ${site.startTime} to ${site.endTime}. The next one is ${nextLong}.`;
|
: `Every ${weekdayName} at ${V.name}, ${site.startTime} to ${site.endTime}. The next one is ${nextLong}.`;
|
||||||
|
|
||||||
const details = [
|
const details = [
|
||||||
{ label: "When", value: `Every ${weekdayName}` },
|
{ label: "When", value: `Every ${weekdayName}` },
|
||||||
{ label: "Time", value: `${site.startTime} to ${site.endTime}` },
|
{ label: "Time", value: `${site.startTime} to ${site.endTime}` },
|
||||||
{ label: "Where", value: `${site.venue}, ${site.city}`, href: site.mapUrl },
|
{ label: "Where", value: `${V.name}, ${V.street}`, href: V.mapUrl },
|
||||||
{ label: "Entry", value: site.entry },
|
{ label: "Entry", value: site.entry },
|
||||||
];
|
];
|
||||||
%><!DOCTYPE html>
|
%><!DOCTYPE html>
|
||||||
|
|
@ -67,6 +79,7 @@
|
||||||
<meta name="twitter:card" content="summary_large_image">
|
<meta name="twitter:card" content="summary_large_image">
|
||||||
|
|
||||||
<link rel="icon" href="/images/favicon.svg" type="image/svg+xml">
|
<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.googleapis.com">
|
||||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
<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 href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=Space+Grotesk:wght@500;600;700&display=swap" rel="stylesheet">
|
||||||
|
|
@ -83,12 +96,12 @@
|
||||||
"@type": "Event",
|
"@type": "Event",
|
||||||
name: site.name,
|
name: site.name,
|
||||||
description: site.description,
|
description: site.description,
|
||||||
startDate: `${nextISO}T19:00:00-04:00`,
|
startDate: startISO,
|
||||||
endDate: `${nextISO}T21:00:00-04:00`,
|
endDate: endISO,
|
||||||
eventSchedule: { "@type": "Schedule", repeatFrequency: "P1W", byDay: "https://schema.org/Thursday", startTime: "19:00", endTime: "21:00" },
|
eventSchedule: { "@type": "Schedule", repeatFrequency: "P1W", byDay: "https://schema.org/Thursday", startTime: "19:00", endTime: "21:00" },
|
||||||
eventStatus: "https://schema.org/EventScheduled",
|
eventStatus: "https://schema.org/EventScheduled",
|
||||||
eventAttendanceMode: "https://schema.org/OfflineEventAttendanceMode",
|
eventAttendanceMode: "https://schema.org/OfflineEventAttendanceMode",
|
||||||
location: { "@type": "Place", name: site.venue, address: { "@type": "PostalAddress", addressLocality: site.city, addressRegion: "FL", addressCountry: "US" } },
|
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 },
|
organizer: { "@type": "Organization", name: site.host.name, url: site.host.url },
|
||||||
isAccessibleForFree: true,
|
isAccessibleForFree: true,
|
||||||
image: [site.images.hero],
|
image: [site.images.hero],
|
||||||
|
|
@ -108,6 +121,7 @@
|
||||||
<nav class="nav-right" aria-label="Primary">
|
<nav class="nav-right" aria-label="Primary">
|
||||||
<a class="nav-link" href="#details">Details</a>
|
<a class="nav-link" href="#details">Details</a>
|
||||||
<a class="nav-link" href="#expect">What to expect</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">RSVP</a>
|
<a class="nav-cta" href="#rsvp">RSVP</a>
|
||||||
</nav>
|
</nav>
|
||||||
</header>
|
</header>
|
||||||
|
|
@ -131,6 +145,31 @@
|
||||||
</figure>
|
</figure>
|
||||||
</section>
|
</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 -->
|
<!-- Details: display-value strip, vertical hairlines only -->
|
||||||
<section class="details" id="details">
|
<section class="details" id="details">
|
||||||
<% if (skippedWeek) { %>
|
<% if (skippedWeek) { %>
|
||||||
|
|
@ -176,6 +215,28 @@
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</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"><%= V.street %><br><%= V.city %>, <%= V.state %> <%= V.zip %></a>
|
||||||
|
</address>
|
||||||
|
<p class="venue-meta">
|
||||||
|
<a href="<%= V.instagramUrl %>" target="_blank" rel="noopener">@<%= 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 -->
|
<!-- RSVP -->
|
||||||
<section class="rsvp" id="rsvp">
|
<section class="rsvp" id="rsvp">
|
||||||
<div class="rsvp-panel reveal">
|
<div class="rsvp-panel reveal">
|
||||||
|
|
@ -213,7 +274,7 @@
|
||||||
|
|
||||||
<div class="rsvp-done" id="rsvp-done" hidden>
|
<div class="rsvp-done" id="rsvp-done" hidden>
|
||||||
<h3>You are on the list.</h3>
|
<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>
|
<p>Check your inbox for a confirmation. See you <%= isTonight ? "tonight" : nextLong %> at <%= V.name %>, <%= V.street %>, <%= 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>
|
<a class="ics" href="/assets/founders-break.ics">Add the whole series to your calendar</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -229,7 +290,7 @@
|
||||||
<span>Hosted by <%= site.host.name %></span>
|
<span>Hosted by <%= site.host.name %></span>
|
||||||
</a>
|
</a>
|
||||||
<a href="mailto:<%= site.host.email %>"><%= site.host.email %></a>
|
<a href="mailto:<%= site.host.email %>"><%= site.host.email %></a>
|
||||||
<a href="<%= site.mapUrl %>" target="_blank" rel="noopener"><%= site.venue %>, <%= site.city %></a>
|
<a href="<%= V.mapUrl %>" target="_blank" rel="noopener"><%= V.name %>, <%= V.street %></a>
|
||||||
</div>
|
</div>
|
||||||
</footer>
|
</footer>
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue