diff --git a/app/[locale]/collectibles/_components/Collectibles/index.tsx b/app/[locale]/collectibles/_components/Collectibles/index.tsx new file mode 100644 index 00000000000..8573de44612 --- /dev/null +++ b/app/[locale]/collectibles/_components/Collectibles/index.tsx @@ -0,0 +1,25 @@ +"use client" + +import React from "react" +import { QueryClient, QueryClientProvider } from "@tanstack/react-query" + +import WalletProviders from "@/components/WalletProviders" + +import type { Badge } from "../../types" +import CollectiblesContent from "../CollectiblesContent/lazy" + +export type CollectiblesPageProps = { + badges: Badge[] +} + +const queryClient = new QueryClient() + +const CollectiblesPage = ({ badges }: CollectiblesPageProps) => ( + + + + + +) + +export default CollectiblesPage diff --git a/app/[locale]/collectibles/_components/Collectibles/lazy.tsx b/app/[locale]/collectibles/_components/Collectibles/lazy.tsx new file mode 100644 index 00000000000..c960f08e647 --- /dev/null +++ b/app/[locale]/collectibles/_components/Collectibles/lazy.tsx @@ -0,0 +1,5 @@ +import dynamic from "next/dynamic" + +import Loading from "./loading" + +export default dynamic(() => import("."), { ssr: false, loading: Loading }) diff --git a/app/[locale]/collectibles/_components/Collectibles/loading.tsx b/app/[locale]/collectibles/_components/Collectibles/loading.tsx new file mode 100644 index 00000000000..c3579330eb6 --- /dev/null +++ b/app/[locale]/collectibles/_components/Collectibles/loading.tsx @@ -0,0 +1,10 @@ +import { Skeleton } from "@/components/ui/skeleton" + +const Loading = () => ( +
+ + +
+) + +export default Loading diff --git a/app/[locale]/collectibles/_components/CollectiblesConnectButton/index.tsx b/app/[locale]/collectibles/_components/CollectiblesConnectButton/index.tsx new file mode 100644 index 00000000000..c0cf25fecf0 --- /dev/null +++ b/app/[locale]/collectibles/_components/CollectiblesConnectButton/index.tsx @@ -0,0 +1,36 @@ +"use client" + +import { ConnectButton } from "@rainbow-me/rainbowkit" + +import { Button } from "@/components/ui/buttons/Button" + +import { useTranslation } from "@/hooks/useTranslation" + +const CollectiblesConnectButton = () => { + const { t } = useTranslation("page-collectibles") + return ( + + {({ account, chain, openConnectModal, mounted }) => { + const connected = mounted && account && chain + + return ( + <> + {(() => { + if (!connected) { + return ( + + ) + } + + return + })()} + + ) + }} + + ) +} + +export default CollectiblesConnectButton diff --git a/app/[locale]/collectibles/_components/CollectiblesConnectButton/lazy.tsx b/app/[locale]/collectibles/_components/CollectiblesConnectButton/lazy.tsx new file mode 100644 index 00000000000..c960f08e647 --- /dev/null +++ b/app/[locale]/collectibles/_components/CollectiblesConnectButton/lazy.tsx @@ -0,0 +1,5 @@ +import dynamic from "next/dynamic" + +import Loading from "./loading" + +export default dynamic(() => import("."), { ssr: false, loading: Loading }) diff --git a/app/[locale]/collectibles/_components/CollectiblesConnectButton/loading.tsx b/app/[locale]/collectibles/_components/CollectiblesConnectButton/loading.tsx new file mode 100644 index 00000000000..e2c8ca622c3 --- /dev/null +++ b/app/[locale]/collectibles/_components/CollectiblesConnectButton/loading.tsx @@ -0,0 +1,5 @@ +import { Skeleton } from "@/components/ui/skeleton" + +const Loading = () => + +export default Loading diff --git a/app/[locale]/collectibles/_components/CollectiblesContent/index.tsx b/app/[locale]/collectibles/_components/CollectiblesContent/index.tsx new file mode 100644 index 00000000000..6cf2fea3595 --- /dev/null +++ b/app/[locale]/collectibles/_components/CollectiblesContent/index.tsx @@ -0,0 +1,160 @@ +"use client" + +import { useMemo } from "react" +import { useIsMounted } from "usehooks-ts" +import { useAccount } from "wagmi" +import { useQuery } from "@tanstack/react-query" + +import { Image } from "@/components/Image" +import { Skeleton } from "@/components/ui/skeleton" + +import { cn } from "@/lib/utils/cn" + +import { COLLECTIBLES_BASE_URL } from "../../constants" +import type { Badge } from "../../types" +import { type CollectiblesPageProps } from "../Collectibles" +import CollectiblesConnectButton from "../CollectiblesConnectButton/lazy" +import CollectiblesCurrentYear from "../CollectiblesCurrentYear" +import CollectiblesPreviousYears from "../CollectiblesPreviousYears" +import CollectiblesProgress from "../CollectiblesProgress/lazy" + +import useTranslation from "@/hooks/useTranslation" +import alreadyContributorImg from "@/public/images/10-year-anniversary/adoption-1.png" + +export type BadgeWithOwned = Badge & { + owned: boolean +} + +const ADDRESS_STATS_API = `${COLLECTIBLES_BASE_URL}/api/stats/` + +const CollectiblesContent = ({ badges }: CollectiblesPageProps) => { + const { t } = useTranslation("page-collectibles") + + const currentYear = new Date().getFullYear().toString() + + const steps = [ + { + title: t("page-collectibles-how-step1-title"), + description: t("page-collectibles-how-step1-desc"), + color: "text-accent-a", + }, + { + title: t("page-collectibles-how-step2-title"), + description: t("page-collectibles-how-step2-desc"), + color: "text-accent-b", + }, + { + title: t("page-collectibles-how-step3-title"), + description: t("page-collectibles-how-step3-desc"), + color: "text-accent-c", + }, + ] + + const isMounted = useIsMounted() + const { address, isConnected } = useAccount() + + const { + data: addressBadges = [], + error, + isLoading, + } = useQuery({ + queryKey: ["addressBadges", address], + queryFn: async (): Promise => { + if (!address) return [] + const response = await fetch(`${ADDRESS_STATS_API}${address}`) + if (!response.ok) { + throw new Error("Failed to fetch address badges") + } + return response.json() + }, + enabled: !!address, + }) + + const badgesWithOwned = useMemo((): BadgeWithOwned[] => { + return badges.map((badge) => { + const addressBadge = addressBadges.find((b) => b.id === badge.id) + return { + ...badge, + owned: addressBadge ? true : false, + } + }) + }, [badges, addressBadges]) + + return ( +
+ {/* Already a contributor? section */} +
+ {t("page-collectibles-contributor-img-alt")} +
+

{t("page-collectibles-already-title")}

+

+ {t("page-collectibles-already-desc")} +

+
+ + + + {isConnected && !isLoading && !error && ( + + )} + {isLoading && ( +
+ + +
+ )} + {error && ( +
+ Error fetching address badges +
+ )} +
+ + {/* How it works section */} +
+
+

{t("page-collectibles-how-title")}

+
+ {steps.map((step, idx) => ( +
+
+ {idx + 1} +
+
+
{step.title}
+
{step.description}
+
+
+ ))} +
+
+ + String(badge.year) === currentYear + )} + address={address} + /> + + +
+
+ ) +} + +export default CollectiblesContent diff --git a/app/[locale]/collectibles/_components/CollectiblesContent/lazy.tsx b/app/[locale]/collectibles/_components/CollectiblesContent/lazy.tsx new file mode 100644 index 00000000000..83168693259 --- /dev/null +++ b/app/[locale]/collectibles/_components/CollectiblesContent/lazy.tsx @@ -0,0 +1,5 @@ +import dynamic from "next/dynamic" + +import Loading from "../Collectibles/loading" + +export default dynamic(() => import("."), { ssr: false, loading: Loading }) diff --git a/app/[locale]/collectibles/_components/CollectiblesCurrentYear.tsx b/app/[locale]/collectibles/_components/CollectiblesCurrentYear.tsx new file mode 100644 index 00000000000..2a41ab0a401 --- /dev/null +++ b/app/[locale]/collectibles/_components/CollectiblesCurrentYear.tsx @@ -0,0 +1,546 @@ +"use client" + +import React from "react" +import { + CircleCheckIcon, + LanguagesIcon, + MessageCircleMoreIcon, + PencilRulerIcon, + Zap, +} from "lucide-react" + +import { ChildOnlyProp } from "@/lib/types" + +import { Image, ImageProps } from "@/components/Image" +import Translation from "@/components/Translation" +import { + Accordion, + AccordionContent, + AccordionItem, + AccordionTrigger, +} from "@/components/ui/accordion" +import { Card } from "@/components/ui/card" +import Link, { ExternalLinkIcon, LinkProps } from "@/components/ui/Link" +import { + ListItem, + ListProps, + OrderedList, + UnorderedList, +} from "@/components/ui/list" + +import { cn } from "@/lib/utils/cn" + +import { BadgeWithOwned } from "./CollectiblesContent" + +import useTranslation from "@/hooks/useTranslation" + +const HighlightCardGrid = ({ + className, + ...props +}: React.HTMLAttributes) => ( +
+) + +const HighlightCard = ({ + className, + ...props +}: React.HTMLAttributes) => ( + +) + +type HighlightCardContentProps = Pick< + React.HTMLAttributes, + "children" | "className" +> & + Pick & + Pick + +const HighlightCardBody = ({ + href, + src, + alt, + className, + children, +}: HighlightCardContentProps) => ( +
+ + {alt} + +
{children}
+
+) + +type HighlightCardFooterProps = React.HTMLAttributes & + Pick + +const HighlightCardFooter = ({ + className, + href, + children, + ...props +}: HighlightCardFooterProps) => ( +
+ + + {children} + +
+) + +const CheckList = ({ className, ...props }: ListProps) => ( + +) + +type CheckItemProps = ChildOnlyProp & { + owned?: boolean +} + +const CheckItem = ({ children, owned }: CheckItemProps) => ( + + + {children} + +) + +type CollectiblesCurrentYearProps = { + badges: BadgeWithOwned[] + address?: `0x${string}` +} + +const CollectiblesCurrentYear = ({ + badges, + address, +}: CollectiblesCurrentYearProps) => { + const { t } = useTranslation("page-collectibles") + + const socialBadges = React.useMemo( + () => badges.filter((b) => b.category === "Events/Calls"), + [badges] + ) + const developerBadge = React.useMemo( + () => + badges.find( + (b) => b.category === "Github" && b.name.startsWith("1 PR merged") + ), + [badges] + ) + const developer5Badge = React.useMemo( + () => + badges.find( + (b) => b.category === "Github" && b.name.startsWith("5 PRs merged") + ), + [badges] + ) + const developer10Badge = React.useMemo( + () => + badges.find( + (b) => b.category === "Github" && b.name.startsWith("10 PRs merged") + ), + [badges] + ) + const writingBadge = React.useMemo( + () => + badges.find( + (b) => + b.category === "Github" && b.name.startsWith("Content contributor") + ), + [badges] + ) + const designBadge = React.useMemo( + () => + badges.find( + (b) => + b.category === "Design" && b.name.startsWith("Design contributor") + ), + [badges] + ) + const userTestingBadge = React.useMemo( + () => + badges.find( + (b) => b.category === "Design" && b.name.startsWith("User testing") + ), + [badges] + ) + const gitpoapBadge = React.useMemo( + () => + badges.find( + (b) => b.category === "Github" && b.name.startsWith("GitPOAP") + ), + [badges] + ) + const translationBadge = React.useMemo( + () => + badges.find( + (b) => + b.category === "Translation" && + b.name.startsWith("250 words translated") + ), + [badges] + ) + const translation1kBadge = React.useMemo( + () => + badges.find( + (b) => + b.category === "Translation" && + b.name.startsWith("1,000 words translated") + ), + [badges] + ) + const translation10kBadge = React.useMemo( + () => + badges.find( + (b) => + b.category === "Translation" && + b.name.startsWith("10,000 words translated") + ), + [badges] + ) + const translation50kBadge = React.useMemo( + () => + badges.find( + (b) => + b.category === "Translation" && + b.name.startsWith("50,000 words translated") + ), + [badges] + ) + + return ( +
+

{t("page-collectibles-current-year-title")}

+ {/* Custom Code & Content block */} +
+
+ +

+ {t("page-collectibles-code-content-title")} +

+
+

{t("page-collectibles-code-content-desc")}

+ + + + {t("page-collectibles-instructions-label")} + + + + + + + + {t("page-collectibles-code-content-instructions-2")} + + + {t("page-collectibles-code-content-instructions-3")} + + + + + + + {/* First row: Developer & Writing */} + {/* Developer */} + {developerBadge && ( + + +

+ {t("page-collectibles-code-content-developer-title")} +

+

{t("page-collectibles-code-content-developer-desc")}

+ + + {t("page-collectibles-code-content-developer-1pr")} + + {developer5Badge && ( + + {t("page-collectibles-code-content-developer-5pr")} + + )} + {developer10Badge && ( + + {t("page-collectibles-code-content-developer-10pr")} + + )} + +
+ + {t("page-collectibles-get-started")} + +
+ )} + {/* Writing */} + {writingBadge && ( + + +

+ {t("page-collectibles-code-content-writing-title")} +

+

{t("page-collectibles-code-content-writing-desc")}

+ + + {t("page-collectibles-code-content-writing-badge-1")} + + +
+ + {t("page-collectibles-get-started")} + +
+ )} + {/* Design */} + {designBadge && userTestingBadge && ( + + +

+ {t("page-collectibles-code-content-design-title")} +

+

{t("page-collectibles-code-content-design-desc")}

+ + + {t("page-collectibles-code-content-design-1issue")} + + + {t("page-collectibles-code-content-design-user-testing")} + + +
+ + {t("page-collectibles-get-started")} + +
+ )} + {/* GitPOAP */} + {gitpoapBadge && ( + + +

+ {t("page-collectibles-code-content-gitpoap-title")} +

+

{t("page-collectibles-code-content-gitpoap-desc")}

+ + + {t("page-collectibles-code-content-gitpoap-1pr")} + + +
+ + {t("page-collectibles-get-started")} + +
+ )} +
+
+ + {/* Translations section */} + {translationBadge && ( +
+
+ +

+ {t("page-collectibles-translations-title")} +

+
+ +

{t("page-collectibles-translations-desc")}

+ + + + + {t("page-collectibles-instructions-label")} + + + + + + + + {t("page-collectibles-translations-instructions-2")} + + + {t("page-collectibles-translations-instructions-3")} + + + + + + + + + +

+ {t("page-collectibles-translations-title")} +

+

{t("page-collectibles-translations-badge-desc")}

+ + + {t("page-collectibles-translations-250")} + + {translation1kBadge && ( + + {t("page-collectibles-translations-1000")} + + )} + {translation10kBadge && ( + + {t("page-collectibles-translations-10000")} + + )} + {translation50kBadge && ( + + {t("page-collectibles-translations-50000")} + + )} + +
+ + {t("page-collectibles-get-started")} + +
+
+
+ )} + + {/* Social section */} +
+
+ +

{t("page-collectibles-social-title")}

+
+ +

{t("page-collectibles-social-desc")}

+ + + + + {t("page-collectibles-instructions-label")} + + + + + + + + {t("page-collectibles-social-instructions-2")} + + + {t("page-collectibles-social-instructions-3")} + + + + + + +
+ {socialBadges.map((badge) => ( + +
+ {badge.name} +
+ {badge.name + .replace(/ - ethereum.org community/, "") + .replace(/^ethereum.org /, "") + .trim()} + +
+
+ + ))} +
+
+
+ ) +} + +export default CollectiblesCurrentYear diff --git a/app/[locale]/collectibles/_components/CollectiblesPreviousYears.tsx b/app/[locale]/collectibles/_components/CollectiblesPreviousYears.tsx new file mode 100644 index 00000000000..ca497cda3a9 --- /dev/null +++ b/app/[locale]/collectibles/_components/CollectiblesPreviousYears.tsx @@ -0,0 +1,120 @@ +"use client" + +import React from "react" +import { useTranslations } from "next-intl" + +import IdAnchor from "@/components/IdAnchor" +import { Image } from "@/components/Image" +import Link, { ExternalLinkIcon } from "@/components/ui/Link" +import { Tag } from "@/components/ui/tag" + +import type { Badge } from "../types" + +interface CollectiblesPreviousYearsProps { + badges: Badge[] +} + +const CollectiblesPreviousYears = ({ + badges, +}: CollectiblesPreviousYearsProps) => { + const t = useTranslations("page-collectibles") + const currentYear = new Date().getFullYear().toString() + const previousYears = badges.filter((badge) => badge.year !== currentYear) + + // Group badges by year + const grouped = previousYears.reduce( + (acc: Record, badge: Badge) => { + const year = badge.year + if (!acc[year]) acc[year] = [] + acc[year].push(badge) + return acc + }, + {} + ) + // Sort years descending + const years = Object.keys(grouped).sort((a, b) => { + return Number(b) - Number(a) + }) + return ( +
+

+ {t("page-collectibles-previous-years")} +

+ {years.length > 0 ? ( + years.map((year) => { + const badgeCount = grouped[year].length + // Sum collectorsCount for all badges in this year + const collectorsCount = grouped[year].reduce( + (sum: number, badge: Badge) => sum + (badge.collectors_count || 0), + 0 + ) + return ( +
+
+

+ + {year} +

+
+ + {t("page-collectibles-previous-years-badge-count", { + count: badgeCount, + })} + + + {t("page-collectibles-previous-years-collectors-count", { + count: collectorsCount, + })} + +
+
+
+ {grouped[year].map((badge: Badge) => { + const sanitizedName = badge.name + .replace(/\s?ethereum.org\s?/i, " ") // Remove "ethereum.org" from label + .replace(new RegExp(` ?${year} ?`), " ") // Remove year (shown in section header) + .replace(/\s?\(\s?\)\s?/, " ") // Remove any empty parentheses + .replace(/\s+/, " ") // Trim sequential whitespace to single space + .trim() // Trim edge whitespace + .replace(/\s(on|-)$/, "") // Remove edge cases of trailing items after sanitizing + const label = + sanitizedName[0].toUpperCase() + sanitizedName.slice(1) // Force capitalize first character + return ( + + {badge.name} +
+ {label} + +
+ + ) + })} +
+
+ ) + }) + ) : ( +
+ {t("page-collectibles-previous-years-no-badges")} +
+ )} +
+ ) +} + +export default CollectiblesPreviousYears diff --git a/app/[locale]/collectibles/_components/CollectiblesProgress/index.tsx b/app/[locale]/collectibles/_components/CollectiblesProgress/index.tsx new file mode 100644 index 00000000000..5b46b0dff26 --- /dev/null +++ b/app/[locale]/collectibles/_components/CollectiblesProgress/index.tsx @@ -0,0 +1,69 @@ +"use client" + +import React from "react" +import { useLocale } from "next-intl" + +import { Progress } from "@/components/ui/progress" + +import { type BadgeWithOwned } from "../CollectiblesContent" + +import useTranslation from "@/hooks/useTranslation" + +type CollectiblesProgressProps = { + badges: BadgeWithOwned[] +} + +const CollectiblesProgress = ({ badges }: CollectiblesProgressProps) => { + const { t } = useTranslation("page-collectibles") + const locale = useLocale() + + const currentYear = new Date().getFullYear().toString() + const currentYearBadges = badges.filter( + (badge) => String(badge.year) === currentYear + ) + const ownedCount = currentYearBadges.filter((b) => b.owned).length + const contributorSinceYear = badges + .filter((b) => b.owned) + .reduce( + // Return the oldest badge date + (prev, curr) => { + const currYear = Number(curr.year) + return !isNaN(currYear) && currYear < prev ? currYear : prev + }, + Infinity + ) + + return ( + <> + {contributorSinceYear < Infinity && ( +

+ {t("page-collectibles-contributing-since")}:{" "} + {new Intl.DateTimeFormat(locale, { + year: "numeric", + }).format(new Date().setFullYear(contributorSinceYear))} +

+ )} +
+
+ + {t("page-collectibles-contributor-progress-label")} ({currentYear}) + + + {ownedCount}/{currentYearBadges.length} + +
+ + +
+

