Skip to content
Merged
Show file tree
Hide file tree
Changes from 14 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
4 changes: 4 additions & 0 deletions assets/icons/Pause_Icon.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 3 additions & 0 deletions assets/icons/Start_Icon.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 2 additions & 0 deletions src/app/globals.css
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@
--color-foreground: var(--foreground);
--font-sans: var(--font-geist-sans);
--font-mono: var(--font-geist-mono);
/* Latest Updates (Gallery) desktop breakpoint: >=1167px */
--breakpoint-gallery-lg: 72.9375rem; /* 1167px */
}

/* @media (prefers-color-scheme: dark) {
Expand Down
61 changes: 37 additions & 24 deletions src/components/LandingPage/Gallery/UpdateCard.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import React from "react";
import Image from "next/image";
import externalLinkIcon from "../../../../assets/icons/external_link_icon.svg";

interface UpdateCardProps {
image: string;
Expand All @@ -16,49 +15,63 @@ const UpdateCard: React.FC<UpdateCardProps> = ({
link,
}) => {
return (
<div className="relative rounded-bl-[20px] rounded-tr-[20px] w-full h-[496px] sm:max-w-[367px]">
<div className="flex flex-col gap-px items-start overflow-hidden relative rounded-[inherit] w-full h-full">
<div className="aspect-[367/310] max-h-[310px] relative shrink-0 w-full sm:max-w-[367px]">
<div className="group relative rounded-bl-[20px] rounded-tr-[20px] w-full h-[278px] max-w-[215px] md:h-[278px] md:max-w-[215px] gallery-lg:h-[476px] gallery-lg:w-[367px] gallery-lg:max-w-[367px] gallery-lg:shrink-0">
<div className="flex flex-col gap-0 items-start overflow-hidden relative rounded-[inherit] w-full h-full">
<div className="h-[181px] md:h-[181px] gallery-lg:h-[310px] relative shrink-0 w-full rounded-tr-[20px] overflow-hidden gallery-lg:w-full">
<Image
src={image}
alt={title}
width={367}
height={310}
className="absolute inset-0 object-cover object-center pointer-events-none w-full h-full"
className="absolute inset-0 object-cover object-center pointer-events-none w-full h-full rounded-tr-[20px]"
/>
</div>
<div className="bg-[#323032] box-border flex flex-col flex-1 items-start overflow-hidden pb-[15px] pt-[11px] px-[18px] relative shrink-0 w-full">
<p className="[font-family:Nunito] font-bold leading-[20px] relative shrink-0 text-[16px] sm:text-[17px] md:text-[18px] text-white w-full mb-[5px] min-h-[40px]">
<div className="bg-[#044249] box-border flex flex-col flex-1 items-start overflow-hidden pb-[14px] pt-[8px] px-[14px] relative shrink-0 w-full md:pb-[14px] md:pt-[8px] md:px-[14px] gallery-lg:px-6 gallery-lg:pt-[14px] gallery-lg:pb-5">
<p className="font-poppins font-semibold leading-[16px] text-[14px] uppercase tracking-[-0.105px] md:tracking-[0.28px] text-[#72f9fb] w-full gallery-lg:mb-2 gallery-lg:min-h-0">
<a
href={link}
target="_blank"
rel="noopener noreferrer"
className="hover:text-[#F6CB0E] cursor-pointer"
className="font-poppins text-[14px] font-semibold not-italic leading-[16px] text-[#72F9FB] hover:text-[#F6CB0E] cursor-pointer md:hover:text-[#72f9fb] gallery-lg:hover:text-[#72f9fb]"
>
{title}
</a>
</p>
<div className="[font-family:Nunito] font-normal leading-[24px] sm:leading-[26px] md:leading-[28px] text-[16px] sm:text-[17px] md:text-[18px] text-white">
<span>
<div className="hidden gallery-lg:block">
<p className="[font-family:Inter] font-normal text-white w-[326px] text-[16px] leading-[22px] mb-2">
{description.length > 100
? `${description.slice(0, 100)}...`
: description}
</span>
<span className="gap-[8px] items-center ml-1 inline-flex">
<a
href={link}
target="_blank"
rel="noopener noreferrer"
className="[text-decoration-skip-ink:none] [text-underline-position:from-font] decoration-solid [font-family:Nunito] font-medium leading-[28px] text-[#76e7dd] text-[14px] underline uppercase cursor-pointer whitespace-nowrap"
>
Read More
</a>
<div className="h-[11.915px] relative shrink-0 w-[11.295px]">
<Image src={externalLinkIcon} alt="external link" className="block max-w-none w-full h-full" width={12} height={12} />
</div>
</span>
</p>
<a
href={link}
target="_blank"
rel="noopener noreferrer"
className="font-inter font-normal not-italic text-[14px] leading-[22px] text-[#aff1ff] underline cursor-pointer hover:text-[#5ef2ff] inline-block mt-auto text-right w-full"
>
Read More &gt;
</a>
</div>
</div>
{/* Tablet/mobile only: hover overlay per Figma 2174-9260 — dark overlay, centered text, teal Read More */}
<div
className="absolute inset-0 rounded-[inherit] bg-black/70 flex flex-col justify-center p-4 opacity-0 transition-opacity duration-200 group-hover:opacity-100 z-[1] gallery-lg:hidden"
aria-hidden
>
<p className="font-inter font-normal text-white text-[14px] leading-[20px] line-clamp-5 mb-3 text-left">
{description.length > 100
? `${description.slice(0, 100)}...`
: description}
</p>
<a
href={link}
target="_blank"
rel="noopener noreferrer"
className="font-inter font-normal text-[14px] leading-[22px] text-[#4BBFC6] underline decoration-[#4BBFC6] hover:text-[#72f9fb] hover:decoration-[#72f9fb] cursor-pointer w-fit text-left"
>
Read More &gt;
</a>
</div>
</div>
</div>
);
Expand Down
204 changes: 184 additions & 20 deletions src/components/LandingPage/Gallery/index.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
"use client";
import React from "react";
import React, { useRef, useState, useCallback, useEffect } from "react";
import Image from "next/image";
import UpdateCard from "./UpdateCard";
import startIcon from '../../../../assets/icons/Start_Icon.svg'
import pauseIcon from '../../../../assets/icons/Pause_Icon.svg'

