From ad22254cc81ab4019c59281f97ec4ea4f87bbc2f Mon Sep 17 00:00:00 2001 From: VuHoangThien Date: Fri, 15 May 2026 16:35:28 +0700 Subject: [PATCH 1/2] feat: add partners carousel, update sponsor section, add Nasalization font --- app/components/home/HomePageClient.tsx | 2 + app/components/home/PartnersCarousel.tsx | 103 ++++++++++++++++++ app/components/sponsor/SponsorContact.tsx | 34 +++--- app/components/sponsor/SponsorHero.tsx | 15 ++- app/components/sponsor/SponsorItemBased.tsx | 27 ++++- app/data/sponsor.json | 34 ++++-- app/data/sponsor.vi.json | 34 ++++-- app/globals.css | 32 +++++- public/assets/img/carousel/Alibaba-Logo.png | Bin 0 -> 94369 bytes .../assets/img/carousel/Equinix_logo.svg.png | Bin 0 -> 97253 bytes .../img/carousel/Huawei_Standard_logo.svg.png | Bin 0 -> 333302 bytes public/assets/img/carousel/Logonetflix.png | Bin 0 -> 32265 bytes public/assets/img/carousel/Maxis-logo.png | Bin 0 -> 11835 bytes public/assets/img/carousel/Microsoft-Logo.png | Bin 0 -> 20089 bytes .../assets/img/carousel/Nvidia_logo.svg.png | Bin 0 -> 196212 bytes .../assets/img/carousel/Singtel_logo.svg.png | Bin 0 -> 172799 bytes .../img/carousel/XL_Axiata-Logo.wine.png | Bin 0 -> 34647 bytes public/assets/img/carousel/airtrunk.png | Bin 0 -> 7988 bytes public/assets/img/carousel/celcomdigi.png | Bin 0 -> 35343 bytes public/assets/img/carousel/grab.png | Bin 0 -> 217041 bytes .../assets/img/carousel/logo_transparent.png | Bin 0 -> 25786 bytes public/assets/img/carousel/nida.png | Bin 0 -> 59554 bytes public/assets/img/carousel/tencent.png | Bin 0 -> 116112 bytes public/assets/img/carousel/vnnic.png | Bin 0 -> 135526 bytes public/assets/webfonts/Nasalization Rg.otf | Bin 0 -> 67384 bytes 25 files changed, 239 insertions(+), 42 deletions(-) create mode 100644 app/components/home/PartnersCarousel.tsx create mode 100644 public/assets/img/carousel/Alibaba-Logo.png create mode 100644 public/assets/img/carousel/Equinix_logo.svg.png create mode 100644 public/assets/img/carousel/Huawei_Standard_logo.svg.png create mode 100644 public/assets/img/carousel/Logonetflix.png create mode 100644 public/assets/img/carousel/Maxis-logo.png create mode 100644 public/assets/img/carousel/Microsoft-Logo.png create mode 100644 public/assets/img/carousel/Nvidia_logo.svg.png create mode 100644 public/assets/img/carousel/Singtel_logo.svg.png create mode 100644 public/assets/img/carousel/XL_Axiata-Logo.wine.png create mode 100644 public/assets/img/carousel/airtrunk.png create mode 100644 public/assets/img/carousel/celcomdigi.png create mode 100644 public/assets/img/carousel/grab.png create mode 100644 public/assets/img/carousel/logo_transparent.png create mode 100644 public/assets/img/carousel/nida.png create mode 100644 public/assets/img/carousel/tencent.png create mode 100644 public/assets/img/carousel/vnnic.png create mode 100644 public/assets/webfonts/Nasalization Rg.otf diff --git a/app/components/home/HomePageClient.tsx b/app/components/home/HomePageClient.tsx index 8cd89e1..ef236b2 100644 --- a/app/components/home/HomePageClient.tsx +++ b/app/components/home/HomePageClient.tsx @@ -5,6 +5,7 @@ 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"; +import PartnersCarousel from "@/app/components/home/PartnersCarousel"; export default function HomePageClient() { const { home } = useTranslation(); @@ -15,6 +16,7 @@ export default function HomePageClient() { + ); } diff --git a/app/components/home/PartnersCarousel.tsx b/app/components/home/PartnersCarousel.tsx new file mode 100644 index 0000000..2092c72 --- /dev/null +++ b/app/components/home/PartnersCarousel.tsx @@ -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 ( +
+
+ {tripled.map((item, i) => ( +
+ {/* eslint-disable-next-line @next/next/no-img-element */} + {item.name} +
+ ))} +
+
+ ); +} + +export default function PartnersCarousel() { + const [base, setBase] = useState(""); + + useEffect(() => { + const match = window.location.pathname.match(/^(\/ipv6)/); + setBase(match ? match[1] : ""); + }, []); + + return ( +
+
+

+ Partners & Sponsors +

+

+ Trusted by Industry Leaders +

+
+ +
+ + +
+
+ ); +} diff --git a/app/components/sponsor/SponsorContact.tsx b/app/components/sponsor/SponsorContact.tsx index be62118..8ec93a9 100644 --- a/app/components/sponsor/SponsorContact.tsx +++ b/app/components/sponsor/SponsorContact.tsx @@ -19,28 +19,28 @@ export default function SponsorContact({ data }: { data: SponsorContactData }) {
{/* Left Content */} -
+
-

+

{data.title}

-

+

{data.description}

-
+
{/* Phone */}
-
-
- call +
+
+ call
-
-

+

+

{data.phone.label}

-

+

{data.phone.value}

@@ -49,15 +49,15 @@ export default function SponsorContact({ data }: { data: SponsorContactData }) { {/* Email */}
-
-
- mail +
+
+ mail
-
-

+

+

{data.email.label}

-

+

{data.email.value}

@@ -67,7 +67,7 @@ export default function SponsorContact({ data }: { data: SponsorContactData }) { + {/* Settings button removed as per request */}
@@ -382,10 +388,33 @@ export default function AdminConsole({ data, view = "all" }: { data: AdminData; VIE
-
- - {t.meta.user.role} - +
+ + + {userDropdown && ( +
+ +
+ )}
diff --git a/app/components/forms/LoginPageClient.tsx b/app/components/forms/LoginPageClient.tsx new file mode 100644 index 0000000..456e923 --- /dev/null +++ b/app/components/forms/LoginPageClient.tsx @@ -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 ( +
+
+
+
+ + + arrow_back + + {backLabel} + +
+ +
+

+ {titleLabel} +

+
+ +
+
+ +
+ {error && ( +
+ {error} +
+ )} + +
+ + setEmail(e.target.value)} + /> +
+ +
+ + setPassword(e.target.value)} + /> +
+ +
+ +
+
+
+
+
+
+ ); +} diff --git a/app/components/layout/Header/HeaderClient.tsx b/app/components/layout/Header/HeaderClient.tsx index 7598299..4ea3173 100644 --- a/app/components/layout/Header/HeaderClient.tsx +++ b/app/components/layout/Header/HeaderClient.tsx @@ -29,7 +29,9 @@ export default function HeaderClient({ }; 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 loginNowLabel = lang === "vi" ? "Đăng Nhập" : "Log In"; const backLabel = lang === "vi" ? "Trang Chủ" : "Home"; useEffect(() => { @@ -106,6 +108,12 @@ export default function HeaderClient({
+ + {loginLabel} +
+ setMobileOpen(false)} + > + {loginNowLabel} + = 0 ? decoded.slice(0, colon) : ""; - const p = colon >= 0 ? decoded.slice(colon + 1) : ""; - - if (u !== user || p !== pass) { - return new NextResponse("Unauthorized", { status: 401 }); + if (!session || session.value !== "true") { + // If not authenticated and trying to access admin, redirect to login + const url = request.nextUrl.clone(); + url.pathname = "/login"; + return NextResponse.redirect(url); } return NextResponse.next(); diff --git a/models/User.ts b/models/User.ts new file mode 100644 index 0000000..7cbd795 --- /dev/null +++ b/models/User.ts @@ -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("User", UserSchema); diff --git a/scripts/seed-admin.ts b/scripts/seed-admin.ts new file mode 100644 index 0000000..e08af41 --- /dev/null +++ b/scripts/seed-admin.ts @@ -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();