commit b359a9a5be9c85aeaed972c6a8bffa4373f37c61 Author: Devin Willis Date: Wed Aug 26 14:11:34 2026 -0400 The Founder's Break: event site for foundersbreak.onetouch.studio diff --git a/README.md b/README.md new file mode 100644 index 0000000..6a104de --- /dev/null +++ b/README.md @@ -0,0 +1,52 @@ +# The Founder's Break + +Event site for the weekly UF startup pool social at Silver Q, Gainesville. +Hosted by OneTouch Studio at **https://foundersbreak.onetouch.studio**. + +Corben EJS site. There is no build step: `deploy_site` copies this repo verbatim. + +``` +site.config.js all schedule, venue and copy data +views/index.ejs the single page, including the next-date logic +public/css/style.css brand tokens lifted from onetouch.studio +public/js/main.js nav state, scroll reveal, RSVP submit +public/assets/ the recurring .ics +public/images/ logo and favicon +``` + +## The date is never hard-coded + +`views/index.ejs` computes the next occurrence at render time from +`weekday` + `firstEvent` in `site.config.js`, so the page is correct on every +load without anyone touching it. After 9:00 PM on an event night it rolls to +the following week. + +## Cancelling a week + +Add the ISO date to `cancelled` in `site.config.js` and redeploy: + +```js +cancelled: ["2026-11-26"], // Thanksgiving +``` + +The page then skips that Thursday, shows a notice naming the cancelled date, +and points at the next one that is actually happening. The `.ics` in +`public/assets/` is a plain weekly recurrence and does not know about +cancellations, so send the newsletter a heads-up too. + +## Deploying + +``` +bulk_upload_git owner=gator repo=founders-break +deploy_site id=4ae63819-3f63-4fea-b0de-eb1e5befe607 repo=gator/founders-break mode=ejs +``` + +Nothing redeploys itself. Every edit needs the deploy step. + +## RSVPs + +The form POSTs to `/forms/rsvp`, which the Corben sites renderer stores in +`site_form_submissions` and announces as a `form.submitted` event. Read them +with `get_site_form_submissions`. JavaScript submits it in place; without +JavaScript the plain POST falls back to `/?rsvp=ok` and the page shows the +same confirmation. diff --git a/public/assets/founders-break.ics b/public/assets/founders-break.ics new file mode 100644 index 0000000..1d7c08a --- /dev/null +++ b/public/assets/founders-break.ics @@ -0,0 +1,40 @@ +BEGIN:VCALENDAR +VERSION:2.0 +PRODID:-//OneTouch Studio//The Founder's Break//EN +CALSCALE:GREGORIAN +METHOD:PUBLISH +X-WR-CALNAME:The Founder's Break +BEGIN:VTIMEZONE +TZID:America/New_York +BEGIN:DAYLIGHT +TZOFFSETFROM:-0500 +TZOFFSETTO:-0400 +TZNAME:EDT +DTSTART:19700308T020000 +RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=2SU +END:DAYLIGHT +BEGIN:STANDARD +TZOFFSETFROM:-0400 +TZOFFSETTO:-0500 +TZNAME:EST +DTSTART:19701101T020000 +RRULE:FREQ=YEARLY;BYMONTH=11;BYDAY=1SU +END:STANDARD +END:VTIMEZONE +BEGIN:VEVENT +UID:founders-break-weekly@foundersbreak.onetouch.studio +DTSTAMP:20260826T180000Z +DTSTART;TZID=America/New_York:20260827T190000 +DTEND;TZID=America/New_York:20260827T210000 +RRULE:FREQ=WEEKLY;BYDAY=TH +SUMMARY:The Founder's Break +LOCATION:Silver Q\, Gainesville\, FL +URL:https://foundersbreak.onetouch.studio +DESCRIPTION:UF startup pool social. Every Thursday\, 7 to 9 PM at Silver Q. + 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. + studio +STATUS:CONFIRMED +TRANSP:TRANSPARENT +END:VEVENT +END:VCALENDAR diff --git a/public/css/style.css b/public/css/style.css new file mode 100644 index 0000000..6c8b065 --- /dev/null +++ b/public/css/style.css @@ -0,0 +1,341 @@ +/* The Founder's Break + Brand tokens are lifted verbatim from onetouch.studio/public/css/variables.css + so this page reads as part of the same studio. One accent (emerald), one theme + (dark), and a two-step radius scale: surfaces 14px, controls 10px. */ + +:root { + --bg: #06060b; + --bg2: #0c0c14; + --surface: rgba(255, 255, 255, 0.03); + --surface2: rgba(255, 255, 255, 0.05); + --accent: #10b981; + --accent2: #34d399; + --accent3: #059669; + --text: #f1f0ff; + --muted: #8b89a6; + --fine: #9c9ab5; /* smallest text: lifted off --muted2 to clear WCAG AA */ + --border: rgba(16, 185, 129, 0.12); + --border-hover: rgba(16, 185, 129, 0.35); + --hairline: rgba(255, 255, 255, 0.08); + --on-accent: #04140f; /* near-black on emerald. White scores 3.9:1 and fails AA. */ + + --r-surface: 14px; + --r-control: 10px; + --ease: cubic-bezier(0.16, 1, 0.3, 1); + --shell: 1200px; + --gut: 2.5rem; +} + +*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; } + +html { + color-scheme: dark; + scroll-behavior: smooth; + scroll-padding-top: 84px; +} + +body { + background: var(--bg); + color: var(--text); + font-family: "Inter", -apple-system, BlinkMacSystemFont, system-ui, sans-serif; + font-size: 16px; + line-height: 1.6; + -webkit-font-smoothing: antialiased; + overflow-x: hidden; +} + +h1, h2, h3 { font-family: "Space Grotesk", "Inter", sans-serif; letter-spacing: -0.02em; line-height: 1.15; } +img { max-width: 100%; height: auto; display: block; } +a { color: var(--accent2); } + +:focus-visible { + outline: 2px solid var(--accent2); + outline-offset: 3px; + border-radius: 4px; +} + +.skip { + position: absolute; left: -9999px; top: 0; z-index: 200; + background: var(--accent); color: var(--on-accent); + padding: 0.7rem 1.1rem; border-radius: 0 0 var(--r-control) 0; font-weight: 600; +} +.skip:focus { left: 0; } + +/* ── Navigation ───────────────────────────────────────────── */ + +.nav { + position: fixed; inset: 0 0 auto 0; z-index: 100; + height: 64px; + display: flex; align-items: center; justify-content: space-between; + padding: 0 var(--gut); + background: rgba(6, 6, 11, 0.72); + backdrop-filter: blur(24px); + -webkit-backdrop-filter: blur(24px); + border-bottom: 1px solid var(--border); + transition: background 0.3s var(--ease); +} +.nav.scrolled { background: rgba(6, 6, 11, 0.95); } + +.brand { display: flex; align-items: center; gap: 0.6rem; text-decoration: none; color: var(--text); } +.brand-mark { opacity: 0.9; flex: none; } +.brand-name { font-family: "Space Grotesk", sans-serif; font-weight: 700; font-size: 1.02rem; white-space: nowrap; } + +.nav-right { display: flex; align-items: center; gap: 1.9rem; } +.nav-link { + color: var(--muted); text-decoration: none; font-size: 0.875rem; font-weight: 500; + white-space: nowrap; transition: color 0.2s var(--ease); +} +.nav-link:hover { color: var(--text); } + +.nav-cta { + background: var(--accent); color: var(--on-accent); + padding: 0.5rem 1.15rem; border-radius: var(--r-control); + font-size: 0.82rem; font-weight: 700; text-decoration: none; white-space: nowrap; + transition: background 0.2s var(--ease), transform 0.2s var(--ease); +} +.nav-cta:hover { background: var(--accent2); } +.nav-cta:active { transform: translateY(1px); } + +/* ── Buttons ──────────────────────────────────────────────── */ + +.btn { + display: inline-flex; align-items: center; justify-content: center; + padding: 0.85rem 1.9rem; border-radius: var(--r-control); + font-family: inherit; font-size: 0.95rem; font-weight: 600; + text-decoration: none; white-space: nowrap; cursor: pointer; border: 1px solid transparent; + transition: transform 0.2s var(--ease), background 0.2s var(--ease), border-color 0.2s var(--ease), box-shadow 0.2s var(--ease); +} +.btn-primary { + background: linear-gradient(135deg, var(--accent), var(--accent3)); + color: var(--on-accent); + box-shadow: 0 4px 22px rgba(16, 185, 129, 0.22); +} +.btn-primary:hover { transform: translateY(-2px); box-shadow: 0 8px 30px rgba(16, 185, 129, 0.3); } +.btn-primary:active { transform: translateY(1px); } +.btn-ghost { background: transparent; color: var(--text); border-color: var(--hairline); } +.btn-ghost:hover { border-color: var(--border-hover); color: var(--accent2); transform: translateY(-2px); } +.btn-ghost:active { transform: translateY(1px); } +.btn-block { width: 100%; margin-top: 0.4rem; } + +.btn[aria-busy="true"] { pointer-events: none; opacity: 0.72; } +.btn[aria-busy="true"]::after { + content: ""; width: 15px; height: 15px; margin-left: 0.6rem; + border: 2px solid rgba(4, 20, 15, 0.3); border-top-color: var(--on-accent); + border-radius: 50%; animation: spin 0.6s linear infinite; +} +@keyframes spin { to { transform: rotate(360deg); } } + +/* ── Hero: asymmetric split ───────────────────────────────── */ + +.hero { + max-width: var(--shell); margin: 0 auto; + min-height: 100dvh; + display: grid; grid-template-columns: 1.04fr 0.96fr; gap: 4rem; align-items: center; + padding: calc(64px + 3rem) var(--gut) 4rem; +} + +.eyebrow { + font-size: 0.72rem; font-weight: 700; letter-spacing: 0.16em; + text-transform: uppercase; color: var(--accent2); margin-bottom: 1.1rem; +} +.hero h1 { + font-size: clamp(2.6rem, 6.2vw, 4.5rem); + font-weight: 700; line-height: 1.02; +} +.hero-sub { + margin-top: 1.5rem; max-width: 46ch; + font-size: clamp(1rem, 1.5vw, 1.14rem); color: var(--muted); +} +.hero-actions { display: flex; flex-wrap: wrap; gap: 0.85rem; margin-top: 2.4rem; } + +.hero-figure { position: relative; } +.hero-figure img { + width: 100%; aspect-ratio: 3 / 2; object-fit: cover; + border-radius: var(--r-surface); border: 1px solid var(--hairline); +} +.hero-figure::after { + content: ""; position: absolute; inset: 0; pointer-events: none; + border-radius: var(--r-surface); + background: linear-gradient(190deg, transparent 55%, rgba(6, 6, 11, 0.5)); +} + +/* ── Details: display-value strip ─────────────────────────── */ + +.details { max-width: var(--shell); margin: 0 auto; padding: 0 var(--gut) 6rem; } + +.notice { + margin-bottom: 1.6rem; padding: 0.85rem 1.15rem; + border: 1px solid var(--border); border-left: 3px solid var(--accent); + border-radius: var(--r-control); background: var(--surface); + color: var(--text); font-size: 0.92rem; +} + +.detail-grid { + display: grid; grid-template-columns: repeat(4, 1fr); + border-top: 1px solid var(--hairline); border-bottom: 1px solid var(--hairline); +} +.detail { padding: 2rem 1.6rem; } +.detail + .detail { border-left: 1px solid var(--hairline); } +.detail dt { + font-size: 0.7rem; font-weight: 600; letter-spacing: 0.1em; + text-transform: uppercase; color: var(--muted); margin-bottom: 0.55rem; +} +.detail dd { + font-family: "Space Grotesk", sans-serif; font-weight: 600; + font-size: clamp(1.05rem, 1.7vw, 1.4rem); line-height: 1.25; +} +.detail dd a { color: inherit; text-decoration: none; border-bottom: 1px solid var(--border-hover); } +.detail dd a:hover { color: var(--accent2); } + +.detail-foot { + display: flex; flex-wrap: wrap; gap: 0.6rem 1.6rem; + margin-top: 1.4rem; font-size: 0.9rem; color: var(--muted); +} +.detail-foot .is-live { color: var(--accent2); font-weight: 600; } +.ics { color: var(--accent2); text-decoration: none; border-bottom: 1px solid var(--border-hover); } +.ics:hover { border-bottom-color: var(--accent2); } + +/* ── Pitch: full-bleed editorial band ─────────────────────── */ + +.pitch { + position: relative; isolation: isolate; + padding: 8rem var(--gut); + border-top: 1px solid var(--hairline); border-bottom: 1px solid var(--hairline); +} +.pitch::before { + content: ""; position: absolute; inset: 0; z-index: -2; + background-image: var(--pitch-image); + background-size: cover; background-position: center; +} +.pitch::after { + content: ""; position: absolute; inset: 0; z-index: -1; + background: linear-gradient(180deg, rgba(6, 6, 11, 0.88), rgba(6, 6, 11, 0.94)); +} +.pitch-inner { max-width: 780px; margin: 0 auto; text-align: center; } +.pitch h2 { + font-size: clamp(1.7rem, 3.6vw, 2.7rem); font-weight: 700; + text-wrap: balance; +} +.pitch p { margin-top: 1.5rem; color: var(--muted); font-size: 1.05rem; } +.pitch-kicker { color: var(--text) !important; font-weight: 600; } + +/* ── What to expect: bento ────────────────────────────────── */ + +.expect { max-width: var(--shell); margin: 0 auto; padding: 7rem var(--gut); } +.section-title { font-size: clamp(1.7rem, 3.4vw, 2.4rem); font-weight: 700; margin-bottom: 2.6rem; } + +.bento { + display: grid; gap: 1rem; + grid-template-columns: 1.15fr 1fr 1fr; + grid-template-rows: auto auto; +} +.cell { + border: 1px solid var(--hairline); border-radius: var(--r-surface); + overflow: hidden; background: var(--surface); +} +.cell-photo { grid-row: span 2; border-color: transparent; background: none; } +.cell-photo img { width: 100%; height: 100%; min-height: 260px; object-fit: cover; } +.cell-text { padding: 2rem 1.9rem; transition: border-color 0.25s var(--ease), transform 0.25s var(--ease); } +.cell-text:hover { border-color: var(--border-hover); transform: translateY(-3px); } +.cell-tint { + background: + radial-gradient(120% 130% at 100% 0%, rgba(16, 185, 129, 0.16), transparent 62%), + var(--surface); +} +.cell-wide { grid-column: span 2; } +.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; } + +/* ── RSVP ─────────────────────────────────────────────────── */ + +.rsvp { padding: 0 var(--gut) 7rem; } +.rsvp-panel { + max-width: 580px; margin: 0 auto; padding: 2.8rem; + background: var(--bg2); border: 1px solid var(--border); border-radius: var(--r-surface); +} +.rsvp-panel h2 { font-size: clamp(1.4rem, 2.6vw, 1.85rem); font-weight: 700; } +.rsvp-sub { margin-top: 0.8rem; color: var(--muted); font-size: 0.96rem; } + +form { margin-top: 2rem; } +.field { display: flex; flex-direction: column; gap: 0.5rem; margin-bottom: 1.3rem; } +.field label { font-size: 0.87rem; font-weight: 600; color: var(--text); } +.field .opt { font-weight: 400; color: var(--muted); } +.field input { + font-family: inherit; font-size: 0.98rem; + padding: 0.8rem 0.95rem; color: var(--text); + background: var(--bg); border: 1px solid var(--hairline); border-radius: var(--r-control); + transition: border-color 0.2s var(--ease); +} +.field input::placeholder { color: var(--fine); } +.field input:hover { border-color: rgba(255, 255, 255, 0.16); } +.field input:focus { border-color: var(--accent); outline: 2px solid var(--accent2); outline-offset: 2px; } +.field-help { font-size: 0.8rem; color: var(--fine); } +.field-error { font-size: 0.8rem; color: #fca5a5; display: none; } +.field.has-error input { border-color: #f87171; } +.field.has-error .field-error { display: block; } + +.hp { position: absolute; left: -9999px; width: 1px; height: 1px; overflow: hidden; } + +.form-status { margin-top: 0.9rem; font-size: 0.88rem; color: #fca5a5; min-height: 1.2em; } +.fine { margin-top: 0.9rem; font-size: 0.78rem; color: var(--fine); text-align: center; } + +.rsvp-done h3 { font-size: 1.4rem; font-weight: 700; } +.rsvp-done p { margin: 0.8rem 0 1.2rem; color: var(--muted); } + +/* ── Footer ───────────────────────────────────────────────── */ + +.foot { + border-top: 1px solid var(--hairline); + padding: 3.2rem var(--gut); + max-width: var(--shell); margin: 0 auto; + display: flex; flex-wrap: wrap; gap: 1.4rem 2rem; + align-items: center; justify-content: space-between; +} +.foot-line { font-family: "Space Grotesk", sans-serif; font-weight: 600; font-size: 1.02rem; } +.foot-meta { display: flex; flex-wrap: wrap; gap: 0.6rem 1.8rem; align-items: center; font-size: 0.87rem; } +.foot-meta a { color: var(--muted); text-decoration: none; transition: color 0.2s var(--ease); } +.foot-meta a:hover { color: var(--accent2); } +.foot-host { display: inline-flex; align-items: center; gap: 0.5rem; } +.foot-host img { opacity: 0.7; } + +/* ── Scroll reveal ────────────────────────────────────────── */ +/* Only hides when JS is running, so a failed script never eats the page. */ + +.js .reveal { opacity: 0; transform: translateY(18px); } +.js .reveal.in { + opacity: 1; transform: none; + transition: opacity 0.65s var(--ease), transform 0.65s var(--ease); +} + +/* ── Responsive ───────────────────────────────────────────── */ + +@media (max-width: 1000px) { + .hero { grid-template-columns: 1fr; gap: 2.8rem; min-height: 0; padding-top: calc(64px + 2.5rem); } + .hero-figure { order: -1; } + .hero-figure img { aspect-ratio: 16 / 10; } + .bento { grid-template-columns: 1fr 1fr; } + .cell-photo { grid-row: auto; grid-column: span 2; } + .cell-photo img { min-height: 220px; max-height: 320px; } + .cell-wide { grid-column: span 2; } +} + +@media (max-width: 760px) { + :root { --gut: 1.25rem; } + .nav-link { display: none; } + .detail-grid { grid-template-columns: 1fr 1fr; } + .detail { padding: 1.4rem 1.1rem; } + .detail:nth-child(odd) { border-left: 0; } + .detail:nth-child(n + 3) { border-top: 1px solid var(--hairline); } + .bento { grid-template-columns: 1fr; } + .cell-photo, .cell-wide { grid-column: auto; } + .pitch { padding: 5rem var(--gut); } + .expect { padding: 5rem var(--gut); } + .rsvp-panel { padding: 1.9rem 1.4rem; } + .foot { flex-direction: column; align-items: flex-start; } +} + +@media (prefers-reduced-motion: reduce) { + html { scroll-behavior: auto; } + .js .reveal, .js .reveal.in { opacity: 1; transform: none; transition: none; } + *, *::before, *::after { animation-duration: 0.01ms !important; transition-duration: 0.01ms !important; } +} diff --git a/public/images/favicon.svg b/public/images/favicon.svg new file mode 100644 index 0000000..06d8f1e --- /dev/null +++ b/public/images/favicon.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/public/images/onetouch-studio.svg b/public/images/onetouch-studio.svg new file mode 100644 index 0000000..4887d2e --- /dev/null +++ b/public/images/onetouch-studio.svg @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/public/js/main.js b/public/js/main.js new file mode 100644 index 0000000..809300a --- /dev/null +++ b/public/js/main.js @@ -0,0 +1,119 @@ +/* The Founder's Break - nav state, scroll reveal, RSVP submit. + No scroll listeners: nav state and reveal both ride IntersectionObserver. */ + +(function () { + "use strict"; + + var reduceMotion = window.matchMedia("(prefers-reduced-motion: reduce)").matches; + + /* ── Nav background once the page has moved off the top ── */ + var nav = document.getElementById("nav"); + var sentinel = document.createElement("div"); + sentinel.style.cssText = "position:absolute;top:0;left:0;width:1px;height:1px;pointer-events:none"; + document.body.prepend(sentinel); + new IntersectionObserver(function (entries) { + nav.classList.toggle("scrolled", !entries[0].isIntersecting); + }).observe(sentinel); + + /* ── Scroll reveal: sequences sections in as they arrive ── */ + var reveals = document.querySelectorAll(".reveal"); + if (reduceMotion) { + reveals.forEach(function (el) { el.classList.add("in"); }); + } else { + var io = new IntersectionObserver(function (entries, obs) { + entries.forEach(function (entry, i) { + if (!entry.isIntersecting) return; + var delay = Math.min(i, 4) * 70; + setTimeout(function () { entry.target.classList.add("in"); }, delay); + obs.unobserve(entry.target); + }); + }, { threshold: 0.15, rootMargin: "0px 0px -8% 0px" }); + reveals.forEach(function (el) { io.observe(el); }); + } + + /* ── RSVP ── */ + var form = document.getElementById("rsvp-form"); + if (!form) return; + + var submit = document.getElementById("rsvp-submit"); + var status = document.getElementById("form-status"); + var done = document.getElementById("rsvp-done"); + var nameField = document.getElementById("f-name"); + var emailField = document.getElementById("f-email"); + + function setError(input, message) { + var field = input.closest(".field"); + field.classList.toggle("has-error", Boolean(message)); + var slot = field.querySelector(".field-error"); + if (slot) slot.textContent = message || ""; + input.setAttribute("aria-invalid", message ? "true" : "false"); + } + + function validate() { + var ok = true; + if (!nameField.value.trim()) { setError(nameField, "We need a name to put on the list."); ok = false; } + else setError(nameField, ""); + + var email = emailField.value.trim(); + if (!email) { setError(emailField, "We need an email to send the reminder to."); ok = false; } + else if (!/^[^\s@]+@[^\s@]+\.[^\s@]{2,}$/.test(email)) { setError(emailField, "That email does not look right."); ok = false; } + else setError(emailField, ""); + + return ok; + } + + [nameField, emailField].forEach(function (input) { + input.addEventListener("input", function () { + if (input.closest(".field").classList.contains("has-error")) validate(); + }); + }); + + form.addEventListener("submit", function (event) { + event.preventDefault(); + status.textContent = ""; + + if (!validate()) { + form.querySelector(".has-error input").focus(); + return; + } + + submit.setAttribute("aria-busy", "true"); + + var payload = new URLSearchParams(); + new FormData(form).forEach(function (value, key) { + if (key !== "_redirect") payload.append(key, value); + }); + + fetch(form.action, { + method: "POST", + headers: { + "Content-Type": "application/x-www-form-urlencoded;charset=UTF-8", + Accept: "application/json", + }, + body: payload.toString(), + }) + .then(function (res) { + if (!res.ok) throw new Error("HTTP " + res.status); + return res.json().catch(function () { return { ok: true }; }); + }) + .then(function (body) { + if (body && body.ok === false) throw new Error(body.error || "rejected"); + showDone(); + }) + .catch(function () { + submit.removeAttribute("aria-busy"); + status.textContent = + "That did not go through. Try again, or email contact@onetouch.studio and we will add you by hand."; + }); + }); + + function showDone() { + form.hidden = true; + done.hidden = false; + done.setAttribute("tabindex", "-1"); + done.focus({ preventScroll: true }); + } + + /* No-JS fallback lands back here with ?rsvp=ok after the plain form POST. */ + if (new URLSearchParams(window.location.search).get("rsvp") === "ok") showDone(); +})(); diff --git a/site.config.js b/site.config.js new file mode 100644 index 0000000..aff3757 --- /dev/null +++ b/site.config.js @@ -0,0 +1,64 @@ +// The Founder's Break - foundersbreak.onetouch.studio +// +// 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 +// listed in `cancelled`. To skip a week (holiday, venue closed, whatever), add the +// ISO date to `cancelled` and redeploy. Do not hand-edit dates anywhere else. + +module.exports = { + name: "The Founder's Break", + tagline: "UF Startup Pool Social", + description: + "A weekly pool social for the people building things in Gainesville. Every Thursday at Silver Q.", + + // Schedule + timezone: "America/New_York", + weekday: 4, // 0 = Sunday, 4 = Thursday + firstEvent: "2026-08-27", + startTime: "7:00 PM", + endTime: "9:00 PM", + endHour24: 21, // after this hour on event day, the page rolls to next week + cancelled: [ + // "2026-11-26", // example: Thanksgiving + ], + + // Venue + venue: "Silver Q", + city: "Gainesville", + mapUrl: "https://www.google.com/maps/search/?api=1&query=Silver+Q+Gainesville+FL", + entry: "Free, 18+", + + // Host + host: { + name: "OneTouch Studio", + url: "https://onetouch.studio", + email: "contact@onetouch.studio", + }, + + url: "https://foundersbreak.onetouch.studio", + + // Images live in the Corben image library rather than the repo so the repo + // stays text-only and the CDN serves them. + images: { + hero: "https://nyc3.digitaloceanspaces.com/corben-sites/images/c23ea202-5442-41b4-9a1b-1586e81c50eb/23832ce5-0ef3-4fb3-96d5-f8de938e47b1/hero.jpg", + breakShot: + "https://nyc3.digitaloceanspaces.com/corben-sites/images/c23ea202-5442-41b4-9a1b-1586e81c50eb/0b5cd98c-b31a-4d09-980f-d31da5873a53/break-shot.jpg", + napkin: + "https://nyc3.digitaloceanspaces.com/corben-sites/images/c23ea202-5442-41b4-9a1b-1586e81c50eb/eb54a5f7-a30e-4a7a-b3cb-0beb5eff43ff/napkin.jpg", + }, + + expect: [ + { + title: "Cross-disciplinary hustle", + body: "Meet your next co-founder, bounce ideas off business minds, and connect with technical creators.", + }, + { + title: "Zero pressure", + body: "No formal pitches, no entry fees, no name tags. Grab a cue, talk shop, make things happen.", + }, + { + title: "A real community", + body: "Be part of the movement putting Gainesville's startup scene on the map.", + }, + ], +}; diff --git a/views/index.ejs b/views/index.ejs new file mode 100644 index 0000000..3fc23af --- /dev/null +++ b/views/index.ejs @@ -0,0 +1,238 @@ +<% + /* ---- 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 }, + ]; +%> + + + + +<%= site.name %> | <%= site.tagline %> + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+
+

