Adding Vietnamese version of IPV6, Rewrite content of README.md

This commit is contained in:
2026-05-13 21:09:35 +07:00
parent 778f5f735d
commit b0d6bde50c
22 changed files with 1412 additions and 385 deletions
+220 -141
View File
@@ -1,218 +1,297 @@
# VisaService - Website Dịch Vụ Visa
# IPv6 Summit 2026 — Hệ Thống Website Chính Thức
Website dịch vụ visa chuyên nghiệp được xây dựng với Next.js 15, TypeScript Tailwind CSS. Hỗ trợ tư vấn visa cho hơn 50 quốc gia với tỷ lệ thành công 99%.
> **IPv6 for AI & Data Centre Summit 2026** — Hội nghị quốc tế về hạ tầng IPv6 cho AI và Trung tâm Dữ liệu khu vực ASEAN. Website được xây dựng với Next.js 16, TypeScript, Tailwind CSS v4 và hỗ trợ song ngữ AnhViệt hoàn chỉnh.
## 🚀 Tính Năng
---
- **8 Trang Chính**: Trang chủ, Giới thiệu, Dịch vụ, Visa, Quốc gia, Bảng giá, Blog, Liên hệ
- **Hệ Thống Đặt Lịch**: Đặt lịch tư vấn với chuyên gia
- **Responsive Design**: Tối ưu cho mọi thiết bị
- **JSON-Based Content**: Dễ dàng quản lý nội dung
- **Interactive Components**: Form, accordion, filter, modal
- **SEO Optimized**: Metadata và structure tối ưu
## ✨ Tổng Quan
Website phục vụ toàn bộ vòng đời của sự kiện: từ trang giới thiệu hội nghị, đăng ký tham dự, thu thập phản hồi (feedback), trưng bày nhà tài trợ, đến bảng quản trị dành riêng cho ban tổ chức. Toàn bộ nội dung được lưu trữ trong các file JSON riêng biệt theo ngôn ngữ, giúp dễ dàng cập nhật mà không cần sửa code.
---
## 🚀 Tính Năng Nổi Bật
| Tính năng | Mô tả |
|---|---|
| 🌐 **Song ngữ (EN / VI)** | Chuyển đổi ngôn ngữ tức thì, mọi nhãn và nội dung đều được Việt hoá hoàn toàn |
| 📋 **Form Đăng Ký** | Đăng ký tham dự hội nghị, lưu trữ vào MongoDB |
| 💬 **Form Phản Hồi** | Đánh giá sự kiện chi tiết sau khi tham dự |
| 🏢 **Trang Nhà Tài Trợ** | Giới thiệu các đối tác và nhà tài trợ của hội nghị |
| 🗂️ **Admin Console** | Bảng quản trị xem, duyệt, từ chối hồ sơ đăng ký; xuất Excel |
| 🔐 **Bảo Mật Admin** | HTTP Basic Auth tuỳ chỉnh cho khu vực `/admin` |
| 📱 **Responsive** | Tối ưu hiển thị trên mọi thiết bị từ mobile đến desktop |
| 🎨 **Premium UI** | Glassmorphism, gold gradient, Material Symbols, micro-animations |
| 🗄️ **MongoDB** | Lưu trữ và truy vấn hồ sơ đăng ký/phản hồi theo thời gian thực |
---
## 🛠️ Công Nghệ Sử Dụng
- **Framework**: Next.js 15 (App Router)
- **Language**: TypeScript
- **Styling**: Tailwind CSS
- **Icons**: Emoji icons
- **Deployment**: Docker support
- **Framework**: Next.js 16 (App Router)
- **Language**: TypeScript 5
- **Styling**: Tailwind CSS v4 + SCSS (custom design tokens)
- **Database**: MongoDB (Mongoose v9)
- **Icons**: Google Material Symbols (Outlined)
- **Font**: Inter (Google Fonts)
- **Containerization**: Docker + Docker Compose
- **HTTP Client**: Axios
---
## 📁 Cấu Trúc Thư Mục
```
app/
├── components/ # Shared components
│ ├── Header.tsx # Navigation header
└── Footer.tsx # Site footer
├── about/ # Trang giới thiệu
├── about.json # Dữ liệu về công ty
── page.tsx # Component trang
├── services/ # Trang dịch vụ
├── services.json # Dữ liệu dịch vụ
└── page.tsx # Component trang
├── visa/ # Trang visa
├── visa.json # Dữ liệu các loại visa
── page.tsx # Component trang
├── countries/ # Trang quốc gia
│ ├── countries.json # Dữ liệu quốc gia
└── page.tsx # Component trang
├── pricing/ # Trang bảng giá
│ ├── pricing.json # Dữ liệu giá cả
└── page.tsx # Component trang
├── blog/ # Trang blog
│ ├── blog.json # Dữ liệu bài viết
── page.tsx # Component trang
├── contact/ # Trang liên hệ
│ ├── contact.json # Thông tin liên hệ
└── page.tsx # Component trang
├── appointment/ # Trang đặt lịch
│ ├── appointment.json # Dữ liệu booking
│ └── page.tsx # Component trang
├── layout.tsx # Root layout
├── page.tsx # Trang chủ
── globals.css # Global styles
├── app/
│ ├── (site)/ # Các trang công khai
│ ├── page.tsx # Trang chủ
│ ├── registration/ # Form đăng ký tham dự
│ │ ├── feedback/ # Form phản hồi sau sự kiện
│ └── sponsor/ # Trang nhà tài trợ
── (internal)/
│ │ └── admin/ # Bảng quản trị (yêu cầu xác thực)
│ ├── page.tsx # Dashboard tổng hợp
│ ├── pending/ # Danh sách chờ duyệt
│ │ ├── approved/ # Danh sách đã duyệt
│ └── rejected/ # Danh sách từ chối
── api/ # API Routes (Next.js)
├── components/
│ ├── home/ # Hero, Stats, Vision, Agenda sections
│ ├── admin/ # AdminConsole component
│ │ ├── forms/ # RegistrationForm, FeedbackForm
│ ├── layout/ # Header, Footer
│ ├── sponsor/ # Sponsor components
│ │ └── ui/ # Shared UI primitives
│ ├── context/ # LanguageContext (EN/VI)
── data/ # JSON content files (song ngữ)
│ │ ├── admin.json / admin.vi.json
│ ├── home.json / home.vi.json
│ ├── registration.json / registration.vi.json
│ │ ├── feedback.json / feedback.vi.json
│ ├── sponsor.json / sponsor.vi.json
│ └── site.json / site.vi.json
│ ├── hooks/ # useTranslation hook
│ └── layout.tsx # Root layout
── models/
│ └── SummitRequest.ts # Mongoose schema
├── lib/
│ └── mongodb.ts # Kết nối MongoDB
├── scripts/
│ └── seed-summit-requests.ts # Seed dữ liệu mẫu
├── types/
│ └── admin-submission.ts # TypeScript types
├── middleware.ts # HTTP Basic Auth cho /admin
├── Dockerfile
├── docker-compose.yml
└── .env.example
```
## 🚀 Hướng Dẫn Chạy Dự Án
---
## ⚙️ Hướng Dẫn Cài Đặt
### Yêu Cầu Hệ Thống
- Node.js 18+
- npm, yarn, hoặc pnpm
- **Node.js** 18+
- **MongoDB** 6+ (local hoặc Atlas)
- npm / yarn / pnpm
### Cài Đặt và Chạy
1. **Clone repository**
### 1. Clone & Cài Dependencies
```bash
git clone <repository-url>
cd visa-service-website
cd IPV6
npm install
```
2. **Cài đặt dependencies**
### 2. Cấu Hình Môi Trường
Sao chép file `.env.example` thành `.env.local` và điền các giá trị:
```bash
npm install
# hoặc
yarn install
# hoặc
pnpm install
cp .env.example .env.local
```
3. **Chạy development server**
```env
# MongoDB — lưu trữ hồ sơ đăng ký và phản hồi
MONGODB_URI=mongodb://localhost:27017/ipv6_summit
# URL công khai (tuỳ chọn)
NEXT_PUBLIC_BASE_URL=http://localhost:3000
# Bảo vệ trang Admin bằng HTTP Basic Auth (tuỳ chọn)
ADMIN_BASIC_AUTH_USER=admin
ADMIN_BASIC_AUTH_PASS=your_password
```
### 3. Seed Dữ Liệu Mẫu (Tuỳ chọn)
```bash
npm run db:seed
```
### 4. Chạy Development Server
```bash
npm run dev
# hoặc
yarn dev
# hoặc
pnpm dev
```
4. **Mở trình duyệt**
Truy cập [http://localhost:3000](http://localhost:3000) để xem website
Truy cập [http://localhost:3000](http://localhost:3000) để xem website.
Truy cập [http://localhost:3000/admin](http://localhost:3000/admin) để vào bảng quản trị.
### Scripts Có Sẵn
---
## 📜 Scripts
```bash
npm run dev # Chạy development server
npm run dev # Chạy development server (Next.js Turbopack)
npm run build # Build production
npm run start # Chạy production server
npm run lint # Kiểm tra code style
npm run lint # Kiểm tra ESLint
npm run db:seed # Seed dữ liệu mẫu vào MongoDB
npm run scss:build # Compile SCSS → CSS (một lần)
npm run scss:watch # Compile SCSS → CSS (watch mode)
```
---
## 🐳 Chạy Với Docker
### Development với Docker
### Development
```bash
# Build và chạy container
# Build và chạy toàn bộ stack (Next.js + MongoDB)
docker-compose up --build
# Chạy ở background
docker-compose up -d
# Dừng container
# Dừng
docker-compose down
```
### Production với Docker
### Production
```bash
# Build production image
docker build -t visa-service .
docker build -t ipv6-summit .
# Chạy container
docker run -p 3000:3000 visa-service
docker run -p 3000:3000 --env-file .env.local ipv6-summit
```
## 📝 Tùy Chỉnh Nội Dung
---
### Cập Nhật Dữ Liệu
## 🌐 Hệ Thống Đa Ngôn Ngữ (i18n)
Mỗi trang có file JSON riêng chứa dữ liệu:
Dự án sử dụng kiến trúc i18n tự xây dựng (không phụ thuộc thư viện bên ngoài):
- `about/about.json` - Thông tin công ty
- `services/services.json` - Danh sách dịch vụ
- `visa/visa.json` - Các loại visa
- `countries/countries.json` - Quốc gia hỗ trợ
- `pricing/pricing.json` - Bảng giá dịch vụ
- `blog/blog.json` - Bài viết blog
- `contact/contact.json` - Thông tin liên hệ
- `appointment/appointment.json` - Cấu hình đặt lịch
- **`LanguageContext`** (`app/context/`) — quản lý trạng thái ngôn ng toàn cục (`en` | `vi`).
- **`useTranslation` hook** (`app/hooks/`) — load nội dung JSON tương ứng với ngôn ngữ hiện tại.
- **File JSON song ngữ** — mỗi module có 2 file: `*.json` (EN) và `*.vi.json` (VI).
### Thêm Trang Mới
Để thêm nội dung mới, chỉ cần cập nhật cả hai file JSON song ngữ tương ứng.
1. Tạo thư mục mới trong `app/`
2. Tạo file `data.json` chứa dữ liệu
3. Tạo file `page.tsx` với React component
4. Cập nhật navigation trong `Header.tsx`
---
### Tùy Chỉnh Styling
## 🗄️ Database Schema
- Sử dụng Tailwind CSS classes
- Tùy chỉnh theme trong `tailwind.config.js`
- Global styles trong `app/globals.css`
Collection `summitrequest` (MongoDB):
## 🌐 Deployment
| Trường | Kiểu | Mô tả |
|---|---|---|
| `publicId` | String | ID công khai (ví dụ: `V6-4821`) |
| `fullName` | String | Họ và tên người đăng ký |
| `jobTitle` | String | Chức vụ |
| `company` | String | Công ty / Tổ chức |
| `email` | String | Địa chỉ email |
| `phone` | String | Số điện thoại |
| `segment` | String | Phân khúc (`tier1`, `cloud`, `enterprise`) |
| `status` | String | Trạng thái (`pending`, `approved`, `rejected`) |
| `source` | String | Nguồn gốc (`registration`, `feedback`) |
| `notes` | String | Ghi chú hoặc nội dung phản hồi |
| `submittedAt` | String | Ngày nộp hồ sơ |
---
## 🔒 Bảo Mật Admin
Khu vực `/admin` được bảo vệ thông qua `middleware.ts` bằng HTTP Basic Auth. Để kích hoạt, thiết lập hai biến môi trường:
```env
ADMIN_BASIC_AUTH_USER=admin
ADMIN_BASIC_AUTH_PASS=your_secure_password
```
Nếu không thiết lập, trang Admin vẫn truy cập được (phù hợp cho môi trường development nội bộ).
---
## 📝 Cập Nhật Nội Dung
Mọi nội dung văn bản đều được tách biệt khỏi code và lưu trong `app/data/`. Để thay đổi nội dung:
1. Mở file JSON tương ứng (ví dụ: `app/data/home.vi.json`)
2. Chỉnh sửa giá trị cần thay đổi
3. Lưu file — Next.js sẽ tự động hot-reload
**Không cần sửa code TypeScript/TSX** khi chỉ thay đổi nội dung văn bản.
---
## 🚀 Triển Khai (Deployment)
### Vercel (Khuyến nghị)
1. Push code lên GitHub
2. Kết nối repository với Vercel
3. Deploy tự động
1. Push code lên GitHub / GitLab
2. Kết nối repository với [Vercel](https://vercel.com)
3. Thêm biến môi trường `MONGODB_URI` trong Vercel dashboard
4. Deploy tự động mỗi khi push lên `main`
### Netlify
### VPS / Server (PM2)
1. Build project: `npm run build`
2. Upload thư mục `out/` lên Netlify
### VPS/Server
1. Build project: `npm run build`
2. Chạy: `npm run start`
3. Sử dụng PM2 hoặc Docker cho production
## 📱 Responsive Design
Website được tối ưu cho:
- **Mobile**: < 768px
- **Tablet**: 768px - 1024px
- **Desktop**: > 1024px
## 🔧 Cấu Hình Môi Trường
Tạo file `.env.local` (tùy chọn):
```env
NEXT_PUBLIC_SITE_URL=http://localhost:3000
NEXT_PUBLIC_CONTACT_EMAIL=info@visaservice.com
NEXT_PUBLIC_PHONE=1900-1234
```bash
npm run build
pm2 start npm --name "ipv6-summit" -- start
```
### Docker Compose (Production)
```bash
docker-compose -f docker-compose.yml up -d --build
```
---
## 📱 Responsive Breakpoints
| Màn hình | Kích thước |
|---|---|
| Mobile | < 768px |
| Tablet | 768px 1024px |
| Laptop | 1024px 1440px |
| Desktop | > 1440px |
---
## 🤝 Đóng Góp
1. Fork repository
2. Tạo feature branch: `git checkout -b feature/new-feature`
3. Commit changes: `git commit -m 'Add new feature'`
4. Push branch: `git push origin feature/new-feature`
5. Tạo Pull Request
## 📄 License
MIT License - xem file [LICENSE](LICENSE) để biết thêm chi tiết.
## 📞 Hỗ Trợ
- **Email**: info@visaservice.com
- **Phone**: 1900 1234
- **Website**: [visaservice.com](https://visaservice.com)
2. Tạo feature branch: `git checkout -b feature/ten-tinh-nang`
3. Commit: `git commit -m 'feat: mô tả thay đổi'`
4. Push: `git push origin feature/ten-tinh-nang`
5. Tạo Pull Request vào nhánh `main`
---
**Phát triển bởi**: VisaService Team
**Phiên bản**: 1.0.0
**Cập nhật**: January 2025
## 📄 License
MIT License — xem file [LICENSE](LICENSE) để biết thêm chi tiết.
---
**Dự án**: IPv6 Summit 2026 — Official Website
**Công nghệ**: Next.js 16 · TypeScript · Tailwind CSS v4 · MongoDB
**Phiên bản**: 0.1.0
**Cập nhật**: Tháng 5, 2026
+2 -34
View File
@@ -1,37 +1,5 @@
import Link from "next/link";
import FeedbackForm from "@/app/components/forms/FeedbackForm";
import data from "@/app/data/feedback.json";
import FeedbackPageClient from "@/app/components/forms/FeedbackPageClient";
export default function FeedbackPage() {
return (
<main className="relative min-h-screen pt-[120px] pb-[80px] px-[var(--spacing-gutter)]">
<div className="pointer-events-none absolute top-0 left-1/2 -translate-x-1/2 w-full max-w-[1200px] h-[min(800px,90vh)] hero-glow -z-10" />
<div className="max-w-[800px] mx-auto">
<div className="mb-8">
<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="text-center mb-10 md:mb-14">
<h1 className="font-[var(--font-display-lg)] text-[clamp(1.75rem,3vw+1rem,3.25rem)] leading-tight font-bold tracking-tighter mb-4 text-primary">
{data.title}
</h1>
<p className="text-on-surface-variant text-lg max-w-2xl mx-auto">{data.subtitle}</p>
</div>
<div className="glass-panel p-6 md:p-10 rounded-xl space-y-8 relative overflow-hidden shadow-2xl">
<div className="absolute top-0 left-0 w-full h-px bg-gradient-to-r from-transparent via-primary/25 to-transparent" />
<FeedbackForm data={data} />
</div>
</div>
</main>
);
return <FeedbackPageClient />;
}
+2 -14
View File
@@ -1,17 +1,5 @@
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";
import HomePageClient from "@/app/components/home/HomePageClient";
export default function HomePage() {
return (
<main className="pt-16 lg:pt-20">
<HeroSection data={home.hero} />
<VisionSection data={home.vision} />
<StatsSection data={home.stats} />
<AgendaSection data={home.agenda} />
</main>
);
return <HomePageClient />;
}
+2 -35
View File
@@ -1,38 +1,5 @@
import Link from "next/link";
import RegistrationForm from "@/app/components/forms/RegistrationForm";
import data from "@/app/data/registration.json";
import RegistrationPageClient from "@/app/components/forms/RegistrationPageClient";
export default function RegistrationPage() {
return (
<main className="pt-[120px] 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(1.75rem,3vw+1rem,3.25rem)] 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>
);
return <RegistrationPageClient />;
}
+4 -15
View File
@@ -1,23 +1,12 @@
import type { Metadata } from "next";
import sponsor from "@/app/data/sponsor.json";
import SponsorHero from "@/app/components/sponsor/SponsorHero";
import SponsorPackages from "@/app/components/sponsor/SponsorPackages";
import SponsorItemBased from "@/app/components/sponsor/SponsorItemBased";
import SponsorContact from "@/app/components/sponsor/SponsorContact";
import SponsorPageClient from "@/app/components/sponsor/SponsorPageClient";
export const metadata: Metadata = {
title: "Sponsorship | IPv6 Summit 2026",
title: "Nhà Tài Trợ | IPv6 Summit 2026",
description:
"Become a sponsor at the IPv6 for AI & Data Centre Summit 2026. Align your brand with the architects of tomorrow's infrastructure.",
"Trở thành nhà tài trợ tại Hội Nghị IPv6 cho AI & Trung Tâm Dữ Liệu 2026. Kết nối thương hiệu của bạn với những kiến trúc sư của hạ tầng ngày mai.",
};
export default function SponsorPage() {
return (
<main className="pt-20">
<SponsorHero data={sponsor.hero} />
<SponsorPackages data={sponsor.packages} />
<SponsorItemBased data={sponsor.itemBased} />
<SponsorContact data={sponsor.contact} />
</main>
);
return <SponsorPageClient />;
}
+287 -110
View File
@@ -5,6 +5,8 @@ import Image from "next/image";
import Link from "next/link";
import { usePathname, useRouter } from "next/navigation";
import { useMemo, useState, useTransition } from "react";
import { useLanguage } from "@/app/context/LanguageContext";
import { useTranslation } from "@/app/hooks/useTranslation";
import type { AdminRequestRow, SubmissionStatus } from "@/types/admin-submission";
type AdminRequest = AdminRequestRow;
@@ -50,6 +52,44 @@ export type AdminData = {
footer: { legal: string[] };
};
function InfoItem({
label,
value,
isEmail = false,
isStatus = false,
status = "",
statusMap = {} as any
}: {
label: string;
value?: string;
isEmail?: boolean;
isStatus?: boolean;
status?: string;
statusMap?: any;
}) {
if (!value && !isStatus) return (
<div className="flex flex-col gap-1">
<span className="text-[10px] font-[var(--font-label-caps)] text-outline uppercase tracking-widest">{label}</span>
<span className="text-sm text-on-surface-variant/40 italic"></span>
</div>
);
return (
<div className="flex flex-col gap-1">
<span className="text-[10px] font-[var(--font-label-caps)] text-outline uppercase tracking-widest">{label}</span>
{isStatus ? (
<span className={`px-2 py-0.5 rounded text-[10px] font-bold border w-fit uppercase tracking-wider ${statusMap[status]?.borderClass || ""}`}>
{value}
</span>
) : (
<span className={`text-sm font-medium ${isEmail ? "text-primary hover:underline cursor-pointer" : "text-on-surface"}`}>
{value}
</span>
)}
</div>
);
}
function StatCard({
item,
}: {
@@ -120,7 +160,7 @@ function formatCount(n: number) {
return n.toLocaleString("en-US");
}
function buildLiveStats(data: AdminData) {
function buildLiveStats(data: AdminData, lang: string) {
const req = data.requests;
const total = req.length;
const pending = req.filter((r) => r.status === "pending").length;
@@ -128,34 +168,45 @@ function buildLiveStats(data: AdminData) {
const rejected = req.filter((r) => r.status === "rejected").length;
const conversionPct = total > 0 ? Math.round((approved / total) * 100) : 0;
const isVi = lang === "vi";
return data.stats.map((s) => {
switch (s.id) {
case "total":
return {
...s,
value: formatCount(total),
hint: `${formatCount(approved)} approved · ${formatCount(pending)} pending · ${formatCount(rejected)} rejected`,
hint: isVi
? `${formatCount(approved)} đã duyệt · ${formatCount(pending)} chờ duyệt · ${formatCount(rejected)} từ chối`
: `${formatCount(approved)} approved · ${formatCount(pending)} pending · ${formatCount(rejected)} rejected`,
};
case "pending":
return {
...s,
value: formatCount(pending),
hint:
pending >= PENDING_THRESHOLD
hint: isVi
? (pending >= PENDING_THRESHOLD
? `Đạt hoặc vượt ngưỡng (${PENDING_THRESHOLD})`
: `Dưới ngưỡng (${PENDING_THRESHOLD})`)
: (pending >= PENDING_THRESHOLD
? `At or above threshold (${PENDING_THRESHOLD})`
: `Below threshold (${PENDING_THRESHOLD})`,
: `Below threshold (${PENDING_THRESHOLD})`),
};
case "confirmed":
return {
...s,
value: formatCount(approved),
hint: total > 0 ? `${Math.round((approved / total) * 100)}% of all requests` : "No requests yet",
hint: total > 0
? (isVi ? `${Math.round((approved / total) * 100)}% trên tổng số yêu cầu` : `${Math.round((approved / total) * 100)}% of all requests`)
: (isVi ? "Chưa có yêu cầu nào" : "No requests yet"),
};
case "conversion":
return {
...s,
value: `${conversionPct}%`,
hint: total > 0 ? `Approved share: ${approved} / ${total}` : "",
hint: total > 0
? (isVi ? `Tỷ lệ đã duyệt: ${approved} / ${total}` : `Approved share: ${approved} / ${total}`)
: "",
progressPercent: conversionPct,
};
default:
@@ -174,8 +225,18 @@ export default function AdminConsole({ data, view = "all" }: { data: AdminData;
const [afterDate, setAfterDate] = useState("");
const [page, setPage] = useState(1);
const [detail, setDetail] = useState<AdminRequest | null>(null);
const { lang, setLang } = useLanguage();
const { admin: t } = useTranslation();
const liveStats = useMemo(() => buildLiveStats(data), [data]);
// Merge real requests from server prop with translated UI labels
const tData = {
...t,
requests: data.requests,
table: { ...t.table, pageSize: data.table.pageSize },
stats: t.stats as AdminData["stats"],
} as AdminData;
const liveStats = useMemo(() => buildLiveStats(tData, lang), [tData, lang]);
const statusFilter = view === "all" ? status : view;
@@ -228,10 +289,10 @@ export default function AdminConsole({ data, view = "all" }: { data: AdminData;
<nav className="flex flex-col gap-8 flex-1" aria-label="Admin sections">
{(
[
{ href: "/admin", icon: "dashboard", label: "Panel" },
{ href: "/admin/pending", icon: "hourglass_empty", label: "Pending" },
{ href: "/admin/approved", icon: "check_circle", label: "Approved" },
{ href: "/admin/rejected", icon: "cancel", label: "Rejected" },
{ href: "/admin", icon: "dashboard", label: lang === "vi" ? "Bảng điều khiển" : "Panel" },
{ href: "/admin/pending", icon: "hourglass_empty", label: lang === "vi" ? "Chờ duyệt" : "Pending" },
{ href: "/admin/approved", icon: "check_circle", label: lang === "vi" ? "Đã duyệt" : "Approved" },
{ href: "/admin/rejected", icon: "cancel", label: lang === "vi" ? "Từ chối" : "Rejected" },
] as const
).map((item) => {
const active = pathname === item.href;
@@ -261,7 +322,7 @@ export default function AdminConsole({ data, view = "all" }: { data: AdminData;
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">Settings</span>
<span className="font-[var(--font-label-caps)] text-[8px] uppercase tracking-widest">{lang === "vi" ? "Cài đặt" : "Settings"}</span>
</button>
</aside>
@@ -270,15 +331,15 @@ export default function AdminConsole({ data, view = "all" }: { data: AdminData;
<div className="hidden md:block min-w-0">
<h1 className="font-[var(--font-display-lg)] text-xl text-primary uppercase tracking-tighter truncate">
{view === "all"
? data.meta.title
? t.meta.title
: view === "pending"
? "Pending approval"
? (lang === "vi" ? "Chờ Duyệt" : "Pending Approval")
: view === "approved"
? "Approved requests"
: "Rejected requests"}
? (lang === "vi" ? "Đã Duyệt" : "Approved Requests")
: (lang === "vi" ? "Từ Chối" : "Rejected Requests")}
</h1>
<p className="font-[var(--font-label-caps)] text-on-surface-variant text-[10px] uppercase tracking-widest">
{data.meta.subtitle}
{t.meta.subtitle}
</p>
</div>
<div className="flex-1 max-w-xl mx-0 lg:mx-8 min-w-0">
@@ -288,7 +349,7 @@ export default function AdminConsole({ data, view = "all" }: { data: AdminData;
</span>
<input
className="w-full bg-surface-container-low border-none rounded-full py-2.5 pl-12 pr-4 text-on-surface placeholder:text-outline/50 focus:outline-none focus:ring-1 focus:ring-primary/40 transition-all"
placeholder={data.filters.searchPlaceholder}
placeholder={t.filters.searchPlaceholder}
type="search"
value={search}
onChange={(e) => {
@@ -299,15 +360,37 @@ export default function AdminConsole({ data, view = "all" }: { data: AdminData;
</div>
</div>
<div className="flex items-center gap-4 shrink-0 justify-end">
{/* Language Switcher — Admin Header */}
<div className="flex items-center gap-1 font-[var(--font-label-caps)] text-[10px]">
<button
type="button"
onClick={() => setLang("en")}
className={`flex items-center gap-1 px-2 py-1 rounded transition-all duration-200 ${
lang === "en" ? "text-primary border-b border-primary" : "text-on-surface-variant/50 hover:text-primary"
}`}
>
<span>EN</span>
</button>
<span className="text-on-surface-variant/30">|</span>
<button
type="button"
onClick={() => setLang("vi")}
className={`flex items-center gap-1 px-2 py-1 rounded transition-all duration-200 ${
lang === "vi" ? "text-primary border-b border-primary" : "text-on-surface-variant/50 hover:text-primary"
}`}
>
<span>VIE</span>
</button>
</div>
<div className="flex flex-col items-end">
<span className="font-[var(--font-label-caps)] text-[10px] text-primary uppercase tracking-widest">
{data.meta.user.role}
{t.meta.user.role}
</span>
</div>
</div>
</header>
<main className="p-[var(--spacing-gutter)] max-w-7xl mx-auto w-full flex flex-col gap-8 flex-1">
<main className="p-4 md:p-6 w-full flex flex-col gap-6 flex-1">
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-[var(--spacing-gutter)]">
{liveStats.map((s) => (
<StatCard key={s.id} item={s} />
@@ -319,7 +402,7 @@ export default function AdminConsole({ data, view = "all" }: { data: AdminData;
{view === "all" ? (
<div className="flex flex-col gap-1 min-w-[140px]">
<label className="font-[var(--font-label-caps)] text-[10px] text-outline uppercase tracking-wider">
{data.filters.status.label}
{t.filters.status.label}
</label>
<select
className="bg-surface-container-high border border-outline/30 text-on-surface text-sm rounded-lg px-3 py-2 focus:outline-none focus:ring-1 focus:ring-primary/50"
@@ -329,7 +412,7 @@ export default function AdminConsole({ data, view = "all" }: { data: AdminData;
setPage(1);
}}
>
{data.filters.status.options.map((o) => (
{t.filters.status.options.map((o) => (
<option key={o.value} value={o.value}>
{o.label}
</option>
@@ -339,16 +422,16 @@ export default function AdminConsole({ data, view = "all" }: { data: AdminData;
) : (
<div className="flex flex-col gap-1 min-w-[140px]">
<span className="font-[var(--font-label-caps)] text-[10px] text-outline uppercase tracking-wider">
Status
{t.filters.status.label}
</span>
<p className="text-sm text-on-surface capitalize py-2 border border-outline/20 rounded-lg px-3 bg-surface-container-high/50">
{view} only
{t.statusMap[view as StatusKey]?.label ?? view}
</p>
</div>
)}
<div className="flex flex-col gap-1 min-w-[160px]">
<label className="font-[var(--font-label-caps)] text-[10px] text-outline uppercase tracking-wider">
{data.filters.companyGroup.label}
{t.filters.companyGroup.label}
</label>
<select
className="bg-surface-container-high border border-outline/30 text-on-surface text-sm rounded-lg px-3 py-2 focus:outline-none focus:ring-1 focus:ring-primary/50"
@@ -358,7 +441,7 @@ export default function AdminConsole({ data, view = "all" }: { data: AdminData;
setPage(1);
}}
>
{data.filters.companyGroup.options.map((o) => (
{t.filters.companyGroup.options.map((o) => (
<option key={o.value} value={o.value}>
{o.label}
</option>
@@ -367,7 +450,7 @@ export default function AdminConsole({ data, view = "all" }: { data: AdminData;
</div>
<div className="flex flex-col gap-1 min-w-[160px]">
<label className="font-[var(--font-label-caps)] text-[10px] text-outline uppercase tracking-wider">
Submitted after
{lang === "vi" ? "Nộp sau ngày" : "Submitted after"}
</label>
<input
className="bg-surface-container-high border border-outline/30 text-on-surface text-sm rounded-lg px-3 py-2 focus:outline-none focus:ring-1 focus:ring-primary/50"
@@ -386,7 +469,7 @@ export default function AdminConsole({ data, view = "all" }: { data: AdminData;
onClick={() => alert("Export is not wired (frontend-only demo).")}
>
<span className="material-symbols-outlined text-sm">download</span>
{data.filters.exportLabel}
{t.filters.exportLabel}
</button>
</section>
@@ -395,50 +478,69 @@ export default function AdminConsole({ data, view = "all" }: { data: AdminData;
<table className="w-full text-left border-collapse min-w-[880px]">
<thead>
<tr className="bg-surface-container-high border-b border-outline/20">
{Object.entries(data.table.columns).map(([key, label]) => (
<th
key={key}
className={`px-6 py-4 font-[var(--font-label-caps)] text-outline text-[11px] uppercase tracking-widest ${
key === "actions" ? "text-right" : ""
}`}
>
{label}
<th className="px-4 py-3 font-[var(--font-label-caps)] text-outline text-[10px] uppercase tracking-widest w-[80px]">
{t.table.columns.id}
</th>
<th className="px-4 py-3 font-[var(--font-label-caps)] text-outline text-[10px] uppercase tracking-widest w-[100px]">
{t.table.columns.submitted}
</th>
<th className="px-4 py-3 font-[var(--font-label-caps)] text-outline text-[10px] uppercase tracking-widest min-w-[160px]">
{t.table.columns.name}
</th>
<th className="px-4 py-3 font-[var(--font-label-caps)] text-outline text-[10px] uppercase tracking-widest min-w-[140px]">
{t.table.columns.company}
</th>
<th className="px-4 py-3 font-[var(--font-label-caps)] text-outline text-[10px] uppercase tracking-widest min-w-[160px]">
{t.table.columns.contact}
</th>
<th className="px-4 py-3 font-[var(--font-label-caps)] text-outline text-[10px] uppercase tracking-widest w-[110px]">
{t.table.columns.status}
</th>
<th className="px-4 py-3 font-[var(--font-label-caps)] text-outline text-[10px] uppercase tracking-widest text-right w-[160px]">
{t.table.columns.actions}
</th>
))}
</tr>
</thead>
<tbody className="divide-y divide-outline/10">
{pageRows.map((row) => {
const sm = data.statusMap[row.status];
const sm = t.statusMap[row.status as StatusKey];
return (
<tr key={row.id} className="hover:bg-surface-container-high/40 transition-colors">
<td className="px-6 py-4 font-[var(--font-mono)] text-sm text-primary whitespace-nowrap">
<tr key={row.id} className="hover:bg-surface-container-high/40 transition-colors border-b border-outline/5 last:border-0">
<td className="px-4 py-3 font-[var(--font-mono)] text-[13px] text-primary whitespace-nowrap">
#{row.id}
</td>
<td className="px-6 py-4 text-sm text-on-surface-variant whitespace-nowrap">{row.displayDate}</td>
<td className="px-6 py-4">
<div className="font-[var(--font-body-base)] text-sm font-semibold text-on-surface">{row.fullName}</div>
<div className="text-xs text-outline">{row.jobTitle}</div>
<td className="px-4 py-3 text-[13px] text-on-surface-variant whitespace-nowrap">
{row.displayDate}
</td>
<td className="px-6 py-4 text-sm">{row.company}</td>
<td className="px-6 py-4">
<div className="text-sm">{row.email}</div>
<div className="text-xs text-outline">{row.phone}</div>
<td className="px-4 py-3">
<div className="font-[var(--font-body-base)] text-[13px] font-semibold text-on-surface truncate max-w-[200px]" title={row.fullName}>
{row.fullName}
</div>
<div className="text-[11px] text-outline truncate max-w-[200px]" title={row.jobTitle}>
{row.jobTitle}
</div>
</td>
<td className="px-6 py-4">
<td className="px-4 py-3 text-[13px] text-on-surface-variant truncate max-w-[150px]" title={row.company}>
{row.company || "—"}
</td>
<td className="px-4 py-3">
<div className="text-[13px] truncate max-w-[180px]" title={row.email}>{row.email || "—"}</div>
<div className="text-[11px] text-outline truncate max-w-[180px]" title={row.phone}>{row.phone || "—"}</div>
</td>
<td className="px-4 py-3">
<span
className={`px-3 py-1 rounded-full text-[10px] font-[var(--font-label-caps)] border uppercase tracking-wider flex items-center gap-1.5 w-fit ${sm.borderClass}`}
className={`px-2 py-0.5 rounded-full text-[9px] font-[var(--font-label-caps)] border uppercase tracking-wider flex items-center gap-1.5 w-fit ${sm.borderClass}`}
>
<span className={`w-1.5 h-1.5 rounded-full status-led ${sm.dotClass}`} />
<span className={`w-1 h-1 rounded-full status-led ${sm.dotClass}`} />
{sm.label}
</span>
</td>
<td className="px-6 py-4 text-right">
<div className="flex items-center justify-end gap-1 flex-wrap justify-end">
<td className="px-4 py-3 text-right">
<div className="flex items-center justify-end gap-0.5">
<button
type="button"
className="p-2 rounded-lg hover:bg-surface-container-highest text-on-surface-variant hover:text-on-surface transition-all disabled:opacity-40"
title="View"
className="p-1.5 rounded-lg hover:bg-surface-container-highest text-on-surface-variant hover:text-on-surface transition-all disabled:opacity-40"
title={lang === "vi" ? "Xem" : "View"}
disabled={isPending}
onClick={() => setDetail(row)}
>
@@ -448,17 +550,17 @@ export default function AdminConsole({ data, view = "all" }: { data: AdminData;
<>
<button
type="button"
className="p-2 rounded-lg hover:bg-primary/20 text-primary transition-all disabled:opacity-40"
title="Edit"
className="p-1.5 rounded-lg hover:bg-primary/20 text-primary transition-all disabled:opacity-40"
title={lang === "vi" ? "Sửa" : "Edit"}
disabled={isPending}
onClick={() => alert(`Edit ${row.id} (not implemented).`)}
onClick={() => alert(lang === "vi" ? `Sửa ${row.id} (chưa hỗ trợ).` : `Edit ${row.id} (not implemented).`)}
>
<span className="material-symbols-outlined text-lg">edit</span>
</button>
<button
type="button"
className="p-2 rounded-lg text-white hover:bg-primary hover:text-white transition-all disabled:opacity-40"
title="Approve"
className="p-1.5 rounded-lg text-white hover:bg-primary hover:text-white transition-all disabled:opacity-40"
title={lang === "vi" ? "Duyệt" : "Approve"}
disabled={isPending}
onClick={() => commitStatus(row.id, "approved")}
>
@@ -468,8 +570,8 @@ export default function AdminConsole({ data, view = "all" }: { data: AdminData;
</button>
<button
type="button"
className="p-2 rounded-lg hover:bg-red-500/15 text-red-300 transition-all disabled:opacity-40"
title="Reject"
className="p-1.5 rounded-lg hover:bg-red-500/15 text-red-300 transition-all disabled:opacity-40"
title={lang === "vi" ? "Từ chối" : "Reject"}
disabled={isPending}
onClick={() => commitStatus(row.id, "rejected")}
>
@@ -480,10 +582,10 @@ export default function AdminConsole({ data, view = "all" }: { data: AdminData;
<>
<button
type="button"
className="p-2 rounded-lg hover:bg-surface-container-highest text-on-surface-variant disabled:opacity-40"
title="History"
className="p-1.5 rounded-lg hover:bg-surface-container-highest text-on-surface-variant disabled:opacity-40"
title={lang === "vi" ? "Lịch sử" : "History"}
disabled={isPending}
onClick={() => alert(`History for ${row.id} (placeholder).`)}
onClick={() => alert(lang === "vi" ? `Lịch sử cho ${row.id} (đang cập nhật).` : `History for ${row.id} (placeholder).`)}
>
<span className="material-symbols-outlined text-lg">history</span>
</button>
@@ -491,8 +593,8 @@ export default function AdminConsole({ data, view = "all" }: { data: AdminData;
<>
<button
type="button"
className="p-2 rounded-lg hover:bg-surface-container-high text-on-surface-variant disabled:opacity-40"
title="Mark pending"
className="p-1.5 rounded-lg hover:bg-surface-container-high text-on-surface-variant disabled:opacity-40"
title={lang === "vi" ? "Hoàn tác" : "Mark pending"}
disabled={isPending}
onClick={() => commitStatus(row.id, "pending")}
>
@@ -500,8 +602,8 @@ export default function AdminConsole({ data, view = "all" }: { data: AdminData;
</button>
<button
type="button"
className="p-2 rounded-lg hover:bg-red-500/15 text-red-300 transition-all disabled:opacity-40"
title="Reject"
className="p-1.5 rounded-lg hover:bg-red-500/15 text-red-300 transition-all disabled:opacity-40"
title={lang === "vi" ? "Từ chối" : "Reject"}
disabled={isPending}
onClick={() => commitStatus(row.id, "rejected")}
>
@@ -512,8 +614,8 @@ export default function AdminConsole({ data, view = "all" }: { data: AdminData;
<>
<button
type="button"
className="p-2 rounded-lg text-white hover:bg-primary hover:text-white transition-all disabled:opacity-40"
title="Approve"
className="p-1.5 rounded-lg text-white hover:bg-primary hover:text-white transition-all disabled:opacity-40"
title={lang === "vi" ? "Duyệt" : "Approve"}
disabled={isPending}
onClick={() => commitStatus(row.id, "approved")}
>
@@ -524,7 +626,7 @@ export default function AdminConsole({ data, view = "all" }: { data: AdminData;
<button
type="button"
className="p-2 rounded-lg hover:bg-surface-container-high text-on-surface-variant disabled:opacity-40"
title="Mark pending"
title={lang === "vi" ? "Hoàn tác" : "Mark pending"}
disabled={isPending}
onClick={() => commitStatus(row.id, "pending")}
>
@@ -544,11 +646,13 @@ export default function AdminConsole({ data, view = "all" }: { data: AdminData;
</div>
<div className="px-6 py-4 border-t border-outline/10 flex flex-col sm:flex-row items-center justify-between gap-4">
<p className="text-xs text-outline">
Showing {showingFrom} to {showingTo} of {filtered.length} entries
{lang === "vi"
? `Hiển thị ${showingFrom}${showingTo} / ${filtered.length} mục`
: `Showing ${showingFrom} to ${showingTo} of ${filtered.length} entries`}
</p>
<div className="flex items-center gap-3">
<span className="text-xs text-on-surface-variant hidden sm:inline">
Page {safePage} of {totalPages}
{lang === "vi" ? `Trang ${safePage} / ${totalPages}` : `Page ${safePage} of ${totalPages}`}
</span>
<button
type="button"
@@ -574,13 +678,15 @@ export default function AdminConsole({ data, view = "all" }: { data: AdminData;
<footer className="mt-auto border-t border-outline/20 py-8 px-[var(--spacing-gutter)] flex flex-col md:flex-row items-center justify-between gap-4">
<div className="flex flex-wrap items-center gap-4 justify-center md:justify-start">
<span className="font-[var(--font-display-lg)] text-sm text-primary uppercase tracking-tighter">IPv6 SUMMIT 2026</span>
<span className="text-xs text-outline">| {data.meta.footerTag}</span>
<span className="text-xs text-outline">| {t.meta.footerTag}</span>
</div>
<p className="text-[11px] font-[var(--font-label-caps)] text-outline tracking-widest text-center">
© 2026 IPv6 for AI & Data Centre Summit. Infrastructure for the Future.
{lang === "vi"
? "© 2026 Hội Nghị IPv6 cho AI & Trung Tâm Dữ Liệu. Hạ Tầng Cho Tương Lai."
: "© 2026 IPv6 for AI & Data Centre Summit. Infrastructure for the Future."}
</p>
<div className="flex items-center gap-6 justify-center">
{data.footer.legal.map((label) => (
{t.footer.legal.map((label) => (
<button
key={label}
type="button"
@@ -610,52 +716,123 @@ export default function AdminConsole({ data, view = "all" }: { data: AdminData;
aria-label="Close"
onClick={() => setDetail(null)}
/>
<div className="relative w-full max-w-lg glass-panel rounded-xl border border-outline/20 p-6 shadow-2xl z-10">
<div className="flex items-start justify-between gap-4 mb-4">
<div className="relative w-full max-w-2xl glass-panel rounded-2xl border border-white/10 shadow-[0_20px_50px_rgba(0,0,0,0.5)] z-10 overflow-hidden flex flex-col max-h-[90vh]">
{/* Modal Header */}
<div className="p-6 border-b border-white/5 bg-white/5 flex items-center justify-between">
<div className="flex items-center gap-4">
<div className="w-10 h-10 gold-gradient-bg rounded-lg flex items-center justify-center shadow-lg">
<span className="material-symbols-outlined text-on-primary">
{detail.source === "feedback" ? "rate_review" : "person_add"}
</span>
</div>
<div>
<h2 id="admin-detail-title" className="font-[var(--font-display-lg)] text-xl text-primary">
#{detail.id}
<h2 id="admin-detail-title" className="font-[var(--font-display-lg)] text-xl text-primary leading-tight">
{detail.fullName || (lang === "vi" ? "Yêu cầu mới" : "New Request")}
</h2>
<p className="text-sm text-on-surface-variant">{detail.displayDate}</p>
<p className="text-[10px] font-[var(--font-label-caps)] text-outline uppercase tracking-widest mt-0.5">
ID: #{detail.id} {detail.displayDate}
</p>
</div>
</div>
<button
type="button"
className="p-2 rounded-lg hover:bg-surface-container-high text-on-surface-variant"
className="w-10 h-10 rounded-full hover:bg-white/10 text-on-surface-variant flex items-center justify-center transition-colors"
onClick={() => setDetail(null)}
>
<span className="material-symbols-outlined">close</span>
<span className="material-symbols-outlined text-2xl">close</span>
</button>
</div>
<dl className="space-y-3 text-sm">
<div>
<dt className="text-outline font-[var(--font-label-caps)] text-[10px] uppercase tracking-widest">Name</dt>
<dd className="text-on-surface">
{detail.fullName} {detail.jobTitle}
</dd>
{/* Modal Body */}
<div className="flex-1 overflow-y-auto p-8 space-y-8 custom-scrollbar">
{/* Primary Info Grid */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-8">
<div className="space-y-6">
<h3 className="text-xs font-[var(--font-label-caps)] text-primary uppercase tracking-[0.2em] border-b border-primary/20 pb-2">
{lang === "vi" ? "Thông tin cá nhân" : "Identity & Professional"}
</h3>
<div className="space-y-4">
<InfoItem label={lang === "vi" ? "Họ và tên" : "Full Name"} value={detail.fullName} />
<InfoItem label={lang === "vi" ? "Chức vụ" : "Job Title"} value={detail.jobTitle} />
<InfoItem label={lang === "vi" ? "Công ty / Tổ chức" : "Company"} value={detail.company} />
<InfoItem label={lang === "vi" ? "Phân khúc" : "Segment"} value={detail.segment} />
</div>
<div>
<dt className="text-outline font-[var(--font-label-caps)] text-[10px] uppercase tracking-widest">Company</dt>
<dd className="text-on-surface">{detail.company}</dd>
</div>
<div>
<dt className="text-outline font-[var(--font-label-caps)] text-[10px] uppercase tracking-widest">Contact</dt>
<dd className="text-on-surface">
{detail.email}
<br />
{detail.phone}
</dd>
<div className="space-y-6">
<h3 className="text-xs font-[var(--font-label-caps)] text-primary uppercase tracking-[0.2em] border-b border-primary/20 pb-2">
{lang === "vi" ? "Thông tin liên hệ" : "Contact Details"}
</h3>
<div className="space-y-4">
<InfoItem label="Email" value={detail.email} isEmail />
<InfoItem label={lang === "vi" ? "Số điện thoại" : "Phone Number"} value={detail.phone} />
<InfoItem
label={lang === "vi" ? "Trạng thái hồ sơ" : "Application Status"}
value={lang === "vi" ? (t.statusMap[detail.status as StatusKey]?.label || detail.status) : detail.status}
isStatus
status={detail.status}
statusMap={t.statusMap}
/>
</div>
<div>
<dt className="text-outline font-[var(--font-label-caps)] text-[10px] uppercase tracking-widest">Status</dt>
<dd className="text-on-surface capitalize">{detail.status}</dd>
</div>
{detail.notes ? (
<div>
<dt className="text-outline font-[var(--font-label-caps)] text-[10px] uppercase tracking-widest">Notes</dt>
<dd className="text-on-surface">{detail.notes}</dd>
</div>
) : null}
</dl>
{/* Form Content / Notes Section */}
{detail.notes && (
<div className="space-y-6">
<h3 className="text-xs font-[var(--font-label-caps)] text-primary uppercase tracking-[0.2em] border-b border-primary/20 pb-2">
{lang === "vi" ? "Nội dung phản hồi" : "Form Submission Content"}
</h3>
<div className="glass-panel p-6 rounded-xl bg-white/5 border border-white/5">
{detail.notes.startsWith("Source:") ? (
<div className="grid grid-cols-1 gap-4">
{detail.notes.split(/\s(?=[A-Z][a-z\s]+:)/).map((part, idx) => {
const [key, ...val] = part.split(":");
if (!val.length) return <p key={idx} className="text-sm text-on-surface-variant leading-relaxed">{part}</p>;
return (
<div key={idx} className="flex flex-col sm:flex-row sm:items-baseline gap-1 sm:gap-4 border-b border-white/5 pb-3 last:border-0 last:pb-0">
<span className="text-[11px] font-[var(--font-label-caps)] text-outline uppercase tracking-wider min-w-[140px] shrink-0">
{key.replace("Source:", "").trim()}
</span>
<span className="text-sm text-on-surface font-medium">
{val.join(":").trim()}
</span>
</div>
);
})}
</div>
) : (
<p className="text-sm text-on-surface-variant leading-relaxed whitespace-pre-wrap italic">
{detail.notes}
</p>
)}
</div>
</div>
)}
</div>
{/* Modal Footer Actions */}
<div className="p-6 border-t border-white/5 bg-white/5 flex items-center justify-end gap-4">
<button
type="button"
className="px-6 py-2.5 rounded-lg border border-white/10 text-on-surface-variant text-xs font-bold uppercase tracking-wider hover:bg-white/5 transition-all"
onClick={() => setDetail(null)}
>
{lang === "vi" ? "Đóng" : "Close"}
</button>
{detail.status === "pending" && (
<button
type="button"
className="px-6 py-2.5 rounded-lg gold-gradient-bg text-on-primary text-xs font-bold uppercase tracking-wider shadow-lg shadow-primary/20 active:scale-95 transition-all"
onClick={() => {
commitStatus(detail.id, "approved");
setDetail(null);
}}
>
{lang === "vi" ? "Phê duyệt hồ sơ" : "Approve Application"}
</button>
)}
</div>
</div>
</div>
) : null}
@@ -0,0 +1,42 @@
"use client";
import Link from "next/link";
import { useTranslation } from "@/app/hooks/useTranslation";
import FeedbackForm from "@/app/components/forms/FeedbackForm";
export default function FeedbackPageClient() {
const { feedback, lang } = useTranslation();
const backLabel = lang === "vi" ? "Quay Về Trang Chủ" : "Back to Home";
return (
<main className="relative min-h-screen pt-[120px] pb-[80px] px-[var(--spacing-gutter)]">
<div className="pointer-events-none absolute top-0 left-1/2 -translate-x-1/2 w-full max-w-[1200px] h-[min(800px,90vh)] hero-glow -z-10" />
<div className="max-w-[800px] mx-auto">
<div className="mb-8">
<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="text-center mb-10 md:mb-14">
<h1 className="font-[var(--font-display-lg)] text-[clamp(1.75rem,3vw+1rem,3.25rem)] leading-tight font-bold tracking-tighter mb-4 text-primary">
{feedback.title}
</h1>
<p className="text-on-surface-variant text-lg max-w-2xl mx-auto">{feedback.subtitle}</p>
</div>
<div className="glass-panel p-6 md:p-10 rounded-xl space-y-8 relative overflow-hidden shadow-2xl">
<div className="absolute top-0 left-0 w-full h-px bg-gradient-to-r from-transparent via-primary/25 to-transparent" />
<FeedbackForm data={feedback as any} />
</div>
</div>
</main>
);
}
@@ -0,0 +1,42 @@
"use client";
import Link from "next/link";
import { useTranslation } from "@/app/hooks/useTranslation";
import RegistrationForm from "@/app/components/forms/RegistrationForm";
export default function RegistrationPageClient() {
const { registration, lang } = useTranslation();
const backLabel = lang === "vi" ? "Quay Về Trang Chủ" : "Back to Home";
return (
<main className="pt-[120px] 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>
{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]">
{registration.title}
</h1>
<p className="text-on-surface-variant text-lg">{registration.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={registration} />
</div>
</div>
</div>
</main>
);
}
+20
View File
@@ -0,0 +1,20 @@
"use client";
import { useTranslation } from "@/app/hooks/useTranslation";
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 HomePageClient() {
const { home } = useTranslation();
return (
<main className="pt-16 lg:pt-20">
<HeroSection data={home.hero} />
<VisionSection data={home.vision} />
<StatsSection data={home.stats} />
<AgendaSection data={home.agenda} />
</main>
);
}
+11 -11
View File
@@ -9,34 +9,34 @@ export default function VisionSection({
};
}) {
return (
<section className="py-16 lg:py-24 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]">
<section className="py-12 lg:py-20 max-w-hd mx-auto px-[var(--spacing-gutter)]">
<div className="grid grid-cols-1 lg:grid-cols-12 gap-8 lg:gap-16 items-start">
<div className="lg:col-span-4">
<span className="font-[var(--font-label-caps)] text-xs text-primary mb-4 lg:mb-6 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(1.75rem,3vw+1rem,3.5rem)]">
<h2 className="font-[var(--font-display-lg)] text-on-surface mb-6 lg:mb-8 leading-tight text-[clamp(1.5rem,2vw+0.75rem,2.5rem)]">
{data.title}
</h2>
<div className="space-y-6 lg:space-y-8 text-on-surface-variant text-base lg:text-[18px] leading-relaxed">
<div className="space-y-4 text-on-surface-variant text-sm lg:text-base 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">
<div className="lg:col-span-8 grid grid-cols-1 sm:grid-cols-3 gap-4 lg:gap-6">
{data.cards.map((c) => (
<div
key={c.title}
className="glass-panel p-6 lg:p-10 rounded-2xl group transition-all duration-500 hover:lg:-translate-y-4 hover:shadow-2xl hover:shadow-primary/5"
className="glass-panel p-5 lg:p-7 rounded-2xl group transition-all duration-500 hover:lg:-translate-y-3 hover:shadow-2xl hover:shadow-primary/5"
>
<span className="material-symbols-outlined text-primary text-[32px] lg:text-[48px] mb-6 lg:mb-8 block">
<span className="material-symbols-outlined text-primary text-[28px] lg:text-[36px] mb-4 lg:mb-5 block">
{c.icon}
</span>
<h3 className="font-[var(--font-display-lg)] text-lg lg:text-[22px] text-on-surface mb-3 lg:mb-4">
<h3 className="font-[var(--font-display-lg)] text-base lg:text-lg text-on-surface mb-2 lg:mb-3">
{c.title}
</h3>
<p className="text-xs lg:text-[16px] text-on-surface-variant/80 leading-relaxed">
<p className="text-xs lg:text-sm text-on-surface-variant/80 leading-relaxed">
{c.description}
</p>
</div>
+8 -4
View File
@@ -1,15 +1,19 @@
"use client";
import Link from "next/link";
import siteData from "@/app/data/site.json";
import { useTranslation } from "@/app/hooks/useTranslation";
export default function Footer() {
const { site } = useTranslation();
return (
<footer className="w-full bg-surface-container-low border-t border-white/5 py-10 lg:py-16">
<div className="flex flex-col lg:flex-row justify-between items-center px-[var(--spacing-gutter)] gap-10 lg:gap-8 w-full max-w-hd mx-auto">
<div className="font-[var(--font-label-caps)] text-base lg:text-xl text-primary tracking-widest font-bold uppercase">
{siteData.brand.name}
{site.brand.name}
</div>
<div className="flex flex-wrap justify-center gap-8 lg:gap-16">
{siteData.footer.links.map((l) => (
{site.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"
@@ -20,7 +24,7 @@ export default function Footer() {
))}
</div>
<div className="text-[13px] lg:text-[14px] text-on-surface-variant/40 text-center lg:text-right max-w-sm leading-relaxed uppercase tracking-widest">
{siteData.footer.copyright}
{site.footer.copyright}
</div>
</div>
</footer>
+68 -11
View File
@@ -3,6 +3,8 @@
import Link from "next/link";
import { usePathname } from "next/navigation";
import { useEffect, useMemo, useState } from "react";
import { useLanguage } from "@/app/context/LanguageContext";
import { useTranslation } from "@/app/hooks/useTranslation";
type NavItem = { label: string; href: string };
@@ -15,9 +17,15 @@ export default function HeaderClient({
}) {
const pathname = usePathname();
const [mobileOpen, setMobileOpen] = useState(false);
const { lang, setLang } = useLanguage();
const { site } = useTranslation();
const isHome = pathname === "/";
const navItems = useMemo(() => nav.filter((x) => x.href !== "/"), [nav]);
const navItems = useMemo(() => site.nav.filter((x) => x.href !== "/"), [site.nav]);
const registerLabel = lang === "vi" ? "Đăng Ký" : "Register";
const registerNowLabel = lang === "vi" ? "Đăng Ký Ngay" : "Register Now";
const backLabel = lang === "vi" ? "Trang Chủ" : "Home";
useEffect(() => {
if (!mobileOpen) return;
@@ -45,7 +53,7 @@ export default function HeaderClient({
} transition-colors duration-300`}
href="/"
>
Home
{backLabel}
</Link>
{navItems.map((item) => {
const active = pathname === item.href;
@@ -63,15 +71,41 @@ export default function HeaderClient({
})}
</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>
<div className="flex items-center gap-4 lg:gap-6">
{/* Language Switcher — Desktop */}
<div className="hidden sm:flex items-center gap-1 font-[var(--font-label-caps)] text-xs">
<button
type="button"
onClick={() => setLang("en")}
className={`flex items-center gap-1 px-2 py-1 rounded transition-all duration-200 ${
lang === "en"
? "text-primary border-b-2 border-primary"
: "text-on-surface-variant/60 hover:text-primary"
}`}
title="English"
>
<span>EN</span>
</button>
<span className="text-on-surface-variant/30">|</span>
<button
type="button"
onClick={() => setLang("vi")}
className={`flex items-center gap-1 px-2 py-1 rounded transition-all duration-200 ${
lang === "vi"
? "text-primary border-b-2 border-primary"
: "text-on-surface-variant/60 hover:text-primary"
}`}
title="Tiếng Việt"
>
<span>VIE</span>
</button>
</div>
<Link
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"
>
Register
{registerLabel}
</Link>
<button
type="button"
@@ -120,7 +154,7 @@ export default function HeaderClient({
href="/"
onClick={() => setMobileOpen(false)}
>
Home
{backLabel}
</Link>
{navItems.map((item) => {
const active = pathname === item.href;
@@ -140,15 +174,38 @@ export default function HeaderClient({
</nav>
<div className="mt-auto pt-10 border-t border-white/5 flex flex-col gap-6">
<div className="text-on-surface-variant/60 font-[var(--font-label-caps)] text-sm uppercase tracking-widest">
Language: EN
{/* Language Switcher — Mobile */}
<div className="flex items-center gap-2 font-[var(--font-label-caps)] text-sm">
<button
type="button"
onClick={() => setLang("en")}
className={`flex items-center gap-1.5 px-3 py-2 rounded-lg border transition-all duration-200 ${
lang === "en"
? "border-primary text-primary bg-primary/10"
: "border-white/10 text-on-surface-variant/60 hover:text-primary hover:border-primary/40"
}`}
>
<span>EN</span>
</button>
<button
type="button"
onClick={() => setLang("vi")}
className={`flex items-center gap-1.5 px-3 py-2 rounded-lg border transition-all duration-200 ${
lang === "vi"
? "border-primary text-primary bg-primary/10"
: "border-white/10 text-on-surface-variant/60 hover:text-primary hover:border-primary/40"
}`}
>
<span>VIE</span>
</button>
</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-widest rounded-xl text-center shadow-2xl"
onClick={() => setMobileOpen(false)}
>
Register Now
{registerNowLabel}
</Link>
</div>
</div>
@@ -0,0 +1,20 @@
"use client";
import { useTranslation } from "@/app/hooks/useTranslation";
import SponsorHero from "@/app/components/sponsor/SponsorHero";
import SponsorPackages from "@/app/components/sponsor/SponsorPackages";
import SponsorItemBased from "@/app/components/sponsor/SponsorItemBased";
import SponsorContact from "@/app/components/sponsor/SponsorContact";
export default function SponsorPageClient() {
const { sponsor } = useTranslation();
return (
<main className="pt-20">
<SponsorHero data={sponsor.hero} />
<SponsorPackages data={sponsor.packages} />
<SponsorItemBased data={sponsor.itemBased} />
<SponsorContact data={sponsor.contact} />
</main>
);
}
+41
View File
@@ -0,0 +1,41 @@
"use client";
import React, { createContext, useContext, useEffect, useState } from "react";
export type Lang = "en" | "vi";
interface LanguageContextValue {
lang: Lang;
setLang: (l: Lang) => void;
}
const LanguageContext = createContext<LanguageContextValue>({
lang: "en",
setLang: () => {},
});
export function LanguageProvider({ children }: { children: React.ReactNode }) {
const [lang, setLangState] = useState<Lang>("en");
useEffect(() => {
const saved = localStorage.getItem("ipv6_lang") as Lang | null;
if (saved === "vi" || saved === "en") {
setLangState(saved);
}
}, []);
const setLang = (l: Lang) => {
setLangState(l);
localStorage.setItem("ipv6_lang", l);
};
return (
<LanguageContext.Provider value={{ lang, setLang }}>
{children}
</LanguageContext.Provider>
);
}
export function useLanguage() {
return useContext(LanguageContext);
}
+168
View File
@@ -0,0 +1,168 @@
{
"meta": {
"title": "Bảng Quản Trị",
"subtitle": "IPv6 SUMMIT 2026 / Hạ Tầng Cốt Lõi",
"user": {
"name": "",
"role": "Quản Trị Hệ Thống",
"avatarUrl": ""
},
"footerTag": "Hạ Tầng Hệ Thống Cốt Lõi v2.4.0"
},
"stats": [
{
"id": "total",
"label": "Tổng yêu cầu",
"value": "1,284",
"hint": "+12% so tuần trước",
"icon": "database",
"accent": "none"
},
{
"id": "pending",
"label": "Chờ duyệt",
"value": "42",
"hint": "Ngưỡng tới hạn: 50",
"icon": "hourglass_empty",
"accent": "pending"
},
{
"id": "confirmed",
"label": "Đã xác nhận",
"value": "856",
"hint": "Sức chứa đạt 74%",
"icon": "check_circle",
"accent": "confirmed"
},
{
"id": "conversion",
"label": "Tỷ lệ chuyển đổi",
"value": "67%",
"hint": "",
"icon": "trending_up",
"accent": "progress",
"progressPercent": 67
}
],
"filters": {
"status": {
"label": "Trạng Thái",
"options": [
{ "value": "all", "label": "Tất cả" },
{ "value": "pending", "label": "Chờ duyệt" },
{ "value": "approved", "label": "Đã duyệt" },
{ "value": "rejected", "label": "Từ chối" }
]
},
"companyGroup": {
"label": "Công Ty",
"options": [
{ "value": "all", "label": "Tất cả phân khúc" },
{ "value": "tier1", "label": "Global Tier 1" },
{ "value": "cloud", "label": "Nhà cung cấp Cloud" },
{ "value": "enterprise", "label": "Doanh nghiệp" }
]
},
"searchPlaceholder": "Tìm theo tên, công ty, email hoặc ID…",
"exportLabel": "Xuất Excel"
},
"table": {
"columns": {
"id": "Mã",
"submitted": "Ngày nộp",
"name": "Họ tên",
"company": "Công ty",
"contact": "Email / ĐT",
"status": "Trạng thái",
"actions": "Thao tác"
},
"pageSize": 10
},
"statusMap": {
"approved": {
"label": "Đã Duyệt",
"borderClass": "border-primary text-primary bg-primary/10",
"dotClass": "bg-primary"
},
"pending": {
"label": "Chờ Duyệt",
"borderClass": "border-secondary text-secondary bg-secondary/10",
"dotClass": "bg-secondary"
},
"rejected": {
"label": "Từ Chối",
"borderClass": "border-red-400 text-red-300 bg-red-500/10",
"dotClass": "bg-red-400"
}
},
"requests": [
{
"id": "V6-4821",
"submittedAt": "2025-10-24",
"displayDate": "24 OCT 2025",
"fullName": "Elena Rodriguez",
"jobTitle": "Network Architect",
"company": "Cloudflare, Inc.",
"segment": "cloud",
"email": "e.rodriguez@cloudflare.com",
"phone": "+1 (555) 012-9988",
"status": "approved",
"notes": "VIP routing — yêu cầu chỗ ngồi hàng đầu."
},
{
"id": "V6-4819",
"submittedAt": "2025-10-23",
"displayDate": "23 OCT 2025",
"fullName": "Marcus Thorne",
"jobTitle": "Senior DevOps",
"company": "Akamai Technologies",
"segment": "tier1",
"email": "m.thorne@akamai.io",
"phone": "+44 20 7946 0123",
"status": "pending",
"notes": "Đang chờ thư xác nhận từ công ty."
},
{
"id": "V6-4790",
"submittedAt": "2025-10-22",
"displayDate": "22 OCT 2025",
"fullName": "Kenji Yamamoto",
"jobTitle": "CTO",
"company": "NTT Data Japan",
"segment": "enterprise",
"email": "yamamoto@ntt.jp",
"phone": "+81 3 1234 5678",
"status": "rejected",
"notes": "Vượt quá sức chứa — đề nghị vào danh sách chờ."
},
{
"id": "V6-4788",
"submittedAt": "2025-10-21",
"displayDate": "21 OCT 2025",
"fullName": "Priya Nair",
"jobTitle": "Solutions Engineer",
"company": "Amazon Web Services",
"segment": "cloud",
"email": "priya.nair@example.aws",
"phone": "+65 6123 8899",
"status": "pending",
"notes": ""
},
{
"id": "V6-4771",
"submittedAt": "2025-10-20",
"displayDate": "20 OCT 2025",
"fullName": "James Okonkwo",
"jobTitle": "Director of Infrastructure",
"company": "Global Telecom Partners",
"segment": "tier1",
"email": "j.okonkwo@gtp.net",
"phone": "+234 803 555 0142",
"status": "approved",
"notes": ""
}
],
"footer": {
"legal": ["Bảo Mật", "Điều Khoản", "Hỗ Trợ"]
}
}
+194
View File
@@ -0,0 +1,194 @@
{
"title": "Phản Hồi 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.",
"sections": [
{
"id": "overall",
"title": "1. TRẢI NGHIỆM TỔNG THỂ",
"subtitle": "Tổng quan về trải nghiệm hội nghị của bạn",
"questions": [
{
"id": "overallExperience",
"type": "rating",
"label": "Bạn đánh giá thế nào về trải nghiệm tổng thể tại IPv6 Summit 2026?",
"min": 1,
"max": 5
},
{
"id": "venueFacilities",
"type": "rating",
"label": "Bạn hài lòng như thế nào với địa điểm, cơ sở vật chất và dịch vụ ăn uống?",
"min": 1,
"max": 5
},
{
"id": "organisationProcess",
"type": "rating",
"label": "Bạn đánh giá thế nào về công tác tổ chức sự kiện và quy trình đăng ký?",
"min": 1,
"max": 5
}
]
},
{
"id": "content",
"title": "2. ĐÁNH GIÁ NỘI DUNG & CHƯƠNG TRÌNH",
"subtitle": "Đánh giá nội dung và các phiên tham luận",
"questions": [
{
"id": "mostValuableSession",
"type": "dropdown",
"label": "Phiên nào bạn thấy có giá trị nhất?",
"placeholder": "Chọn một phiên",
"options": [
{ "value": "opening", "label": "Phiên Khai Mạc" },
{ "value": "keynote1", "label": "Bài Phát Biểu Chính 1" },
{ "value": "panel1", "label": "Thảo Luận Nhóm 1" },
{ "value": "technical", "label": "Chủ Đề Kỹ Thuật" },
{ "value": "policy", "label": "Chủ Đề Chính Sách" }
]
},
{
"id": "relevance",
"type": "scale",
"label": "Nội dung hội nghị liên quan đến lĩnh vực chuyên môn của bạn ở mức độ nào?",
"options": [
{ "value": "1", "label": "Không Liên Quan" },
{ "value": "2", "label": "Ít Liên Quan" },
{ "value": "3", "label": "Khá Liên Quan" },
{ "value": "4", "label": "Liên Quan" },
{ "value": "5", "label": "Rất Liên Quan" }
]
},
{
"id": "speakerRating",
"type": "rating_labeled",
"label": "Bạn đánh giá thế nào về các diễn giả chính và thảo luận nhóm?",
"options": [
{ "value": "1", "label": "1 Cần Cải Thiện" },
{ "value": "2", "label": "2 Trung Bình" },
{ "value": "3", "label": "3 Tốt" },
{ "value": "4", "label": "4 Khá Tốt" },
{ "value": "5", "label": "5 Xuất Sắc" }
]
},
{
"id": "networkingUtility",
"type": "scale",
"label": "Các cơ hội kết nối trong Hội Nghị hữu ích như thế nào?",
"options": [
{ "value": "1", "label": "Không Hữu Ích" },
{ "value": "2", "label": "Ít Hữu Ích" },
{ "value": "3", "label": "Hữu Ích" },
{ "value": "4", "label": "Rất Hữu Ích" },
{ "value": "5", "label": "Cực Kỳ Giá Trị" }
]
},
{
"id": "futureTopics",
"type": "checkbox_group",
"label": "Bạn muốn thấy thêm chủ đề nào trong các lần tổ chức tiếp theo?",
"hasOther": true,
"options": [
{ "value": "deployment", "label": "Chiến Lược Triển Khai IPv6" },
{ "value": "cybersecurity", "label": "An Ninh Mạng & Hạ Tầng" },
{ "value": "smart-cities", "label": "Thành Phố Thông Minh & IoT" },
{ "value": "ai-networks", "label": "AI & Mạng Tương Lai" },
{ "value": "government", "label": "Chính Sách Chính Phủ" },
{ "value": "cloud-dc", "label": "Cloud & Trung Tâm Dữ Liệu" },
{ "value": "case-studies", "label": "Case Study Từ Doanh Nghiệp" }
]
}
]
},
{
"id": "future",
"title": "4. THAM GIA TRONG TƯƠNG LAI",
"subtitle": "Khả năng bạn tham dự các sự kiện tiếp theo",
"questions": [
{
"id": "attendAgain",
"type": "scale",
"label": "Bạn có tham dự IPv6 Summit năm 2027 không?",
"options": [
{ "value": "definitely-yes", "label": "Chắc Chắn Có" },
{ "value": "probably-yes", "label": "Có Thể Có" },
{ "value": "not-sure", "label": "Chưa Chắc" },
{ "value": "probably-no", "label": "Có Thể Không" },
{ "value": "definitely-no", "label": "Chắc Chắn Không" }
]
},
{
"id": "recommend",
"type": "scale",
"label": "Bạn có giới thiệu IPv6 Summit cho đồng nghiệp hoặc đối tác không?",
"options": [
{ "value": "yes", "label": "Có" },
{ "value": "maybe", "label": "Có Thể" },
{ "value": "no", "label": "Không" }
]
}
]
},
{
"id": "qualitative",
"title": "5. PHẢN HỒI ĐỊNH TÍNH",
"subtitle": "Ý kiến chung và góp ý",
"questions": [
{
"id": "enjoyedMost",
"type": "long_text",
"label": "Điều gì bạn thích nhất tại Hội Nghị?"
},
{
"id": "improvementAreas",
"type": "long_text",
"label": "Những lĩnh vực nào chúng tôi cần cải thiện cho các sự kiện tiếp theo?"
},
{
"id": "additionalComments",
"type": "long_text",
"label": "Bạn có thêm ý kiến, góp ý hoặc quan tâm đến hợp tác không?"
}
]
},
{
"id": "profile",
"title": "HỒ SƠ NGƯỜI THAM DỰ (TÙY CHỌN)",
"subtitle": "Thông tin về nền tảng chuyên môn của bạn",
"questions": [
{
"id": "industry",
"type": "checkbox_group",
"label": "Ngành / Lĩnh Vực",
"hasOther": true,
"options": [
{ "value": "government", "label": "Chính Phủ" },
{ "value": "telecom", "label": "Viễn Thông" },
{ "value": "education", "label": "Giáo Dục" },
{ "value": "enterprise", "label": "Doanh Nghiệp" },
{ "value": "technology", "label": "Công Nghệ" },
{ "value": "startup", "label": "Khởi Nghiệp" }
]
},
{
"id": "role",
"type": "checkbox_group",
"label": "Vai trò của bạn",
"hasOther": true,
"options": [
{ "value": "executive", "label": "Lãnh Đạo / Điều Hành" },
{ "value": "engineer", "label": "Kỹ Sư / Chuyên Gia Kỹ Thuật" },
{ "value": "researcher", "label": "Nhà Nghiên Cứu / Học Thuật" },
{ "value": "student", "label": "Sinh Viên" },
{ "value": "bizdev", "label": "Phát Triển Kinh Doanh" }
]
}
]
}
],
"submit": {
"label": "Gửi Phản Hồi",
"disclaimer": "CẢM ƠN BẠN ĐÃ ĐÓNG GÓP Ý KIẾN QUÝ BÁU. Phản hồi của bạn giúp chúng tôi xây dựng hệ sinh thái IPv6 mạnh mẽ và có tác động hơn trong tương lai."
}
}
+69
View File
@@ -0,0 +1,69 @@
{
"hero": {
"badgeIcon": "calendar_today",
"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",
"subtitle": "Khai Phá Tiềm Năng Hạ Tầng Thế Hệ Mớ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.",
"primaryCta": { "label": "Đăng Ký", "href": "/registration" },
"secondaryCta": { "label": "Xem Lịch Trình", "href": "#agenda" }
},
"vision": {
"eyebrow": "TẦM NHÌN & MỤC TIÊU",
"title": "Tương Lai Số của ASEAN",
"paragraphs": [
"Việt Nam và khu vực ASEAN đang đứng trước bước ngoặt quan trọng trong chuyển đổi số. Việc chuyển đổi sang IPv6 là nền tảng hạ tầng thiết yếu cho sự phát triển AI và mở rộng quy mô trung tâm dữ liệu.",
"IPv6 là xương sống cho điện toán đám mây có thể mở rộng, kết nối bảo mật và mạng sẵn sàng cho AI trên toàn khu vực."
],
"cards": [
{
"icon": "hub",
"title": "Kết Nối",
"description": "Giao lưu cùng lãnh đạo cấp C, nhà hoạch định chính sách và các chuyên gia kỹ thuật hàng đầu trong hệ sinh thái."
},
{
"icon": "insights",
"title": "Công Nghệ Tiên Phong",
"description": "Phân tích chuyên sâu về khối lượng công việc AI, hạ tầng siêu quy mô và vận hành IPv6 thế hệ mới."
},
{
"icon": "gavel",
"title": "Chính Sách",
"description": "Diễn đàn về tiêu chuẩn hạ tầng quốc gia, quản trị và hợp tác xuyên biên giới."
}
]
},
"stats": {
"id": "stats",
"items": [
{ "value": "130+", "label": "Diễn Giả Quốc Tế" },
{ "value": "14", "label": "Quốc Gia ASEAN" },
{ "value": "50+", "label": "Đối Tác Công Nghệ" }
]
},
"agenda": {
"id": "agenda",
"eyebrow": "LỊCH TRÌNH",
"title": "Chương Trình Sự Kiện",
"description": "Tổng quan chiến lược về các phiên kỹ thuật và chiến lược.",
"items": [
{
"order": "01",
"time": "08:30",
"title": "Lễ Khai Mạc",
"description": "Phát biểu chào mừng của Bộ TT&TT Việt Nam và các lãnh đạo kinh tế số khu vực."
},
{
"order": "02",
"time": "10:00",
"title": "AI trong Trung Tâm Dữ Liệu",
"description": "IPv6 hỗ trợ xử lý siêu quy mô và kết cấu độ trễ thấp như thế nào."
},
{
"order": "03",
"time": "14:00",
"title": "Diễn Đàn: Chính Sách & Quy Mô",
"description": "Thảo luận kín dành cho cơ quan quản lý và lãnh đạo ngành."
}
]
}
}
+38
View File
@@ -0,0 +1,38 @@
{
"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 Dữ Liệu.",
"sections": {
"personal": {
"title": "Thông Tin Cá Nhân",
"fullName": "Họ và Tên",
"phone": "Số Điện Thoại",
"email": "Email Doanh Nghiệp"
},
"professional": {
"title": "Thông Tin Nghề Nghiệp",
"company": "Tên Công Ty",
"jobTitle": "Chức Danh"
},
"industry": {
"title": "Ngành Nghề",
"label": "Chọn Ngành Nghề",
"placeholder": "Vui lòng chọn...",
"options": [
{ "value": "government", "label": "Chính Phủ" },
{ "value": "telecom", "label": "Viễn Thông" },
{ "value": "cloud", "label": "Cloud / Trung Tâm Dữ Liệu" },
{ "value": "ai", "label": "AI / Phần Mềm" },
{ "value": "others", "label": "Khác" }
]
},
"additional": {
"title": "Thông Tin Bổ Sung",
"label": "Yêu cầu đặc biệt hoặc chế độ ăn kiêng",
"placeholder": "Tùy chọn ăn chay, nhu cầu tiếp cận, v.v."
}
},
"submit": {
"label": "Gửi Đăng Ký",
"disclaimer": "Bằng cách đăng ký, bạn đồng ý với Chính Sách Bảo Mật và Điều Khoản Dịch Vụ của chúng tôi."
}
}
+20
View File
@@ -0,0 +1,20 @@
{
"brand": {
"name": "IPv6 SUMMIT 2026"
},
"nav": [
{ "label": "Trang Chủ", "href": "/" },
{ "label": "Đăng Ký", "href": "/registration" },
{ "label": "Phản Hồi", "href": "/feedback" },
{ "label": "Nhà Tài Trợ", "href": "/sponsor" }
],
"footer": {
"links": [
{ "label": "Bảo Mật", "href": "#" },
{ "label": "Điều Khoản", "href": "#" },
{ "label": "Báo Chí", "href": "#" },
{ "label": "Liên Hệ", "href": "#" }
],
"copyright": "© 2026 Hội Nghị IPv6 cho AI & Trung Tâm Dữ Liệu. Hạ Tầng Cho Tương Lai."
}
}
+114
View File
@@ -0,0 +1,114 @@
{
"hero": {
"eyebrow": "CƠ HỘI ĐANG CHỜ",
"title": "Trở Thành Nhà Tài Trợ",
"subtitle": "Hội Nghị IPv6 cho AI & Trung Tâm Dữ Liệu 2026 — 18 Tháng 6 năm 2026, TP. Hồ Chí Minh",
"description": "Cùng các nhà lãnh đạo ngành ở tuyến đầu tiến hóa công nghệ. Chúng tôi mời các công ty đổi mới đồng tài trợ cho sự kiện đẳng cấp tập trung vào AI, IPv6, Điện Toán Đám Mây, An Ninh Mạng, Blockchain và Trung Tâm Dữ Liệu. Kết nối thương hiệu của bạn với những kiến trúc sư của hạ tầng ngày mai.",
"primaryCta": { "label": "Tải Hồ Sơ Tài Trợ", "href": "#" },
"secondaryCta": { "label": "Liên Hệ Chuyên Viên", "href": "#contact" }
},
"packages": {
"eyebrow": "GÓI TÀI TRỢ",
"title": "Gói Tài Trợ & Quyền Lợi",
"items": [
{
"id": "diamond",
"tier": "NHÀ TÀI TRỢ KIM CƯƠNG",
"badge": "UY TÍN NHẤT",
"price": "80,000,000 VNĐ",
"featured": true,
"cta": "Chọn Kim Cương",
"benefits": [
"Logo nổi bật trên tất cả tài liệu thương hiệu",
"Logo trên phông sân khấu & màn hình LED",
"Không gian triển lãm cao cấp",
"1 suất phát biểu độc quyền",
"07 vé VIP đại biểu"
],
"boldBenefit": "1 suất phát biểu độc quyền"
},
{
"id": "gold",
"tier": "NHÀ TÀI TRỢ VÀNG",
"badge": null,
"price": "50,000,000 VNĐ",
"featured": false,
"cta": "Chọn Vàng",
"benefits": [
"Logo trên phông sân khấu & màn hình LED",
"Vị trí triển lãm standee",
"05 vé VIP đại biểu"
],
"boldBenefit": null
},
{
"id": "silver",
"tier": "NHÀ TÀI TRỢ BẠC",
"badge": null,
"price": "40,000,000 VNĐ",
"featured": false,
"cta": "Chọn Bạc",
"benefits": [
"Logo trên phông sân khấu & màn hình LED",
"Standee triển lãm tiêu chuẩn",
"04 vé VIP đại biểu"
],
"boldBenefit": null
},
{
"id": "partner",
"tier": "ĐỐI TÁC TÀI TRỢ",
"badge": null,
"price": "20,000,000 VNĐ",
"featured": false,
"cta": "Chọn Đối Tác",
"benefits": [
"Logo trên website chính thức & truyền thông sự kiện",
"02 vé VIP đại biểu"
],
"boldBenefit": null
}
]
},
"itemBased": {
"eyebrow": "À LA CARTE",
"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ị",
"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": "groups", "title": "Khu Vực Kết Nối", "status": "Còn Trống" },
{ "icon": "newsmode", "title": "Đối Tác Truyền Thông", "status": "Đăng Ký Ngay" }
],
"highlights": [
{
"eyebrow": "QUYỀN LỢI TIÊU CHUẨN",
"title": "Logo trên phông & màn hình LED",
"icon": "branding_watermark"
},
{
"eyebrow": "GÓI TIẾP CẬN",
"title": "02 vé VIP đại biểu được bao gồm",
"icon": "confirmation_number"
}
]
},
"contact": {
"id": "contact",
"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.",
"phone": {
"label": "ĐIỆN THOẠI",
"value": "+84 941 523 498"
},
"email": {
"label": "EMAIL TRỰC TIẾP",
"value": "events@techvanguard.vn"
},
"cta": "Liên Hệ Để Tài Trợ",
"image": {
"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"
}
}
}
+29
View File
@@ -0,0 +1,29 @@
import { useLanguage } from "@/app/context/LanguageContext";
import siteEn from "@/app/data/site.json";
import siteVi from "@/app/data/site.vi.json";
import homeEn from "@/app/data/home.json";
import homeVi from "@/app/data/home.vi.json";
import registrationEn from "@/app/data/registration.json";
import registrationVi from "@/app/data/registration.vi.json";
import feedbackEn from "@/app/data/feedback.json";
import feedbackVi from "@/app/data/feedback.vi.json";
import sponsorEn from "@/app/data/sponsor.json";
import sponsorVi from "@/app/data/sponsor.vi.json";
import adminEn from "@/app/data/admin.json";
import adminVi from "@/app/data/admin.vi.json";
export function useTranslation() {
const { lang } = useLanguage();
const isVi = lang === "vi";
return {
lang,
site: isVi ? siteVi : siteEn,
home: isVi ? homeVi : homeEn,
registration: isVi ? registrationVi : registrationEn,
feedback: isVi ? feedbackVi : feedbackEn,
sponsor: isVi ? sponsorVi : sponsorEn,
admin: isVi ? adminVi : adminEn,
};
}
+2 -1
View File
@@ -1,6 +1,7 @@
import type { Metadata } from "next";
import "../public/assets/css/all.min.css";
import "./globals.css";
import { LanguageProvider } from "@/app/context/LanguageContext";
export const metadata: Metadata = {
metadataBase: new URL(process.env.NEXT_PUBLIC_BASE_URL || "http://localhost:3000"),
@@ -32,7 +33,7 @@ export default function RootLayout({ children }: { children: React.ReactNode })
</head>
<body suppressHydrationWarning>
{children}
<LanguageProvider>{children}</LanguageProvider>
</body>
</html>
);