Skip to content

Commit f1b9466

Browse files
committed
feat: add Hackathon section
1 parent b24e8ae commit f1b9466

File tree

11 files changed

+192
-27
lines changed

11 files changed

+192
-27
lines changed
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
import { CommunityConference } from "@/lib/types"
2+
3+
import { Image } from "@/components/Image"
4+
import CardImage from "@/components/Image/CardImage"
5+
import {
6+
Card,
7+
CardBanner,
8+
CardContent,
9+
CardHighlight,
10+
CardSubTitle,
11+
CardTitle,
12+
} from "@/components/ui/card"
13+
14+
import EventFallback from "@/public/images/events/event-placeholder.png"
15+
16+
type HackathonCardProps = {
17+
event: CommunityConference
18+
className?: string
19+
eventCategory: string
20+
}
21+
22+
const HackathonCard = ({
23+
event,
24+
className,
25+
eventCategory = "Developers",
26+
}: HackathonCardProps) => {
27+
const { title, href, description, imageUrl, formattedDate, location } = event
28+
return (
29+
<Card
30+
href={href}
31+
key={title + description}
32+
customEventOptions={{
33+
eventCategory,
34+
eventAction: "hackathons",
35+
eventName: title,
36+
}}
37+
className={className}
38+
>
39+
<CardBanner className="h-36 w-full sm:w-[270px] 2xl:w-full">
40+
{imageUrl ? (
41+
<CardImage src={imageUrl} />
42+
) : (
43+
<Image src={EventFallback} alt="" sizes="276px" />
44+
)}
45+
</CardBanner>
46+
<CardContent>
47+
<CardTitle>{title}</CardTitle>
48+
<CardSubTitle>{formattedDate} </CardSubTitle>
49+
<CardHighlight>{location}</CardHighlight>
50+
</CardContent>
51+
</Card>
52+
)
53+
}
54+
55+
export default HackathonCard
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
"use client"
2+
3+
import type { CommunityConference } from "@/lib/types"
4+
5+
import { Swiper, SwiperSlide } from "@/components/ui/swiper"
6+
7+
import HackathonCard from "../HackathonCard"
8+
9+
type HackathonSwiperProps = {
10+
events: CommunityConference[]
11+
eventCategory: string
12+
}
13+
14+
const HackathonSwiper = ({ events, eventCategory }: HackathonSwiperProps) => (
15+
<Swiper spaceBetween={16} slidesPerView={1.25}>
16+
{events.map((event, idx) => (
17+
<SwiperSlide key={idx} className="max-2xl:first:ms-8 max-2xl:last:pe-16">
18+
<HackathonCard event={event} eventCategory={eventCategory} />
19+
</SwiperSlide>
20+
))}
21+
</Swiper>
22+
)
23+
24+
export default HackathonSwiper
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import dynamic from "next/dynamic"
2+
3+
import Loading from "./loading"
4+
5+
export default dynamic(() => import("."), { ssr: false, loading: Loading })
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import { SkeletonCardGrid } from "@/components/ui/skeleton"
2+
3+
const Loading = () => <SkeletonCardGrid />
4+
5+
export default Loading

app/[locale]/developers/page.tsx

Lines changed: 27 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,18 +14,19 @@ import { VStack } from "@/components/ui/flex"
1414
import Link from "@/components/ui/Link"
1515
import InlineLink from "@/components/ui/Link"
1616
import { Section } from "@/components/ui/section"
17-
import { SkeletonCardGrid } from "@/components/ui/skeleton"
1817

1918
import { cn } from "@/lib/utils/cn"
2019
import { getMetadata } from "@/lib/utils/metadata"
2120
import { screens } from "@/lib/utils/screen"
2221

2322
import BuilderCard from "./_components/BuilderCard"
2423
import BuilderSwiper from "./_components/BuilderSwiper/lazy"
24+
import HackathonCard from "./_components/HackathonCard"
25+
import HackathonSwiper from "./_components/HackathonSwiper/lazy"
2526
import SpeedRunCard from "./_components/SpeedRunCard"
2627
import VideoCourseCard from "./_components/VideoCourseCard"
2728
import VideoCourseSwiper from "./_components/VideoCourseSwiper/lazy"
28-
import { getBuilderPaths, getVideoCourses } from "./utils"
29+
import { getBuilderPaths, getHackathons, getVideoCourses } from "./utils"
2930