+ {t("page-collectibles-index-frequency")} +

+ + ) +} + +export default CollectiblesProgress diff --git a/app/[locale]/collectibles/_components/CollectiblesProgress/lazy.tsx b/app/[locale]/collectibles/_components/CollectiblesProgress/lazy.tsx new file mode 100644 index 00000000000..c960f08e647 --- /dev/null +++ b/app/[locale]/collectibles/_components/CollectiblesProgress/lazy.tsx @@ -0,0 +1,5 @@ +import dynamic from "next/dynamic" + +import Loading from "./loading" + +export default dynamic(() => import("."), { ssr: false, loading: Loading }) diff --git a/app/[locale]/collectibles/_components/CollectiblesProgress/loading.tsx b/app/[locale]/collectibles/_components/CollectiblesProgress/loading.tsx new file mode 100644 index 00000000000..01a105d6f4a --- /dev/null +++ b/app/[locale]/collectibles/_components/CollectiblesProgress/loading.tsx @@ -0,0 +1,16 @@ +import { Skeleton } from "@/components/ui/skeleton" + +const Loading = () => ( +
+
+ + +
+ +
+ +
+
+) + +export default Loading diff --git a/app/[locale]/collectibles/constants.ts b/app/[locale]/collectibles/constants.ts new file mode 100644 index 00000000000..c6a01c1ae54 --- /dev/null +++ b/app/[locale]/collectibles/constants.ts @@ -0,0 +1,2 @@ +export const COLLECTIBLES_BASE_URL = + "https://ethereum-org-collectibles.vercel.app" diff --git a/app/[locale]/collectibles/page.tsx b/app/[locale]/collectibles/page.tsx new file mode 100644 index 00000000000..3b6b6b99a56 --- /dev/null +++ b/app/[locale]/collectibles/page.tsx @@ -0,0 +1,148 @@ +import { pick } from "lodash" +import { + getMessages, + getTranslations, + setRequestLocale, +} from "next-intl/server" + +import { Lang } from "@/lib/types" + +import { HubHero } from "@/components/Hero" +import I18nProvider from "@/components/I18nProvider" +import MainArticle from "@/components/MainArticle" +import Link from "@/components/ui/Link" +import { Section } from "@/components/ui/section" + +import { getMetadata } from "@/lib/utils/metadata" +import { getRequiredNamespacesForPage } from "@/lib/utils/translations" + +import CollectiblesPage from "./_components/Collectibles/lazy" +import { COLLECTIBLES_BASE_URL } from "./constants" +import type { Badge, Stats } from "./types" + +import communityHeroImg from "@/public/images/heroes/community-hero.png" + +// API endpoints +const BADGES_API = `${COLLECTIBLES_BASE_URL}/api/badges` +const STATS_API = `${COLLECTIBLES_BASE_URL}/api/stats` + +// Data fetching +async function fetchBadges() { + const res = await fetch(BADGES_API) + if (!res.ok) throw new Error("Failed to fetch badges") + return res.json() +} +async function fetchStats() { + const res = await fetch(STATS_API) + if (!res.ok) throw new Error("Failed to fetch stats") + return res.json() +} + +export default async function Page({ + params, +}: { + params: Promise<{ locale: Lang }> +}) { + const { locale } = await params + const t = await getTranslations({ locale, namespace: "page-collectibles" }) + setRequestLocale(locale) + + // Fetch data + const [badges, stats]: [Badge[], Stats] = await Promise.all([ + fetchBadges(), + fetchStats(), + ]) + + // Get i18n messages + const allMessages = await getMessages({ locale }) + const requiredNamespaces = getRequiredNamespacesForPage("/collectibles/") + const pickedMessages = pick(allMessages, requiredNamespaces) + + return ( + + + +
+
+