<%= site.tagline %>

+

<%= site.name %>

+

<%= heroLine %>

+ +
+
+ Four people talking beside a lit pool table in a dark bar. +
+
+ + +
+ <% if (skippedWeek) { %> +

No Break on <%= new Intl.DateTimeFormat("en-US", { timeZone: "UTC", month: "long", day: "numeric" }).format(toDate(skippedWeek)) %>. Back on <%= nextLong %>.

+ <% } %> +
+ <% details.forEach(function (d) { %> +
+
<%= d.label %>
+
<% if (d.href) { %><%= d.value %><% } else { %><%= d.value %><% } %>
+
+ <% }) %> +
+

+ class="is-live"<% } %>><%= isTonight ? "Happening tonight" : `Next: ${nextShort}` %> + Add the whole series to your calendar +

+
+ + +
+
+

We are building Gainesville's startup culture, one terrible pool shot at a time.

+

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.

+

Zero pool skills required. Seriously. We are here to build startups, not run the table.

+
+
+ + +
+

What to expect

+
+
+ Three students laughing over a napkin sketch at the rail of a pool table. +
+ <% site.expect.forEach(function (item, i) { %> +
<%= i === 1 ? " cell-tint" : "" %> reveal"> +

<%= item.title %>

+

<%= item.body %>

+
+ <% }) %> +
+
+ + +
+
+

RSVP for <%= isTonight ? "tonight" : nextLong %>

+

Not required, but it helps us know how many tables to hold. We will email you a reminder the day before.

+ +
+ + + + +
+ + +

+
+ +
+ + +

For the weekly reminder. Nothing else.

+

+
+ +
+ + +

A few words is plenty. It helps us introduce you to the right people.

+
+ + +

+

One email a week at most. Unsubscribe any time.

+
+ + +
+
+ +
+ + + + + +