Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 4 additions & 3 deletions app/[locale]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,8 @@ import type {
import { CodeExample } from "@/lib/interfaces"

import ActivityStats from "@/components/ActivityStats"
import FusakaBanner from "@/components/Banners/FusakaBanner"
import { ChevronNext } from "@/components/Chevron"
import DevconnectBannerVariation1 from "@/components/DevconnectBanner/Variation1"
import HomeHero from "@/components/Hero/HomeHero"
import BentoCard from "@/components/Homepage/BentoCard"
import CodeExamples from "@/components/Homepage/CodeExamples"
Expand Down Expand Up @@ -96,6 +96,7 @@ import { fetchAttestantPosts } from "@/lib/api/fetchPosts"
import { fetchRSS } from "@/lib/api/fetchRSS"
import { fetchTotalValueLocked } from "@/lib/api/fetchTotalValueLocked"
import EventFallback from "@/public/images/events/event-placeholder.png"
import RoadmapFusakaImage from "@/public/images/roadmap/roadmap-fusaka.png"

const BentoCardSwiper = dynamic(
() => import("@/components/Homepage/BentoCardSwiper"),
Expand Down Expand Up @@ -436,8 +437,8 @@ const Page = async ({ params }: { params: PageParams }) => {
<>
<IndexPageJsonLD locale={locale} />
<MainArticle className="flex w-full flex-col items-center" dir={dir}>
<DevconnectBannerVariation1 />
<HomeHero />
<FusakaBanner />
<HomeHero image={RoadmapFusakaImage} alt="Fusaka Hero" />
<div className="w-full space-y-32 px-4 md:mx-6 lg:space-y-48">
<div className="my-20 grid w-full grid-cols-2 gap-x-4 gap-y-8 md:grid-cols-4 md:gap-x-10">
{subHeroCTAs.map(
Expand Down
179 changes: 179 additions & 0 deletions src/components/Banners/FusakaBanner/FusakaCountdown.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,179 @@
"use client"

import { useEffect, useState } from "react"
import humanizeDuration from "humanize-duration"
import { useLocale, useTranslations } from "next-intl"

const fusakaDate = new Date("2025-12-03T21:49:11.000Z")
const fusakaDateTime = fusakaDate.getTime()
const SECONDS = 1000

type TimeUnits = {
days: number
hours: number
minutes: number
seconds: number | null
isExpired: boolean
}

type TimeLabels = {
days: string
hours: string
minutes: string
seconds: string
}

const getTimeUnits = (): TimeUnits => {
const now = Date.now()
const timeLeft = fusakaDateTime - now

if (timeLeft < 0) {
return {
days: 0,
hours: 0,
minutes: 0,
seconds: null,
isExpired: true,
}
}

const days = Math.floor(timeLeft / (24 * 60 * 60 * 1000))
const hours = Math.floor(
(timeLeft % (24 * 60 * 60 * 1000)) / (60 * 60 * 1000)
)
const minutes = Math.floor((timeLeft % (60 * 60 * 1000)) / (60 * 1000))
const seconds =
days === 0 ? Math.floor((timeLeft % (60 * 1000)) / 1000) : null

return {
days,
hours,
minutes,
seconds,
isExpired: false,
}
}

const getTimeLabels = (locale: string): TimeLabels => {
const baseOptions = {
round: true,
language: locale,
}

try {
// Use humanizeDuration to get translated unit names (plural forms)
// Format 2 units of each type to get plural forms
const twoDays = humanizeDuration(2 * 24 * 60 * 60 * 1000, {
...baseOptions,
units: ["d"],
})
const twoHours = humanizeDuration(2 * 60 * 60 * 1000, {
...baseOptions,
units: ["h"],
})
const twoMinutes = humanizeDuration(2 * 60 * 1000, {
...baseOptions,
units: ["m"],
})
const twoSeconds = humanizeDuration(2 * 1000, {
...baseOptions,
units: ["s"],
})

// Extract unit names (remove the number)
const extractUnit = (str: string): string => {
// Remove leading numbers, whitespace, and any separators
// Handles formats like "1 day", "1d", "1 jour", etc.
return str
.replace(/^\d+\s*/, "") // Remove leading number and space
.replace(/^\d+/, "") // Remove any remaining leading number (for formats like "1d")
.trim()
.split(/\s+/)[0] // Take first word in case of multiple words
}

return {
days: extractUnit(twoDays),
hours: extractUnit(twoHours),
minutes: extractUnit(twoMinutes),
seconds: extractUnit(twoSeconds),
}
} catch {
// Fallback to English if translation fails
return {
days: "days",
hours: "hours",
minutes: "minutes",
seconds: "seconds",
}
}
}

const FusakaCountdown = () => {
const locale = useLocale()
const t = useTranslations("page-index")
const [timeUnits, setTimeUnits] = useState<TimeUnits>(() => getTimeUnits())
const [labels, setLabels] = useState<TimeLabels>(() => getTimeLabels(locale))

useEffect(() => {
setLabels(getTimeLabels(locale))
}, [locale])

useEffect(() => {
const updateCountdown = () => {
setTimeUnits(getTimeUnits())
}

const interval = setInterval(updateCountdown, SECONDS)

return () => clearInterval(interval)
}, [])

if (timeUnits.isExpired) {
return (
<p className="text-2xl font-extrabold text-white">
{t("page-index-fusaka-live-now")}
</p>
)
}

return (
<div className="flex items-center justify-center gap-4">
{timeUnits.days > 0 && (
<div className="flex flex-col items-center">
<p className="text-xl font-extrabold text-white md:text-3xl">
{String(timeUnits.days).padStart(2, "0")}
</p>
<p className="text-xs font-bold uppercase text-white">
{labels.days}
</p>
</div>
)}
<div className="flex flex-col items-center">
<p className="text-xl font-extrabold text-white md:text-3xl">
{String(timeUnits.hours).padStart(2, "0")}
</p>
<p className="text-xs font-bold uppercase text-white">{labels.hours}</p>
</div>
<div className="flex flex-col items-center">
<p className="text-xl font-extrabold text-white md:text-3xl">
{String(timeUnits.minutes).padStart(2, "0")}
</p>
<p className="text-xs font-bold uppercase text-white">
{labels.minutes}
</p>
</div>
{timeUnits.seconds !== null && (
<div className="flex flex-col items-center">
<p className="text-xl font-extrabold text-white md:text-3xl">
{String(timeUnits.seconds).padStart(2, "0")}
</p>
<p className="text-xs font-bold uppercase text-white">
{labels.seconds}
</p>
</div>
)}
</div>
)
}

export default FusakaCountdown
45 changes: 45 additions & 0 deletions src/components/Banners/FusakaBanner/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { getLocale, getTranslations } from "next-intl/server"

import { LinkBox, LinkOverlay } from "@/components/ui/link-box"

import FusakaCountdown from "./FusakaCountdown"

const FusakaBanner = async () => {
const locale = getLocale()
const t = await getTranslations({ locale, namespace: "page-index" })

return (
<LinkBox className="w-full bg-[#333369] p-2 text-center text-white md:p-4 md:px-8">
<div className="flex flex-col items-center justify-center gap-2 md:flex-row md:gap-16">
<div className="flex flex-col items-center justify-center">
<p className="text-xl font-extrabold uppercase !leading-none md:text-2xl">
FUSAKA
</p>
<p className="text-sm font-bold uppercase text-purple-100">
{t("page-index-fusaka-network-upgrade")}
</p>
</div>
<p className="text-xs text-white md:text-sm">
{t("page-index-fusaka-description")}{" "}
<LinkOverlay
href="/roadmap/fusaka"
className="text-white hover:text-purple-300"
>
{t("page-index-fusaka-read-more")}
</LinkOverlay>
.
</p>
<div className="flex flex-row items-center justify-center gap-4 md:mt-0 md:flex-col md:gap-0">
<p className="text-xs font-bold uppercase text-gray-200">
{t.rich("page-index-fusaka-going-live-in", {
br: () => <br className="md:hidden" />,
})}
</p>
<FusakaCountdown />
</div>
</div>
</LinkBox>
)
}

export default FusakaBanner
4 changes: 0 additions & 4 deletions src/components/DevconnectBanner/Variation1/banner.svg

This file was deleted.

54 changes: 0 additions & 54 deletions src/components/DevconnectBanner/Variation1/index.tsx

This file was deleted.

53 changes: 0 additions & 53 deletions src/components/DevconnectBanner/Variation2/index.tsx

This file was deleted.

Loading