const AUTO_ROTATE_INTERVAL_MS = 5000;

export interface GalleryUpdate {
image: string;
Expand All @@ -19,27 +24,183 @@ export interface GalleryConfig {
}

interface GalleryProps {
data: { gallery: GalleryConfig};
data: { gallery: GalleryConfig };
}

const CARD_WIDTH_MOBILE = 214;
const CARD_GAP_MOBILE = 28;
const SLIDE_STEP = CARD_WIDTH_MOBILE + CARD_GAP_MOBILE; // 242
const MOBILE_VIEWPORT_OFFSET = -SLIDE_STEP; // -242: show indices 1,2,3

/** Build initial mobile list [c, a, b, c, a, b] from updates [a, b, c] so visible indices 1,2,3 show a,b,c */
function buildMobileRotatingList(updates: GalleryUpdate[] | undefined): GalleryUpdate[] {
if (!updates || updates.length < 3) return [];
const [a, b, c] = updates;
return [c, a, b, c, a, b];
}

const MOBILE_MEDIA_QUERY = "(max-width: 767px)";

const Gallery: React.FC<GalleryProps> = ({ data }) => {
const config = data?.gallery;

const [rotatingList, setRotatingList] = useState<GalleryUpdate[]>([]);
const [isPaused, setIsPaused] = useState(false);
const [slideOffset, setSlideOffset] = useState(MOBILE_VIEWPORT_OFFSET);
const [slideTransitionEnabled, setSlideTransitionEnabled] = useState(true);
const [isMobileViewport, setIsMobileViewport] = useState(false);
const slideDirectionRef = useRef<"next" | "prev" | null>(null);
const slideTrackRef = useRef<HTMLDivElement>(null);

useEffect(() => {
const mql = window.matchMedia(MOBILE_MEDIA_QUERY);
const update = () => setIsMobileViewport(mql.matches);
update();
mql.addEventListener("change", update);
return () => mql.removeEventListener("change", update);
}, []);

useEffect(() => {
if (config?.updates && config.updates.length >= 3) {
setRotatingList(buildMobileRotatingList(config.updates));
}
}, [config?.updates]);

const handleSlideTransitionEnd = useCallback(() => {
const dir = slideDirectionRef.current;
if (dir === "next") {
setRotatingList((prev) =>
prev.length === 6 ? [...prev.slice(1), prev[0]] : prev
);
} else if (dir === "prev") {
setRotatingList((prev) =>
prev.length === 6 ? [prev[5], ...prev.slice(0, 5)] : prev
);
}
slideDirectionRef.current = null;
setSlideTransitionEnabled(false);
setSlideOffset(MOBILE_VIEWPORT_OFFSET);
requestAnimationFrame(() => {
requestAnimationFrame(() => setSlideTransitionEnabled(true));
});
}, []);

const goToNext = useCallback(() => {
if (rotatingList.length !== 6 || slideDirectionRef.current) return;
slideDirectionRef.current = "next";
setSlideOffset(-2 * SLIDE_STEP); // show indices 2,3,4
}, [rotatingList.length]);

const goToPrev = useCallback(() => {
if (rotatingList.length !== 6 || slideDirectionRef.current) return;
slideDirectionRef.current = "prev";
setSlideOffset(0); // show indices 0,1,2
}, [rotatingList.length]);

useEffect(() => {
if (!isMobileViewport || rotatingList.length !== 6 || isPaused) return;
const id = setInterval(goToNext, AUTO_ROTATE_INTERVAL_MS);
return () => clearInterval(id);
}, [isMobileViewport, rotatingList.length, isPaused, goToNext]);

if (!config) return null;

return (
<section className="flex flex-col items-center px-3 sm:px-8 xl:px-[136px] pb-[10px] pt-[36px] max-w-[1444px] mx-auto mb-[40px]" aria-labelledby="latest-updates-heading">
<h2
id="latest-updates-heading"
className="text-[#345D85] text-[24px] sm:text-[28px] md:text-[32px] [font-family:Inter] font-semibold leading-[38px] w-full"
>
{config.title}
</h2>
<div className="w-full mt-[31px] sm:mt-8 md:mt-[31px]">
<div className="w-full">
<div className="flex flex-wrap sm:flex-nowrap justify-center md:justify-end gap-4 lg:gap-[32px]">
<section
className="flex flex-col items-center px-4 pt-[24px] pb-[45px] sm:pt-[36px] sm:pb-[10px] md:pt-[30px] md:pb-[10px] gallery-lg:px-0 max-w-[1440px] mx-auto mb-[40px]"
aria-labelledby="latest-updates-heading"
>
<div className="w-full gallery-lg:max-w-[1165px] gallery-lg:mx-auto">
<h2
id="latest-updates-heading"
className="text-[#345D85] text-[24px] sm:text-[28px] md:text-[32px] [font-family:Inter] font-semibold leading-[38px] w-full"
>
{config.title}
</h2>
<div className="w-full mt-6 sm:mt-8 md:mt-[24px] gallery-lg:mt-[31px]">
<div className="w-full flex flex-col">
{/* Mobile: 6-item sliding track; viewport shows 3 cards; smooth slide then reset for infinite */}
<div className="md:hidden w-full">
<div
className="overflow-hidden mx-auto"
style={{ width: 3 * CARD_WIDTH_MOBILE + 2 * CARD_GAP_MOBILE }}
>
<div
ref={slideTrackRef}
className="flex gap-[28px] py-1"
style={{
width: 6 * CARD_WIDTH_MOBILE + 5 * CARD_GAP_MOBILE,
transform: `translateX(${slideOffset}px)`,
transition: slideTransitionEnabled
? "transform 0.45s ease-in-out"
: "none",
}}
onTransitionEnd={handleSlideTransitionEnd}
role="region"
aria-label="Latest updates carousel"
>
{rotatingList.map((update: GalleryUpdate, idx: number) => (
<div
key={`${idx}-${update.title}-${update.link}`}
className="flex-shrink-0 w-[214px] rounded-[0px_28px_0px_28px]"
>
<UpdateCard
image={update.image}
title={update.title}
description={update.description}
link={update.link}
/>
</div>
))}
</div>
</div>
{/* Pagination: Left | Pause | Right — infinite, no disabled */}
<div className="flex gap-[18px] items-center justify-center shrink-0 w-full mt-[30px]">
<button
type="button"
onClick={goToPrev}
aria-label="Previous slide"
className="group flex items-center justify-center rounded-full border border-[#4BBFC6] bg-transparent w-[39.158px] h-[39.158px] text-[#6B7280] touch-manipulation cursor-pointer"
>
<div
className="mr-[3px] w-0 h-0 border-l-[8px] border-l-transparent border-r-[8px] border-r-transparent border-b-[11px] border-b-[#C9C9C9] group-hover:border-b-[#8D9096] border-t-0 border-t-transparent -rotate-90"
aria-hidden
/>
</button>
<button
type="button"
onClick={() => setIsPaused((p) => !p)}
aria-label={isPaused ? "Play carousel" : "Pause carousel"}
className="flex items-center justify-center rounded-full border border-[#4BBFC6] bg-transparent w-[39.158px] h-[39.158px] text-[#6B7280] touch-manipulation cursor-pointer shadow-[0px_2.49px_9.32px_0px_#00000073]"
>
<Image
src={isPaused ? startIcon : pauseIcon}
alt={isPaused ? "Play carousel" : "Pause carousel"}
width={isPaused ? 20 : 18}
height={isPaused ? 22 : 18}
className={isPaused ? "w-[20px] h-[22px] pl-[4px]" : "w-[18px] h-[18px]"}
/>
</button>
<button
type="button"
onClick={goToNext}
aria-label="Next slide"
className="group flex items-center justify-center rounded-full border border-[#4BBFC6] bg-transparent w-[39.158px] h-[39.158px] text-[#6B7280] touch-manipulation cursor-pointer"
>
<div
className="ml-[3px] w-0 h-0 border-l-[8px] border-l-transparent border-r-[8px] border-r-transparent border-t-[11px] border-t-[#C9C9C9] group-hover:border-t-[#8D9096] border-b-0 border-b-transparent -rotate-90"
aria-hidden
/>
</button>
</div>
</div>

{/* Tablet + Desktop: row of cards; desktop only title and cards left-aligned */}
<div className="hidden md:flex flex-wrap sm:flex-nowrap justify-center md:justify-center gallery-lg:justify-start gap-4 md:gap-[33px] gallery-lg:gap-[32px]">
{config.updates.map((update: GalleryUpdate, idx: number) => (
<div className="w-full sm:flex-1 sm:min-w-0 sm:max-w-[367px]" key={idx}>
<div
className="w-full sm:flex-1 sm:min-w-0 sm:max-w-[367px] md:w-[215px] md:min-w-0 md:max-w-[215px] md:shrink-0 gallery-lg:flex-1 gallery-lg:max-w-[367px]"
key={idx}
>
<div className="flex items-center rounded-[0px_28px_0px_28px]">
<div className="my-auto w-full">
<UpdateCard
Expand All @@ -53,9 +214,11 @@ const Gallery: React.FC<GalleryProps> = ({ data }) => {
</div>
))}
</div>
{/* Button below the cards */}
<div className="flex justify-end mt-[22px] w-full">
<a

{/* Button: centered on mobile; on tablet align right with rightmost card (711px); on desktop in 1165px container */}
<div className="w-full md:max-w-[711px] md:mx-auto gallery-lg:max-w-full gallery-lg:mx-0">
<div className="flex justify-center mt-[24px] w-full md:justify-end">
<a
href={config.newsletterButtonLink}
target="_blank"
rel="noopener noreferrer"
Expand All @@ -68,13 +231,14 @@ const Gallery: React.FC<GalleryProps> = ({ data }) => {
textAlign: "center",
}}
>
<span className="bg-[#06324E] text-white text-center [font-family:Poppins] text-[12px] font-semibold leading-[16px] tracking-[0.24px] uppercase flex h-[41px] py-[12px] px-[30px] justify-center items-center relative pb-[14px]">
<span className="bg-[#06324E] text-white text-center [font-family:Poppins] text-[12px] font-semibold leading-[16px] tracking-[0.24px] uppercase flex h-[41px] py-[12px] px-[30px] justify-center items-center rounded-[5px]">
{config.newsletterButtonText}
</span>
</a>
{/* End button */}
</div>
</div>
</div>
</div>
</div>
</section>
);
Expand Down
Loading