Skip to content

Commit 36b4342

Browse files
authored
Merge pull request #15860 from damianmarti/collectibles
Add Ethereum.org Collectibles page
2 parents 2027c4d + 36939c6 commit 36b4342

File tree

28 files changed

+1377
-21
lines changed

28 files changed

+1377
-21
lines changed
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
"use client"
2+
3+
import React from "react"
4+
import { QueryClient, QueryClientProvider } from "@tanstack/react-query"
5+
6+
import WalletProviders from "@/components/WalletProviders"
7+
8+
import type { Badge } from "../../types"
9+
import CollectiblesContent from "../CollectiblesContent/lazy"
10+
11+
export type CollectiblesPageProps = {
12+
badges: Badge[]
13+
}
14+
15+
const queryClient = new QueryClient()
16+
17+
const CollectiblesPage = ({ badges }: CollectiblesPageProps) => (
18+
<QueryClientProvider client={queryClient}>
19+
<WalletProviders>
20+
<CollectiblesContent badges={badges} />
21+
</WalletProviders>
22+
</QueryClientProvider>
23+
)
24+
25+
export default CollectiblesPage
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: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import { Skeleton } from "@/components/ui/skeleton"
2+
3+
const Loading = () => (
4+
<div className="flex w-full flex-col gap-x-8 gap-y-6 xl:flex-row">
5+
<Skeleton className="h-80 w-full rounded-2xl xl:sticky xl:top-28 xl:max-w-xs" />
6+
<Skeleton className="h-[75vh] w-full rounded-2xl" />
7+
</div>
8+
)
9+
10+
export default Loading
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
"use client"
2+
3+
import { ConnectButton } from "@rainbow-me/rainbowkit"
4+
5+
import { Button } from "@/components/ui/buttons/Button"
6+
7+
import { useTranslation } from "@/hooks/useTranslation"
8+
9+
const CollectiblesConnectButton = () => {
10+
const { t } = useTranslation("page-collectibles")
11+
return (
12+
<ConnectButton.Custom>
13+
{({ account, chain, openConnectModal, mounted }) => {
14+
const connected = mounted && account && chain
15+
16+
return (
17+
<>
18+
{(() => {
19+
if (!connected) {
20+
return (
21+
<Button onClick={openConnectModal} variant="outline">
22+
{t("page-collectibles-connect-wallet")}
23+
</Button>
24+
)
25+
}
26+
27+
return <ConnectButton showBalance={false} />
28+
})()}
29+
</>
30+
)
31+
}}
32+
</ConnectButton.Custom>
33+
)
34+
}
35+
36+
export default CollectiblesConnectButton
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 { Skeleton } from "@/components/ui/skeleton"
2+
3+
const Loading = () => <Skeleton className="h-10 w-full" />
4+
5+
export default Loading
Lines changed: 160 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,160 @@
1+
"use client"
2+
3+
import { useMemo } from "react"
4+
import { useIsMounted } from "usehooks-ts"
5+
import { useAccount } from "wagmi"
6+
import { useQuery } from "@tanstack/react-query"
7+
8+
import { Image } from "@/components/Image"
9+
import { Skeleton } from "@/components/ui/skeleton"
10+
11+
import { cn } from "@/lib/utils/cn"
12+
13+
import { COLLECTIBLES_BASE_URL } from "../../constants"
14+
import type { Badge } from "../../types"
15+
import { type CollectiblesPageProps } from "../Collectibles"
16+
import CollectiblesConnectButton from "../CollectiblesConnectButton/lazy"
17+
import CollectiblesCurrentYear from "../CollectiblesCurrentYear"
18+
import CollectiblesPreviousYears from "../CollectiblesPreviousYears"
19+
import CollectiblesProgress from "../CollectiblesProgress/lazy"
20+
21+
import useTranslation from "@/hooks/useTranslation"
22+
import alreadyContributorImg from "@/public/images/10-year-anniversary/adoption-1.png"
23+
24+
export type BadgeWithOwned = Badge & {
25+
owned: boolean
26+
}
27+
28+
const ADDRESS_STATS_API = `${COLLECTIBLES_BASE_URL}/api/stats/`
29+
30+
const CollectiblesContent = ({ badges }: CollectiblesPageProps) => {
31+
const { t } = useTranslation("page-collectibles")
32+
33+
const currentYear = new Date().getFullYear().toString()
34+
35+
const steps = [
36+
{
37+
title: t("page-collectibles-how-step1-title"),
38+
description: t("page-collectibles-how-step1-desc"),
39+
color: "text-accent-a",
40+
},
41+
{
42+
title: t("page-collectibles-how-step2-title"),
43+
description: t("page-collectibles-how-step2-desc"),
44+
color: "text-accent-b",
45+
},
46+
{
47+
title: t("page-collectibles-how-step3-title"),
48+
description: t("page-collectibles-how-step3-desc"),
49+
color: "text-accent-c",
50+
},
51+
]
52+
53+
const isMounted = useIsMounted()
54+
const { address, isConnected } = useAccount()
55+
56+
const {
57+
data: addressBadges = [],
58+
error,
59+
isLoading,
60+
} = useQuery({
61+
queryKey: ["addressBadges", address],
62+
queryFn: async (): Promise<Badge[]> => {
63+
if (!address) return []
64+
const response = await fetch(`${ADDRESS_STATS_API}${address}`)
65+
if (!response.ok) {
66+
throw new Error("Failed to fetch address badges")
67+
}
68+
return response.json()
69+
},
70+
enabled: !!address,
71+
})
72+
73+
const badgesWithOwned = useMemo((): BadgeWithOwned[] => {
74+
return badges.map((badge) => {
75+
const addressBadge = addressBadges.find((b) => b.id === badge.id)
76+
return {
77+
...badge,
78+
owned: addressBadge ? true : false,
79+
}
80+
})
81+
}, [badges, addressBadges])
82+
83+
return (
84+
<div className="flex flex-col gap-8 xl:flex-row">
85+
{/* Already a contributor? section */}
86+
<div className="flex h-fit w-full flex-col gap-y-4 rounded-2xl border border-accent-a/5 bg-gradient-to-b from-accent-a/5 to-accent-a/10 px-6 py-6 xl:sticky xl:top-28 xl:max-w-xs dark:from-accent-a/10 dark:to-accent-a/20">
87+
<Image
88+
src={alreadyContributorImg}
89+
alt={t("page-collectibles-contributor-img-alt")}
90+
className="h-32 w-32 object-cover"
91+
sizes="128px"
92+
/>
93+
<div>
94+
<h3 className="text-lg">{t("page-collectibles-already-title")}</h3>
95+
<p className="text-body-medium">
96+
{t("page-collectibles-already-desc")}
97+
</p>
98+
</div>
99+
100+
<CollectiblesConnectButton />
101+
102+
{isConnected && !isLoading && !error && (
103+
<CollectiblesProgress badges={badgesWithOwned} />
104+
)}
105+
{isLoading && (
106+
<div className="flex w-full flex-col gap-y-4">
107+
<Skeleton className="h-10 w-full rounded-2xl" />
108+
<Skeleton className="h-10 w-full rounded-2xl" />
109+
</div>
110+
)}
111+
{error && (
112+
<div className="text-body-medium text-red-500">
113+
Error fetching address badges
114+
</div>
115+
)}
116+
</div>
117+
118+
{/* How it works section */}
119+
<div className="flex-1 space-y-8">
120+
<section className="mx-auto space-y-6 border-b p-2 pb-6">
121+
<h2 className="text-4xl">{t("page-collectibles-how-title")}</h2>
122+
<div className="flex flex-col justify-center gap-8 py-4 md:flex-row md:items-center md:gap-12">
123+
{steps.map((step, idx) => (
124+
<div
125+
key={step.title}
126+
className="flex w-full flex-1 flex-row items-center gap-4 max-md:max-w-xs"
127+
>
128+
<div
129+
className={cn(
130+
"flex size-16 shrink-0 items-center justify-center rounded-full border text-2xl font-bold shadow-2xl xl:size-20",
131+
step.color
132+
)}
133+
>
134+
{idx + 1}
135+
</div>
136+
<div className="space-y-1 text-lg">
137+
<div className="font-bold">{step.title}</div>
138+
<div>{step.description}</div>
139+
</div>
140+
</div>
141+
))}
142+
</div>
143+
</section>
144+
145+
<CollectiblesCurrentYear
146+
badges={badgesWithOwned.filter(
147+
(badge) => String(badge.year) === currentYear
148+
)}
149+
address={address}
150+
/>
151+
152+
<CollectiblesPreviousYears
153+
badges={isMounted() && isConnected ? addressBadges : badges}
154+
/>
155+
</div>
156+
</div>
157+
)
158+
}
159+
160+
export default CollectiblesContent
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 "../Collectibles/loading"
4+
5+
export default dynamic(() => import("."), { ssr: false, loading: Loading })

0 commit comments

Comments
 (0)