Merge pull request 'fea/thien-15052026-fe-partners-carousel-sponsor-update' (#8) from fea/thien-15052026-fe-partners-carousel-sponsor-update into feat/duong-11052026-IPV6SUBMIT

Reviewed-on: UKSOURCE/ipv6#8
This commit is contained in:
2026-05-15 09:39:37 +00:00
25 changed files with 239 additions and 42 deletions
+2
View File
@@ -5,6 +5,7 @@ import HeroSection from "@/app/components/home/HeroSection";
import VisionSection from "@/app/components/home/VisionSection"; import VisionSection from "@/app/components/home/VisionSection";
import StatsSection from "@/app/components/home/StatsSection"; import StatsSection from "@/app/components/home/StatsSection";
import AgendaSection from "@/app/components/home/AgendaSection"; import AgendaSection from "@/app/components/home/AgendaSection";
import PartnersCarousel from "@/app/components/home/PartnersCarousel";
export default function HomePageClient() { export default function HomePageClient() {
const { home } = useTranslation(); const { home } = useTranslation();
@@ -15,6 +16,7 @@ export default function HomePageClient() {
<VisionSection data={home.vision} /> <VisionSection data={home.vision} />
<StatsSection data={home.stats} /> <StatsSection data={home.stats} />
<AgendaSection data={home.agenda} /> <AgendaSection data={home.agenda} />
<PartnersCarousel />
</main> </main>
); );
} }
+103
View File
@@ -0,0 +1,103 @@
"use client";
import { useEffect, useState } from "react";
// Tên file chính xác trong public/assets/img/carousel/
const ROW_1 = [
{ name: "NVIDIA", file: "Nvidia_logo.svg.png" },
{ name: "Maxis", file: "Maxis-logo.png" },
{ name: "Singtel", file: "Singtel_logo.svg.png" },
{ name: "NIDA", file: "nida.png" },
{ name: "VNNIC", file: "vnnic.png" },
{ name: "Netflix", file: "Logonetflix.png" },
{ name: "Microsoft",file: "Microsoft-Logo.png" },
{ name: "Alibaba", file: "Alibaba-Logo.png" },
];
const ROW_2 = [
{ name: "Tencent", file: "tencent.png" },
{ name: "AirTrunk", file: "airtrunk.png" },
{ name: "Celcomdigi",file: "celcomdigi.png" },
{ name: "Grab", file: "grab.png" },
{ name: "Logo 1", file: "logo_transparent.png" },
{ name: "Logo 2", file: "Equinix_logo.svg.png" },
{ name: "Logo 3", file: "XL_Axiata-Logo.wine.png" },
{ name: "Logo 4", file: "Huawei_Standard_logo.svg.png" },
];
function CarouselRow({
items,
base,
reverse = false,
}: {
items: { name: string; file: string }[];
base: string;
reverse?: boolean;
}) {
// Lặp 3 lần để đảm bảo không bao giờ thấy khoảng trống
const tripled = [...items, ...items, ...items];
// Mỗi item: w=150px, gap=20px → 1 set = items.length * 170 - 20
const setWidth = items.length * 150 + (items.length - 1) * 20;
return (
<div className="overflow-hidden w-full">
<div
className="flex gap-5 w-max"
style={{
animation: `${reverse ? "scroll-reverse" : "scroll-fwd"} ${items.length * 3}s linear infinite`,
["--set-width" as string]: `${setWidth}px`,
}}
>
{tripled.map((item, i) => (
<div
key={`${item.file}-${i}`}
className="flex items-center justify-center shrink-0 w-[150px] h-[76px] rounded-xl border border-white/10 hover:border-primary/40 transition-colors duration-300 overflow-hidden"
style={{ background: "rgba(255,255,255,0.82)" }}
>
{/* eslint-disable-next-line @next/next/no-img-element */}
<img
src={`${base}/assets/img/carousel/${item.file}`}
alt={item.name}
style={{
maxHeight: 48,
maxWidth: 118,
width: "auto",
height: "auto",
objectFit: "contain",
mixBlendMode: "multiply",
filter: "contrast(1.05)",
}}
/>
</div>
))}
</div>
</div>
);
}
export default function PartnersCarousel() {
const [base, setBase] = useState("");
useEffect(() => {
const match = window.location.pathname.match(/^(\/ipv6)/);
setBase(match ? match[1] : "");
}, []);
return (
<section className="w-full py-16 lg:py-24 overflow-hidden border-t border-white/5">
<div className="mb-10 px-[var(--spacing-gutter)] max-w-hd mx-auto">
<p className="text-primary font-[var(--font-label-caps)] text-xs tracking-[0.3em] uppercase mb-3">
Partners &amp; Sponsors
</p>
<h2 className="text-on-surface font-[var(--font-display-lg)] text-2xl lg:text-4xl font-bold">
Trusted by Industry Leaders
</h2>
</div>
<div className="flex flex-col gap-5">
<CarouselRow items={ROW_1} base={base} reverse={false} />
<CarouselRow items={ROW_2} base={base} reverse={true} />
</div>
</section>
);
}
+17 -17
View File
@@ -19,28 +19,28 @@ export default function SponsorContact({ data }: { data: SponsorContactData }) {
<div className="relative z-10 grid grid-cols-1 lg:grid-cols-2 gap-12 lg:gap-24 items-center"> <div className="relative z-10 grid grid-cols-1 lg:grid-cols-2 gap-12 lg:gap-24 items-center">
{/* Left Content */} {/* Left Content */}
<div className="space-y-12"> <div className="space-y-8">
<div> <div>
<h2 className="font-[var(--font-display-lg)] text-[clamp(2rem,4vw,4rem)] text-on-surface mb-6 lg:mb-8 leading-[1.1]"> <h2 className="font-[var(--font-display-lg)] text-[clamp(1.75rem,3.5vw,3rem)] text-on-surface mb-4 leading-[1.1]">
{data.title} {data.title}
</h2> </h2>
<p className="font-body-base text-base lg:text-[20px] text-on-surface-variant max-w-xl leading-relaxed opacity-80"> <p className="font-body-base text-sm lg:text-[17px] text-on-surface-variant max-w-xl leading-relaxed opacity-80">
{data.description} {data.description}
</p> </p>
</div> </div>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-8 lg:gap-12"> <div className="grid grid-cols-1 sm:grid-cols-2 gap-5">
{/* Phone */} {/* Phone */}
<div className="group"> <div className="group">
<div className="flex items-center gap-6"> <div className="flex items-center gap-4">
<div className="w-16 h-16 border border-primary/20 bg-primary/5 rounded-2xl flex items-center justify-center group-hover:border-primary/50 group-hover:bg-primary/10 transition-all duration-300"> <div className="w-13 h-13 shrink-0 border border-primary/20 bg-primary/5 rounded-xl flex items-center justify-center group-hover:border-primary/50 group-hover:bg-primary/10 transition-all duration-300">
<span className="material-symbols-outlined text-primary text-3xl">call</span> <span className="material-symbols-outlined text-primary text-2xl">call</span>
</div> </div>
<div> <div className="min-w-0">
<p className="font-[var(--font-label-caps)] text-[10px] lg:text-xs font-bold tracking-[0.25em] uppercase text-primary mb-2 opacity-60"> <p className="font-[var(--font-label-caps)] text-[10px] font-bold tracking-[0.2em] uppercase text-primary mb-1 opacity-60">
{data.phone.label} {data.phone.label}
</p> </p>
<p className="font-[var(--font-display-lg)] text-lg lg:text-2xl text-on-surface"> <p className="font-[var(--font-display-lg)] text-base text-on-surface whitespace-nowrap">
{data.phone.value} {data.phone.value}
</p> </p>
</div> </div>
@@ -49,15 +49,15 @@ export default function SponsorContact({ data }: { data: SponsorContactData }) {
{/* Email */} {/* Email */}
<div className="group"> <div className="group">
<div className="flex items-center gap-6"> <div className="flex items-center gap-4">
<div className="w-16 h-16 border border-primary/20 bg-primary/5 rounded-2xl flex items-center justify-center group-hover:border-primary/50 group-hover:bg-primary/10 transition-all duration-300"> <div className="w-13 h-13 shrink-0 border border-primary/20 bg-primary/5 rounded-xl flex items-center justify-center group-hover:border-primary/50 group-hover:bg-primary/10 transition-all duration-300">
<span className="material-symbols-outlined text-primary text-3xl">mail</span> <span className="material-symbols-outlined text-primary text-2xl">mail</span>
</div> </div>
<div> <div className="min-w-0">
<p className="font-[var(--font-label-caps)] text-[10px] lg:text-xs font-bold tracking-[0.25em] uppercase text-primary mb-2 opacity-60"> <p className="font-[var(--font-label-caps)] text-[10px] font-bold tracking-[0.2em] uppercase text-primary mb-1 opacity-60">
{data.email.label} {data.email.label}
</p> </p>
<p className="font-[var(--font-display-lg)] text-lg lg:text-2xl text-on-surface break-all"> <p className="font-[var(--font-display-lg)] text-base text-on-surface truncate">
{data.email.value} {data.email.value}
</p> </p>
</div> </div>
@@ -67,7 +67,7 @@ export default function SponsorContact({ data }: { data: SponsorContactData }) {
<button <button
type="button" type="button"
className="gold-gradient-bg text-on-primary w-full sm:w-auto px-12 lg:px-16 py-5 lg:py-6 rounded-2xl font-bold uppercase tracking-widest text-sm lg:text-base shadow-2xl shadow-primary/20 hover:shadow-primary/40 transition-all duration-500 active:scale-95 flex items-center justify-center gap-4 group" className="gold-gradient-bg text-on-primary w-full sm:w-auto px-8 lg:px-10 py-3.5 rounded-xl font-bold uppercase tracking-widest text-sm shadow-2xl shadow-primary/20 hover:shadow-primary/40 transition-all duration-500 active:scale-95 flex items-center justify-center gap-3 group"
> >
{data.cta} {data.cta}
<span className="material-symbols-outlined group-hover:translate-x-2 transition-transform"> <span className="material-symbols-outlined group-hover:translate-x-2 transition-transform">
+14 -1
View File
@@ -1,4 +1,7 @@
"use client";
import Link from "next/link"; import Link from "next/link";
import { useLanguage } from "@/app/context/LanguageContext";
type SponsorHeroData = { type SponsorHeroData = {
eyebrow: string; eyebrow: string;
@@ -9,7 +12,15 @@ type SponsorHeroData = {
secondaryCta: { label: string; href: string }; secondaryCta: { label: string; href: string };
}; };
const PROSPECTUS_LINKS: Record<string, string> = {
en: "https://drive.google.com/file/d/12uNVGFQ5ixuL2UwJpb6d-x-iuYjAt7Pc/view",
vi: "https://drive.google.com/file/d/1YDpYwdWJHbxKa66u0Iq5PQ4aEaWgvkfI/view",
};
export default function SponsorHero({ data }: { data: SponsorHeroData }) { export default function SponsorHero({ data }: { data: SponsorHeroData }) {
const { lang } = useLanguage();
const prospectusHref = PROSPECTUS_LINKS[lang] ?? PROSPECTUS_LINKS.en;
return ( return (
<section className="relative py-16 md:py-32 overflow-hidden text-center px-[var(--spacing-gutter)]"> <section className="relative py-16 md:py-32 overflow-hidden text-center px-[var(--spacing-gutter)]">
<div className="relative z-10 max-w-4xl mx-auto w-full"> <div className="relative z-10 max-w-4xl mx-auto w-full">
@@ -36,7 +47,9 @@ export default function SponsorHero({ data }: { data: SponsorHeroData }) {
{/* CTAs */} {/* CTAs */}
<div className="flex flex-col sm:flex-row items-center justify-center gap-4 md:gap-6"> <div className="flex flex-col sm:flex-row items-center justify-center gap-4 md:gap-6">
<Link <Link
href={data.primaryCta.href} href={prospectusHref}
target="_blank"
rel="noopener noreferrer"
className="gold-gradient-bg text-on-primary px-10 py-4 rounded-xl font-bold uppercase tracking-widest text-xs md:text-sm shadow-xl shadow-primary/20 hover:shadow-primary/30 transition-all duration-300 active:scale-95 w-full sm:w-auto" className="gold-gradient-bg text-on-primary px-10 py-4 rounded-xl font-bold uppercase tracking-widest text-xs md:text-sm shadow-xl shadow-primary/20 hover:shadow-primary/30 transition-all duration-300 active:scale-95 w-full sm:w-auto"
> >
{data.primaryCta.label} {data.primaryCta.label}
+21 -6
View File
@@ -2,6 +2,7 @@ type ItemCard = {
icon: string; icon: string;
title: string; title: string;
status: string; status: string;
benefits?: string[];
}; };
type Highlight = { type Highlight = {
@@ -24,6 +25,11 @@ export default function SponsorItemBased({ data }: { data: SponsorItemBasedData
<div className="max-w-hd mx-auto"> <div className="max-w-hd mx-auto">
{/* Header */} {/* Header */}
<div className="text-center mb-16 lg:mb-28"> <div className="text-center mb-16 lg:mb-28">
{data.eyebrow && (
<p className="text-primary font-[var(--font-label-caps)] text-xs tracking-[0.3em] uppercase mb-4 opacity-80">
{data.eyebrow}
</p>
)}
<h2 className="font-[var(--font-display-lg)] text-[clamp(1.75rem,3.5vw,3.5rem)] mb-4 md:mb-6 text-on-surface leading-tight"> <h2 className="font-[var(--font-display-lg)] text-[clamp(1.75rem,3.5vw,3.5rem)] mb-4 md:mb-6 text-on-surface leading-tight">
{data.title} {data.title}
</h2> </h2>
@@ -33,21 +39,30 @@ export default function SponsorItemBased({ data }: { data: SponsorItemBasedData
</div> </div>
{/* 4-col cards */} {/* 4-col cards */}
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-6 lg:gap-10 mb-16 lg:mb-24"> <div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-6 lg:gap-8 mb-16 lg:mb-24">
{data.items.map((item) => ( {data.items.map((item) => (
<div <div
key={item.title} key={item.title}
className="glass-panel p-8 lg:p-12 rounded-3xl text-center group hover:bg-surface-container-high transition-all duration-500 transform hover:-translate-y-2 border-primary/10" className="glass-panel p-8 rounded-3xl group hover:bg-surface-container-high transition-all duration-500 transform hover:-translate-y-2 border-primary/10 flex flex-col items-center text-center"
> >
<div className="w-16 h-16 gold-gradient-bg rounded-2xl flex items-center justify-center mx-auto mb-8 rotate-3 group-hover:rotate-0 transition-transform duration-500 shadow-xl shadow-primary/20"> <div className="w-14 h-14 gold-gradient-bg rounded-2xl flex items-center justify-center mb-6 rotate-3 group-hover:rotate-0 transition-transform duration-500 shadow-xl shadow-primary/20">
<span className="material-symbols-outlined text-on-primary text-3xl lg:text-4xl"> <span className="material-symbols-outlined text-on-primary text-2xl">
{item.icon} {item.icon}
</span> </span>
</div> </div>
<h4 className="font-[var(--font-display-lg)] text-xl lg:text-2xl mb-3 text-on-surface"> <h4 className="font-[var(--font-display-lg)] text-lg lg:text-xl mb-2 text-on-surface leading-tight">
{item.title} {item.title}
</h4> </h4>
<p className="text-[11px] font-bold tracking-[0.3em] uppercase text-primary"> {item.benefits && item.benefits.length > 0 && (
<ul className="mt-3 mb-4 space-y-1.5 flex-1">
{item.benefits.map((b) => (
<li key={b} className="text-sm lg:text-base text-on-surface-variant/80">
{b}
</li>
))}
</ul>
)}
<p className="text-xs font-bold tracking-[0.3em] uppercase text-primary mt-auto">
{item.status} {item.status}
</p> </p>
</div> </div>
+27 -7
View File
@@ -71,24 +71,44 @@
] ]
}, },
"itemBased": { "itemBased": {
"eyebrow": "À LA CARTE", "eyebrow": "ENGAGEMENT CATEGORIES",
"title": "Item-Based Sponsorship", "title": "Item-Based Sponsorship",
"description": "Targeted branding opportunities for specific summit pillars", "description": "Targeted branding opportunities for specific summit pillars",
"items": [ "items": [
{ "icon": "hotel", "title": "Hotel Accommodation", "status": "Limited Space" }, {
{ "icon": "meeting_room", "title": "Conference Hall", "status": "Sold Out" }, "icon": "location_city",
{ "icon": "groups", "title": "Networking Zone", "status": "Available" }, "title": "Venue Sponsor",
{ "icon": "newsmode", "title": "Media Partner", "status": "Apply Now" } "status": "Register Now",
"benefits": ["Hotel venue", "Conference hall", "Networking area"]
},
{
"icon": "computer",
"title": "Tech Sponsor",
"status": "Register Now",
"benefits": ["AV equipment", "WiFi / Internet", "Tech support"]
},
{
"icon": "newspaper",
"title": "Media Sponsor",
"status": "Register Now",
"benefits": ["Press coverage", "Social media", "Media coverage"]
},
{
"icon": "card_giftcard",
"title": "Gift & F&B Sponsor",
"status": "Register Now",
"benefits": ["Gift set", "Coffee break", "Networking dinner"]
}
], ],
"highlights": [ "highlights": [
{ {
"eyebrow": "STANDARD BENEFIT", "eyebrow": "STANDARD BENEFIT",
"title": "Logo on backdrop & LED screens", "title": "Logo on backdrop, website, social media & LED",
"icon": "branding_watermark" "icon": "branding_watermark"
}, },
{ {
"eyebrow": "ACCESS PACK", "eyebrow": "ACCESS PACK",
"title": "02 VIP Delegate tickets included", "title": "01 VIP Pass + Networking Dinner",
"icon": "confirmation_number" "icon": "confirmation_number"
} }
] ]
+27 -7
View File
@@ -71,24 +71,44 @@
] ]
}, },
"itemBased": { "itemBased": {
"eyebrow": "À LA CARTE", "eyebrow": "HẠNG MỤC ĐỒNG HÀNH",
"title": "Tài Trợ Theo Hạng Mục", "title": "Tài Trợ Theo Hạng Mục",
"description": "Cơ hội thương hiệu có mục tiêu cho các trụ cột cụ thể của hội nghị", "description": "Cơ hội thương hiệu có mục tiêu cho các trụ cột cụ thể của hội nghị",
"items": [ "items": [
{ "icon": "hotel", "title": "Chỗ Ở Khách Sạn", "status": "Còn Ít Chỗ" }, {
{ "icon": "meeting_room", "title": "Hội Trường Hội Nghị", "status": "Đã Hết" }, "icon": "location_city",
{ "icon": "groups", "title": "Khu Vực Kết Nối", "status": "Còn Trống" }, "title": "Tài Trợ Địa Điểm",
{ "icon": "newsmode", "title": "Đối Tác Truyền Thông", "status": "Đăng Ký Ngay" } "status": "Đăng Ký Ngay",
"benefits": ["Khách sạn", "Hội trường", "Khu vực networking"]
},
{
"icon": "computer",
"title": "Tài Trợ Công Nghệ",
"status": "Đăng Ký Ngay",
"benefits": ["Thiết bị trình chiếu", "WiFi / Internet", "Hạ tầng kỹ thuật"]
},
{
"icon": "newspaper",
"title": "Tài Trợ Truyền Thông",
"status": "Đăng Ký Ngay",
"benefits": ["Báo chí", "Social media", "Media coverage"]
},
{
"icon": "card_giftcard",
"title": "Tài Trợ Quà Tặng / F&B",
"status": "Đăng Ký Ngay",
"benefits": ["Gift set", "Coffee break", "Networking dinner"]
}
], ],
"highlights": [ "highlights": [
{ {
"eyebrow": "QUYỀN LỢI TIÊU CHUẨN", "eyebrow": "QUYỀN LỢI TIÊU CHUẨN",
"title": "Logo trên phông & màn hình LED", "title": "Logo tại backdrop, website, social media & LED",
"icon": "branding_watermark" "icon": "branding_watermark"
}, },
{ {
"eyebrow": "GÓI TIẾP CẬN", "eyebrow": "GÓI TIẾP CẬN",
"title": "02 vé VIP đại biểu được bao gồm", "title": "01 vé VIP + tham dự Networking Dinner",
"icon": "confirmation_number" "icon": "confirmation_number"
} }
] ]
+28 -4
View File
@@ -1,5 +1,13 @@
@import "tailwindcss"; @import "tailwindcss";
@font-face {
font-family: "Nasalization";
src: url("/assets/webfonts/Nasalization Rg.otf") format("opentype");
font-weight: normal;
font-style: normal;
font-display: swap;
}
@theme { @theme {
/* IPV6 Summit design tokens */ /* IPV6 Summit design tokens */
--color-primary: #c5a059; --color-primary: #c5a059;
@@ -16,9 +24,9 @@
--color-on-surface-variant: #e0e0e0; --color-on-surface-variant: #e0e0e0;
--color-outline: #c5a059; --color-outline: #c5a059;
--font-body-base: "Inter", system-ui, sans-serif; --font-body-base: "Nasalization", system-ui, sans-serif;
--font-display-lg: "Space Grotesk", system-ui, sans-serif; --font-display-lg: "Nasalization", system-ui, sans-serif;
--font-label-caps: "Geist", system-ui, sans-serif; --font-label-caps: "Nasalization", system-ui, sans-serif;
--font-mono: "JetBrains Mono", ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", --font-mono: "JetBrains Mono", ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono",
"Courier New", monospace; "Courier New", monospace;
@@ -37,7 +45,7 @@ html {
body { body {
background-color: var(--color-background); background-color: var(--color-background);
color: var(--color-on-surface-variant); color: var(--color-on-surface-variant);
font-family: var(--font-body-base); font-family: "Nasalization", system-ui, sans-serif;
overflow-x: hidden; overflow-x: hidden;
} }
@@ -98,3 +106,19 @@ textarea::placeholder {
box-shadow: 0 0 8px currentColor; box-shadow: 0 0 8px currentColor;
} }
/* Partners Carousel animations */
@keyframes scroll-fwd {
0% { transform: translateX(0); }
100% { transform: translateX(calc(-1 * var(--set-width) - 20px)); }
}
@keyframes scroll-reverse {
0% { transform: translateX(calc(-1 * var(--set-width) - 20px)); }
100% { transform: translateX(0); }
}
.animate-scroll:hover,
.animate-scroll-reverse:hover {
animation-play-state: paused;
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 92 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 95 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 326 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 192 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 169 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 212 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 58 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 113 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 132 KiB

Binary file not shown.