-
Notifications
You must be signed in to change notification settings - Fork 5.3k
Fusaka banner #16644
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: dev
Are you sure you want to change the base?
Fusaka banner #16644
Changes from all commits
784eac2
37e594a
50580f0
de9bc60
039bffb
bfada53
15be3b5
6c180a6
fca36ee
d4932fb
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
This file was deleted.
This file was deleted.
This file was deleted.
| 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 |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,70 @@ | ||
| import { getLocale, getTranslations } from "next-intl/server" | ||
|
|
||
| import LanguageMorpher from "@/components/Homepage/LanguageMorpher" | ||
| import { Image } from "@/components/Image" | ||
| import { LinkBox, LinkOverlay } from "@/components/ui/link-box" | ||
|
|
||
| import FusakaCountdown from "./FusakaCountdown" | ||
|
|
||
| import RoadmapFusakaImage from "@/public/images/roadmap/roadmap-fusaka.png" | ||
|
|
||
| const FusakaHero = async () => { | ||
| const locale = getLocale() | ||
| const t = await getTranslations({ locale, namespace: "page-index" }) | ||
|
|
||
| return ( | ||
| <div className="relative w-full"> | ||
| <LinkBox className="bg-[#333369] p-2 text-center text-white"> | ||
| <div className="flex flex-col items-center justify-between 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> | ||
|
|
||
| <div className="relative z-0 h-[240px] overflow-hidden md:h-[480px]"> | ||
| <Image | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This image does not follow the optimizations implemented in the |
||
| src={RoadmapFusakaImage} | ||
| alt="Fusaka Hero" | ||
| className="h-full w-full object-cover" | ||
| priority | ||
| /> | ||
| </div> | ||
|
|
||
| <div className="flex flex-col items-center px-4 py-10 text-center"> | ||
| <LanguageMorpher /> | ||
| <div className="flex flex-col items-center gap-y-5 md:max-w-2xl"> | ||
| <h1 className="font-black">{t("page-index-title")}</h1> | ||
| <p className="max-w-96 text-md text-body-medium md:text-lg"> | ||
| {t("page-index-description")} | ||
| </p> | ||
| </div> | ||
| </div> | ||
| </div> | ||
| ) | ||
| } | ||
|
|
||
| export default FusakaHero | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
To be honest, I’m not a fan of this banner from a design perspective.
Proposal: