Skip to content

fix: start index for Roadmap carousel #15540

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

Merged
merged 1 commit into from
May 29, 2025
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
131 changes: 69 additions & 62 deletions app/[locale]/roadmap/_components/ReleaseCarousel.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
"use client"

import { useEffect, useState } from "react"
import { useCallback, useEffect, useMemo, useState } from "react"
import { useLocale } from "next-intl"

import { Image } from "@/components/Image"
import { ButtonLink } from "@/components/ui/buttons/Button"
Expand All @@ -16,45 +17,39 @@ import {
import { cn } from "@/lib/utils/cn"
import { formatDate } from "@/lib/utils/date"

import { releasesData } from "@/data/roadmap/releases"
import { Release, releasesData } from "@/data/roadmap/releases"

const findLatestReleaseIndex = () => {
const today = new Date()
const twoMonthsFromNow = new Date()
twoMonthsFromNow.setMonth(today.getMonth() + 2)
const ReleaseCarousel = () => {
const locale = useLocale()

// First try to find a release within the next 2 months
const upcomingReleaseIndex = releasesData.findIndex((release) => {
const releaseDate = new Date(release.releaseDate)
return releaseDate > today && releaseDate <= twoMonthsFromNow
})
const [api1, setApi1] = useState<CarouselApi>()
const [api2, setApi2] = useState<CarouselApi>()

// If no upcoming release found, find the most recent release up to today
if (upcomingReleaseIndex === -1) {
const pastReleases = releasesData.filter(
(release) => new Date(release.releaseDate) <= today
)
if (pastReleases.length > 0) {
const mostRecentRelease = pastReleases[pastReleases.length - 1]
return releasesData.findIndex(
(release) => release.releaseDate === mostRecentRelease.releaseDate
)
}
}
const startIndex = useMemo(() => {
const now = new Date()

return upcomingReleaseIndex
}
// Production: has a releaseDate in the past
const productionReleases = releasesData.filter((release) => {
if (!("releaseDate" in release) || !release.releaseDate) return false
const releaseDate = new Date(release.releaseDate)
return releaseDate <= now
})

const ReleaseCarousel = () => {
const todayDate = new Date()
const twoMonthsFromNow = new Date()
twoMonthsFromNow.setMonth(todayDate.getMonth() + 2)
// Upcoming: has a releaseDate, but is in the future
const upcomingReleases = releasesData.filter((release) => {
if (!("releaseDate" in release) || !release.releaseDate) return false
const releaseDate = new Date(release.releaseDate)
return releaseDate > now
})

const [api1, setApi1] = useState<CarouselApi>()
const [api2, setApi2] = useState<CarouselApi>()
const [currentIndex, setCurrentIndex] = useState(() =>
findLatestReleaseIndex()
)
// If upcoming releases exist, start index after production releases
if (upcomingReleases.length > 0) return productionReleases.length

// If no upcoming releases, start at the last production release
return productionReleases.length - 1
}, [])

const [currentIndex, setCurrentIndex] = useState(startIndex)

useEffect(() => {
if (!api1 || !api2) {
Expand All @@ -72,6 +67,27 @@ const ReleaseCarousel = () => {
})
}, [api1, api2])

const getStatus = useCallback((release: Release) => {
if (!("releaseDate" in release) || !release.releaseDate) return "dev"
if (new Date(release.releaseDate) <= new Date()) return "prod"
return "soon"
}, [])

const getDisplayDate = (release: Release): string => {
if (!("releaseDate" in release || "plannedReleaseYear" in release))
return ""

if ("plannedReleaseYear" in release && release.plannedReleaseYear)
return new Intl.DateTimeFormat(locale, {
year: "numeric",
}).format(new Date(Number(release.plannedReleaseYear), 0, 1))

if ("releaseDate" in release && release.releaseDate)
return formatDate(release.releaseDate)

return ""
}

return (
<div className="w-full max-w-[100vw] overflow-hidden">
<div className="mx-auto w-full max-w-screen-2xl px-4 sm:px-6">
Expand All @@ -85,29 +101,21 @@ const ReleaseCarousel = () => {
align: "center",
containScroll: false,
loop: false,
startIndex: findLatestReleaseIndex(),
startIndex,
}}
>
<CarouselContent>
{releasesData.map((release, index) => {
const releaseDate = new Date(release.releaseDate)
const nextRelease =
releaseDate > todayDate && releaseDate <= twoMonthsFromNow
const labelType =
releaseDate < todayDate
? 1
: releaseDate < twoMonthsFromNow
? 2
: 3

const status = getStatus(release)
const displayDate = getDisplayDate(release)
return (
<CarouselItem
key={release.releaseName}
className="w-full md:basis-1/3"
>
<div className="flex w-full flex-col items-center justify-center gap-3">
<div className="mb-3 !h-6">
{labelType === 1 && (
{status === "prod" && (
<div
className={cn(
"w-fit rounded-lg bg-primary-low-contrast px-2 py-1",
Expand All @@ -117,7 +125,7 @@ const ReleaseCarousel = () => {
<p className="text-sm font-bold">In production</p>
</div>
)}
{labelType === 2 && (
{status === "soon" && (
<div
className={cn(
"w-fit rounded-lg bg-warning-light px-2 py-1",
Expand All @@ -129,7 +137,7 @@ const ReleaseCarousel = () => {
</p>
</div>
)}
{labelType === 3 && (
{status === "dev" && (
<div
className={cn(
"w-fit rounded-lg bg-card-gradient-secondary-hover px-2 py-1",
Expand All @@ -142,14 +150,15 @@ const ReleaseCarousel = () => {
</div>
)}
</div>
{/* Line-circle-line decoration —•— */}
<div className="flex w-full items-center justify-center text-center">
<div
className={cn(
"flex h-1 flex-1",
index !== 0
? nextRelease
? status === "soon"
? "bg-gradient-to-r from-primary to-primary-low-contrast"
: releaseDate.getTime() < todayDate.getTime()
: status === "prod"
? "bg-primary"
: "bg-primary-low-contrast"
: "bg-transparent"
Expand All @@ -158,21 +167,21 @@ const ReleaseCarousel = () => {
<div
className={cn(
"h-7 w-7 rounded-full",
releaseDate.getTime() < todayDate.getTime()
status === "prod"
? "bg-primary"
: "bg-primary-low-contrast",
nextRelease &&
status === "soon" &&
"border-2 border-primary bg-background"
)}
/>
<div
className={cn(
"flex h-1 flex-1",
index !== releasesData.length - 1
? index < findLatestReleaseIndex()
? "bg-primary"
: "bg-primary-low-contrast"
: "bg-transparent"
index < startIndex
? "bg-primary"
: "bg-primary-low-contrast",
index === releasesData.length - 1 &&
"bg-transparent"
)}
/>
</div>
Expand All @@ -181,7 +190,7 @@ const ReleaseCarousel = () => {
{release.releaseName}
</p>
<p className="font-mono text-sm text-body-medium">
{formatDate(release.releaseDate)}
{displayDate}
</p>
</div>
</div>
Expand All @@ -203,7 +212,7 @@ const ReleaseCarousel = () => {
align: "center",
containScroll: false,
loop: false,
startIndex: findLatestReleaseIndex(),
startIndex,
}}
>
<CarouselContent>
Expand All @@ -225,9 +234,7 @@ const ReleaseCarousel = () => {
<h2 className="text-4xl font-bold lg:text-6xl">
{release.releaseName}
</h2>
<p className="text-md">
{formatDate(release.releaseDate)}
</p>
<p className="text-md">{getDisplayDate(release)}</p>
</div>

<div>
Expand Down
30 changes: 26 additions & 4 deletions src/data/roadmap/releases.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,36 @@ import Layer2HubHeroImage from "@/public/images/heroes/layer-2-hub-hero.jpg"
import QuizzesHubHeroImage from "@/public/images/heroes/quizzes-hub-hero.png"
import FusakaImage from "@/public/images/roadmap/roadmap-fusaka.png"
import PectraImage from "@/public/images/roadmap/roadmap-pectra.png"
interface Release {

type DateString =
`2${number}${number}${number}-${number}${number}-${number}${number}`
type YearString = `2${number}${number}${number}`

interface BaseRelease {
image: StaticImageData
releaseName: string
releaseDate: string
content: React.ReactNode
href: string
}

interface ReleaseWithDate extends BaseRelease {
releaseDate: DateString
plannedReleaseYear?: never
}

interface ReleaseWithYear extends BaseRelease {
releaseDate?: never
plannedReleaseYear: YearString
}

interface ReleaseUnscheduled extends BaseRelease {
releaseDate?: never
plannedReleaseYear?: never
}

// Release may have either a releaseDate or a plannedReleaseYear, but not both.
export type Release = ReleaseWithDate | ReleaseWithYear | ReleaseUnscheduled

export const releasesData: Release[] = [
{
image: DevelopersHubHeroImage,
Expand Down Expand Up @@ -144,7 +166,7 @@ export const releasesData: Release[] = [
{
image: FusakaImage,
releaseName: "Fusaka",
releaseDate: "2025",
plannedReleaseYear: "2025",
content: (
<div>
<p className="font-bold">
Expand Down Expand Up @@ -172,7 +194,7 @@ export const releasesData: Release[] = [
{
image: GuidesHubHeroImage,
releaseName: "Glamsterdam",
releaseDate: "2026",
plannedReleaseYear: "2026",
content: (
<div>
<p className="font-bold">Discussed for Glamsterdam</p>
Expand Down