3031
import resourcesBanner from "@/public/images/developers/resources-banner.png"
3132
import scaffoldDebugScreenshot from "@/public/images/developers/scaffold-debug-screenshot.png"
@@ -84,6 +85,10 @@ const DevelopersPage = async ({
8485

8586
const courses = await getVideoCourses()
8687

88+
const hackathons = (await getHackathons()).slice(0, 5)
89+
90+
const eventCategory = `Developers - ${locale}`
91+
8792
return (
8893
<VStack className="mx-auto my-0 w-full">
8994
<HubHero
@@ -407,7 +412,26 @@ const DevelopersPage = async ({
407412
<Section id="hackathons" className="space-y-4 py-10 md:py-12">
408413
<h2>{t("page-developers-hackathons-title")}</h2>
409414
<p>{t("page-developers-hackathons-desc")}</p>
410-
<SkeletonCardGrid />
415+
416+
{/* DESKTOP */}
417+
<Scroller>
418+
{hackathons.map((event, idx) => (
419+
<HackathonCard
420+
key={idx}
421+
event={event}
422+
eventCategory={eventCategory}
423+
className="flex-1"
424+
/>
425+
))}
426+
</Scroller>
427+
{/* MOBILE */}
428+
<div className="-mx-8 sm:hidden">
429+
<HackathonSwiper
430+
events={hackathons}
431+
eventCategory={eventCategory}
432+
/>
433+
</div>
434+
411435
<div className="flex justify-center">
412436
<ButtonLink href="https://ethglobal.com/">
413437
{t("page-developers-visit-ethglobal")}

app/[locale]/developers/utils.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
11
import { getLocale, getTranslations } from "next-intl/server"
22

3+
import { CommunityConference } from "@/lib/types"
4+
5+
import events from "@/data/community-events.json"
6+
7+
import { getUpcomingEvents } from "../utils"
8+
39
import type { DevelopersPath, VideoCourse } from "./types"
410

511
import cyfrinBasicBanner from "@/public/images/developers/cyfrin-basic-banner.webp"
@@ -99,3 +105,9 @@ export const getVideoCourses = async (): Promise<VideoCourse[]> => {
99105
},
100106
]
101107
}
108+
109+
export const getHackathons = async (): Promise<CommunityConference[]> => {
110+
const locale = await getLocale()
111+
const allUpcomingEvents = getUpcomingEvents(events, locale)
112+
return allUpcomingEvents.filter((e) => e.hackathon) as CommunityConference[]
113+
}

app/[locale]/page.tsx

Lines changed: 5 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@ import type {
88
CommunityBlog,
99
ValuesPairing,
1010
} from "@/lib/types"
11-
import type { EventCardProps } from "@/lib/types"
1211
import type { Lang } from "@/lib/types"
1312
import { CodeExample } from "@/lib/interfaces"
1413

@@ -82,7 +81,7 @@ import {
8281
} from "@/lib/constants"
8382

8483
import TenYearHomeBanner from "./10years/_components/TenYearHomeBanner"
85-
import { getActivity } from "./utils"
84+
import { getActivity, getUpcomingEvents } from "./utils"
8685

8786
import SimpleDomainRegistryContent from "!!raw-loader!@/data/SimpleDomainRegistry.sol"
8887
import SimpleTokenContent from "!!raw-loader!@/data/SimpleToken.sol"
@@ -402,18 +401,8 @@ const Page = async ({ params }: { params: Promise<{ locale: Lang }> }) => {
402401
},
403402
]
404403

405-
const upcomingEvents = events
406-
.filter((event) => {
407-
const isValid = isValidDate(event.endDate)
408-
const beginningOfEndDate = new Date(event.endDate).getTime()
409-
const endOfEndDate = beginningOfEndDate + 24 * 60 * 60 * 1000
410-
const isUpcoming = endOfEndDate >= new Date().getTime()
411-
return isValid && isUpcoming
412-
})
413-
.sort(
414-
(a, b) => new Date(a.endDate).getTime() - new Date(b.endDate).getTime()
415-
)
416-
.slice(0, 3) as EventCardProps[] // Show 3 events ending soonest
404+
const allUpcomingEvents = getUpcomingEvents(events, locale)
405+
const upcomingEvents = allUpcomingEvents.slice(0, 3)
417406

418407
const metricResults: AllHomepageActivityData = {
419408
ethPrice,
@@ -906,9 +895,9 @@ const Page = async ({ params }: { params: Promise<{ locale: Lang }> }) => {
906895
className="max-w-full object-cover object-center"
907896
/>
908897
) : (
909-
<Image src={EventFallback} alt="" />
898+
<Image src={EventFallback} alt="" sizes="276px" />
910899
)}
911-
<Image src={EventFallback} alt="" />
900+
<Image src={EventFallback} alt="" sizes="276px" />
912901
</CardBanner>
913902
<CardContent>
914903
<CardTitle>{title}</CardTitle>

app/[locale]/utils.ts

Lines changed: 44 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,18 @@
55

66
import { getTranslations } from "next-intl/server"
77

8-
import type { AllHomepageActivityData, Lang, StatsBoxMetric } from "@/lib/types"
8+
import type {
9+
AllHomepageActivityData,
10+
CommunityConference,
11+
Lang,
12+
StatsBoxMetric,
13+
} from "@/lib/types"
914

15+
import { isValidDate } from "@/lib/utils/date"
1016
import { getLocaleForNumberFormat } from "@/lib/utils/translations"
1117

18+
import { DEFAULT_LOCALE } from "@/lib/constants"
19+
1220
const formatLargeUSD = (value: number, locale: string): string => {
1321
return new Intl.NumberFormat(locale, {
1422
style: "currency",
@@ -124,3 +132,38 @@ export const getActivity = async (
124132

125133
return metrics
126134
}
135+
136+
export const getUpcomingEvents = (
137+
events: CommunityConference[],
138+
locale = DEFAULT_LOCALE
139+
): CommunityConference[] =>
140+
events
141+
.filter((event) => {
142+
const isValid = isValidDate(event.endDate)
143+
const beginningOfEndDate = new Date(event.endDate).getTime()
144+
const endOfEndDate = beginningOfEndDate + 24 * 60 * 60 * 1000
145+
const isUpcoming = endOfEndDate >= new Date().getTime()
146+
return isValid && isUpcoming
147+
})
148+
.sort(
149+
(a, b) => new Date(a.endDate).getTime() - new Date(b.endDate).getTime()
150+
)
151+
.map(({ startDate, endDate, ...event }) => {
152+
const formattedDate =
153+
isValidDate(startDate) || isValidDate(endDate)
154+
? new Intl.DateTimeFormat(locale, {
155+
month: "long",
156+
day: "numeric",
157+
year: "numeric",
158+
}).formatRange(
159+
new Date(isValidDate(startDate) ? startDate : endDate),
160+
new Date(isValidDate(endDate) ? endDate : startDate)
161+
)
162+
: ""
163+
return {
164+
...event,
165+
startDate,
166+
endDate,
167+
formattedDate,
168+
}
169+
})

src/data/community-events.json

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,8 @@
1515
"href": "https://ethglobal.com/events/pragma-newyork2025",
1616
"location": "New York, NYC, USA",
1717
"description": "Bringing developers onchain to build the future of the internet.",
18-
"imageUrl": "https://ethglobal.com/og.png"
18+
"imageUrl": "https://ethglobal.com/og.png",
19+
"hackathon": true
1920
},
2021
{
2122
"title": "SBC (The Science of Blockchain Conference 2025)",
@@ -33,7 +34,8 @@
3334
"href": "https://ethglobal.com/events/newyork2025",
3435
"location": "New York, NYC, USA",
3536
"description": "Bringing developers onchain to build the future of the internet.",
36-
"imageUrl": "https://ethglobal.com/og.png"
37+
"imageUrl": "https://ethglobal.com/og.png",
38+
"hackathon": true
3739
},
3840
{
3941
"title": "ProdFest Jos",
@@ -87,7 +89,8 @@
8789
"href": "https://ethglobal.com/events/pragma-newdelhi",
8890
"location": "New Delhi, IND",
8991
"description": "Bringing developers onchain to build the future of the internet.",
90-
"imageUrl": "https://ethglobal.com/og.png"
92+
"imageUrl": "https://ethglobal.com/og.png",
93+
"hackathon": true
9194
},
9295
{
9396
"title": "ETHGlobal New Delhi",
@@ -96,7 +99,8 @@
9699
"href": "https://ethglobal.com/events/newdelhi",
97100
"location": "New Delhi, IND",
98101
"description": "Bringing developers onchain to build the future of the internet.",
99-
"imageUrl": "https://ethglobal.com/og.png"
102+
"imageUrl": "https://ethglobal.com/og.png",
103+
"hackathon": true
100104
},
101105
{
102106
"title": "EthAccra",
@@ -123,7 +127,8 @@
123127
"href": "https://ethsafari.xyz/",
124128
"location": "Nairobi, Kenya",
125129
"description": "ETHSafari 2025 - web3 builders conference East Africa Kenya",
126-
"imageUrl": "https://ethsafari.xyz/_next/image?url=%2FHackathon.png&w=640&q=75"
130+
"imageUrl": "https://ethsafari.xyz/_next/image?url=%2FHackathon.png&w=640&q=75",
131+
"hackathon": true
127132
},
128133
{
129134
"title": "ETH Boston",
@@ -168,7 +173,8 @@
168173
"href": "https://ethistanbul.io/",
169174
"location": "Istanbul, TR",
170175
"description": "ETHIstanbul is a conference and hackathon connecting you with global talents, industry professionals, and web3 companies advancing technology.",
171-
"imageUrl": "https://ethistanbul.io/_next/image?url=%2F_next%2Fstatic%2Fmedia%2Fistanbul-background.99d6ad2a.webp&w=3840&q=75"
176+
"imageUrl": "https://ethistanbul.io/_next/image?url=%2F_next%2Fstatic%2Fmedia%2Fistanbul-background.99d6ad2a.webp&w=3840&q=75",
177+
"hackathon": true
172178
},
173179
{
174180
"title": "ETHSofia",

src/intl/en/page-developers-index.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,7 @@
9090
"page-developers-video-courses-title": "Video courses",
9191
"page-developers-video-courses-desc": "Want to kickstart your professional career in blockchain? These courses will prepare you to get hired as blockchain developer.",
9292
"page-developers-docs-section-desc": "Understand the core concepts of Ethereum and blockchains",
93-
"page-developers-hackathons-title": "Join hackathons (TODO)",
93+
"page-developers-hackathons-title": "Join hackathons",
9494
"page-developers-hackathons-desc": "Hackathons are great opportunities to network and learn from others as well as start projects and earn prizes",
9595
"page-developers-visit-ethglobal": "Visit EthGlobal",
9696
"page-developers-founders-title": "Are you a founder?",

0 commit comments

Comments
 (0)