This commit is contained in:
2026-05-11 16:24:14 +07:00
commit 6a48d6351c
313 changed files with 41936 additions and 0 deletions
+13
View File
@@ -0,0 +1,13 @@
import Header from "@/app/components/layout/Header/Header";
import Footer from "@/app/components/layout/Footer/Footer";
export default function SiteLayout({ children }: { children: React.ReactNode }) {
return (
<>
<Header />
{children}
<Footer />
</>
);
}
+17
View File
@@ -0,0 +1,17 @@
import home from "@/app/data/home.json";
import HeroSection from "@/app/components/home/HeroSection";
import VisionSection from "@/app/components/home/VisionSection";
import StatsSection from "@/app/components/home/StatsSection";
import AgendaSection from "@/app/components/home/AgendaSection";
export default function HomePage() {
return (
<main className="pt-24 lg:pt-32">
<HeroSection data={home.hero} />
<VisionSection data={home.vision} />
<StatsSection data={home.stats} />
<AgendaSection data={home.agenda} />
</main>
);
}
+38
View File
@@ -0,0 +1,38 @@
import Link from "next/link";
import RegistrationForm from "@/app/components/forms/RegistrationForm";
import data from "@/app/data/registration.json";
export default function RegistrationPage() {
return (
<main className="pt-[160px] pb-[80px] px-[var(--spacing-gutter)] flex justify-center">
<div className="w-full max-w-[1280px]">
<div className="max-w-[800px] mx-auto">
<div className="mb-[32px]">
<Link
className="group flex items-center gap-2 text-outline hover:text-primary transition-colors"
href="/"
>
<span className="material-symbols-outlined text-[20px] transition-transform group-hover:-translate-x-1">
arrow_back
</span>
Back to Home
</Link>
</div>
<div className="mb-[32px]">
<h1 className="font-[var(--font-display-lg)] text-[clamp(2.25rem,3vw+1rem,4.5rem)] text-on-surface mb-2 leading-[1.1]">
{data.title}
</h1>
<p className="text-on-surface-variant text-lg">{data.subtitle}</p>
</div>
<div className="glass-panel p-[32px] md:p-12 rounded-xl shadow-2xl relative overflow-hidden">
<div className="absolute top-0 left-0 w-full h-[1px] bg-gradient-to-r from-transparent via-primary/20 to-transparent" />
<RegistrationForm data={data} />
</div>
</div>
</div>
</main>
);
}
+167
View File
@@ -0,0 +1,167 @@
"use client";
import { useMemo, useState } from "react";
type IndustryOption = { value: string; label: string };
export default function RegistrationForm({
data,
}: {
data: {
sections: {
personal: { title: string; fullName: string; phone: string; email: string };
professional: { title: string; company: string; jobTitle: string };
industry: { title: string; label: string; placeholder: string; options: IndustryOption[] };
additional: { title: string; label: string; placeholder: string };
};
submit: { label: string; disclaimer: string };
};
}) {
const industries = useMemo(() => data.sections.industry.options, [data.sections.industry.options]);
const [form, setForm] = useState({
fullName: "",
phone: "",
email: "",
company: "",
jobTitle: "",
industry: "",
notes: "",
});
return (
<form
className="space-y-[32px]"
onSubmit={(e) => {
e.preventDefault();
// frontend-only scope
console.log("[registration]", form);
alert("Saved locally (frontend-only).");
}}
>
<div className="space-y-[16px]">
<h2 className="font-[var(--font-display-lg)] text-[20px] text-primary border-l-2 border-primary pl-3">
{data.sections.personal.title}
</h2>
<div className="grid grid-cols-1 md:grid-cols-2 gap-[16px]">
<div className="space-y-1">
<label className="text-outline uppercase tracking-wider">{data.sections.personal.fullName}</label>
<input
className="w-full bg-surface-container-low border border-white/10 rounded-lg px-4 py-3 text-on-surface placeholder:text-outline/40 transition-all"
placeholder="John Doe"
value={form.fullName}
onChange={(e) => setForm((s) => ({ ...s, fullName: e.target.value }))}
/>
</div>
<div className="space-y-1">
<label className="text-outline uppercase tracking-wider">{data.sections.personal.phone}</label>
<input
className="w-full bg-surface-container-low border border-white/10 rounded-lg px-4 py-3 text-on-surface placeholder:text-outline/40 transition-all"
placeholder="+84 ..."
value={form.phone}
onChange={(e) => setForm((s) => ({ ...s, phone: e.target.value }))}
/>
</div>
<div className="md:col-span-2 space-y-1">
<label className="text-outline uppercase tracking-wider">{data.sections.personal.email}</label>
<input
className="w-full bg-surface-container-low border border-white/10 rounded-lg px-4 py-3 text-on-surface placeholder:text-outline/40 transition-all"
placeholder="john.doe@company.com"
type="email"
value={form.email}
onChange={(e) => setForm((s) => ({ ...s, email: e.target.value }))}
/>
</div>
</div>
</div>
<hr className="border-white/5" />
<div className="space-y-[16px]">
<h2 className="font-[var(--font-display-lg)] text-[20px] text-primary border-l-2 border-primary pl-3">
{data.sections.professional.title}
</h2>
<div className="grid grid-cols-1 md:grid-cols-2 gap-[16px]">
<div className="space-y-1">
<label className="text-outline uppercase tracking-wider">{data.sections.professional.company}</label>
<input
className="w-full bg-surface-container-low border border-white/10 rounded-lg px-4 py-3 text-on-surface placeholder:text-outline/40 transition-all"
placeholder="Company"
value={form.company}
onChange={(e) => setForm((s) => ({ ...s, company: e.target.value }))}
/>
</div>
<div className="space-y-1">
<label className="text-outline uppercase tracking-wider">{data.sections.professional.jobTitle}</label>
<input
className="w-full bg-surface-container-low border border-white/10 rounded-lg px-4 py-3 text-on-surface placeholder:text-outline/40 transition-all"
placeholder="Network Architect"
value={form.jobTitle}
onChange={(e) => setForm((s) => ({ ...s, jobTitle: e.target.value }))}
/>
</div>
</div>
</div>
<hr className="border-white/5" />
<div className="space-y-[16px]">
<h2 className="font-[var(--font-display-lg)] text-[20px] text-primary border-l-2 border-primary pl-3">
{data.sections.industry.title}
</h2>
<div className="space-y-1">
<label className="text-outline uppercase tracking-wider">{data.sections.industry.label}</label>
<div className="relative">
<select
className="w-full bg-surface-container-low border border-white/10 rounded-lg px-4 py-3 text-on-surface appearance-none transition-all"
value={form.industry}
onChange={(e) => setForm((s) => ({ ...s, industry: e.target.value }))}
>
<option value="" disabled>
{data.sections.industry.placeholder}
</option>
{industries.map((opt) => (
<option key={opt.value} value={opt.value}>
{opt.label}
</option>
))}
</select>
<span className="material-symbols-outlined absolute right-4 top-1/2 -translate-y-1/2 text-outline pointer-events-none">
expand_more
</span>
</div>
</div>
</div>
<hr className="border-white/5" />
<div className="space-y-[16px]">
<h2 className="font-[var(--font-display-lg)] text-[20px] text-primary border-l-2 border-primary pl-3">
{data.sections.additional.title}
</h2>
<div className="space-y-1">
<label className="text-outline uppercase tracking-wider">{data.sections.additional.label}</label>
<textarea
className="w-full bg-surface-container-low border border-white/10 rounded-lg px-4 py-3 text-on-surface placeholder:text-outline/40 transition-all resize-none"
placeholder={data.sections.additional.placeholder}
rows={4}
value={form.notes}
onChange={(e) => setForm((s) => ({ ...s, notes: e.target.value }))}
/>
</div>
</div>
<div className="pt-[16px]">
<button
className="w-full gold-gradient-bg py-5 rounded-lg text-on-primary font-[var(--font-display-lg)] text-[18px] font-bold shadow-[0_10px_30px_rgba(197,160,89,0.15)] hover:shadow-[0_15px_40px_rgba(197,160,89,0.25)] hover:-translate-y-0.5 active:translate-y-0 active:scale-[0.98] transition-all"
type="submit"
>
{data.submit.label}
</button>
<p className="mt-[16px] text-center text-outline text-[11px] tracking-widest opacity-60 uppercase">
{data.submit.disclaimer}
</p>
</div>
</form>
);
}
+57
View File
@@ -0,0 +1,57 @@
export default function AgendaSection({
data,
}: {
data: {
id: string;
eyebrow: string;
title: string;
description: string;
items: { order: string; time: string; title: string; description: string }[];
};
}) {
return (
<section className="py-16 lg:py-32 max-w-hd mx-auto px-[var(--spacing-gutter)]" id={data.id}>
<div className="flex flex-col xl:flex-row justify-between xl:items-end mb-12 lg:mb-24 gap-8">
<div className="max-w-3xl">
<span className="font-[var(--font-label-caps)] text-xs lg:text-sm text-primary block mb-4 lg:mb-8 tracking-[0.25em]">
{data.eyebrow}
</span>
<h2 className="font-[var(--font-display-lg)] text-on-surface uppercase tracking-tight text-[clamp(2rem,4vw+1rem,4.5rem)]">
{data.title}
</h2>
</div>
<p className="text-on-surface-variant/60 text-base lg:text-[20px] max-w-md xl:text-right">
{data.description}
</p>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-6 lg:gap-10">
{data.items.map((item) => (
<div
key={item.order}
className="glass-panel p-8 lg:p-12 rounded-3xl relative overflow-hidden group hover:border-primary/60 transition-colors"
>
<div className="absolute top-0 right-0 p-6 lg:p-8">
<span className="font-[var(--font-display-lg)] text-2xl lg:text-[40px] text-primary/20 group-hover:text-primary transition-colors">
{item.order}
</span>
</div>
<div className="mb-6 lg:mb-10">
<span className="font-[var(--font-display-lg)] text-2xl lg:text-[36px] text-primary">
{item.time}
</span>
</div>
<h3 className="font-[var(--font-display-lg)] text-2xl lg:text-[32px] text-on-surface mb-4 lg:mb-6">
{item.title}
</h3>
<p className="text-sm lg:text-[18px] text-on-surface-variant/80 leading-relaxed mb-6 lg:mb-8">
{item.description}
</p>
<div className="w-full h-1 bg-primary/10 group-hover:bg-primary transition-colors duration-500" />
</div>
))}
</div>
</section>
);
}
+52
View File
@@ -0,0 +1,52 @@
import Link from "next/link";
export default function HeroSection({
data,
}: {
data: {
badgeIcon: string;
badgeText: string;
title: string;
subtitle: string;
primaryCta: { label: string; href: string };
secondaryCta: { label: string; href: string };
};
}) {
return (
<section className="relative min-h-[90vh] lg:min-h-screen flex items-center overflow-hidden bg-background hero-glow px-[var(--spacing-gutter)]">
<div className="relative z-10 max-w-hd mx-auto w-full">
<div className="max-w-7xl">
<div className="inline-flex items-center gap-3 lg:gap-4 px-4 lg:px-8 py-2 lg:py-3 rounded-full bg-surface-container-high border border-primary/30 mb-8 lg:mb-16">
<span className="material-symbols-outlined text-primary text-[20px] lg:text-[26px]">
{data.badgeIcon}
</span>
<span className="font-[var(--font-label-caps)] text-xs lg:text-[16px] text-on-surface">
{data.badgeText}
</span>
</div>
<h1 className="font-[var(--font-display-lg)] text-on-surface mb-6 lg:mb-10 leading-[1.1] text-[clamp(2.25rem,5vw+1rem,6.875rem)]">
{data.title}
</h1>
<p className="text-on-surface-variant text-lg lg:text-[28px] mb-10 lg:mb-20 max-w-4xl leading-relaxed opacity-90">
{data.subtitle}
</p>
<div className="flex flex-col sm:flex-row gap-4 lg:gap-10">
<Link
href={data.primaryCta.href}
className="gold-gradient-bg text-on-primary px-8 lg:px-16 py-5 lg:py-7 font-[var(--font-display-lg)] text-sm lg:text-[20px] font-bold uppercase tracking-widest rounded-xl hover:shadow-[0_0_50px_rgba(197,160,89,0.4)] transition-all text-center"
>
{data.primaryCta.label}
</Link>
<Link
href={data.secondaryCta.href}
className="bg-surface-container-high border border-primary/40 text-on-surface px-8 lg:px-16 py-5 lg:py-7 font-[var(--font-display-lg)] text-sm lg:text-[20px] font-bold uppercase tracking-widest rounded-xl hover:bg-surface-container-highest transition-all text-center"
>
{data.secondaryCta.label}
</Link>
</div>
</div>
</div>
</section>
);
}
+28
View File
@@ -0,0 +1,28 @@
export default function StatsSection({
data,
}: {
data: { id: string; items: { value: string; label: string }[] };
}) {
return (
<section
className="py-16 lg:py-32 bg-surface-container-low border-y border-primary/10"
id={data.id}
>
<div className="max-w-hd mx-auto px-[var(--spacing-gutter)]">
<div className="grid grid-cols-1 md:grid-cols-3 gap-12 lg:gap-32 text-center">
{data.items.map((x) => (
<div key={x.label} className="space-y-3 lg:space-y-6">
<div className="font-[var(--font-display-lg)] text-5xl lg:text-[96px] text-primary">
{x.value}
</div>
<div className="font-[var(--font-label-caps)] text-sm lg:text-[18px] text-on-surface/80 tracking-widest uppercase">
{x.label}
</div>
</div>
))}
</div>
</div>
</section>
);
}
+49
View File
@@ -0,0 +1,49 @@
export default function VisionSection({
data,
}: {
data: {
eyebrow: string;
title: string;
paragraphs: string[];
cards: { icon: string; title: string; description: string }[];
};
}) {
return (
<section className="py-16 lg:py-32 max-w-hd mx-auto px-[var(--spacing-gutter)]">
<div className="grid grid-cols-1 lg:grid-cols-12 gap-12 lg:gap-32 items-start">
<div className="lg:col-span-5">
<span className="font-[var(--font-label-caps)] text-xs lg:text-sm text-primary mb-6 lg:mb-10 block tracking-[0.25em]">
{data.eyebrow}
</span>
<h2 className="font-[var(--font-display-lg)] text-on-surface mb-8 lg:mb-12 leading-tight text-[clamp(2rem,4vw+1rem,4.5rem)]">
{data.title}
</h2>
<div className="space-y-6 lg:space-y-10 text-on-surface-variant text-lg lg:text-[22px] leading-relaxed">
{data.paragraphs.map((p) => (
<p key={p}>{p}</p>
))}
</div>
</div>
<div className="lg:col-span-7 grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-6 lg:gap-10">
{data.cards.map((c) => (
<div
key={c.title}
className="glass-panel p-8 lg:p-12 rounded-2xl group transition-all duration-500 hover:lg:-translate-y-4 hover:shadow-2xl hover:shadow-primary/5"
>
<span className="material-symbols-outlined text-primary text-[40px] lg:text-[56px] mb-8 lg:mb-12 block">
{c.icon}
</span>
<h3 className="font-[var(--font-display-lg)] text-xl lg:text-[28px] text-on-surface mb-4 lg:mb-6">
{c.title}
</h3>
<p className="text-sm lg:text-[18px] text-on-surface-variant/80 leading-relaxed">
{c.description}
</p>
</div>
))}
</div>
</div>
</section>
);
}
+28
View File
@@ -0,0 +1,28 @@
import Link from "next/link";
import siteData from "@/app/data/site.json";
export default function Footer() {
return (
<footer className="w-full bg-surface-container-low border-t border-white/5 py-12 lg:py-20">
<div className="flex flex-col xl:flex-row justify-between items-center px-[var(--spacing-gutter)] gap-12 w-full max-w-hd mx-auto">
<div className="font-[var(--font-label-caps)] text-xl lg:text-[24px] text-primary tracking-[0.4em] font-bold uppercase">
{siteData.brand.name}
</div>
<div className="flex flex-wrap justify-center gap-8 lg:gap-16">
{siteData.footer.links.map((l) => (
<Link
key={l.label}
className="text-on-surface-variant font-[var(--font-label-caps)] text-xs lg:text-sm hover:text-primary transition-colors"
href={l.href}
>
{l.label}
</Link>
))}
</div>
<div className="text-[14px] lg:text-[16px] text-on-surface-variant/40 text-center xl:text-right max-w-sm leading-relaxed uppercase tracking-widest">
{siteData.footer.copyright}
</div>
</div>
</footer>
);
}
+6
View File
@@ -0,0 +1,6 @@
import HeaderClient from "./HeaderClient";
import siteData from "@/app/data/site.json";
export default function Header() {
return <HeaderClient brand={siteData.brand} nav={siteData.nav} />;
}
@@ -0,0 +1,113 @@
"use client";
import Link from "next/link";
import { usePathname } from "next/navigation";
import { useEffect, useMemo, useState } from "react";
type NavItem = { label: string; href: string };
export default function HeaderClient({
brand,
nav,
}: {
brand: { name: string };
nav: NavItem[];
}) {
const pathname = usePathname();
const [mobileOpen, setMobileOpen] = useState(false);
const isHome = pathname === "/";
const navItems = useMemo(() => nav.filter((x) => x.href !== "/"), [nav]);
useEffect(() => {
if (!mobileOpen) return;
document.body.style.overflow = "hidden";
return () => {
document.body.style.overflow = "";
};
}, [mobileOpen]);
return (
<header className="fixed top-0 w-full z-50 bg-surface/80 backdrop-blur-xl border-b border-white/5">
<div className="flex justify-between items-center px-[var(--spacing-gutter)] py-4 lg:py-8 w-full max-w-hd mx-auto">
<Link
href="/"
className="font-[var(--font-display-lg)] text-[20px] sm:text-[22px] lg:text-[36px] text-primary tracking-tighter uppercase font-bold"
>
{brand.name}
</Link>
<nav className="hidden xl:flex items-center gap-10 2xl:gap-16">
<Link
className={`font-[var(--font-label-caps)] text-sm ${
isHome ? "text-primary border-b-2 border-primary pb-1" : "text-on-surface-variant hover:text-primary"
} transition-colors duration-300`}
href="/"
>
Home
</Link>
{navItems.map((item) => (
<Link
key={item.href}
className="text-on-surface-variant font-[var(--font-label-caps)] text-sm hover:text-primary transition-colors duration-300"
href={item.href}
>
{item.label}
</Link>
))}
</nav>
<div className="flex items-center gap-4 lg:gap-10">
<span className="hidden sm:block text-on-surface-variant/60 font-[var(--font-label-caps)] text-xs">
EN
</span>
<Link
href="/registration"
className="hidden sm:inline-flex gold-gradient-bg text-on-primary px-6 lg:px-10 py-3 lg:py-4 font-[var(--font-body-base)] text-sm lg:text-[18px] font-bold uppercase tracking-wider rounded-lg active:scale-95 transition-transform shadow-lg shadow-primary/10"
>
Register
</Link>
<button
type="button"
className="xl:hidden text-primary p-2"
aria-label="Toggle menu"
aria-expanded={mobileOpen}
onClick={() => setMobileOpen((v) => !v)}
>
<span className="material-symbols-outlined text-3xl">{mobileOpen ? "close" : "menu"}</span>
</button>
</div>
</div>
<div
className={`fixed inset-0 top-[72px] lg:top-[104px] bg-surface z-40 flex-col p-[var(--spacing-gutter)] gap-8 xl:hidden border-t border-white/5 ${
mobileOpen ? "flex" : "hidden"
}`}
>
<Link className="text-2xl font-[var(--font-display-lg)] text-on-surface" href="/" onClick={() => setMobileOpen(false)}>
Home
</Link>
{navItems.map((item) => (
<Link
key={item.href}
className="text-2xl font-[var(--font-display-lg)] text-on-surface"
href={item.href}
onClick={() => setMobileOpen(false)}
>
{item.label}
</Link>
))}
<div className="mt-auto flex flex-col gap-6">
<div className="text-on-surface-variant/60 font-[var(--font-label-caps)] text-sm">Language: EN</div>
<Link
href="/registration"
className="gold-gradient-bg text-on-primary w-full py-5 font-[var(--font-body-base)] text-lg font-bold uppercase tracking-wider rounded-lg text-center"
onClick={() => setMobileOpen(false)}
>
Register Now
</Link>
</div>
</div>
</header>
);
}
+70
View File
@@ -0,0 +1,70 @@
{
"hero": {
"badgeIcon": "calendar_today",
"badgeText": "June 2026 | Ho Chi Minh City, Vietnam",
"title": "IPv6 for AI & Data Centre Summit 2026",
"subtitle": "Unlocking the Potential of Next-Gen Infrastructure. Join global leaders in redefining ASEAN's digital connectivity for the age of AI.",
"primaryCta": { "label": "Register", "href": "/registration" },
"secondaryCta": { "label": "View Agenda", "href": "#agenda" }
},
"vision": {
"eyebrow": "VISION & GOALS",
"title": "ASEAN's Digital Future",
"paragraphs": [
"Vietnam and the broader ASEAN region are at a critical juncture in digital transformation. The transition to IPv6 is the fundamental infrastructure for AI growth and massive data centre scaling.",
"IPv6 is the backbone for scalable cloud, secure connectivity, and AI-ready networking across the region."
],
"cards": [
{
"icon": "hub",
"title": "Networking",
"description": "Connect with C-level executives, policymakers, and technical leaders across the ecosystem."
},
{
"icon": "insights",
"title": "Tech Insights",
"description": "Deep dives into AI workloads, hyperscale infrastructure, and next-gen IPv6 operations."
},
{
"icon": "gavel",
"title": "Policy",
"description": "A forum for national infrastructure standards, governance, and cross-border collaboration."
}
]
},
"stats": {
"id": "stats",
"items": [
{ "value": "130+", "label": "Global Speakers" },
{ "value": "14", "label": "ASEAN Nations" },
{ "value": "50+", "label": "Tech Partners" }
]
},
"agenda": {
"id": "agenda",
"eyebrow": "SCHEDULE",
"title": "Event Agenda",
"description": "A strategic overview of the technical and strategic sessions.",
"items": [
{
"order": "01",
"time": "08:30",
"title": "Opening Ceremony",
"description": "Welcome address by MIC Vietnam and regional digital economy leaders."
},
{
"order": "02",
"time": "10:00",
"title": "AI in Data Centres",
"description": "How IPv6 enables hyperscale processing and low-latency fabric."
},
{
"order": "03",
"time": "14:00",
"title": "Forum: Policy & Scale",
"description": "Closed-door discussion for regulators and industry leaders."
}
]
}
}
+27
View File
@@ -0,0 +1,27 @@
{
"title": "Summit Registration",
"subtitle": "Secure your presence at the forefront of IPv6 innovation for AI and Data Centres.",
"sections": {
"personal": { "title": "Personal Info", "fullName": "Full Name", "phone": "Phone Number", "email": "Business Email" },
"professional": { "title": "Professional Info", "company": "Company Name", "jobTitle": "Job Title" },
"industry": {
"title": "Industry",
"label": "Select Industry",
"placeholder": "Please choose...",
"options": [
{ "value": "government", "label": "Government" },
{ "value": "telecom", "label": "Telecom" },
{ "value": "cloud", "label": "Cloud / Data Centre" },
{ "value": "ai", "label": "AI / Software" },
{ "value": "others", "label": "Others" }
]
},
"additional": {
"title": "Additional Details",
"label": "Special requirements or dietary restrictions",
"placeholder": "Vegetarian options, accessibility needs, etc."
}
},
"submit": { "label": "Submit Registration", "disclaimer": "By registering, you agree to our Privacy Policy and Terms of Service." }
}
+22
View File
@@ -0,0 +1,22 @@
{
"brand": {
"name": "IPv6 SUMMIT 2026"
},
"nav": [
{ "label": "Home", "href": "/" },
{ "label": "Registration", "href": "/registration" },
{ "label": "Feedback", "href": "/feedback" },
{ "label": "Sponsor", "href": "/sponsor" }
],
"footer": {
"links": [
{ "label": "Privacy", "href": "#" },
{ "label": "Terms", "href": "#" },
{ "label": "Press", "href": "#" },
{ "label": "Contact", "href": "#" }
],
"copyright":
"© 2026 IPv6 for AI & Data Centre Summit. Infrastructure for the Future."
}
}
+84
View File
@@ -0,0 +1,84 @@
@import "tailwindcss";
@theme {
/* IPV6 Summit design tokens */
--color-primary: #c5a059;
--color-secondary: #8e6d3d;
--color-on-primary: #261900;
--color-background: #0f0f0f;
--color-surface: #0f0f0f;
--color-surface-container-low: #141414;
--color-surface-container-high: #1f1f1f;
--color-surface-container-highest: #292929;
--color-on-surface: #ffffff;
--color-on-surface-variant: #e0e0e0;
--color-outline: #c5a059;
--font-body-base: "Inter", system-ui, sans-serif;
--font-display-lg: "Space Grotesk", system-ui, sans-serif;
--font-label-caps: "Geist", system-ui, sans-serif;
--font-mono: "JetBrains Mono", ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono",
"Courier New", monospace;
--spacing-gutter: clamp(1rem, 4vw, 3.75rem);
--spacing-container-max: 1800px;
}
html {
scroll-behavior: smooth;
}
body {
background-color: var(--color-background);
color: var(--color-on-surface-variant);
font-family: var(--font-body-base);
overflow-x: hidden;
}
input::placeholder,
textarea::placeholder {
color: rgba(224, 224, 224, 0.4);
}
.material-symbols-outlined {
font-variation-settings: "FILL" 0, "wght" 400, "GRAD" 0, "opsz" 24;
vertical-align: middle;
}
.max-w-hd {
max-width: 1920px;
}
.glass-panel {
background: rgba(15, 15, 15, 0.7);
backdrop-filter: blur(8px);
border: 1px solid rgba(197, 160, 89, 0.2);
}
@media (min-width: 1024px) {
.glass-panel {
backdrop-filter: blur(12px);
border: 1px solid rgba(197, 160, 89, 0.3);
}
}
.gold-gradient-bg {
background: linear-gradient(135deg, #c5a059 0%, #8e6d3d 100%);
}
.hero-glow {
background: radial-gradient(circle at 50% 40%, rgba(197, 160, 89, 0.08) 0%, rgba(15, 15, 15, 0) 70%);
}
@media (min-width: 1024px) {
.hero-glow {
background: radial-gradient(circle at 50% 50%, rgba(197, 160, 89, 0.12) 0%, rgba(15, 15, 15, 0) 80%);
}
}
::-webkit-scrollbar {
display: none;
}
+130
View File
@@ -0,0 +1,130 @@
{
"hero": {
"badge": "Top Ranked Online Education",
"title": "Advance Your Career Without Boundaries.",
"description": "Access world-class education from anywhere. Flexible schedules, accredited programs, and affordable tuition designed for the modern professional.",
"searchPlaceholder": "e.g. Business Administration",
"buttonLabel": "Find Program",
"image": "https://storage.googleapis.com/uxpilot-auth.appspot.com/d9383b2d27-55d93496971925dee83d.png",
"imageAlt": "diverse group of modern adult students studying online with laptops, professional lighting, cinematic, high quality",
"floatingBadge": {
"icon": "fa-users",
"value": "50,000+",
"label": "Active Students"
}
},
"quickLinks": [
{
"icon": "fa-trophy",
"title": "Milestones",
"description": "Explore our history of academic excellence and major achievements since our founding.",
"linkText": "Explore History",
"href": "/about/history"
},
{
"icon": "fa-certificate",
"title": "Accreditations",
"description": "Review our globally recognized credentials ensuring the highest quality of education.",
"linkText": "View Credentials",
"href": "/about/accreditation"
},
{
"icon": "fa-handshake",
"title": "Partnerships",
"description": "Discover our network of industry leaders providing career pathways for graduates.",
"linkText": "See Partners",
"href": "/about/partnerships"
},
{
"icon": "fa-newspaper",
"title": "Latest Blog",
"description": "Read insights, student success stories, and updates from our academic community.",
"linkText": "Read Articles",
"href": "/blog"
}
],
"valueProp": {
"badge": "Why Choose Us",
"title": "Education Engineered for the Modern World",
"description": "We believe that quality education should be accessible to everyone, regardless of location or schedule. Our platform delivers an immersive learning experience backed by industry-leading technology.",
"features": [
{
"icon": "fa-laptop-code",
"title": "100% Online Flexibility",
"description": "Study on your own time with asynchronous classes designed to fit around your work and life commitments."
},
{
"icon": "fa-piggy-bank",
"title": "Affordable Tuition",
"description": "Graduate with less debt. Our transparent pricing and financial aid options make your degree attainable."
},
{
"icon": "fa-briefcase",
"title": "Career-Focused Curriculum",
"description": "Programs developed in partnership with industry leaders to ensure you learn the skills employers actually want."
}
],
"stats": [
{
"value": "94%",
"label": "Employment rate within 6 months of graduation",
"image": "https://storage.googleapis.com/uxpilot-auth.appspot.com/b1e2e6a5a4-7c815d7e47c7e7471c5e.png",
"imageAlt": "student studying late at night looking focused"
},
{
"value": "200+",
"label": "Accredited degree and certificate programs",
"image": "https://storage.googleapis.com/uxpilot-auth.appspot.com/8e7cd5934f-dc6192e3a13ea24a5312.png",
"imageAlt": "graduation cap and diploma abstract professional setup"
}
]
},
"programs": {
"heading": "Featured Programs",
"description": "Discover our most popular career-focused degrees and certificates designed for the modern job market.",
"items": [
{
"category": "Tech & Software",
"image": "https://storage.googleapis.com/uxpilot-auth.appspot.com/f6c491e508-064cc521a89dee9776d1.png",
"duration": "12-18 Months",
"rating": "4.9",
"title": "B.S. Computer Science",
"description": "Master full-stack development, algorithms, and system design with hands-on projects.",
"studentCount": "+1k",
"href": "/programmes/bs-cs"
},
{
"category": "Data Analysis",
"image": "https://storage.googleapis.com/uxpilot-auth.appspot.com/e17be2a4e6-91247c5d9a4eed4d28a9.png",
"duration": "8-12 Months",
"rating": "4.8",
"title": "Data Analytics Certificate",
"description": "Learn SQL, Python, and Tableau to transform complex data into actionable business insights.",
"studentCount": "+800",
"href": "/programmes/cert-da"
},
{
"category": "Business",
"image": "https://storage.googleapis.com/uxpilot-auth.appspot.com/c53c1f7b37-66e95dbe58513f7bce4b.png",
"duration": "18-24 Months",
"rating": "4.9",
"title": "MBA in Leadership",
"description": "Develop strategic management skills and leadership qualities for the modern corporate world.",
"studentCount": "+2k",
"href": "/programmes/mba-db"
}
]
},
"requestInfo": {
"heading": "Take the Next Step in Your Career.",
"description": "Request more information about our programs, tuition, and admissions process. Our advisors are ready to help you plan your future.",
"phone": "12345678-GLOBAL-U",
"email": " info@lams.ac",
"programs": [
"Computer Science",
"Business Administration",
"Data Analytics",
"Nursing"
]
}
}
+44
View File
@@ -0,0 +1,44 @@
import type { Metadata } from "next";
import "../public/assets/css/all.min.css";
import "./globals.css";
export const metadata: Metadata = {
metadataBase: new URL(process.env.NEXT_PUBLIC_BASE_URL || "http://localhost:3000"),
title: "IPv6 Summit 2026 | Infrastructure for AI",
description:
"IPv6 for AI & Data Centre Summit 2026 — a premier event for next-gen infrastructure in ASEAN.",
};
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en">
<head>
{/* Fonts */}
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossOrigin="anonymous" />
<link
href="https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@300;400;500;600;700&family=Inter:wght@300;400;500;600;700&family=Geist:wght@400;600&family=JetBrains+Mono:wght@400;500&display=swap"
rel="stylesheet"
/>
{/* Icons */}
<link
rel="stylesheet"
href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.2/css/all.min.css"
/>
<link
href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&display=swap"
rel="stylesheet"
/>
{/* Favicon */}
<link
rel="shortcut icon"
href="/assets/img/favicon.png"
type="image/x-icon"
/>
</head>
<body suppressHydrationWarning>
{children}
</body>
</html>
);
}
+33
View File
@@ -0,0 +1,33 @@
import Link from "next/link";
export default function NotFound() {
return (
<div className="min-h-[60vh] flex flex-col items-center justify-center py-24 px-6 md:px-12 lg:px-24 bg-uni-grey">
<div className="max-w-2xl w-full text-center bg-white p-10 md:p-16 rounded-[32px] ref-card-shadow border border-slate-100">
{/* Icon */}
<div className="w-20 h-20 mx-auto rounded-full bg-uni-orange/10 flex items-center justify-center text-uni-orange mb-8">
<i className="fa-solid fa-triangle-exclamation text-4xl"></i>
</div>
{/* Content */}
<h1 className="font-display text-5xl md:text-6xl font-bold text-uni-dark mb-6">404</h1>
<h2 className="font-display text-2xl md:text-3xl font-bold text-uni-dark mb-4">Page Not Found</h2>
<p className="text-slate-600 text-lg mb-10 leading-relaxed">
The page you're looking for could not be found. It may have been moved or deleted.
Please check the URL or return to our home page.
</p>
{/* Action Button */}
<div className="flex justify-center">
<Link
href="/"
className="px-8 py-4 bg-uni-dark hover:bg-slate-800 text-white rounded-full font-semibold text-lg transition-all flex items-center justify-center gap-2 shadow-lg"
>
Back to Home <i className="fa-solid fa-arrow-right text-sm"></i>
</Link>
</div>
</div>
</div>
);
}
+146
View File
@@ -0,0 +1,146 @@
/**
* Converts an EditorJS JSON output (string or object) into an HTML string.
* Supports all block types used in the CMS blog editor.
*/
export function editorjsToHtml(raw: string | object | null | undefined): string {
if (!raw) return "";
let data: { blocks?: Array<{ type: string; data: Record<string, unknown> }> };
if (typeof raw === "string") {
try {
data = JSON.parse(raw);
} catch {
// Not JSON — treat as plain HTML string (legacy content)
return raw;
}
} else {
data = raw as typeof data;
}
if (!data || !Array.isArray(data.blocks) || data.blocks.length === 0) {
return "";
}
return data.blocks.map(renderBlock).join("\n");
}
function esc(text: unknown): string {
return String(text ?? "")
.replace(/&/g, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;");
}
function renderBlock(block: { type: string; data: Record<string, unknown> }): string {
const { type, data } = block;
switch (type) {
case "paragraph": {
const text = data.text as string ?? "";
return `<p>${text}</p>`;
}
case "header": {
const level = Number(data.level) || 2;
const tag = `h${Math.min(Math.max(level, 1), 6)}`;
return `<${tag}>${data.text}</${tag}>`;
}
case "list": {
const style = data.style === "ordered" ? "ol" : "ul";
const items = (data.items as string[]) ?? [];
const lis = items.map((item) => `<li>${item}</li>`).join("\n");
return `<${style}>\n${lis}\n</${style}>`;
}
case "checklist": {
const items = (data.items as Array<{ text: string; checked: boolean }>) ?? [];
const lis = items
.map(
(item) =>
`<li class="editorjs-checklist-item${item.checked ? " checked" : ""}">` +
`<span class="editorjs-checkbox">${item.checked ? "✓" : "○"}</span> ${item.text}` +
`</li>`
)
.join("\n");
return `<ul class="editorjs-checklist">\n${lis}\n</ul>`;
}
case "image": {
const file = (data.file as { url?: string }) ?? {};
const url = file.url ?? (data.url as string) ?? "";
const caption = (data.caption as string) ?? "";
const stretched = data.stretched ? ' class="editorjs-image-stretched"' : "";
const withBg = data.withBackground ? ' style="background:#f5f5f5; padding:1rem;"' : "";
return (
`<figure${withBg}>` +
`<img src="${esc(url)}" alt="${esc(caption)}"${stretched} loading="lazy" />` +
(caption ? `<figcaption>${caption}</figcaption>` : "") +
`</figure>`
);
}
case "quote": {
const text = data.text as string ?? "";
const caption = data.caption as string ?? "";
return (
`<blockquote class="editorjs-quote">` +
`<p>${text}</p>` +
(caption ? `<cite>${caption}</cite>` : "") +
`</blockquote>`
);
}
case "code": {
return `<pre><code>${esc(data.code)}</code></pre>`;
}
case "delimiter": {
return `<hr class="editorjs-delimiter" />`;
}
case "table": {
const content = (data.content as string[][]) ?? [];
const withHeadings = data.withHeadings as boolean;
const rows = content.map((row, rowIdx) => {
const tag = withHeadings && rowIdx === 0 ? "th" : "td";
const cells = row.map((cell) => `<${tag}>${cell}</${tag}>`).join("");
return `<tr>${cells}</tr>`;
});
return `<table class="editorjs-table">\n${rows.join("\n")}\n</table>`;
}
case "embed": {
const service = (data.service as string ?? "").toLowerCase();
const embedUrl = (data.embed as string) ?? "";
const caption = (data.caption as string) ?? "";
const width = (data.width as number) ?? 560;
const height = (data.height as number) ?? 315;
// EditorJS Embed stores the ready-to-use iframe src in data.embed
let iframeSrc = esc(embedUrl);
// Fallback: if data.embed is the watch URL, convert it
if (service === "youtube" && iframeSrc.includes("watch?v=")) {
iframeSrc = iframeSrc.replace("watch?v=", "embed/").split("&")[0];
} else if (service === "vimeo" && !iframeSrc.includes("player.vimeo.com")) {
const vimeoId = iframeSrc.split("/").pop() ?? "";
iframeSrc = `https://player.vimeo.com/video/${vimeoId}`;
}
return (
`<figure class="editorjs-embed">` +
`<iframe src="${iframeSrc}" width="${width}" height="${height}" frameborder="0" allowfullscreen loading="lazy"></iframe>` +
(caption ? `<figcaption>${caption}</figcaption>` : "") +
`</figure>`
);
}
default:
// Unknown block — skip silently
return "";
}
}
+13
View File
@@ -0,0 +1,13 @@
export const imageUrl = (path?: string) => {
// Không có ảnh → ảnh mặc định
if (!path) return "/_images/default.jpg";
// Đã là full URL
if (path.startsWith("http")) return path;
const base = (
process.env.NEXT_PUBLIC_API_URL || "http://localhost:3001"
).replace(/\/$/, "");
return `${base}/${path.replace(/^\//, "")}`;
};