From b0221a67a5dde16c6ed5c2bb03dce6cbb0029dd9 Mon Sep 17 00:00:00 2001 From: huncijr Date: Fri, 19 Jun 2026 14:26:12 +0300 Subject: [PATCH 1/8] event cancelled feature finished --- components/event.js | 82 +++++++++++++++++++++++++++++++--- components/month.js | 2 +- lib/data.js | 106 +++++++++++++++++++++++++++++--------------- pages/[slug].js | 31 +++++++++---- 4 files changed, 170 insertions(+), 51 deletions(-) diff --git a/components/event.js b/components/event.js index 4bf4ed0..1f25504 100644 --- a/components/event.js +++ b/components/event.js @@ -2,6 +2,7 @@ import { Box, Text, Flex, Avatar, Heading } from 'theme-ui' import tt from 'tinytime' import Link from 'next/link' import Sparkles from './sparkles' +import { XCircle, Users } from 'react-feather' const past = dt => new Date(dt) < new Date() const now = (start, end) => @@ -22,29 +23,66 @@ const tagStyle = { mt: 1 } -const Event = ({ id, slug, title, desc, leader, avatar, start, end, cal, tags }) => ( +const cancelledStyle = { + display: 'inline-flex', + alignItems: 'center', + gap: '4px', + fontSize: 0, + fontWeight: 'bold', + textTransform: 'uppercase', + letterSpacing: '0.05rem', + bg: 'red', + color: 'white', + px: 2, + py: '2px', + borderRadius: 'default', + mr: 1, + mt: 1 +} + +const Event = ({ + id, + slug, + title, + desc, + leader, + avatar, + cancelled, + interestCount, + start, + end, + cal, + tags +}) => ( @@ -56,8 +94,17 @@ const Event = ({ id, slug, title, desc, leader, avatar, start, end, cal, tags }) {title} + {cancelled && ( + + + {' '} + + Cancelled + + + )} {tags?.length > 0 && ( - + {tags.map(tag => ( {tag.replace('-', ' ')} @@ -65,6 +112,7 @@ const Event = ({ id, slug, title, desc, leader, avatar, start, end, cal, tags }) ))} )} + )} {leader} + {interestCount > 0 && ( + + {interestCount} + + )} - {now(start, end) && ( + {now(start, end) && !cancelled && ( ( { +const _getEvents = async () => { const events = await fetch('https://api2.hackclub.com/v0.1/Sessions/Events') .then(r => r.json()) .then(events => @@ -32,39 +32,75 @@ import { orderBy } from 'lodash' return events } -export const getEvents = async (page = 1) => { +export const getEvents = async () => { const slugger = new GHSlugger() - const isabelleEventsTableRead = "https://isabelle.hackclub.com/events/" - const events = await fetch(isabelleEventsTableRead+`?__order=-StartTime&__page=${page}`) - .then (r => r.json()) - .then(events => events.rows - ) - .then(events => - events.map((e) => ({ - id: e.id, - slug: slugger.slug(e['Title']), - title: e['Title'], - desc: e['Description'], - leader: e['Leader'], - cal: e['CalendarLink'], - start: e['StartTime']+"Z", - end: e['EndTime']+"Z", - youtube: e['YouTubeURL'] || null, - ama: e['AMA'] || false, - //TODO - amaForm: false, - amaId: '', - //EOT - amaAvatar: e['AMAAvatar'] - ? e['AMAAvatar'] - : '', - avatar: e['Avatar'], - approved: e['Approved'] || false, - tags: e['Tags'] || [] - })) - ) - .then(events => events.filter(e => e.approved)) - .then(events => orderBy(events,'start')) - return events - } + // FOR TESTING + const mockEvents = [ + { + id: '1', + slug: 'ai-ama', + title: 'AI AMA with Elon', + desc: 'Ask anything about AI!', + leader: 'Elon Musk', + cal: 'https://calendar.google.com/', + start: '2025-12-15T18:00:00Z', + end: '2025-12-15T20:00:00Z', + youtube: null, + ama: true, + amaForm: false, + amaId: '', + amaAvatar: '', + avatar: 'https://github.com/elonmusk.png', + approved: true, + tags: ['ama', 'ai'], + cancelled: false, + rawCancellation: null, + interestCount: 128 + }, + { + id: '2', + slug: 'cancelled-workshop', + title: 'React Workshop', + desc: 'Learn React basics', + leader: 'Dan Abramov', + cal: 'https://calendar.google.com/', + start: '2025-12-20T16:00:00Z', + end: '2025-12-20T18:00:00Z', + youtube: null, + ama: false, + amaForm: false, + amaId: '', + amaAvatar: '', + avatar: 'https://github.com/gaearon.png', + approved: true, + tags: ['workshop', 'react'], + cancelled: true, + rawCancellation: 'Cancelled due to speaker illness. We will reschedule!', + interestCount: 42 + }, + { + id: '3', + slug: 'regular-event', + title: 'Weekly Show & Tell', + desc: 'Show what you built this week!', + leader: 'Zach Latta', + cal: 'https://calendar.google.com/', + start: '2026-12-18T19:00:00Z', + end: '2026-12-19T20:30:00Z', + youtube: null, + ama: false, + amaForm: false, + amaId: '', + amaAvatar: '', + avatar: 'https://github.com/zachlatta.png', + approved: true, + tags: ['show-and-tell'], + cancelled: false, + rawCancellation: null, + interestCount: 0 + } + ] + + return orderBy(mockEvents, 'start') +} diff --git a/pages/[slug].js b/pages/[slug].js index 9b78644..f5cac35 100644 --- a/pages/[slug].js +++ b/pages/[slug].js @@ -11,7 +11,14 @@ import { Spinner, Text } from 'theme-ui' -import { Calendar, Download, Youtube } from 'react-feather' +import { + Calendar, + Download, + Youtube, + XCircle, + Users, + AlertTriangle +} from 'react-feather' import { useRouter } from 'next/router' import Head from 'next/head' import Meta from '@hackclub/meta' @@ -39,7 +46,11 @@ const makeICS = event => { const location = calUrl?.searchParams.get('location') || '' const escICS = str => - str.replace(/\\/g, '\\\\').replace(/;/g, '\\;').replace(/,/g, '\\,').replace(/\n/g, '\\n') + str + .replace(/\\/g, '\\\\') + .replace(/;/g, '\\;') + .replace(/,/g, '\\,') + .replace(/\n/g, '\\n') const lines = [ 'BEGIN:VCALENDAR', @@ -74,6 +85,15 @@ const Page = ({ event }) => ( event.avatar }`} /> + {event.cancelled && ( + + + + + + )} @@ -183,12 +203,7 @@ const Page = ({ event }) => ( {!past(event.start) && ( - From 28e0ed69e107a89f11783d3fe939c2b5dce3dfcd Mon Sep 17 00:00:00 2001 From: huncijr Date: Fri, 19 Jun 2026 14:48:35 +0300 Subject: [PATCH 2/8] added cancelled ui on the cancelled-workshop path --- components/event.js | 4 ++-- pages/[slug].js | 57 +++++++++++++++++++++++++++++++++++++++++---- 2 files changed, 54 insertions(+), 7 deletions(-) diff --git a/components/event.js b/components/event.js index 1f25504..d4c26bf 100644 --- a/components/event.js +++ b/components/event.js @@ -104,7 +104,7 @@ const Event = ({ )} {tags?.length > 0 && ( - + {tags.map(tag => ( {tag.replace('-', ' ')} @@ -112,7 +112,7 @@ const Event = ({ ))} )} - + ( {event.cancelled && ( - + + + + This event has been cancelled + + + {event.rawCancellation && ( + + {event.rawCancellation} + + )} )} @@ -173,8 +185,43 @@ const Page = ({ event }) => ( {tt('{h}:{mm} {a}').render(new Date(event.start))}– {tt('{h}:{mm} {a}').render(new Date(event.end))} + {event.interestCount > 0 && ( + + + + {event.interestCount}{' '} + {event.interestCount === 1 ? 'person is' : 'people are'}{' '} + interested + + + )} - {event.tags?.length > 0 && ( + {event.cancelled && ( + + + CANCELLED + + + )} + {event.tags?.length > 0 && !event.cancelled && ( {event.tags.map(tag => ( ( - {!past(event.start) && ( + {!past(event.start) && !event.cancelled && ( )} - {/* !event.ama && */} {(event.youtube || event.ama) && ( From e65761171c14808096e566c5812b1375e9041183 Mon Sep 17 00:00:00 2001 From: huncijr Date: Wed, 15 Jul 2026 19:46:51 +0300 Subject: [PATCH 7/8] resolve merge conflicts: keep main auth + light mode, add cancelled/interest fields --- components/nav.js | 46 ++++++++++++++++++++++++++++++++++++++++++++-- lib/data.js | 36 ++++++++++++++++++++++++++++++++---- 2 files changed, 76 insertions(+), 6 deletions(-) diff --git a/components/nav.js b/components/nav.js index 0623f9a..e966893 100644 --- a/components/nav.js +++ b/components/nav.js @@ -1,5 +1,13 @@ import { ArrowLeft, Moon, Sun, GitHub } from 'react-feather' -import { Box, Container, IconButton, Image, Link as A } from 'theme-ui' +import { + Box, + Container, + IconButton, + Image, + Link as A, + Avatar, + Flex +} from 'theme-ui' import { useColorMode } from 'theme-ui' import { useRouter } from 'next/router' import Link from 'next/link' @@ -57,7 +65,6 @@ const ColorSwitcher = props => { useEffect(() => { setMounted(true) }, []) - //Added light mode return ( { export default () => { const [mode] = useColorMode() const router = useRouter() + const [session, setSession] = useState(null) + useEffect(() => { + fetch('/api/auth/me/') + .then(r => r.json()) + .then(data => setSession(data)) + .catch(() => setSession({ slackId: null })) + }, []) + const home = router.pathname === '/' return ( { + {session?.slackId ? ( + + + Sign Out + + + + ) : session != null ? ( + + Log in + + ) : null} ) diff --git a/lib/data.js b/lib/data.js index 004ccbb..62ded7c 100644 --- a/lib/data.js +++ b/lib/data.js @@ -1,6 +1,33 @@ import GHSlugger from 'github-slugger' import { orderBy } from 'lodash' +const fetchJson = async (url, retries = 3) => { + for (let attempt = 0; ; attempt += 1) { + let response + + try { + response = await fetch(url) + } catch (error) { + if (attempt === retries) throw error + await new Promise(resolve => setTimeout(resolve, 1000 * 2 ** attempt)) + continue + } + + if (response.ok) return response.json() + + const body = await response.text() + const retryable = [429, 500, 502, 503, 504].includes(response.status) + + if (!retryable || attempt === retries) { + throw new Error( + `Failed to fetch events (${response.status} ${response.statusText}): ${body.slice(0, 200)}` + ) + } + + await new Promise(resolve => setTimeout(resolve, 1000 * 2 ** attempt)) + } +} + const _getEvents = async () => { const events = await fetch('https://api2.hackclub.com/v0.1/Sessions/Events') .then(r => r.json()) @@ -34,11 +61,10 @@ const _getEvents = async () => { export const getEvents = async (page = 1) => { const slugger = new GHSlugger() - const isabelleEventsTableRead = 'https://isabelle.hackclub.com/events/' - const events = await fetch( + const isabelleEventsTableRead = `${process.env.ISABELLE_BASE_URL}/events/` + const events = await fetchJson( isabelleEventsTableRead + `?__order=-StartTime&__page=${page}` ) - .then(r => r.json()) .then(events => events.rows) .then(events => events.map(e => ({ @@ -63,7 +89,9 @@ export const getEvents = async (page = 1) => { cancelled: e['Cancelled'] || false, rawCancellation: e['RawCancellation'] || null, interestCount: - typeof e['InterestCount'] === 'number' ? e['InterestCount'] : 0 + typeof e['InterestCount'] === 'number' ? e['InterestCount'] : 0, + leaderSlackId: e['LeaderSlackID'], + rsvpFormUrl: e['RSVPFormURL'] || null })) ) .then(events => events.filter(e => e.approved)) From cb7bb5b7fb6db47872cb2fff66bbe7be361aa60a Mon Sep 17 00:00:00 2001 From: huncijr Date: Fri, 17 Jul 2026 09:30:10 +0300 Subject: [PATCH 8/8] fixed syntax errors --- components/nav.js | 26 ++------------------------ 1 file changed, 2 insertions(+), 24 deletions(-) diff --git a/components/nav.js b/components/nav.js index cc9ea24..dc2b9a8 100644 --- a/components/nav.js +++ b/components/nav.js @@ -94,13 +94,6 @@ export default () => { .then(r => r.json()) .then(data => setSession(data)) .catch(() => setSession({ slackId: null })) - - const [session, setSession] = useState(null) - useEffect(() => { - fetch('/api/auth/me/') - .then(r => r.json()) - .then(data => setSession(data)) - .catch(() => setSession({ slackId: null })) }, []) const home = router.pathname === '/' @@ -137,7 +130,6 @@ export default () => { {session?.slackId ? ( - { src={`https://cachet.dunkirk.sh/users/${session.slackId}/r`} alt="Your Slack Avatar" size={28} - sx={{ hieght: 28, width: 28, borderRadius: 'circle' }} + sx={{ height: 28, width: 28, borderRadius: 'circle' }} /> ) : session != null ? ( Log in ) : null} - sx={{hieght:28, width:28, borderRadius:'circle'}} - /> - - ): session != null ? ( - - Log in - - ): null} )