Merge pull request 'feat/duong-11052026-IPV6SUBMIT' (#9) from feat/duong-11052026-IPV6SUBMIT into develop

Reviewed-on: UKSOURCE/ipv6#9
This commit is contained in:
2026-05-15 12:51:20 +00:00
38 changed files with 569 additions and 104 deletions
+2
View File
@@ -1 +1,3 @@
PORT=3000 PORT=3000
# MongoDB — summit request queue (registration + feedback)
MONGODB_URI=mongodb://localhost:27017/ipv6_summit
+5
View File
@@ -0,0 +1,5 @@
import LoginPageClient from "@/app/components/forms/LoginPageClient";
export default function LoginPage() {
return <LoginPageClient />;
}
+48
View File
@@ -0,0 +1,48 @@
import { NextResponse } from "next/server";
import dbConnect from "@/lib/mongodb";
import User from "@/models/User";
import crypto from "crypto";
function hashPassword(password: string) {
return crypto.createHash("sha256").update(password).digest("hex");
}
export async function POST(request: Request) {
try {
await dbConnect();
const { email, password } = await request.json();
// Tìm user và chuẩn hóa email về chữ thường
const user = await User.findOne({ email: email.toLowerCase().trim() });
if (!user) {
console.log(`Login failed: User not found -> ${email}`);
return NextResponse.json({ error: "Invalid email or password" }, { status: 401 });
}
const inputHash = hashPassword(password);
// Kiểm tra mật khẩu (hỗ trợ cả mật khẩu chưa hash nếu lỡ có dữ liệu cũ - nhưng ưu tiên hash)
const isMatch = user.password === inputHash || user.password === password;
if (!isMatch) {
console.log(`Login failed: Password mismatch for -> ${email}`);
return NextResponse.json({ error: "Invalid email or password" }, { status: 401 });
}
const response = NextResponse.json({ success: true });
response.cookies.set("admin_session", "true", {
httpOnly: true,
secure: process.env.NODE_ENV === "production",
sameSite: "lax",
path: "/",
maxAge: 60 * 60 * 24,
});
return response;
} catch (error) {
console.error("Login error:", error);
return NextResponse.json({ error: "Internal server error" }, { status: 500 });
}
}
+13
View File
@@ -0,0 +1,13 @@
import { NextResponse } from "next/server";
export async function POST() {
const response = NextResponse.json({ success: true });
// Xóa cookie bằng cách set maxAge = 0
response.cookies.set("admin_session", "", {
path: "/",
maxAge: 0,
});
return response;
}
+41 -12
View File
@@ -227,6 +227,16 @@ export default function AdminConsole({ data, view = "all" }: { data: AdminData;
const [detail, setDetail] = useState<AdminRequest | null>(null); const [detail, setDetail] = useState<AdminRequest | null>(null);
const { lang, setLang } = useLanguage(); const { lang, setLang } = useLanguage();
const { admin: t } = useTranslation(); const { admin: t } = useTranslation();
const [userDropdown, setUserDropdown] = useState(false);
const handleLogout = async () => {
try {
await fetch("/ipv6/api/auth/logout", { method: "POST" });
router.push("/login");
} catch (error) {
console.error("Logout failed", error);
}
};
// Merge real requests from server prop with translated UI labels // Merge real requests from server prop with translated UI labels
const tData = { const tData = {
@@ -295,7 +305,9 @@ export default function AdminConsole({ data, view = "all" }: { data: AdminData;
{ href: "/admin/rejected", icon: "cancel", label: lang === "vi" ? "Từ chối" : "Rejected" }, { href: "/admin/rejected", icon: "cancel", label: lang === "vi" ? "Từ chối" : "Rejected" },
] as const ] as const
).map((item) => { ).map((item) => {
const active = pathname === item.href; const cleanPathname = pathname.endsWith("/") ? pathname.slice(0, -1) : pathname;
const cleanHref = item.href.endsWith("/") ? item.href.slice(0, -1) : item.href;
const active = cleanPathname === cleanHref;
return ( return (
<Link <Link
key={item.href} key={item.href}
@@ -317,13 +329,7 @@ export default function AdminConsole({ data, view = "all" }: { data: AdminData;
); );
})} })}
</nav> </nav>
<button {/* Settings button removed as per request */}
type="button"
className="group flex flex-col items-center gap-1 text-on-surface-variant hover:text-primary mt-auto"
>
<span className="material-symbols-outlined text-2xl transition-all duration-300 group-hover:rotate-45">settings</span>
<span className="font-[var(--font-label-caps)] text-[8px] uppercase tracking-widest">{lang === "vi" ? "Cài đặt" : "Settings"}</span>
</button>
</aside> </aside>
<div className="flex-1 ml-20 md:ml-24 flex flex-col min-w-0"> <div className="flex-1 ml-20 md:ml-24 flex flex-col min-w-0">
@@ -382,10 +388,33 @@ export default function AdminConsole({ data, view = "all" }: { data: AdminData;
<span>VIE</span> <span>VIE</span>
</button> </button>
</div> </div>
<div className="flex flex-col items-end"> <div className="relative">
<span className="font-[var(--font-label-caps)] text-[10px] text-primary uppercase tracking-widest"> <button
{t.meta.user.role} type="button"
</span> onClick={() => setUserDropdown(!userDropdown)}
className="flex flex-col items-end group"
>
<span className="font-[var(--font-label-caps)] text-[10px] text-primary uppercase tracking-widest group-hover:text-primary/80 transition-colors">
{t.meta.user.role}
</span>
<span className="material-symbols-outlined text-outline group-hover:text-primary transition-colors text-xs">
{userDropdown ? "expand_less" : "expand_more"}
</span>
</button>
{userDropdown && (
<div className="absolute right-0 mt-2 w-48 glass-panel rounded-xl shadow-2xl border border-white/10 z-[60] overflow-hidden">
<button
onClick={handleLogout}
className="w-full px-4 py-3 flex items-center gap-3 text-left hover:bg-white/5 text-on-surface transition-colors border-b border-white/5"
>
<span className="material-symbols-outlined text-red-400 text-lg">logout</span>
<span className="text-xs font-[var(--font-label-caps)] uppercase tracking-wider">
{lang === "vi" ? "Đăng xuất" : "Log Out"}
</span>
</button>
</div>
)}
</div> </div>
</div> </div>
</header> </header>
+119
View File
@@ -0,0 +1,119 @@
"use client";
import Link from "next/link";
import { useState } from "react";
import { useRouter } from "next/navigation";
import { useLanguage } from "@/app/context/LanguageContext";
export default function LoginPageClient() {
const { lang } = useLanguage();
const router = useRouter();
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
const [error, setError] = useState("");
const [loading, setLoading] = useState(false);
const backLabel = lang === "vi" ? "Quay Về Trang Chủ" : "Back to Home";
const titleLabel = lang === "vi" ? "Đăng Nhập Quản Trị" : "Admin Login";
const emailLabel = lang === "vi" ? "Email" : "Email Address";
const passwordLabel = lang === "vi" ? "Mật Khẩu" : "Password";
const buttonLabel = lang === "vi" ? "Đăng Nhập" : "Log In";
const loadingLabel = lang === "vi" ? "Đang xử lý..." : "Processing...";
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setLoading(true);
setError("");
try {
const res = await fetch("/ipv6/api/auth/login", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ email, password }),
});
if (res.ok) {
router.push("/admin");
} else {
const data = await res.json();
setError(data.error || (lang === "vi" ? "Đăng nhập thất bại" : "Login failed"));
}
} catch (err) {
setError(lang === "vi" ? "Đã xảy ra lỗi" : "An error occurred");
} finally {
setLoading(false);
}
};
return (
<main className="pt-[120px] pb-[80px] px-[var(--spacing-gutter)] flex justify-center min-h-screen">
<div className="w-full max-w-[1280px]">
<div className="max-w-[500px] 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>
{backLabel}
</Link>
</div>
<div className="mb-[32px]">
<h1 className="font-[var(--font-display-lg)] text-[clamp(1.75rem,3vw+1rem,3.25rem)] text-on-surface mb-2 leading-[1.1]">
{titleLabel}
</h1>
</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" />
<form onSubmit={handleSubmit} className="space-y-6">
{error && (
<div className="bg-error/10 border border-error/20 text-error px-4 py-3 rounded-lg text-sm">
{error}
</div>
)}
<div className="space-y-1">
<label className="text-outline uppercase tracking-wider text-xs">{emailLabel}</label>
<input
type="email"
required
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 focus:border-primary/50 outline-none"
placeholder=""
value={email}
onChange={(e) => setEmail(e.target.value)}
/>
</div>
<div className="space-y-1">
<label className="text-outline uppercase tracking-wider text-xs">{passwordLabel}</label>
<input
type="password"
required
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 focus:border-primary/50 outline-none"
placeholder=""
value={password}
onChange={(e) => setPassword(e.target.value)}
/>
</div>
<div className="pt-4">
<button
type="submit"
disabled={loading}
className="gold-gradient-bg text-on-primary w-full px-6 py-4 rounded-xl font-[var(--font-display-lg)] text-lg uppercase tracking-wider hover:shadow-[0_0_30px_rgba(197,160,89,0.4)] transition-all duration-300 active:scale-[0.98] disabled:opacity-60"
>
{loading ? loadingLabel : buttonLabel}
</button>
</div>
</form>
</div>
</div>
</div>
</main>
);
}
+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>
);
}
@@ -29,7 +29,9 @@ export default function HeaderClient({
}; };
const registerLabel = lang === "vi" ? "Đăng Ký" : "Register"; const registerLabel = lang === "vi" ? "Đăng Ký" : "Register";
const loginLabel = lang === "vi" ? "Đăng Nhập" : "Log In";
const registerNowLabel = lang === "vi" ? "Đăng Ký Ngay" : "Register Now"; const registerNowLabel = lang === "vi" ? "Đăng Ký Ngay" : "Register Now";
const loginNowLabel = lang === "vi" ? "Đăng Nhập" : "Log In";
const backLabel = lang === "vi" ? "Trang Chủ" : "Home"; const backLabel = lang === "vi" ? "Trang Chủ" : "Home";
useEffect(() => { useEffect(() => {
@@ -106,6 +108,12 @@ export default function HeaderClient({
</button> </button>
</div> </div>
<Link
href="/login"
className="hidden sm:inline-flex gold-gradient-bg text-on-primary px-4 lg:px-6 py-2 lg:py-2.5 font-[var(--font-body-base)] text-xs lg:text-sm font-bold uppercase tracking-wider rounded-lg active:scale-95 transition-transform shadow-lg shadow-primary/10"
>
{loginLabel}
</Link>
<Link <Link
href="/registration" href="/registration"
className="hidden sm:inline-flex gold-gradient-bg text-on-primary px-4 lg:px-6 py-2 lg:py-2.5 font-[var(--font-body-base)] text-xs lg:text-sm font-bold uppercase tracking-wider rounded-lg active:scale-95 transition-transform shadow-lg shadow-primary/10" className="hidden sm:inline-flex gold-gradient-bg text-on-primary px-4 lg:px-6 py-2 lg:py-2.5 font-[var(--font-body-base)] text-xs lg:text-sm font-bold uppercase tracking-wider rounded-lg active:scale-95 transition-transform shadow-lg shadow-primary/10"
@@ -205,6 +213,13 @@ export default function HeaderClient({
</button> </button>
</div> </div>
<Link
href="/login"
className="gold-gradient-bg text-on-primary w-full py-5 font-[var(--font-body-base)] text-lg font-bold uppercase tracking-widest rounded-xl text-center shadow-2xl"
onClick={() => setMobileOpen(false)}
>
{loginNowLabel}
</Link>
<Link <Link
href="/registration" href="/registration"
className="gold-gradient-bg text-on-primary w-full py-5 font-[var(--font-body-base)] text-lg font-bold uppercase tracking-widest rounded-xl text-center shadow-2xl" className="gold-gradient-bg text-on-primary w-full py-5 font-[var(--font-body-base)] text-lg font-bold uppercase tracking-widest rounded-xl text-center shadow-2xl"
+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>
+3 -3
View File
@@ -1,10 +1,10 @@
{ {
"title": "Phản Hồi Sự Kiện", "title": "Khảo sát Sự Kiện",
"subtitle": "Ý kiến của bạn định hướng tương lai hạ tầng IPv6. Hãy giúp chúng tôi cải thiện trải nghiệm năm 2027.", "subtitle": "Giúp chúng tôi nâng tầm trải nghiệm 2027, thúc đẩy tương lai hạ tầng IPv6 bằng đánh giá của bạn.",
"sections": [ "sections": [
{ {
"id": "overall", "id": "overall",
"title": "1. TRẢI NGHIỆM TỔNG THỂ", "title": "1. MỨC ĐỘ HÀI LÒNG TỔNG QUAN",
"subtitle": "Tổng quan về trải nghiệm hội nghị của bạn", "subtitle": "Tổng quan về trải nghiệm hội nghị của bạn",
"questions": [ "questions": [
{ {
+2 -2
View File
@@ -2,8 +2,8 @@
"hero": { "hero": {
"badgeIcon": "calendar_today", "badgeIcon": "calendar_today",
"badgeText": "Tháng 6/2026 | TP. Hồ Chí Minh, Việt Nam", "badgeText": "Tháng 6/2026 | TP. Hồ Chí Minh, Việt Nam",
"title": "Hội Nghị IPv6 cho AI & Trung Tâm Dữ Liệu 2026", "title": "IPv6 for AI & Data Centre Summit 2026",
"subtitle": "Khai PTiềm Năng Hạ Tầng Thế HMới. Cùng các nhà lãnh đạo toàn cầu định hình lại kết nối số ASEAN trong kỷ nguyên AI.", "subtitle": "Khám ptiềm năng Hạ tầng thế hmới. Chung sức định hình kỷ nguyên kết nối số tại ASEAN cùng doanh nghiệp hàng đầu quốc tế.",
"primaryCta": { "label": "Đăng Ký", "href": "/registration" }, "primaryCta": { "label": "Đăng Ký", "href": "/registration" },
"secondaryCta": { "label": "Xem Lịch Trình", "href": "#agenda" } "secondaryCta": { "label": "Xem Lịch Trình", "href": "#agenda" }
}, },
+8 -8
View File
@@ -1,6 +1,6 @@
{ {
"title": "Đăng Ký Tham Dự Hội Nghị", "title": "Đăng Ký Tham Dự Hội Nghị",
"subtitle": "Đảm bảo sự hiện diện của bạn ở tuyến đầu đổi mới IPv6 cho AI và Trung Tâm DLiệu.", "subtitle": "Nắm bắt cơ hội tiên phong thời kỳ đổi mới IPv6 cho AI và trung tâm dliệu.",
"sections": { "sections": {
"personal": { "personal": {
"title": "Thông Tin Cá Nhân", "title": "Thông Tin Cá Nhân",
@@ -9,13 +9,13 @@
"email": "Email Doanh Nghiệp" "email": "Email Doanh Nghiệp"
}, },
"professional": { "professional": {
"title": "Thông Tin Nghề Nghiệp", "title": "Thông Tin Doanh Nghiệp",
"company": "Tên Công Ty", "company": "Tên Doanh Nghiệp",
"jobTitle": "Chức Danh" "jobTitle": "Vị Trí"
}, },
"industry": { "industry": {
"title": "Ngành Nghề", "title": "Lĩnh Vực",
"label": "Chọn Ngành Nghề", "label": "Chọn Lĩnh Vực",
"placeholder": "Vui lòng chọn...", "placeholder": "Vui lòng chọn...",
"options": [ "options": [
{ "value": "government", "label": "Chính Phủ" }, { "value": "government", "label": "Chính Phủ" },
@@ -27,8 +27,8 @@
}, },
"additional": { "additional": {
"title": "Thông Tin Bổ Sung", "title": "Thông Tin Bổ Sung",
"label": "Yêu cầu đặc biệt hoặc chế độ ăn kiêng", "label": "Yêu cầu đặc biệt hoặc chế độ dinh dưỡng",
"placeholder": "Tùy chọn ăn chay, nhu cầu tiếp cận, v.v." "placeholder": "Chế độ ăn chay, hỗ trợ người khuyết tật, v.v"
} }
}, },
"submit": { "submit": {
+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, LED",
"icon": "branding_watermark" "icon": "branding_watermark"
}, },
{ {
"eyebrow": "ACCESS PACK", "eyebrow": "ACCESS PACK",
"title": "02 VIP Delegate tickets included", "title": "02 VIP Pass",
"icon": "confirmation_number" "icon": "confirmation_number"
} }
] ]
+31 -11
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 nổi bật trên backdrop, màn LED",
"icon": "branding_watermark" "icon": "branding_watermark"
}, },
{ {
"eyebrow": "GÓI TIẾP CẬN", "eyebrow": "QUYỀN LỢI THAM DỰ",
"title": "02 vé VIP đại biểu được bao gồm", "title": "Đã bao gồm 02 vé VIP đại biểu",
"icon": "confirmation_number" "icon": "confirmation_number"
} }
] ]
@@ -96,16 +116,16 @@
"contact": { "contact": {
"id": "contact", "id": "contact",
"title": "Hợp Tác Cùng Chúng Tôi", "title": "Hợp Tác Cùng Chúng Tôi",
"description": "Nâng tầm thương hiệu của bạn trong hành lang công nghệ Ấn Độ - Thái Bình Dương. Hội nghị của chúng tôi mang đến khả năng tiếp cận vô song tới các nhà ra quyết định, nhà nghiên cứu và quan chức chính phủ đang thúc đẩy thế hệ hạ tầng trung tâm dữ liệu và phát triển AI.", "description": "Nâng tầm thương hiệu doanh nghiệp trên khu vực công nghệ Ấn Độ - Thái Bình Dương. Hội thảo mang đến cơ hội hiếm có đến các tổ chức chính phủ, lãnh đạo chủ chốt, đơn vị nghiên cứu trực tiếp tham giao quá trình phát triển AI và Cơ sở hạ tầng dữ liệu số.",
"phone": { "phone": {
"label": "ĐIỆN THOẠI", "label": "ĐIỆN THOẠI",
"value": "+84 941 523 498" "value": "+84 941 523 498"
}, },
"email": { "email": {
"label": "EMAIL TRỰC TIẾP", "label": "EMAIL TƯ VẤN",
"value": "events@techvanguard.vn" "value": "events@techvanguard.vn"
}, },
"cta": "Liên Hệ Để Tài Trợ", "cta": "Kết nối nhà tài trợ",
"image": { "image": {
"src": "https://lh3.googleusercontent.com/aida-public/AB6AXuATSk4rhRYAo9_OfX0cJwIC7rUdUmzD9kO7cJM24ECjPHwglU_s0wjBn6gdK0d7tk4ExLvG9DUJQ1L-PbP5ucVH2THO05fev6bWgyvpkElf_94JXJNeWj0Mo4-5-l2ofY-kHE6kPLhitbRzarR7u41favHCt_V9WM0yYb7tcZZQM8eAN5jIVT5OVKSGvfpZNYe05dJm_h3pW0qkWhX1XrUtg_xhhjdvTi-Mei7zHDUkwwRrydg7BHmO17W6ScfALH5rMXojTHub7UDN", "src": "https://lh3.googleusercontent.com/aida-public/AB6AXuATSk4rhRYAo9_OfX0cJwIC7rUdUmzD9kO7cJM24ECjPHwglU_s0wjBn6gdK0d7tk4ExLvG9DUJQ1L-PbP5ucVH2THO05fev6bWgyvpkElf_94JXJNeWj0Mo4-5-l2ofY-kHE6kPLhitbRzarR7u41favHCt_V9WM0yYb7tcZZQM8eAN5jIVT5OVKSGvfpZNYe05dJm_h3pW0qkWhX1XrUtg_xhhjdvTi-Mei7zHDUkwwRrydg7BHmO17W6ScfALH5rMXojTHub7UDN",
"alt": "Cuộc họp kinh doanh chuyên nghiệp trong phòng hội thảo công nghệ cao" "alt": "Cuộc họp kinh doanh chuyên nghiệp trong phòng hội thảo công nghệ cao"
+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;
}
+6 -33
View File
@@ -1,41 +1,14 @@
import type { NextRequest } from "next/server"; import type { NextRequest } from "next/server";
import { NextResponse } from "next/server"; import { NextResponse } from "next/server";
function unauthorized() {
return new NextResponse("Authentication required", {
status: 401,
headers: {
"WWW-Authenticate": 'Basic realm="IPv6 Admin"',
},
});
}
export function middleware(request: NextRequest) { export function middleware(request: NextRequest) {
const user = process.env.ADMIN_BASIC_AUTH_USER; const session = request.cookies.get("admin_session");
const pass = process.env.ADMIN_BASIC_AUTH_PASS;
if (!user || !pass) { if (!session || session.value !== "true") {
return NextResponse.next(); // If not authenticated and trying to access admin, redirect to login
} const url = request.nextUrl.clone();
url.pathname = "/login";
const auth = request.headers.get("authorization"); return NextResponse.redirect(url);
if (!auth?.startsWith("Basic ")) {
return unauthorized();
}
let decoded: string;
try {
decoded = atob(auth.slice(6));
} catch {
return unauthorized();
}
const colon = decoded.indexOf(":");
const u = colon >= 0 ? decoded.slice(0, colon) : "";
const p = colon >= 0 ? decoded.slice(colon + 1) : "";
if (u !== user || p !== pass) {
return new NextResponse("Unauthorized", { status: 401 });
} }
return NextResponse.next(); return NextResponse.next();
+18
View File
@@ -0,0 +1,18 @@
import mongoose, { Schema, Document } from "mongoose";
export interface IUser extends Document {
email: string;
password: string;
createdAt: Date;
updatedAt: Date;
}
const UserSchema: Schema = new Schema(
{
email: { type: String, required: true, unique: true },
password: { type: String, required: true },
},
{ timestamps: true }
);
export default mongoose.models.User || mongoose.model<IUser>("User", UserSchema);
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.
+46
View File
@@ -0,0 +1,46 @@
import mongoose from "mongoose";
import User from "../models/User";
import * as dotenv from "dotenv";
import path from "path";
import crypto from "crypto";
dotenv.config({ path: path.join(__dirname, "../.env") });
function hashPassword(password: string) {
return crypto.createHash("sha256").update(password).digest("hex");
}
async function seedAdmin() {
const MONGODB_URI = process.env.MONGODB_URI;
if (!MONGODB_URI) {
console.error("MONGODB_URI not found");
process.exit(1);
}
try {
await mongoose.connect(MONGODB_URI);
console.log("Connected to MongoDB");
const adminEmail = "adminipv6@mail.com".toLowerCase().trim();
const adminPassword = "adminipv6A@a";
const hashedPassword = hashPassword(adminPassword);
// Xóa sạch user cũ để tránh xung đột
await User.deleteMany({ email: adminEmail });
await User.create({
email: adminEmail,
password: hashedPassword,
});
console.log("SUCCESS: Admin account created/refreshed with hashed password.");
console.log(`Email: ${adminEmail}`);
} catch (error) {
console.error("Error:", error);
} finally {
await mongoose.disconnect();
}
}
seedAdmin();