+ {t("page-collectibles-improve-title")} +

+

+ {t.rich("page-collectibles-improve-desc-1", { + strong: (chunks) => {chunks}, + })} +

+

+ {t.rich("page-collectibles-improve-desc-2", { + strong: (chunks) => {chunks}, + a: (chunks) => ( + + {chunks} + + ), + })} +

+
+ +
+ {/* Minted */} +
+
+ {stats.collectorsCount?.toLocaleString(locale) ?? "-"} +
+
+ {t("page-collectibles-stats-minted")} +
+
+ {/* Collectors */} +
+
+ {stats.uniqueAddressesCount?.toLocaleString(locale) ?? "-"} +
+
+ {t("page-collectibles-stats-collectors")} +
+
+ {/* Unique Badges */} +
+
+ {stats.collectiblesCount?.toLocaleString(locale) ?? "-"} +
+
+ {t("page-collectibles-stats-unique-badges")} +
+
+
+
+ +
+ +
+
+
+ ) +} + +export async function generateMetadata({ + params, +}: { + params: Promise<{ locale: string }> +}) { + const { locale } = await params + const t = await getTranslations({ locale, namespace: "page-collectibles" }) + return await getMetadata({ + locale, + slug: ["collectibles"], + title: t("page-collectibles-hero-header"), + description: t("page-collectibles-hero-description"), + image: "/images/heroes/community-hero.png", + }) +} diff --git a/app/[locale]/collectibles/types.ts b/app/[locale]/collectibles/types.ts new file mode 100644 index 00000000000..04ea8f3dbf5 --- /dev/null +++ b/app/[locale]/collectibles/types.ts @@ -0,0 +1,16 @@ +export type Badge = { + id: string + name: string + image: string + link: string + category: string + year: string + description: string + collectors_count: number +} + +export type Stats = { + collectorsCount: number + uniqueAddressesCount: number + collectiblesCount: number +} diff --git a/app/[locale]/start/page.tsx b/app/[locale]/start/page.tsx index 51bf42a9b7d..11416f3a7f6 100644 --- a/app/[locale]/start/page.tsx +++ b/app/[locale]/start/page.tsx @@ -45,7 +45,7 @@ const Page = async ({ params }: { params: Promise<{ locale: Lang }> }) => {
- +
diff --git a/next.config.js b/next.config.js index 5c39cb7693e..1f819d3b2e3 100644 --- a/next.config.js +++ b/next.config.js @@ -93,6 +93,18 @@ module.exports = (phase, { defaultConfig }) => { protocol: "https", hostname: "coin-images.coingecko.com", }, + { + protocol: "https", + hostname: "cdn.galxe.com", + }, + { + protocol: "https", + hostname: "assets.poap.xyz", + }, + { + protocol: "https", + hostname: "unavatar.io", + }, ], }, async headers() { diff --git a/src/components/Nav/useNavigation.ts b/src/components/Nav/useNavigation.ts index 3a88df8de89..fd247467f2c 100644 --- a/src/components/Nav/useNavigation.ts +++ b/src/components/Nav/useNavigation.ts @@ -402,6 +402,11 @@ export const useNavigation = () => { description: t("nav-translation-program-description"), href: "/contributing/translation-program/", }, + { + label: t("nav-collectibles-label"), + description: t("nav-collectibles-description"), + href: "/collectibles/", + }, { label: t("about-ethereum-org"), description: t("nav-about-description"), diff --git a/src/components/StartWithEthereumFlow/index.tsx b/src/components/StartWithEthereumFlow/index.tsx index 3ab7fdf29f7..cafad0256ba 100644 --- a/src/components/StartWithEthereumFlow/index.tsx +++ b/src/components/StartWithEthereumFlow/index.tsx @@ -36,10 +36,8 @@ const queryClient = new QueryClient() const StartWithEthereumFlow = ({ newToCryptoWallets, - locale, }: { newToCryptoWallets: Wallet[] - locale: string }) => { const swiperRef = useRef(null) const containerRef = useRef(null) @@ -100,7 +98,7 @@ const StartWithEthereumFlow = ({ return ( - + { +const WalletProviders = ({ children }: WalletProvidersProps) => { + const locale = useLocale() return ( { + const { twFlipForRtl } = useRtlFlip() + return ( + + ) +} + type BaseProps = { hideArrow?: boolean isPartiallyActive?: boolean @@ -53,8 +66,6 @@ export const BaseLink = forwardRef(function Link( ref ) { const pathname = usePathname() - const { twFlipForRtl } = useRtlFlip() - if (!href) { // If troubleshooting this warning, check for multiple h1's in markdown content—these will result in broken id hrefs console.warn(`Link component missing href prop, pathname: ${pathname}`) @@ -116,15 +127,7 @@ export const BaseLink = forwardRef(function Link( {isMailto ? "opens email client" : "opens in a new tab"} - {!hideArrow && !isMailto && ( - - )} + {!hideArrow && !isMailto && } ) } diff --git a/src/components/ui/accordion.tsx b/src/components/ui/accordion.tsx index d2f725add36..585908bde16 100644 --- a/src/components/ui/accordion.tsx +++ b/src/components/ui/accordion.tsx @@ -25,7 +25,7 @@ const AccordionTrigger = React.forwardRef< svg]:rotate-90 [&[data-state=open]>svg]:-rotate-90 [&[data-state=open]]:bg-background-highlight [&[data-state=open]]:text-primary-high-contrast", + "flex flex-1 items-center justify-between gap-2 px-2 py-2 font-medium transition-all hover:bg-background-highlight hover:text-primary-hover focus-visible:outline-1 focus-visible:-outline-offset-1 focus-visible:outline-primary-hover md:px-4 [&[data-state=open]:dir(rtl)_svg]:rotate-90 [&[data-state=open]]:bg-background-highlight [&[data-state=open]]:text-primary-high-contrast [&[data-state=open]_svg]:-rotate-90", className )} {...props} diff --git a/src/components/ui/progress.tsx b/src/components/ui/progress.tsx index 483dace9cd3..ca13ff8031e 100644 --- a/src/components/ui/progress.tsx +++ b/src/components/ui/progress.tsx @@ -1,22 +1,44 @@ import * as React from "react" +import { cva, VariantProps } from "class-variance-authority" import * as ProgressPrimitive from "@radix-ui/react-progress" import { cn } from "@/lib/utils/cn" +const progressIndicatorVariants = cva( + "h-full w-full flex-1 rounded-full transition-all", + { + variants: { + color: { + disabled: "bg-disabled", + primary: "bg-primary", + }, + }, + defaultVariants: { + color: "disabled", + }, + } +) + +type ProgressProps = React.ComponentPropsWithoutRef< + typeof ProgressPrimitive.Root +> & + VariantProps + const Progress = React.forwardRef< React.ElementRef, - React.ComponentPropsWithoutRef ->(({ className, value, ...props }, ref) => ( + ProgressProps +>(({ className, value, color, ...props }, ref) => ( diff --git a/src/intl/en/common.json b/src/intl/en/common.json index a4c1fb95edc..a986f48d1e5 100644 --- a/src/intl/en/common.json +++ b/src/intl/en/common.json @@ -239,6 +239,8 @@ "nav-builders-home-description": "A builder's manual for Ethereum—by builders, for builders", "nav-builders-home-label": "Builder's home", "nav-code-of-conduct": "Code of conduct", + "nav-collectibles-description": "Contributor dashboard for ethereum.org contributor collectibles", + "nav-collectibles-label": "ethereum.org collectibles", "nav-contribute-description": "If you want to help, this will guide you", "nav-contribute-label": "Contributing to ethereum.org", "nav-dao-description": "Member-owned communities without centralized authority", diff --git a/src/intl/en/page-collectibles.json b/src/intl/en/page-collectibles.json new file mode 100644 index 00000000000..03455feddf3 --- /dev/null +++ b/src/intl/en/page-collectibles.json @@ -0,0 +1,67 @@ +{ + "page-collectibles-already-desc": "Check your progress", + "page-collectibles-already-title": "Already a contributor?", + "page-collectibles-code-content-desc": "Fix issues, write or improve articles or propose design improvements to the website.", + "page-collectibles-code-content-design-1issue": "Design issue solved badge", + "page-collectibles-code-content-design-desc": "Do design crits, improve our design system or participate in user testing.", + "page-collectibles-code-content-design-title": "Design", + "page-collectibles-code-content-design-user-testing": "Participate in user testing badge", + "page-collectibles-code-content-developer-10pr": "10 PRs merged badge", + "page-collectibles-code-content-developer-1pr": "1 PR merged badge", + "page-collectibles-code-content-developer-5pr": "5 PRs merged badge", + "page-collectibles-code-content-developer-desc": "Any improvement merged to the website.", + "page-collectibles-code-content-developer-title": "Developer", + "page-collectibles-code-content-gitpoap-1pr": "PR merged badge", + "page-collectibles-code-content-gitpoap-desc": "Automatically claimable after your PR is merged.", + "page-collectibles-code-content-gitpoap-title": "GitPOAP", + "page-collectibles-code-content-instructions-1": "Go to our GitHub repository", + "page-collectibles-code-content-instructions-2": "Choose an issue to work on", + "page-collectibles-code-content-instructions-3": "Commit a fix or improvement", + "page-collectibles-code-content-title": "Code & Content", + "page-collectibles-code-content-writing-badge-1": "Content contribution badge", + "page-collectibles-code-content-writing-desc": "For any content improvement merged to master.", + "page-collectibles-code-content-writing-title": "Writing", + "page-collectibles-connect-wallet": "Connect wallet", + "page-collectibles-contributing-since": "Contributing since", + "page-collectibles-contributor-img-alt": "Two contributors chatting", + "page-collectibles-contributor-progress-label": "Claimed", + "page-collectibles-current-year-title": "Current year", + "page-collectibles-get-started": "Get Started", + "page-collectibles-hero-description": "Prove that you worked on ethereum.org, onchain.", + "page-collectibles-hero-header": "Ethereum.org Collectibles", + "page-collectibles-hero-title": "Badges", + "page-collectibles-how-step1-desc": "to the website", + "page-collectibles-how-step1-title": "Contribute", + "page-collectibles-how-step2-desc": "on Discord", + "page-collectibles-how-step2-title": "Get verified", + "page-collectibles-how-step3-desc": "on Galxe", + "page-collectibles-how-step3-title": "Claim NFT", + "page-collectibles-how-title": "How it works", + "page-collectibles-improve-desc-1": "Earn unique NFTs by helping maintain and expand ethereum.org website. These badges recognize your participation onchain.", + "page-collectibles-improve-desc-2": "Top holders get contributor swag or discounted tickets to events like Devcon. Your onchain badges makes it easy for others to support you.", + "page-collectibles-improve-title": "Improve ethereum.org", + "page-collectibles-index-frequency": "Results updated once daily at 15:15 UTC", + "page-collectibles-instructions-label": "Instructions", + "page-collectibles-previous-years-badge-count": "{count, plural, =0 {no badges} =1 {1 badge} other {# badges}}", + "page-collectibles-previous-years-collectors-count": "{count, plural, =0 {no collectors} =1 {1 collector} other {# collectors}}", + "page-collectibles-previous-years-no-badges": "No badges minted", + "page-collectibles-previous-years": "Previous years", + "page-collectibles-social-desc": "Join any of our Discord calls to bug test the website before releases or keep up with the ethereum.org news on our monthly community calls.", + "page-collectibles-social-instructions-1": "Join our Discord server", + "page-collectibles-social-instructions-2": "See schedule", + "page-collectibles-social-instructions-3": "Join!", + "page-collectibles-social-title": "Social", + "page-collectibles-stats-collectors": "Collectors", + "page-collectibles-stats-minted": "Minted", + "page-collectibles-stats-unique-badges": "Unique Badges", + "page-collectibles-translations-1000": "1,000 words badge", + "page-collectibles-translations-10000": "10,000 words badge", + "page-collectibles-translations-250": "250 words badge", + "page-collectibles-translations-50000": "50,000 words badge", + "page-collectibles-translations-badge-desc": "To any language.", + "page-collectibles-translations-desc": "Most users do not speak English, therefore it's crucial to help translate our articles to other languages, no translation experience needed.", + "page-collectibles-translations-instructions-1": "Register on Crowdin", + "page-collectibles-translations-instructions-2": "Select language", + "page-collectibles-translations-instructions-3": "Start translating", + "page-collectibles-translations-title": "Translations" +} diff --git a/src/intl/es/page-collectibles.json b/src/intl/es/page-collectibles.json new file mode 100644 index 00000000000..a3a298f0689 --- /dev/null +++ b/src/intl/es/page-collectibles.json @@ -0,0 +1,67 @@ +{ + "page-collectibles-already-desc": "Revisa tu progreso", + "page-collectibles-already-title": "¿Ya eres colaborador?", + "page-collectibles-code-content-desc": "Corrige errores, escribe o mejora artículos o propone mejoras de diseño para el sitio web.", + "page-collectibles-code-content-design-1issue": "Insignia por resolver un issue de diseño", + "page-collectibles-code-content-design-desc": "Haz críticas de diseño, mejora nuestro sistema de diseño o participa en pruebas de usuario.", + "page-collectibles-code-content-design-title": "Diseño", + "page-collectibles-code-content-design-user-testing": "Insignia por participar en pruebas de usuario", + "page-collectibles-code-content-developer-10pr": "Insignia por 10 PRs mergeados", + "page-collectibles-code-content-developer-1pr": "Insignia por 1 PR mergeado", + "page-collectibles-code-content-developer-5pr": "Insignia por 5 PRs mergeados", + "page-collectibles-code-content-developer-desc": "Cualquier mejora mergeada al sitio web.", + "page-collectibles-code-content-developer-title": "Desarrollador", + "page-collectibles-code-content-gitpoap-1pr": "Insignia por 1 PR mergeado", + "page-collectibles-code-content-gitpoap-desc": "Reclamable automáticamente después de que tu PR sea mergeada.", + "page-collectibles-code-content-gitpoap-title": "GitPOAP", + "page-collectibles-code-content-instructions-1": "Ve a nuestro repositorio de GitHub", + "page-collectibles-code-content-instructions-2": "Elige un issue en el que trabajar", + "page-collectibles-code-content-instructions-3": "Confirma una corrección o mejora", + "page-collectibles-code-content-title": "Código y Contenido", + "page-collectibles-code-content-writing-badge-1": "Insignia por contribución de contenido", + "page-collectibles-code-content-writing-desc": "Por cualquier mejora de contenido mergeada a master.", + "page-collectibles-code-content-writing-title": "Redacción", + "page-collectibles-connect-wallet": "Conectar billetera", + "page-collectibles-contributing-since": "Contribuyendo desde", + "page-collectibles-contributor-img-alt": "Dos colaboradores conversando", + "page-collectibles-contributor-progress-label": "Reclamado", + "page-collectibles-current-year-title": "Año actual", + "page-collectibles-get-started": "Comenzar", + "page-collectibles-hero-description": "Prueba que has trabajado en ethereum.org, onchain.", + "page-collectibles-hero-header": "Coleccionables Ethereum.org", + "page-collectibles-hero-title": "Insignias", + "page-collectibles-how-step1-desc": "al sitio web", + "page-collectibles-how-step1-title": "Contribuye", + "page-collectibles-how-step2-desc": "en Discord", + "page-collectibles-how-step2-title": "Verifícate", + "page-collectibles-how-step3-desc": "en Galxe", + "page-collectibles-how-step3-title": "Reclama NFT", + "page-collectibles-how-title": "Cómo funciona", + "page-collectibles-improve-desc-1": "Gana NFTs únicos ayudando a mantener y expandir el sitio web ethereum.org. Estas insignias reconocen tu participación onchain.", + "page-collectibles-improve-desc-2": "Los principales poseedores obtienen mercaderia de colaborador o entradas con descuento a eventos como Devcon. Tu insignia onchain facilita que otros te apoyen.", + "page-collectibles-improve-title": "Mejora ethereum.org", + "page-collectibles-index-frequency": "Resultados actualizados una vez al día a las 15:15 UTC", + "page-collectibles-instructions-label": "Instrucciones", + "page-collectibles-previous-years-badge-count": "{count, plural, =0 {ninguna insignia} =1 {1 insignia} other {# insignias}}", + "page-collectibles-previous-years-collectors-count": "{count, plural, =0 {ningún coleccionista} =1 {1 coleccionista} other {# coleccionistas}}", + "page-collectibles-previous-years-no-badges": "No se han obtenido insignias", + "page-collectibles-previous-years": "Años anteriores", + "page-collectibles-social-desc": "Únete a cualquiera de nuestras llamadas de Discord para probar el sitio web antes de los lanzamientos o mantente al día con las noticias de ethereum.org en nuestras llamadas comunitarias mensuales.", + "page-collectibles-social-instructions-1": "Únete a nuestro servidor de Discord", + "page-collectibles-social-instructions-2": "Ver horario", + "page-collectibles-social-instructions-3": "¡Únete!", + "page-collectibles-social-title": "Social", + "page-collectibles-stats-collectors": "Coleccionistas", + "page-collectibles-stats-minted": "Reclamados", + "page-collectibles-stats-unique-badges": "Insignias únicas", + "page-collectibles-translations-1000": "1,000 palabras", + "page-collectibles-translations-10000": "10,000 palabras", + "page-collectibles-translations-250": "250 palabras", + "page-collectibles-translations-50000": "50,000 palabras", + "page-collectibles-translations-badge-desc": "A cualquier idioma.", + "page-collectibles-translations-desc": "La mayoría de los usuarios no hablan inglés, por lo que es crucial ayudar a traducir nuestros artículos a otros idiomas, no se necesita experiencia previa.", + "page-collectibles-translations-instructions-1": "Regístrate en Crowdin", + "page-collectibles-translations-instructions-2": "Selecciona idioma", + "page-collectibles-translations-instructions-3": "Comienza a traducir", + "page-collectibles-translations-title": "Traducciones" +} diff --git a/src/lib/utils/translations.ts b/src/lib/utils/translations.ts index 7ebca26de51..027a814f7d0 100644 --- a/src/lib/utils/translations.ts +++ b/src/lib/utils/translations.ts @@ -70,6 +70,10 @@ const getRequiredNamespacesForPath = (relativePath: string) => { requiredNamespaces = [...requiredNamespaces, "page-10-year-anniversary"] } + if (path === "/collectibles/") { + primaryNamespace = "page-collectibles" + } + if (path === "/contributing/translation-program/acknowledgements/") { primaryNamespace = "page-contributing-translation-program-acknowledgements" }