Skip to content

Commit 953df7e

Browse files
committed
Add YSWS API and events page
1 parent dabecbe commit 953df7e

2 files changed

Lines changed: 342 additions & 0 deletions

File tree

pages/api/ysws.js

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
export default async function handler(req, res) {
2+
try {
3+
const response = await fetch('https://ysws.hackclub.com/api.json')
4+
const data = await response.json()
5+
res.setHeader('Cache-Control', 's-maxage=3600, stale-while-revalidate')
6+
res.status(200).json(data)
7+
} catch (err) {
8+
res.status(500).json({ error: 'Failed to fetch YSWS data' })
9+
}
10+
}

pages/events.js

Lines changed: 332 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,332 @@
1+
/** @jsxImportSource theme-ui */
2+
import Meta from '@hackclub/meta'
3+
import Head from 'next/head'
4+
import { Box, Heading, Text, Link as ThemeLink, Input } from 'theme-ui'
5+
import { useState, useMemo, useEffect } from 'react'
6+
import Footer from '../components/footer'
7+
import ForceTheme from '../components/force-theme'
8+
import Nav from '../components/nav'
9+
10+
const StatusBadge = ({ status, deadline }) => {
11+
const isEndingSoon =
12+
status === 'active' &&
13+
deadline &&
14+
new Date(deadline) - new Date() < 7 * 24 * 60 * 60 * 1000
15+
16+
const label = isEndingSoon ? 'Ending Soon' : status
17+
const colors = {
18+
active: { bg: '#d4f5e0', color: '#1a7f3c' },
19+
'ending soon': { bg: '#fff3cd', color: '#856404' },
20+
draft: { bg: '#e2e8f0', color: '#475569' },
21+
ended: { bg: '#fee2e2', color: '#991b1b' }
22+
}
23+
const style = colors[label.toLowerCase()] || colors['draft']
24+
25+
return (
26+
<Box
27+
sx={{
28+
display: 'inline-block',
29+
px: 2,
30+
py: '2px',
31+
borderRadius: '999px',
32+
fontSize: '0.7rem',
33+
fontWeight: 700,
34+
textTransform: 'uppercase',
35+
letterSpacing: '0.05em',
36+
bg: style.bg,
37+
color: style.color
38+
}}
39+
>
40+
{label}
41+
</Box>
42+
)
43+
}
44+
45+
const EventCard = ({ program }) => (
46+
<Box
47+
as="a"
48+
href={program.website || program.slack || 'https://ysws.hackclub.com'}
49+
target="_blank"
50+
rel="noopener noreferrer"
51+
sx={{
52+
display: 'flex',
53+
flexDirection: 'column',
54+
gap: 2,
55+
p: 3,
56+
bg: 'white',
57+
borderRadius: '12px',
58+
border: '1px solid',
59+
borderColor: 'smoke',
60+
borderTop: '4px solid',
61+
borderTopColor: 'primary',
62+
textDecoration: 'none',
63+
transition: 'all 0.2s ease-in-out',
64+
cursor: 'pointer',
65+
'&:hover': {
66+
transform: 'translateY(-4px)',
67+
boxShadow: '0 8px 24px rgba(236, 55, 80, 0.15)',
68+
borderTopColor: 'orange'
69+
}
70+
}}
71+
>
72+
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start', gap: 2 }}>
73+
<Text sx={{ fontWeight: 800, fontSize: '1rem', color: 'primary', lineHeight: 1.3 }}>
74+
{program.name}
75+
</Text>
76+
<StatusBadge status={program.status} deadline={program.deadline} />
77+
</Box>
78+
<Text sx={{ fontSize: '0.85rem', color: 'slate', lineHeight: 1.5, flexGrow: 1 }}>
79+
{program.description}
80+
</Text>
81+
{program.slackChannel && (
82+
<Text sx={{ fontSize: '0.75rem', color: 'muted', fontWeight: 600, fontFamily: 'monospace' }}>
83+
{program.slackChannel}
84+
</Text>
85+
)}
86+
{program.deadline && program.status === 'active' && (
87+
<Text sx={{ fontSize: '0.72rem', color: 'muted', fontWeight: 500 }}>
88+
Ends {new Date(program.deadline).toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' })}
89+
</Text>
90+
)}
91+
</Box>
92+
)
93+
94+
const FilterButton = ({ active, onClick, children }) => (
95+
<Box
96+
as="button"
97+
type="button"
98+
onClick={onClick}
99+
aria-pressed={active}
100+
sx={{
101+
px: 4,
102+
py: 2,
103+
borderRadius: '999px',
104+
border: '2px solid',
105+
borderColor: active ? 'primary' : 'smoke',
106+
bg: active ? 'primary' : 'white',
107+
color: active ? 'white' : 'slate',
108+
fontWeight: 700,
109+
fontSize: '0.9rem',
110+
cursor: 'pointer',
111+
fontFamily: 'inherit',
112+
transition: 'all 0.15s ease',
113+
'&:hover': {
114+
borderColor: 'primary',
115+
color: active ? 'white' : 'primary'
116+
}
117+
}}
118+
>
119+
{children}
120+
</Box>
121+
)
122+
123+
const EventsPage = () => {
124+
const [search, setSearch] = useState('')
125+
const [filter, setFilter] = useState('active')
126+
const [programs, setPrograms] = useState([])
127+
const [loading, setLoading] = useState(true)
128+
const [error, setError] = useState(false)
129+
130+
useEffect(() => {
131+
fetch('/api/ysws')
132+
.then((r) => r.json())
133+
.then((data) => {
134+
const all = [
135+
...(data.limitedTime || []),
136+
...(data.indefinite || []),
137+
...(data.recentlyEnded || []),
138+
...(data.drafts || [])
139+
]
140+
setPrograms(all)
141+
setLoading(false)
142+
})
143+
.catch(() => {
144+
setError(true)
145+
setLoading(false)
146+
})
147+
}, [])
148+
149+
const counts = useMemo(() => ({
150+
active: programs.filter((p) => p.status === 'active').length,
151+
ended: programs.filter((p) => p.status === 'ended').length,
152+
draft: programs.filter((p) => p.status === 'draft').length,
153+
all: programs.length
154+
}), [programs])
155+
156+
const filtered = useMemo(() => {
157+
return programs.filter((p) => {
158+
const matchesSearch =
159+
p.name?.toLowerCase().includes(search.toLowerCase()) ||
160+
p.description?.toLowerCase().includes(search.toLowerCase()) ||
161+
p.slackChannel?.toLowerCase().includes(search.toLowerCase())
162+
const matchesFilter = filter === 'all' || p.status === filter
163+
return matchesSearch && matchesFilter
164+
})
165+
}, [programs, search, filter])
166+
167+
return (
168+
<Box
169+
sx={{
170+
backgroundImage: 'url(/pattern.svg)',
171+
backgroundRepeat: 'repeat',
172+
backgroundAttachment: 'fixed',
173+
minHeight: '100vh',
174+
backgroundColor: 'snow'
175+
}}
176+
>
177+
<Meta
178+
as={Head}
179+
name="YSWS Programs – Hack Club Slack"
180+
description="Browse all Hack Club You Ship We Ship programs. Build something amazing and get rewarded!"
181+
/>
182+
<ForceTheme theme="light" />
183+
<Nav />
184+
185+
{/* Hero */}
186+
<Box
187+
sx={{
188+
backgroundImage: (t) => t.util.gx('orange', 'red'),
189+
pt: ['7rem', '8rem'],
190+
pb: ['3rem', '4rem'],
191+
px: ['1.5rem', '3rem'],
192+
textAlign: 'center'
193+
}}
194+
>
195+
<Heading
196+
as="h1"
197+
sx={{
198+
fontSize: ['2.5rem', '4rem'],
199+
color: 'white',
200+
fontWeight: 800,
201+
mb: 2,
202+
textShadow: '0 2px 12px rgba(0,0,0,0.2)'
203+
}}
204+
>
205+
You Ship, We Ship
206+
</Heading>
207+
<Text sx={{ color: 'white', fontSize: ['1rem', '1.25rem'], opacity: 0.9, mb: 3 }}>
208+
Build something awesome. Hack Club ships you something epic in return.
209+
</Text>
210+
{!loading && !error && (
211+
<Text sx={{ color: 'white', fontSize: '0.95rem', opacity: 0.75 }}>
212+
{counts.active} active program{counts.active !== 1 ? 's' : ''} · {counts.all} total
213+
</Text>
214+
)}
215+
</Box>
216+
217+
{/* Content */}
218+
<Box sx={{ maxWidth: '1200px', mx: 'auto', px: ['1.5rem', '3rem'], py: ['1.5rem', '2rem'] }}>
219+
220+
{/* Search + Filter */}
221+
<Box
222+
sx={{
223+
bg: 'white',
224+
borderRadius: '16px',
225+
p: ['1.25rem', '1.75rem'],
226+
boxShadow: 'card',
227+
border: '1px solid',
228+
borderColor: 'smoke',
229+
mb: ['1.5rem', '2rem']
230+
}}
231+
>
232+
<Input
233+
placeholder="Search programs..."
234+
value={search}
235+
onChange={(e) => setSearch(e.target.value)}
236+
sx={{
237+
mb: 3,
238+
fontSize: '1rem',
239+
borderRadius: '8px',
240+
border: '2px solid',
241+
borderColor: 'smoke',
242+
px: 3,
243+
py: 2,
244+
fontFamily: 'inherit',
245+
'&:focus': { borderColor: 'primary', outline: 'none' }
246+
}}
247+
/>
248+
<Box sx={{ display: 'flex', gap: 2, flexWrap: 'wrap' }}>
249+
<FilterButton active={filter === 'active'} onClick={() => setFilter('active')}>
250+
Active ({counts.active})
251+
</FilterButton>
252+
<FilterButton active={filter === 'all'} onClick={() => setFilter('all')}>
253+
All ({counts.all})
254+
</FilterButton>
255+
<FilterButton active={filter === 'ended'} onClick={() => setFilter('ended')}>
256+
Ended ({counts.ended})
257+
</FilterButton>
258+
<FilterButton active={filter === 'draft'} onClick={() => setFilter('draft')}>
259+
Draft ({counts.draft})
260+
</FilterButton>
261+
</Box>
262+
</Box>
263+
264+
{loading && (
265+
<Box sx={{ textAlign: 'center', py: '4rem', color: 'muted' }}>
266+
<Text sx={{ fontSize: '1rem', fontWeight: 600 }}>Loading programs...</Text>
267+
</Box>
268+
)}
269+
270+
{error && (
271+
<Box sx={{ textAlign: 'center', py: '4rem', color: 'muted' }}>
272+
<Text sx={{ fontSize: '1rem', fontWeight: 600 }}>
273+
Couldn&apos;t load programs. Try again later.
274+
</Text>
275+
</Box>
276+
)}
277+
278+
{!loading && !error && (
279+
<>
280+
<Text sx={{ color: 'muted', fontSize: '0.9rem', mb: 3, fontWeight: 500 }}>
281+
{filtered.length === 0
282+
? 'No programs found'
283+
: `Showing ${filtered.length} program${filtered.length !== 1 ? 's' : ''}`}
284+
</Text>
285+
286+
{filtered.length > 0 ? (
287+
<Box
288+
sx={{
289+
display: 'grid',
290+
gridTemplateColumns: ['1fr', 'repeat(2, 1fr)', 'repeat(3, 1fr)'],
291+
gap: ['0.75rem', '1rem']
292+
}}
293+
>
294+
{filtered.map((program, i) => (
295+
<EventCard key={program.name + i} program={program} />
296+
))}
297+
</Box>
298+
) : (
299+
<Box sx={{ textAlign: 'center', py: '4rem', color: 'muted' }}>
300+
<Text sx={{ fontSize: '1.15rem', fontWeight: 600 }}>
301+
No programs match &quot;{search}&quot;
302+
</Text>
303+
<Text sx={{ fontSize: '0.95rem', mt: 1 }}>Try a different search term</Text>
304+
</Box>
305+
)}
306+
</>
307+
)}
308+
309+
<Box sx={{ textAlign: 'center', mt: ['3rem', '4rem'], mb: '2rem', display: 'flex', gap: 4, justifyContent: 'center', flexWrap: 'wrap' }}>
310+
<ThemeLink
311+
href="https://ysws.hackclub.com"
312+
target="_blank"
313+
rel="noopener noreferrer"
314+
sx={{ color: 'primary', fontWeight: 700, fontSize: '1rem', textDecoration: 'none', '&:hover': { textDecoration: 'underline' } }}
315+
>
316+
View full YSWS catalog →
317+
</ThemeLink>
318+
<ThemeLink
319+
href="/"
320+
sx={{ color: 'primary', fontWeight: 700, fontSize: '1rem', textDecoration: 'none', '&:hover': { textDecoration: 'underline' } }}
321+
>
322+
Back to Hack Club Slack
323+
</ThemeLink>
324+
</Box>
325+
</Box>
326+
327+
<Footer />
328+
</Box>
329+
)
330+
}
331+
332+
export default EventsPage

0 commit comments

Comments
 (0)