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

Reviewed-on: UKSOURCE/ipv6#6
This commit is contained in:
2026-05-13 15:07:52 +00:00
65 changed files with 11724 additions and 2453 deletions
+1 -1
View File
@@ -1 +1 @@
PORT=3000
PORT=3000
+4 -1
View File
@@ -24,6 +24,9 @@
.DS_Store
*.pem
# local form submissions (written by /api/submissions)
/data/user-submissions.json
# debug
npm-debug.log*
yarn-debug.log*
@@ -42,7 +45,7 @@ next-env.d.ts
#vs code
/.vscode
package-lock.json
# package-lock.json should be committed for consistent dependencies
standalone-deploy.tar.gz
#ai-agent
+222 -143
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 build # Build production
npm run start # Chạy production server
npm run lint # Kiểm tra code style
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 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
-400
View File
@@ -1,400 +0,0 @@
<!DOCTYPE html>
<html class="dark" lang="en"><head>
<meta charset="utf-8"/>
<meta content="width=device-width, initial-scale=1.0" name="viewport"/>
<title>IPv6 SUMMIT 2026 | Admin Console</title>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&amp;family=Space+Grotesk:wght@600;700&amp;family=Geist:wght@600&amp;family=JetBrains+Mono:wght@500&amp;display=swap" rel="stylesheet"/>
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&amp;display=swap" rel="stylesheet"/>
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&amp;display=swap" rel="stylesheet"/>
<script src="https://cdn.tailwindcss.com?plugins=forms,container-queries"></script>
<script id="tailwind-config">
tailwind.config = {
darkMode: "class",
theme: {
extend: {
"colors": {
"secondary-fixed": "#ffddb2",
"outline-variant": "#4e4639",
"surface-container-highest": "#353534",
"on-tertiary-fixed": "#1c1b20",
"inverse-surface": "#e5e2e1",
"tertiary-fixed": "#e5e1e8",
"background": "#0F0F0F",
"primary": "#e9c176",
"surface-bright": "#3a3939",
"inverse-on-surface": "#313030",
"on-background": "#e5e2e1",
"on-secondary-fixed": "#291800",
"inverse-primary": "#775a19",
"on-tertiary-fixed-variant": "#48464c",
"on-primary": "#412d00",
"surface-dim": "#131313",
"on-surface": "#e5e2e1",
"on-tertiary-container": "#3c3a40",
"primary-fixed-dim": "#e9c176",
"primary-fixed": "#ffdea5",
"on-surface-variant": "#d1c5b4",
"surface-container-lowest": "#0e0e0e",
"on-primary-container": "#4e3700",
"on-secondary-fixed-variant": "#5d4216",
"secondary-container": "#604418",
"tertiary-fixed-dim": "#c9c5cc",
"surface-variant": "#353534",
"primary-container": "#c5a059",
"surface-container": "#201f1f",
"surface-container-low": "#1c1b1b",
"secondary": "#e9c088",
"error": "#ffb4ab",
"surface": "#131313",
"on-primary-fixed-variant": "#5d4201",
"tertiary-container": "#a7a4ab",
"on-secondary-container": "#d9b27c",
"error-container": "#93000a",
"surface-tint": "#e9c176",
"on-tertiary": "#313035",
"on-error": "#690005",
"surface-container-high": "#2a2a2a",
"outline": "#9a8f80",
"secondary-fixed-dim": "#e9c088",
"on-primary-fixed": "#261900",
"on-error-container": "#ffdad6",
"tertiary": "#c9c5cc",
"on-secondary": "#442b02"
},
"borderRadius": {
"DEFAULT": "0.125rem",
"lg": "0.25rem",
"xl": "0.5rem",
"full": "0.75rem"
},
"spacing": {
"stack-sm": "8px",
"stack-lg": "32px",
"stack-md": "16px",
"unit": "4px",
"section-gap": "80px",
"margin-safe": "40px",
"gutter": "24px"
},
"fontFamily": {
"body-base": ["Inter"],
"title-sm": ["Inter"],
"display-lg": ["Space Grotesk"],
"label-caps": ["Geist"],
"headline-md": ["Space Grotesk"],
"mono": ["JetBrains Mono"]
},
"fontSize": {
"body-base": ["16px", {"lineHeight": "1.6", "letterSpacing": "0", "fontWeight": "400"}],
"title-sm": ["20px", {"lineHeight": "1.4", "letterSpacing": "0.02em", "fontWeight": "600"}],
"display-lg": ["48px", {"lineHeight": "1.1", "letterSpacing": "-0.02em", "fontWeight": "700"}],
"label-caps": ["12px", {"lineHeight": "1", "letterSpacing": "0.1em", "fontWeight": "600"}],
"headline-md": ["32px", {"lineHeight": "1.2", "letterSpacing": "-0.01em", "fontWeight": "600"}]
}
},
},
}
</script>
<style>
.glass-panel {
background: rgba(26, 25, 30, 0.7);
backdrop-filter: blur(12px);
border: 1px solid rgba(255, 255, 255, 0.05);
}
.gold-border-focus:focus {
border-bottom: 1px solid #e9c176;
outline: none;
}
.metallic-gold-gradient {
background: linear-gradient(135deg, #e9c176 0%, #c5a059 100%);
}
.status-led {
box-shadow: 0 0 8px currentColor;
}
</style>
</head>
<body class="bg-background text-on-surface font-body-base min-h-screen flex">
<!-- Sidebar: Fixed, slim -->
<aside class="fixed left-0 top-0 h-full w-20 md:w-24 bg-surface-container-lowest border-r border-outline-variant/20 flex flex-col items-center py-stack-lg z-50">
<div class="mb-12">
<div class="w-12 h-12 metallic-gold-gradient flex items-center justify-center rounded-lg shadow-lg">
<span class="material-symbols-outlined text-on-primary text-3xl" style="font-variation-settings: 'FILL' 1;">cloud_done</span>
</div>
</div>
<nav class="flex flex-col gap-8 flex-1">
<button class="group flex flex-col items-center gap-1 text-primary">
<span class="material-symbols-outlined text-2xl transition-all duration-300 group-hover:scale-110" style="font-variation-settings: 'FILL' 1;">dashboard</span>
<span class="font-label-caps text-[8px] uppercase tracking-widest opacity-0 group-hover:opacity-100 transition-opacity">Panel</span>
</button>
<button class="group flex flex-col items-center gap-1 text-on-surface-variant hover:text-primary">
<span class="material-symbols-outlined text-2xl transition-all duration-300 group-hover:scale-110">group</span>
<span class="font-label-caps text-[8px] uppercase tracking-widest opacity-0 group-hover:opacity-100 transition-opacity">Users</span>
</button>
<button class="group flex flex-col items-center gap-1 text-on-surface-variant hover:text-primary">
<span class="material-symbols-outlined text-2xl transition-all duration-300 group-hover:scale-110">hourglass_empty</span>
<span class="font-label-caps text-[8px] uppercase tracking-widest opacity-0 group-hover:opacity-100 transition-opacity">Pending</span>
</button>
<button class="group flex flex-col items-center gap-1 text-on-surface-variant hover:text-primary">
<span class="material-symbols-outlined text-2xl transition-all duration-300 group-hover:scale-110">check_circle</span>
<span class="font-label-caps text-[8px] uppercase tracking-widest opacity-0 group-hover:opacity-100 transition-opacity">Approved</span>
</button>
<button class="group flex flex-col items-center gap-1 text-on-surface-variant hover:text-primary">
<span class="material-symbols-outlined text-2xl transition-all duration-300 group-hover:scale-110">cancel</span>
<span class="font-label-caps text-[8px] uppercase tracking-widest opacity-0 group-hover:opacity-100 transition-opacity">Rejected</span>
</button>
</nav>
<button class="group flex flex-col items-center gap-1 text-on-surface-variant hover:text-primary">
<span class="material-symbols-outlined text-2xl transition-all duration-300 group-hover:rotate-45">settings</span>
<span class="font-label-caps text-[8px] uppercase tracking-widest">Settings</span>
</button>
</aside>
<!-- Main Content Wrapper -->
<div class="flex-1 ml-20 md:ml-24 flex flex-col">
<!-- Header: Sticky Glass Panel -->
<header class="sticky top-0 w-full z-40 glass-panel py-4 px-gutter flex items-center justify-between border-b border-outline-variant/10">
<div class="flex items-center gap-4">
<div class="hidden md:block">
<h1 class="font-display-lg text-title-sm text-primary uppercase tracking-tighter">Admin Console</h1>
<p class="font-label-caps text-on-surface-variant text-[10px]">IPv6 SUMMIT 2026 / Infrastructure Core</p>
</div>
</div>
<div class="flex-1 max-w-xl mx-8">
<div class="relative group">
<span class="material-symbols-outlined absolute left-4 top-1/2 -translate-y-1/2 text-outline">search</span>
<input class="w-full bg-surface-container-low border-none rounded-full py-2.5 pl-12 pr-4 text-body-base text-on-surface focus:ring-1 focus:ring-primary/30 gold-border-focus transition-all" placeholder="Command Search (e.g. attendee name or company)" type="text"/>
</div>
</div>
<div class="flex items-center gap-6">
<div class="flex flex-col items-end hidden lg:flex">
<span class="font-title-sm text-sm text-on-surface">Director Chen</span>
<span class="font-label-caps text-[10px] text-primary">Systems Admin</span>
</div>
<div class="w-10 h-10 rounded-full border border-primary/40 p-0.5 overflow-hidden">
<img alt="Admin Profile" class="w-full h-full object-cover rounded-full bg-surface-container-high" data-alt="A professional, high-resolution portrait illustration of a tech executive. The person has a modern, minimalist appearance with subtle glowing cybernetic highlights. The background is a soft, dark gray charcoal with elegant gold lighting accents to match a premium high-tech UI theme." src="https://lh3.googleusercontent.com/aida-public/AB6AXuBL5I0HI-Q2dFLcJYnL30VBYPbnA_kiLY4AfC84YxaO3pOwYn3wVHKoCU78x037a47B6SVaLtkXaIWkgeNJl1Y3ymDBWGzVq_jey8olRQvKfUfRl4coRUe5NennfYDf2SnyPHxmWjXzEeE1yE_sbWQNv0HBLWeKXMBzx7NEzdjO9ZgCKQe7LHIV7HP3kfyw_V7rKNwPwCRUi0Za5P6-cOn6yvMOb-rbOu_dNJymmqL_lGFYtJG9rfaracohIUqfdQRfWiD9ZQZhaDKO"/>
</div>
</div>
</header>
<!-- Main Dashboard Canvas -->
<main class="p-gutter max-w-7xl mx-auto w-full flex flex-col gap-stack-lg">
<!-- Top Stat Cards -->
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-gutter">
<!-- Total -->
<div class="glass-panel p-6 rounded-xl relative overflow-hidden group">
<div class="absolute top-0 right-0 p-4 opacity-10 group-hover:opacity-20 transition-opacity">
<span class="material-symbols-outlined text-6xl">database</span>
</div>
<p class="font-label-caps text-on-surface-variant mb-2">Total Requests</p>
<h3 class="font-display-lg text-headline-md text-on-surface">1,284</h3>
<div class="mt-4 flex items-center gap-2">
<span class="text-xs text-primary">+12% vs last week</span>
</div>
</div>
<!-- Pending -->
<div class="glass-panel p-6 rounded-xl relative overflow-hidden border-l-4 border-secondary group">
<p class="font-label-caps text-secondary mb-2 flex items-center gap-2">
<span class="status-led w-2 h-2 rounded-full bg-secondary"></span>
Pending Approval
</p>
<h3 class="font-display-lg text-headline-md text-on-surface">42</h3>
<div class="mt-4 flex items-center gap-2">
<span class="text-xs text-secondary-fixed">Critical threshold: 50</span>
</div>
</div>
<!-- Confirmed -->
<div class="glass-panel p-6 rounded-xl relative overflow-hidden border-l-4 border-primary group">
<p class="font-label-caps text-primary mb-2 flex items-center gap-2">
<span class="status-led w-2 h-2 rounded-full bg-primary"></span>
Confirmed
</p>
<h3 class="font-display-lg text-headline-md text-on-surface">856</h3>
<div class="mt-4 flex items-center gap-2">
<span class="text-xs text-primary-fixed-dim">Capacity at 74%</span>
</div>
</div>
<!-- Conversion -->
<div class="glass-panel p-6 rounded-xl relative overflow-hidden group">
<p class="font-label-caps text-on-surface-variant mb-2">Conversion Rate</p>
<h3 class="font-display-lg text-headline-md text-on-surface">67%</h3>
<div class="mt-4 w-full bg-surface-container-high h-1.5 rounded-full overflow-hidden">
<div class="h-full metallic-gold-gradient w-2/3"></div>
</div>
</div>
</div>
<!-- Filter Bar -->
<section class="glass-panel px-6 py-4 rounded-xl flex flex-col lg:flex-row items-center justify-between gap-stack-md">
<div class="flex flex-wrap items-center gap-4 w-full lg:w-auto">
<div class="flex flex-col gap-1">
<label class="font-label-caps text-[10px] text-outline">Status</label>
<select class="bg-surface-container border border-outline-variant/30 text-on-surface text-sm rounded-lg px-3 py-1.5 focus:ring-primary/50 focus:border-primary">
<option>All Operations</option>
<option>Pending</option>
<option>Approved</option>
<option>Rejected</option>
</select>
</div>
<div class="flex flex-col gap-1">
<label class="font-label-caps text-[10px] text-outline">Company</label>
<select class="bg-surface-container border border-outline-variant/30 text-on-surface text-sm rounded-lg px-3 py-1.5 focus:ring-primary/50 focus:border-primary">
<option>Global Tier 1</option>
<option>Cloud Providers</option>
<option>Enterprise</option>
</select>
</div>
<div class="flex flex-col gap-1">
<label class="font-label-caps text-[10px] text-outline">Timeframe</label>
<input class="bg-surface-container border border-outline-variant/30 text-on-surface text-sm rounded-lg px-3 py-1.5" type="date"/>
</div>
</div>
<button class="w-full lg:w-auto border border-primary text-primary font-label-caps px-6 py-2 rounded transition-all hover:bg-primary hover:text-on-primary flex items-center justify-center gap-2">
<span class="material-symbols-outlined text-sm">download</span>
Export to Excel
</button>
</section>
<!-- Data Table -->
<div class="glass-panel rounded-xl overflow-hidden shadow-2xl">
<div class="overflow-x-auto">
<table class="w-full text-left border-collapse">
<thead>
<tr class="bg-surface-container border-b border-outline-variant/20">
<th class="px-6 py-4 font-label-caps text-outline text-[11px] uppercase tracking-widest">ID</th>
<th class="px-6 py-4 font-label-caps text-outline text-[11px] uppercase tracking-widest">Submit Date</th>
<th class="px-6 py-4 font-label-caps text-outline text-[11px] uppercase tracking-widest">Name</th>
<th class="px-6 py-4 font-label-caps text-outline text-[11px] uppercase tracking-widest">Company</th>
<th class="px-6 py-4 font-label-caps text-outline text-[11px] uppercase tracking-widest">Email / Phone</th>
<th class="px-6 py-4 font-label-caps text-outline text-[11px] uppercase tracking-widest">Status</th>
<th class="px-6 py-4 font-label-caps text-outline text-[11px] uppercase tracking-widest text-right">Actions</th>
</tr>
</thead>
<tbody class="divide-y divide-outline-variant/10">
<!-- Row 1 -->
<tr class="hover:bg-surface-container-high/40 transition-colors">
<td class="px-6 py-4 font-mono text-sm text-primary">#V6-4821</td>
<td class="px-6 py-4 text-sm text-on-surface-variant">24 OCT 2025</td>
<td class="px-6 py-4">
<div class="font-title-sm text-sm">Elena Rodriguez</div>
<div class="text-xs text-outline">Network Architect</div>
</td>
<td class="px-6 py-4 text-sm">Cloudflare, Inc.</td>
<td class="px-6 py-4">
<div class="text-sm">e.rodriguez@cloudflare.com</div>
<div class="text-xs text-outline">+1 (555) 012-9988</div>
</td>
<td class="px-6 py-4">
<span class="px-3 py-1 rounded-full text-[10px] font-label-caps border border-primary text-primary bg-primary/10 flex items-center gap-1.5 w-fit">
<span class="w-1.5 h-1.5 rounded-full bg-primary status-led"></span>
Approved
</span>
</td>
<td class="px-6 py-4 text-right">
<div class="flex items-center justify-end gap-2">
<button class="p-2 rounded-lg hover:bg-surface-container-highest text-on-surface-variant hover:text-on-surface transition-all">
<span class="material-symbols-outlined text-lg">visibility</span>
</button>
<button class="p-2 rounded-lg hover:bg-primary/20 text-primary transition-all">
<span class="material-symbols-outlined text-lg">edit</span>
</button>
</div>
</td>
</tr>
<!-- Row 2 -->
<tr class="hover:bg-surface-container-high/40 transition-colors">
<td class="px-6 py-4 font-mono text-sm text-primary">#V6-4819</td>
<td class="px-6 py-4 text-sm text-on-surface-variant">23 OCT 2025</td>
<td class="px-6 py-4">
<div class="font-title-sm text-sm">Marcus Thorne</div>
<div class="text-xs text-outline">Senior Devops</div>
</td>
<td class="px-6 py-4 text-sm">Akamai Technologies</td>
<td class="px-6 py-4">
<div class="text-sm">m.thorne@akamai.io</div>
<div class="text-xs text-outline">+44 20 7946 0123</div>
</td>
<td class="px-6 py-4">
<span class="px-3 py-1 rounded-full text-[10px] font-label-caps border border-secondary text-secondary bg-secondary/10 flex items-center gap-1.5 w-fit">
<span class="w-1.5 h-1.5 rounded-full bg-secondary status-led"></span>
Pending
</span>
</td>
<td class="px-6 py-4 text-right">
<div class="flex items-center justify-end gap-2">
<button class="p-2 rounded-lg hover:bg-surface-container-highest text-on-surface-variant hover:text-on-surface">
<span class="material-symbols-outlined text-lg">visibility</span>
</button>
<button class="p-2 rounded-lg hover:bg-primary text-on-primary" title="Approve">
<span class="material-symbols-outlined text-lg" style="font-variation-settings: 'FILL' 1;">check</span>
</button>
<button class="p-2 rounded-lg hover:bg-error/20 text-error" title="Reject">
<span class="material-symbols-outlined text-lg">close</span>
</button>
</div>
</td>
</tr>
<!-- Row 3 -->
<tr class="hover:bg-surface-container-high/40 transition-colors">
<td class="px-6 py-4 font-mono text-sm text-primary">#V6-4790</td>
<td class="px-6 py-4 text-sm text-on-surface-variant">22 OCT 2025</td>
<td class="px-6 py-4">
<div class="font-title-sm text-sm">Kenji Yamamoto</div>
<div class="text-xs text-outline">CTO</div>
</td>
<td class="px-6 py-4 text-sm">NTT Data Japan</td>
<td class="px-6 py-4">
<div class="text-sm">yamamoto@ntt.jp</div>
<div class="text-xs text-outline">+81 3 1234 5678</div>
</td>
<td class="px-6 py-4">
<span class="px-3 py-1 rounded-full text-[10px] font-label-caps border border-error text-error bg-error/10 flex items-center gap-1.5 w-fit">
<span class="w-1.5 h-1.5 rounded-full bg-error status-led"></span>
Rejected
</span>
</td>
<td class="px-6 py-4 text-right">
<div class="flex items-center justify-end gap-2">
<button class="p-2 rounded-lg hover:bg-surface-container-highest text-on-surface-variant hover:text-on-surface">
<span class="material-symbols-outlined text-lg">visibility</span>
</button>
<button class="p-2 rounded-lg hover:bg-surface-container-highest text-on-surface-variant">
<span class="material-symbols-outlined text-lg">history</span>
</button>
</div>
</td>
</tr>
</tbody>
</table>
</div>
<!-- Pagination -->
<div class="px-6 py-4 border-t border-outline-variant/10 flex items-center justify-between">
<p class="text-xs text-outline">Showing 1 to 10 of 1,284 entries</p>
<div class="flex items-center gap-2">
<button class="w-8 h-8 flex items-center justify-center rounded border border-outline-variant/30 text-outline hover:border-primary hover:text-primary transition-all">
<span class="material-symbols-outlined text-sm">chevron_left</span>
</button>
<button class="w-8 h-8 flex items-center justify-center rounded bg-primary text-on-primary text-xs font-bold">1</button>
<button class="w-8 h-8 flex items-center justify-center rounded border border-outline-variant/30 text-on-surface-variant hover:border-primary hover:text-primary transition-all text-xs">2</button>
<button class="w-8 h-8 flex items-center justify-center rounded border border-outline-variant/30 text-on-surface-variant hover:border-primary hover:text-primary transition-all text-xs">3</button>
<button class="w-8 h-8 flex items-center justify-center rounded border border-outline-variant/30 text-outline hover:border-primary hover:text-primary transition-all">
<span class="material-symbols-outlined text-sm">chevron_right</span>
</button>
</div>
</div>
</div>
</main>
<!-- Footer -->
<footer class="mt-auto border-t border-outline-variant/20 py-8 px-gutter flex flex-col md:flex-row items-center justify-between gap-4">
<div class="flex items-center gap-4">
<span class="font-headline-md text-sm text-primary uppercase">IPv6 SUMMIT 2026</span>
<span class="text-xs text-outline">| Systems Infrastructure Core v2.4.0</span>
</div>
<p class="text-[11px] font-label-caps text-outline tracking-widest">© 2026 IPv6 for AI &amp; Data Centre Summit. Infrastructure for the Future.</p>
<div class="flex items-center gap-6">
<a class="text-[10px] font-label-caps text-on-surface-variant hover:text-primary transition-colors" href="#">Privacy</a>
<a class="text-[10px] font-label-caps text-on-surface-variant hover:text-primary transition-colors" href="#">Terms</a>
<a class="text-[10px] font-label-caps text-on-surface-variant hover:text-primary transition-colors" href="#">Support</a>
</div>
</footer>
</div>
<!-- Background Decorative Glows -->
<div class="fixed top-[-10%] left-[-10%] w-[40%] h-[40%] bg-primary/5 blur-[120px] rounded-full -z-10"></div>
<div class="fixed bottom-[-10%] right-[-10%] w-[30%] h-[30%] bg-secondary/5 blur-[100px] rounded-full -z-10"></div>
</body></html>
+24
View File
@@ -0,0 +1,24 @@
"use server";
import { updateSummitRequestStatus } from "@/lib/summit-requests";
import type { SubmissionStatus } from "@/types/admin-submission";
import { revalidatePath } from "next/cache";
const ADMIN_PATHS = ["/admin", "/admin/pending", "/admin/approved", "/admin/rejected"] as const;
const ALLOWED: SubmissionStatus[] = ["pending", "approved", "rejected"];
export async function setRequestStatusAction(publicId: string, status: SubmissionStatus) {
if (!ALLOWED.includes(status)) {
throw new Error("Invalid status");
}
if (!publicId || typeof publicId !== "string") {
throw new Error("Invalid id");
}
await updateSummitRequestStatus(publicId.trim(), status);
for (const p of ADMIN_PATHS) {
revalidatePath(p);
}
}
+15
View File
@@ -0,0 +1,15 @@
import type { Metadata } from "next";
import AdminConsole from "@/app/components/admin/AdminConsole";
import { loadAdminConsoleData } from "@/lib/load-admin-console-data";
export const dynamic = "force-dynamic";
export const metadata: Metadata = {
title: "Approved | Admin Console",
robots: { index: false, follow: false },
};
export default async function AdminApprovedPage() {
const data = await loadAdminConsoleData();
return <AdminConsole data={data} view="approved" />;
}
+16
View File
@@ -0,0 +1,16 @@
import type { Metadata } from "next";
import AdminConsole from "@/app/components/admin/AdminConsole";
import { loadAdminConsoleData } from "@/lib/load-admin-console-data";
export const dynamic = "force-dynamic";
export const metadata: Metadata = {
title: "Admin Console | IPv6 Summit 2026",
description: "Internal request management.",
robots: { index: false, follow: false },
};
export default async function AdminPage() {
const data = await loadAdminConsoleData();
return <AdminConsole data={data} view="all" />;
}
+15
View File
@@ -0,0 +1,15 @@
import type { Metadata } from "next";
import AdminConsole from "@/app/components/admin/AdminConsole";
import { loadAdminConsoleData } from "@/lib/load-admin-console-data";
export const dynamic = "force-dynamic";
export const metadata: Metadata = {
title: "Pending approval | Admin Console",
robots: { index: false, follow: false },
};
export default async function AdminPendingPage() {
const data = await loadAdminConsoleData();
return <AdminConsole data={data} view="pending" />;
}
+15
View File
@@ -0,0 +1,15 @@
import type { Metadata } from "next";
import AdminConsole from "@/app/components/admin/AdminConsole";
import { loadAdminConsoleData } from "@/lib/load-admin-console-data";
export const dynamic = "force-dynamic";
export const metadata: Metadata = {
title: "Rejected | Admin Console",
robots: { index: false, follow: false },
};
export default async function AdminRejectedPage() {
const data = await loadAdminConsoleData();
return <AdminConsole data={data} view="rejected" />;
}
+3
View File
@@ -0,0 +1,3 @@
export default function InternalLayout({ children }: { children: React.ReactNode }) {
return children;
}
+5
View File
@@ -0,0 +1,5 @@
import FeedbackPageClient from "@/app/components/forms/FeedbackPageClient";
export default function FeedbackPage() {
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-24 lg:pt-32">
<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-[160px] pb-[80px] px-[var(--spacing-gutter)] flex justify-center">
<div className="w-full max-w-[1280px]">
<div className="max-w-[800px] mx-auto">
<div className="mb-[32px]">
<Link
className="group flex items-center gap-2 text-outline hover:text-primary transition-colors"
href="/"
>
<span className="material-symbols-outlined text-[20px] transition-transform group-hover:-translate-x-1">
arrow_back
</span>
Back to Home
</Link>
</div>
<div className="mb-[32px]">
<h1 className="font-[var(--font-display-lg)] text-[clamp(2.25rem,3vw+1rem,4.5rem)] text-on-surface mb-2 leading-[1.1]">
{data.title}
</h1>
<p className="text-on-surface-variant text-lg">{data.subtitle}</p>
</div>
<div className="glass-panel p-[32px] md:p-12 rounded-xl shadow-2xl relative overflow-hidden">
<div className="absolute top-0 left-0 w-full h-[1px] bg-gradient-to-r from-transparent via-primary/20 to-transparent" />
<RegistrationForm data={data} />
</div>
</div>
</div>
</main>
);
return <RegistrationPageClient />;
}
+12
View File
@@ -0,0 +1,12 @@
import type { Metadata } from "next";
import SponsorPageClient from "@/app/components/sponsor/SponsorPageClient";
export const metadata: Metadata = {
title: "Nhà Tài Trợ | IPv6 Summit 2026",
description:
"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 <SponsorPageClient />;
}
+161
View File
@@ -0,0 +1,161 @@
import { createSummitRequest } from "@/lib/summit-requests";
import type { AdminRequestRow } from "@/types/admin-submission";
import { NextResponse } from "next/server";
export const runtime = "nodejs";
const INDUSTRY_SEGMENT: Record<string, string> = {
government: "enterprise",
telecom: "tier1",
cloud: "cloud",
ai: "enterprise",
others: "enterprise",
};
function displayDateNow(): string {
return new Date()
.toLocaleDateString("en-GB", { day: "2-digit", month: "short", year: "numeric" })
.toUpperCase()
.replace(/ /g, " ");
}
function newId(): string {
return `V6-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
}
type RegistrationBody = {
type: "registration";
fullName: string;
phone: string;
email: string;
company: string;
jobTitle: string;
industry: string;
industryLabel?: string;
notes: string;
};
type FeedbackBody = {
type: "feedback";
[key: string]: any;
};
function buildFeedbackNotes(b: FeedbackBody): string {
const lines = [
"Source: Event feedback",
b.overallExperience && `Overall experience: ${b.overallExperience}/5`,
b.venueFacilities && `Venue & Facilities: ${b.venueFacilities}/5`,
b.organisationProcess && `Organisation & Registration: ${b.organisationProcess}/5`,
b.mostValuableSession && `Most valuable session: ${b.mostValuableSession}`,
b.relevance && `Content relevance: ${b.relevance}`,
b.speakerRating && `Speakers/Panels rating: ${b.speakerRating}`,
b.networkingUtility && `Networking utility: ${b.networkingUtility}`,
b.futureTopics && `Future topics: ${Array.isArray(b.futureTopics) ? b.futureTopics.join(", ") : b.futureTopics}${b.futureTopics_other ? ` (Other: ${b.futureTopics_other})` : ""}`,
b.attendAgain && `Attend again in 2027: ${b.attendAgain}`,
b.recommend && `Recommend to others: ${b.recommend}`,
b.enjoyedMost && `Enjoyed most: ${b.enjoyedMost}`,
b.improvementAreas && `Improvement areas: ${b.improvementAreas}`,
b.additionalComments && `Additional comments: ${b.additionalComments}`,
b.industry && `Industry: ${Array.isArray(b.industry) ? b.industry.join(", ") : b.industry}${b.industry_other ? ` (Other: ${b.industry_other})` : ""}`,
b.role && `Role: ${Array.isArray(b.role) ? b.role.join(", ") : b.role}${b.role_other ? ` (Other: ${b.role_other})` : ""}`,
];
return lines.filter(Boolean).join("\n");
}
export async function POST(req: Request) {
let body: unknown;
try {
body = await req.json();
} catch {
return NextResponse.json({ error: "Invalid JSON" }, { status: 400 });
}
if (!body || typeof body !== "object" || !("type" in body)) {
return NextResponse.json({ error: "Missing type" }, { status: 400 });
}
const t = (body as { type: string }).type;
const submittedAt = new Date().toISOString().slice(0, 10);
const displayDate = displayDateNow();
if (t === "registration") {
const b = body as RegistrationBody;
const fullName = typeof b.fullName === "string" ? b.fullName.trim() : "";
const email = typeof b.email === "string" ? b.email.trim() : "";
if (!fullName || !email) {
return NextResponse.json({ error: "Full name and email are required" }, { status: 400 });
}
const industry = typeof b.industry === "string" ? b.industry : "";
const segment = INDUSTRY_SEGMENT[industry] ?? "enterprise";
const industryLabel = typeof b.industryLabel === "string" ? b.industryLabel : industry;
const noteParts = [
typeof b.notes === "string" && b.notes.trim() ? b.notes.trim() : "",
industryLabel ? `Industry: ${industryLabel}` : "",
].filter(Boolean);
const row: AdminRequestRow = {
id: newId(),
submittedAt,
displayDate,
fullName,
jobTitle: typeof b.jobTitle === "string" ? b.jobTitle.trim() : "",
company: typeof b.company === "string" ? b.company.trim() : "",
segment,
email,
phone: typeof b.phone === "string" ? b.phone.trim() : "",
status: "pending",
notes: noteParts.join("\n"),
source: "registration",
};
try {
await createSummitRequest(row);
} catch (e: unknown) {
const code = e && typeof e === "object" && "code" in e ? (e as { code?: number }).code : undefined;
if (code === 11000) {
return NextResponse.json({ error: "Duplicate submission id" }, { status: 409 });
}
console.error(e);
return NextResponse.json({ error: "Could not save submission" }, { status: 500 });
}
return NextResponse.json({ ok: true, id: row.id });
}
if (t === "feedback") {
const b = body as FeedbackBody;
const sessionTitle =
typeof b.mostValuableSession === "string" && b.mostValuableSession
? b.mostValuableSession
: "General";
const row: AdminRequestRow = {
id: newId(),
submittedAt,
displayDate,
fullName: "Event feedback",
jobTitle: sessionTitle,
company: "—",
segment: "enterprise",
email: "—",
phone: "—",
status: "pending",
notes: buildFeedbackNotes(b),
source: "feedback",
};
try {
await createSummitRequest(row);
} catch (e: unknown) {
const code = e && typeof e === "object" && "code" in e ? (e as { code?: number }).code : undefined;
if (code === 11000) {
return NextResponse.json({ error: "Duplicate submission id" }, { status: 409 });
}
console.error(e);
return NextResponse.json({ error: "Could not save submission" }, { status: 500 });
}
return NextResponse.json({ ok: true, id: row.id });
}
return NextResponse.json({ error: "Unknown type" }, { status: 400 });
}
+841
View File
@@ -0,0 +1,841 @@
"use client";
import { setRequestStatusAction } from "@/app/(internal)/admin/actions";
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;
type StatusKey = SubmissionStatus;
export type AdminConsoleView = "all" | "pending" | "approved" | "rejected";
export type AdminData = {
meta: {
title: string;
subtitle: string;
user: { name: string; role: string; avatarUrl: string };
footerTag: string;
};
stats: Array<{
id: string;
label: string;
value: string;
hint: string;
icon: string;
accent: "none" | "pending" | "confirmed" | "progress";
progressPercent?: number;
}>;
filters: {
status: { label: string; options: { value: string; label: string }[] };
companyGroup: { label: string; options: { value: string; label: string }[] };
searchPlaceholder: string;
exportLabel: string;
};
table: {
columns: Record<string, string>;
pageSize: number;
};
statusMap: Record<
StatusKey,
{
label: string;
borderClass: string;
dotClass: string;
}
>;
requests: AdminRequest[];
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,
}: {
item: AdminData["stats"][0];
}) {
const accent =
item.accent === "pending"
? "border-l-4 border-secondary"
: item.accent === "confirmed"
? "border-l-4 border-primary"
: "";
return (
<div className={`glass-panel p-6 rounded-xl relative overflow-hidden group ${accent}`}>
<div className="absolute top-0 right-0 p-4 opacity-10 group-hover:opacity-20 transition-opacity pointer-events-none">
<span className="material-symbols-outlined text-6xl">{item.icon}</span>
</div>
<p
className={`font-[var(--font-label-caps)] text-[11px] uppercase tracking-widest mb-2 flex items-center gap-2 ${
item.accent === "pending"
? "text-secondary"
: item.accent === "confirmed"
? "text-primary"
: "text-outline"
}`}
>
{item.accent === "pending" || item.accent === "confirmed" ? (
<span
className={`status-led w-2 h-2 rounded-full ${
item.accent === "pending" ? "bg-secondary" : "bg-primary"
}`}
/>
) : null}
{item.label}
</p>
<h3 className="font-[var(--font-display-lg)] text-2xl md:text-3xl text-on-surface">{item.value}</h3>
{item.accent === "progress" && typeof item.progressPercent === "number" ? (
<div className="mt-4 w-full bg-surface-container-high h-1.5 rounded-full overflow-hidden">
<div
className="h-full gold-gradient-bg transition-all"
style={{ width: `${item.progressPercent}%` }}
/>
</div>
) : (
<div className="mt-4 flex items-center gap-2 min-h-[20px]">
{item.hint ? (
<span
className={`text-xs ${
item.accent === "pending"
? "text-secondary"
: item.accent === "confirmed"
? "text-primary"
: "text-primary"
}`}
>
{item.hint}
</span>
) : null}
</div>
)}
</div>
);
}
const PENDING_THRESHOLD = 50;
function formatCount(n: number) {
return n.toLocaleString("en-US");
}
function buildLiveStats(data: AdminData, lang: string) {
const req = data.requests;
const total = req.length;
const pending = req.filter((r) => r.status === "pending").length;
const approved = req.filter((r) => r.status === "approved").length;
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: 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: 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})`),
};
case "confirmed":
return {
...s,
value: formatCount(approved),
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
? (isVi ? `Tỷ lệ đã duyệt: ${approved} / ${total}` : `Approved share: ${approved} / ${total}`)
: "",
progressPercent: conversionPct,
};
default:
return s;
}
});
}
export default function AdminConsole({ data, view = "all" }: { data: AdminData; view?: AdminConsoleView }) {
const router = useRouter();
const pathname = usePathname();
const [isPending, startTransition] = useTransition();
const [search, setSearch] = useState("");
const [status, setStatus] = useState("all");
const [companyGroup, setCompanyGroup] = useState("all");
const [afterDate, setAfterDate] = useState("");
const [page, setPage] = useState(1);
const [detail, setDetail] = useState<AdminRequest | null>(null);
const { lang, setLang } = useLanguage();
const { admin: t } = useTranslation();
// 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;
const filtered = useMemo(() => {
let q = search.trim().toLowerCase();
// Remove leading # if searching for ID
if (q.startsWith("#")) {
q = q.substring(1);
}
return data.requests.filter((r) => {
if (statusFilter !== "all" && r.status !== statusFilter) return false;
if (companyGroup !== "all" && r.segment !== companyGroup) return false;
if (afterDate && r.submittedAt < afterDate) return false;
if (!q) return true;
const hay = `${r.id} ${r.fullName} ${r.company} ${r.email} ${r.phone} ${r.jobTitle} ${r.status}`.toLowerCase();
return hay.includes(q);
});
}, [data.requests, search, statusFilter, companyGroup, afterDate]);
async function commitStatus(publicId: string, next: SubmissionStatus) {
try {
await setRequestStatusAction(publicId, next);
startTransition(() => router.refresh());
} catch (e) {
alert(e instanceof Error ? e.message : "Could not update status");
}
}
const pageSize = data.table.pageSize;
const totalPages = Math.max(1, Math.ceil(filtered.length / pageSize));
const safePage = Math.min(page, totalPages);
const sliceStart = (safePage - 1) * pageSize;
const pageRows = filtered.slice(sliceStart, sliceStart + pageSize);
const showingFrom = filtered.length === 0 ? 0 : sliceStart + 1;
const showingTo = sliceStart + pageRows.length;
return (
<div className="bg-background text-on-surface font-[var(--font-body-base)] min-h-screen flex relative">
<aside className="fixed left-0 top-0 h-full w-20 md:w-24 bg-surface-container-low border-r border-outline/20 flex flex-col items-center py-8 z-50">
<div className="mb-12">
<div className="w-12 h-12 gold-gradient-bg flex items-center justify-center rounded-lg shadow-lg">
<span className="material-symbols-outlined text-on-primary text-3xl" style={{ fontVariationSettings: "'FILL' 1" }}>
cloud_done
</span>
</div>
</div>
<nav className="flex flex-col gap-8 flex-1" aria-label="Admin sections">
{(
[
{ 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;
return (
<Link
key={item.href}
href={item.href}
className={`group flex flex-col items-center gap-1 ${
active ? "text-primary" : "text-on-surface-variant hover:text-primary"
}`}
>
<span
className="material-symbols-outlined text-2xl transition-all duration-300 group-hover:scale-110"
style={active ? { fontVariationSettings: "'FILL' 1" } : undefined}
>
{item.icon}
</span>
<span className="font-[var(--font-label-caps)] text-[8px] uppercase tracking-widest opacity-0 group-hover:opacity-100 transition-opacity">
{item.label}
</span>
</Link>
);
})}
</nav>
<button
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>
<div className="flex-1 ml-20 md:ml-24 flex flex-col min-w-0">
<header className="sticky top-0 w-full z-40 glass-panel py-4 px-[var(--spacing-gutter)] flex flex-col lg:flex-row items-stretch lg:items-center justify-between gap-4 border-b border-outline/10">
<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"
? t.meta.title
: view === "pending"
? (lang === "vi" ? "Chờ Duyệt" : "Pending Approval")
: view === "approved"
? (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">
{t.meta.subtitle}
</p>
</div>
<div className="flex-1 max-w-xl mx-0 lg:mx-8 min-w-0">
<div className="relative group">
<span className="material-symbols-outlined absolute left-4 top-1/2 -translate-y-1/2 text-outline pointer-events-none">
search
</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={t.filters.searchPlaceholder}
type="search"
value={search}
onChange={(e) => {
setSearch(e.target.value);
setPage(1);
}}
/>
</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">
{t.meta.user.role}
</span>
</div>
</div>
</header>
<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} />
))}
</div>
<section className="glass-panel px-6 py-4 rounded-xl flex flex-col lg:flex-row items-stretch lg:items-end justify-between gap-4">
<div className="flex flex-wrap items-end gap-4 w-full lg:w-auto">
{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">
{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"
value={status}
onChange={(e) => {
setStatus(e.target.value);
setPage(1);
}}
>
{t.filters.status.options.map((o) => (
<option key={o.value} value={o.value}>
{o.label}
</option>
))}
</select>
</div>
) : (
<div className="flex flex-col gap-1 min-w-[140px]">
<span className="font-[var(--font-label-caps)] text-[10px] text-outline uppercase tracking-wider">
{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">
{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">
{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"
value={companyGroup}
onChange={(e) => {
setCompanyGroup(e.target.value);
setPage(1);
}}
>
{t.filters.companyGroup.options.map((o) => (
<option key={o.value} value={o.value}>
{o.label}
</option>
))}
</select>
</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">
{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"
type="date"
value={afterDate}
onChange={(e) => {
setAfterDate(e.target.value);
setPage(1);
}}
/>
</div>
</div>
<button
type="button"
className="w-full lg:w-auto border border-primary text-primary font-[var(--font-label-caps)] px-6 py-2 rounded-lg transition-all hover:bg-primary hover:text-on-primary flex items-center justify-center gap-2 uppercase tracking-wider text-xs"
onClick={() => alert("Export is not wired (frontend-only demo).")}
>
<span className="material-symbols-outlined text-sm">download</span>
{t.filters.exportLabel}
</button>
</section>
<div className="glass-panel rounded-xl overflow-hidden shadow-2xl">
<div className="overflow-x-auto">
<table className="w-full text-left border-collapse min-w-[880px]">
<thead>
<tr className="bg-surface-container-high border-b border-outline/20">
<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 = t.statusMap[row.status as StatusKey];
return (
<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-4 py-3 text-[13px] text-on-surface-variant whitespace-nowrap">
{row.displayDate}
</td>
<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-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-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 h-1 rounded-full status-led ${sm.dotClass}`} />
{sm.label}
</span>
</td>
<td className="px-4 py-3 text-right">
<div className="flex items-center justify-end gap-0.5">
<button
type="button"
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)}
>
<span className="material-symbols-outlined text-lg">visibility</span>
</button>
{row.status === "pending" ? (
<>
<button
type="button"
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(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-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")}
>
<span className="material-symbols-outlined text-lg" style={{ fontVariationSettings: "'FILL' 1" }}>
check
</span>
</button>
<button
type="button"
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")}
>
<span className="material-symbols-outlined text-lg">close</span>
</button>
</>
) : (
<>
<button
type="button"
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(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>
{row.status === "approved" ? (
<>
<button
type="button"
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")}
>
<span className="material-symbols-outlined text-lg">undo</span>
</button>
<button
type="button"
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")}
>
<span className="material-symbols-outlined text-lg">close</span>
</button>
</>
) : (
<>
<button
type="button"
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")}
>
<span className="material-symbols-outlined text-lg" style={{ fontVariationSettings: "'FILL' 1" }}>
check
</span>
</button>
<button
type="button"
className="p-2 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")}
>
<span className="material-symbols-outlined text-lg">undo</span>
</button>
</>
)}
</>
)}
</div>
</td>
</tr>
);
})}
</tbody>
</table>
</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">
{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">
{lang === "vi" ? `Trang ${safePage} / ${totalPages}` : `Page ${safePage} of ${totalPages}`}
</span>
<button
type="button"
className="w-8 h-8 flex items-center justify-center rounded border border-outline/30 text-outline hover:border-primary hover:text-primary transition-all disabled:opacity-40"
disabled={safePage <= 1}
onClick={() => setPage((p) => Math.max(1, p - 1))}
>
<span className="material-symbols-outlined text-sm">chevron_left</span>
</button>
<button
type="button"
className="w-8 h-8 flex items-center justify-center rounded border border-outline/30 text-outline hover:border-primary hover:text-primary transition-all disabled:opacity-40"
disabled={safePage >= totalPages}
onClick={() => setPage((p) => Math.min(totalPages, p + 1))}
>
<span className="material-symbols-outlined text-sm">chevron_right</span>
</button>
</div>
</div>
</div>
</main>
<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">| {t.meta.footerTag}</span>
</div>
<p className="text-[11px] font-[var(--font-label-caps)] text-outline tracking-widest text-center">
{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">
{t.footer.legal.map((label) => (
<button
key={label}
type="button"
className="text-[10px] font-[var(--font-label-caps)] text-on-surface-variant hover:text-primary transition-colors uppercase tracking-widest"
onClick={() => alert(`${label} (placeholder).`)}
>
{label}
</button>
))}
</div>
</footer>
</div>
<div className="fixed top-[-10%] left-[-10%] w-[40%] h-[40%] bg-primary/5 blur-[120px] rounded-full -z-10 pointer-events-none" />
<div className="fixed bottom-[-10%] right-[-10%] w-[30%] h-[30%] bg-secondary/5 blur-[100px] rounded-full -z-10 pointer-events-none" />
{detail ? (
<div
className="fixed inset-0 z-[100] flex items-end sm:items-center justify-center p-4 bg-black/60 backdrop-blur-sm"
role="dialog"
aria-modal="true"
aria-labelledby="admin-detail-title"
>
<button
type="button"
className="absolute inset-0 cursor-default"
aria-label="Close"
onClick={() => setDetail(null)}
/>
<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 leading-tight">
{detail.fullName || (lang === "vi" ? "Yêu cầu mới" : "New Request")}
</h2>
<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="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 text-2xl">close</span>
</button>
</div>
{/* 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>
<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>
</div>
{/* 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}
</div>
);
}
+275
View File
@@ -0,0 +1,275 @@
"use client";
import SubmitToast from "@/app/components/ui/SubmitToast";
import { useMemo, useState } from "react";
type QuestionOption = { value: string; label: string };
type Question = {
id: string;
type: "rating" | "dropdown" | "scale" | "rating_labeled" | "checkbox_group" | "long_text";
label: string;
min?: number;
max?: number;
placeholder?: string;
options?: QuestionOption[];
hasOther?: boolean;
};
type Section = {
id: string;
title: string;
subtitle: string;
questions: Question[];
};
type FeedbackJson = {
title: string;
subtitle: string;
sections: Section[];
submit: { label: string; disclaimer: string };
};
function RadioScale({
name,
label,
min = 1,
max = 5,
value,
onChange,
}: {
name: string;
label: string;
min?: number;
max?: number;
value: string;
onChange: (v: string) => void;
}) {
const values = useMemo(() => {
const out: number[] = [];
for (let i = min; i <= max; i++) out.push(i);
return out;
}, [min, max]);
return (
<div className="flex gap-2 sm:gap-4 flex-wrap">
{values.map((n) => (
<label key={n} className="cursor-pointer group">
<input
className="peer sr-only"
name={name}
type="radio"
value={String(n)}
checked={value === String(n)}
onChange={() => onChange(String(n))}
/>
<div className="w-11 h-11 sm:w-12 sm:h-12 flex items-center justify-center border border-white/10 rounded-lg peer-checked:bg-primary peer-checked:text-on-primary hover:border-primary transition-all">
{n}
</div>
</label>
))}
</div>
);
}
export default function FeedbackForm({ data }: { data: FeedbackJson }) {
const [form, setForm] = useState<Record<string, any>>({});
const [submitState, setSubmitState] = useState<"idle" | "sending" | "ok" | "error">("idle");
const updateField = (id: string, value: any) => {
setForm((s) => ({ ...s, [id]: value }));
};
const toggleCheckbox = (id: string, value: string) => {
setForm((s) => {
const current = s[id] || [];
return {
...s,
[id]: current.includes(value) ? current.filter((v: string) => v !== value) : [...current, value],
};
});
};
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setSubmitState("sending");
try {
const res = await fetch("/api/submissions", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
type: "feedback",
...form,
}),
});
if (!res.ok) throw new Error("Submission failed");
setSubmitState("ok");
} catch {
setSubmitState("error");
}
};
return (
<form className="space-y-12 md:space-y-16" onSubmit={handleSubmit}>
{data.sections.map((section) => (
<section key={section.id} className="space-y-8">
<div className="border-l-4 border-primary pl-5">
<h2 className="font-[var(--font-display-lg)] text-2xl md:text-3xl text-on-surface tracking-tight leading-none mb-2">
{section.title}
</h2>
<p className="text-primary/80 font-medium">{section.subtitle}</p>
</div>
<div className="space-y-10">
{section.questions.map((q) => (
<div key={q.id} className="space-y-4">
<div className="space-y-1">
<label className="block text-lg font-semibold text-on-surface">{q.label}</label>
</div>
{q.type === "rating" && (
<RadioScale
name={q.id}
label={q.label}
min={q.min}
max={q.max}
value={form[q.id] || ""}
onChange={(v) => updateField(q.id, v)}
/>
)}
{q.type === "dropdown" && (
<div className="relative max-w-md">
<select
className="w-full bg-surface-container-low border border-white/10 rounded-xl px-5 py-4 text-on-surface appearance-none focus:outline-none focus:border-primary/50 transition-colors"
value={form[q.id] || ""}
onChange={(e) => updateField(q.id, e.target.value)}
>
<option value="">{q.placeholder}</option>
{q.options?.map((opt) => (
<option key={opt.value} value={opt.value}>
{opt.label}
</option>
))}
</select>
<span className="material-symbols-outlined absolute right-4 top-1/2 -translate-y-1/2 text-outline pointer-events-none">
expand_more
</span>
</div>
)}
{q.type === "scale" && (
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-3">
{q.options?.map((opt) => (
<label
key={opt.value}
className="flex items-center gap-4 cursor-pointer group rounded-xl border border-white/10 bg-surface-container-low/30 px-5 py-4 hover:border-primary/40 hover:bg-surface-container-low/50 transition-all"
>
<input
className="size-5 accent-primary cursor-pointer"
name={q.id}
type="radio"
value={opt.value}
checked={form[q.id] === opt.value}
onChange={() => updateField(q.id, opt.value)}
/>
<span className="text-on-surface font-medium">{opt.label}</span>
</label>
))}
</div>
)}
{q.type === "rating_labeled" && (
<div className="space-y-3 max-w-md">
{q.options?.map((opt) => (
<label
key={opt.value}
className="flex items-center gap-4 cursor-pointer group rounded-xl border border-white/10 bg-surface-container-low/30 px-5 py-4 hover:border-primary/40 hover:bg-surface-container-low/50 transition-all"
>
<input
className="size-5 accent-primary cursor-pointer"
name={q.id}
type="radio"
value={opt.value}
checked={form[q.id] === opt.value}
onChange={() => updateField(q.id, opt.value)}
/>
<span className="text-on-surface font-medium">{opt.label}</span>
</label>
))}
</div>
)}
{q.type === "checkbox_group" && (
<div className="space-y-4">
<div className="grid grid-cols-1 md:grid-cols-2 gap-3">
{q.options?.map((opt) => (
<label
key={opt.value}
className="flex items-start gap-4 cursor-pointer group rounded-xl border border-white/10 bg-surface-container-low/30 px-5 py-4 hover:border-primary/40 hover:bg-surface-container-low/50 transition-all"
>
<input
type="checkbox"
className="mt-1 size-5 accent-primary cursor-pointer"
checked={(form[q.id] || []).includes(opt.value)}
onChange={() => toggleCheckbox(q.id, opt.value)}
/>
<span className="text-on-surface font-medium">{opt.label}</span>
</label>
))}
</div>
{q.hasOther && (
<div className="flex items-center gap-4 px-5 py-2">
<span className="text-on-surface-variant font-medium whitespace-nowrap">Others:</span>
<input
className="flex-1 bg-transparent border-b border-white/10 py-2 text-on-surface focus:outline-none focus:border-primary transition-colors"
placeholder="Please specify..."
type="text"
value={form[`${q.id}_other`] || ""}
onChange={(e) => updateField(`${q.id}_other`, e.target.value)}
/>
</div>
)}
</div>
)}
{q.type === "long_text" && (
<textarea
className="w-full bg-surface-container-low border border-white/10 rounded-xl px-5 py-4 text-on-surface focus:outline-none focus:border-primary/50 transition-colors min-h-[120px] resize-none"
placeholder="Your message / suggestions..."
rows={4}
value={form[q.id] || ""}
onChange={(e) => updateField(q.id, e.target.value)}
/>
)}
</div>
))}
</div>
</section>
))}
<div className="pt-10 border-t border-white/5 flex flex-col items-center gap-6">
<button
className="gold-gradient-bg text-on-primary w-full md:w-auto px-16 py-5 rounded-xl font-[var(--font-display-lg)] text-xl 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 disabled:pointer-events-none"
type="submit"
disabled={submitState === "sending"}
>
{submitState === "sending" ? "Submitting…" : data.submit.label}
</button>
<SubmitToast
state={submitState}
successTitle="Feedback Submitted"
successMessage={data.submit.disclaimer}
onDismiss={() => {
setSubmitState("idle");
setForm({});
}}
/>
</div>
</form>
);
}
@@ -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>
);
}
+49 -16
View File
@@ -1,9 +1,20 @@
"use client";
import SubmitToast from "@/app/components/ui/SubmitToast";
import { useMemo, useState } from "react";
type IndustryOption = { value: string; label: string };
const emptyRegistration = {
fullName: "",
phone: "",
email: "",
company: "",
jobTitle: "",
industry: "",
notes: "",
};
export default function RegistrationForm({
data,
}: {
@@ -18,24 +29,34 @@ export default function RegistrationForm({
};
}) {
const industries = useMemo(() => data.sections.industry.options, [data.sections.industry.options]);
const [form, setForm] = useState({
fullName: "",
phone: "",
email: "",
company: "",
jobTitle: "",
industry: "",
notes: "",
});
const [form, setForm] = useState(emptyRegistration);
const [submitState, setSubmitState] = useState<"idle" | "sending" | "ok" | "error">("idle");
return (
<form
className="space-y-[32px]"
onSubmit={(e) => {
onSubmit={async (e) => {
e.preventDefault();
// frontend-only scope
console.log("[registration]", form);
alert("Saved locally (frontend-only).");
setSubmitState("sending");
const industryLabel = industries.find((o) => o.value === form.industry)?.label ?? "";
try {
const res = await fetch("/api/submissions", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
type: "registration",
...form,
industryLabel,
}),
});
if (!res.ok) {
const err = await res.json().catch(() => ({}));
throw new Error(typeof err?.error === "string" ? err.error : res.statusText);
}
setSubmitState("ok");
} catch {
setSubmitState("error");
}
}}
>
<div className="space-y-[16px]">
@@ -150,13 +171,25 @@ export default function RegistrationForm({
</div>
</div>
<div className="pt-[16px]">
<div className="pt-10 border-t border-white/5 flex flex-col items-center gap-6">
<button
className="w-full gold-gradient-bg py-5 rounded-lg text-on-primary font-[var(--font-display-lg)] text-[18px] font-bold shadow-[0_10px_30px_rgba(197,160,89,0.15)] hover:shadow-[0_15px_40px_rgba(197,160,89,0.25)] hover:-translate-y-0.5 active:translate-y-0 active:scale-[0.98] transition-all"
className="gold-gradient-bg text-on-primary w-full md:w-auto px-16 py-5 rounded-xl font-[var(--font-display-lg)] text-xl 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 disabled:pointer-events-none"
type="submit"
disabled={submitState === "sending"}
>
{data.submit.label}
{submitState === "sending" ? "Submitting…" : data.submit.label}
</button>
<SubmitToast
state={submitState}
successTitle="Registration received"
successMessage="Thank you. Your registration was saved and will appear in the admin console under Pending."
onDismiss={() => {
setSubmitState("idle");
setForm({ ...emptyRegistration });
}}
/>
<p className="mt-[16px] text-center text-outline text-[11px] tracking-widest opacity-60 uppercase">
{data.submit.disclaimer}
</p>
@@ -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>
);
}
+9 -9
View File
@@ -10,17 +10,17 @@ export default function AgendaSection({
};
}) {
return (
<section className="py-16 lg:py-32 max-w-hd mx-auto px-[var(--spacing-gutter)]" id={data.id}>
<div className="flex flex-col xl:flex-row justify-between xl:items-end mb-12 lg:mb-24 gap-8">
<section className="py-16 lg:py-24 max-w-hd mx-auto px-[var(--spacing-gutter)]" id={data.id}>
<div className="flex flex-col lg:flex-row justify-between lg:items-end mb-12 lg:mb-24 gap-8">
<div className="max-w-3xl">
<span className="font-[var(--font-label-caps)] text-xs lg:text-sm text-primary block mb-4 lg:mb-8 tracking-[0.25em]">
{data.eyebrow}
</span>
<h2 className="font-[var(--font-display-lg)] text-on-surface uppercase tracking-tight text-[clamp(2rem,4vw+1rem,4.5rem)]">
<h2 className="font-[var(--font-display-lg)] text-on-surface uppercase tracking-tight text-[clamp(1.75rem,3vw+1rem,3.5rem)]">
{data.title}
</h2>
</div>
<p className="text-on-surface-variant/60 text-base lg:text-[20px] max-w-md xl:text-right">
<p className="text-on-surface-variant/60 text-base lg:text-lg max-w-md xl:text-right">
{data.description}
</p>
</div>
@@ -29,22 +29,22 @@ export default function AgendaSection({
{data.items.map((item) => (
<div
key={item.order}
className="glass-panel p-8 lg:p-12 rounded-3xl relative overflow-hidden group hover:border-primary/60 transition-colors"
className="glass-panel p-6 lg:p-10 rounded-3xl relative overflow-hidden group hover:border-primary/60 transition-colors"
>
<div className="absolute top-0 right-0 p-6 lg:p-8">
<span className="font-[var(--font-display-lg)] text-2xl lg:text-[40px] text-primary/20 group-hover:text-primary transition-colors">
<span className="font-[var(--font-display-lg)] text-xl lg:text-[32px] text-primary/20 group-hover:text-primary transition-colors">
{item.order}
</span>
</div>
<div className="mb-6 lg:mb-10">
<span className="font-[var(--font-display-lg)] text-2xl lg:text-[36px] text-primary">
<span className="font-[var(--font-display-lg)] text-xl lg:text-[28px] text-primary">
{item.time}
</span>
</div>
<h3 className="font-[var(--font-display-lg)] text-2xl lg:text-[32px] text-on-surface mb-4 lg:mb-6">
<h3 className="font-[var(--font-display-lg)] text-lg lg:text-[24px] text-on-surface mb-3 lg:mb-4">
{item.title}
</h3>
<p className="text-sm lg:text-[18px] text-on-surface-variant/80 leading-relaxed mb-6 lg:mb-8">
<p className="text-xs lg:text-base text-on-surface-variant/80 leading-relaxed mb-6 lg:mb-8">
{item.description}
</p>
<div className="w-full h-1 bg-primary/10 group-hover:bg-primary transition-colors duration-500" />
+6 -6
View File
@@ -13,33 +13,33 @@ export default function HeroSection({
};
}) {
return (
<section className="relative min-h-[90vh] lg:min-h-screen flex items-center overflow-hidden bg-background hero-glow px-[var(--spacing-gutter)]">
<section className="relative min-h-[80vh] lg:min-h-[90vh] flex items-center overflow-hidden bg-background hero-glow px-[var(--spacing-gutter)]">
<div className="relative z-10 max-w-hd mx-auto w-full">
<div className="max-w-7xl">
<div className="inline-flex items-center gap-3 lg:gap-4 px-4 lg:px-8 py-2 lg:py-3 rounded-full bg-surface-container-high border border-primary/30 mb-8 lg:mb-16">
<span className="material-symbols-outlined text-primary text-[20px] lg:text-[26px]">
{data.badgeIcon}
</span>
<span className="font-[var(--font-label-caps)] text-xs lg:text-[16px] text-on-surface">
<span className="font-[var(--font-label-caps)] text-xs lg:text-[14px] text-on-surface">
{data.badgeText}
</span>
</div>
<h1 className="font-[var(--font-display-lg)] text-on-surface mb-6 lg:mb-10 leading-[1.1] text-[clamp(2.25rem,5vw+1rem,6.875rem)]">
<h1 className="font-[var(--font-display-lg)] text-on-surface mb-6 lg:mb-8 leading-[1.1] text-[clamp(1.75rem,3.5vw+1rem,4.25rem)]">
{data.title}
</h1>
<p className="text-on-surface-variant text-lg lg:text-[28px] mb-10 lg:mb-20 max-w-4xl leading-relaxed opacity-90">
<p className="text-on-surface-variant text-base lg:text-[22px] mb-8 lg:mb-16 max-w-3xl leading-relaxed opacity-90">
{data.subtitle}
</p>
<div className="flex flex-col sm:flex-row gap-4 lg:gap-10">
<Link
href={data.primaryCta.href}
className="gold-gradient-bg text-on-primary px-8 lg:px-16 py-5 lg:py-7 font-[var(--font-display-lg)] text-sm lg:text-[20px] font-bold uppercase tracking-widest rounded-xl hover:shadow-[0_0_50px_rgba(197,160,89,0.4)] transition-all text-center"
className="gold-gradient-bg text-on-primary px-8 lg:px-12 py-4 lg:py-5 font-[var(--font-display-lg)] text-sm lg:text-[18px] font-bold uppercase tracking-widest rounded-xl hover:shadow-[0_0_50px_rgba(197,160,89,0.4)] transition-all text-center"
>
{data.primaryCta.label}
</Link>
<Link
href={data.secondaryCta.href}
className="bg-surface-container-high border border-primary/40 text-on-surface px-8 lg:px-16 py-5 lg:py-7 font-[var(--font-display-lg)] text-sm lg:text-[20px] font-bold uppercase tracking-widest rounded-xl hover:bg-surface-container-highest transition-all text-center"
className="bg-surface-container-high border border-primary/40 text-on-surface px-8 lg:px-12 py-4 lg:py-5 font-[var(--font-display-lg)] text-sm lg:text-[18px] font-bold uppercase tracking-widest rounded-xl hover:bg-surface-container-highest transition-all text-center"
>
{data.secondaryCta.label}
</Link>
+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>
);
}
+3 -3
View File
@@ -5,17 +5,17 @@ export default function StatsSection({
}) {
return (
<section
className="py-16 lg:py-32 bg-surface-container-low border-y border-primary/10"
className="py-12 lg:py-20 bg-surface-container-low border-y border-primary/10"
id={data.id}
>
<div className="max-w-hd mx-auto px-[var(--spacing-gutter)]">
<div className="grid grid-cols-1 md:grid-cols-3 gap-12 lg:gap-32 text-center">
{data.items.map((x) => (
<div key={x.label} className="space-y-3 lg:space-y-6">
<div className="font-[var(--font-display-lg)] text-5xl lg:text-[96px] text-primary">
<div className="font-[var(--font-display-lg)] text-4xl lg:text-7xl text-primary leading-none">
{x.value}
</div>
<div className="font-[var(--font-label-caps)] text-sm lg:text-[18px] text-on-surface/80 tracking-widest uppercase">
<div className="font-[var(--font-label-caps)] text-xs lg:text-sm text-on-surface/70 tracking-widest uppercase">
{x.label}
</div>
</div>
+11 -11
View File
@@ -9,34 +9,34 @@ export default function VisionSection({
};
}) {
return (
<section className="py-16 lg:py-32 max-w-hd mx-auto px-[var(--spacing-gutter)]">
<div className="grid grid-cols-1 lg:grid-cols-12 gap-12 lg:gap-32 items-start">
<div className="lg:col-span-5">
<span className="font-[var(--font-label-caps)] text-xs lg:text-sm text-primary mb-6 lg:mb-10 block tracking-[0.25em]">
<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(2rem,4vw+1rem,4.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-10 text-on-surface-variant text-lg lg:text-[22px] 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-8 lg:p-12 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-[40px] lg:text-[56px] mb-8 lg:mb-12 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-xl lg:text-[28px] text-on-surface mb-4 lg:mb-6">
<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-sm lg:text-[18px] 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>
+12 -8
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-12 lg:py-20">
<div className="flex flex-col xl:flex-row justify-between items-center px-[var(--spacing-gutter)] gap-12 w-full max-w-hd mx-auto">
<div className="font-[var(--font-label-caps)] text-xl lg:text-[24px] text-primary tracking-[0.4em] font-bold uppercase">
{siteData.brand.name}
<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">
{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"
@@ -19,8 +23,8 @@ export default function Footer() {
</Link>
))}
</div>
<div className="text-[14px] lg:text-[16px] text-on-surface-variant/40 text-center xl:text-right max-w-sm leading-relaxed uppercase tracking-widest">
{siteData.footer.copyright}
<div className="text-[13px] lg:text-[14px] text-on-surface-variant/40 text-center lg:text-right max-w-sm leading-relaxed uppercase tracking-widest">
{site.footer.copyright}
</div>
</div>
</footer>
+167 -65
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;
@@ -28,86 +36,180 @@ export default function HeaderClient({
}, [mobileOpen]);
return (
<header className="fixed top-0 w-full z-50 bg-surface/80 backdrop-blur-xl border-b border-white/5">
<div className="flex justify-between items-center px-[var(--spacing-gutter)] py-4 lg:py-8 w-full max-w-hd mx-auto">
<Link
href="/"
className="font-[var(--font-display-lg)] text-[20px] sm:text-[22px] lg:text-[36px] text-primary tracking-tighter uppercase font-bold"
>
{brand.name}
</Link>
<nav className="hidden xl:flex items-center gap-10 2xl:gap-16">
<>
<header className="fixed top-0 w-full z-50 bg-surface/90 backdrop-blur-xl border-b border-white/10 shadow-2xl">
<div className="flex justify-between items-center px-[var(--spacing-gutter)] py-4 lg:py-6 w-full max-w-hd mx-auto">
<Link
className={`font-[var(--font-label-caps)] text-sm ${
isHome ? "text-primary border-b-2 border-primary pb-1" : "text-on-surface-variant hover:text-primary"
} transition-colors duration-300`}
href="/"
className="font-[var(--font-display-lg)] text-[16px] sm:text-[18px] lg:text-[22px] text-primary tracking-tighter uppercase font-bold"
>
Home
{brand.name}
</Link>
{navItems.map((item) => (
<nav className="hidden lg:flex items-center gap-8 2xl:gap-12">
<Link
key={item.href}
className="text-on-surface-variant font-[var(--font-label-caps)] text-sm hover:text-primary transition-colors duration-300"
href={item.href}
className={`font-[var(--font-label-caps)] text-sm ${
isHome ? "text-primary border-b-2 border-primary pb-1" : "text-on-surface-variant hover:text-primary"
} transition-colors duration-300`}
href="/"
>
{item.label}
{backLabel}
</Link>
))}
</nav>
{navItems.map((item) => {
const active = pathname === item.href;
return (
<Link
key={item.href}
className={`font-[var(--font-label-caps)] text-sm transition-colors duration-300 ${
active ? "text-primary border-b-2 border-primary pb-1" : "text-on-surface-variant hover:text-primary"
}`}
href={item.href}
>
{item.label}
</Link>
);
})}
</nav>
<div className="flex items-center gap-4 lg:gap-10">
<span className="hidden sm:block text-on-surface-variant/60 font-[var(--font-label-caps)] text-xs">
EN
</span>
<Link
href="/registration"
className="hidden sm:inline-flex gold-gradient-bg text-on-primary px-6 lg:px-10 py-3 lg:py-4 font-[var(--font-body-base)] text-sm lg:text-[18px] font-bold uppercase tracking-wider rounded-lg active:scale-95 transition-transform shadow-lg shadow-primary/10"
>
Register
</Link>
<button
type="button"
className="xl:hidden text-primary p-2"
aria-label="Toggle menu"
aria-expanded={mobileOpen}
onClick={() => setMobileOpen((v) => !v)}
>
<span className="material-symbols-outlined text-3xl">{mobileOpen ? "close" : "menu"}</span>
</button>
<div 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"
>
{registerLabel}
</Link>
<button
type="button"
className="lg:hidden text-primary p-2"
aria-label="Toggle menu"
aria-expanded={mobileOpen}
onClick={() => setMobileOpen((v) => !v)}
>
<span className="material-symbols-outlined text-3xl">{mobileOpen ? "close" : "menu"}</span>
</button>
</div>
</div>
</div>
</header>
{/* Mobile Menu Overlay */}
<div
className={`fixed inset-0 top-[72px] lg:top-[104px] bg-surface z-40 flex-col p-[var(--spacing-gutter)] gap-8 xl:hidden border-t border-white/5 ${
mobileOpen ? "flex" : "hidden"
className={`fixed inset-0 bg-black/80 backdrop-blur-md z-[60] transition-opacity duration-500 lg:hidden ${
mobileOpen ? "opacity-100 pointer-events-auto" : "opacity-0 pointer-events-none"
}`}
onClick={() => setMobileOpen(false)}
/>
{/* Mobile Menu Side Drawer */}
<div
className={`fixed inset-y-0 right-0 w-[80%] sm:w-[380px] z-[70] flex flex-col bg-[#1a1a1a] shadow-[-20px_0_60px_rgba(0,0,0,1)] transition-transform duration-500 ease-out lg:hidden ${
mobileOpen ? "translate-x-0" : "translate-x-full"
}`}
>
<Link className="text-2xl font-[var(--font-display-lg)] text-on-surface" href="/" onClick={() => setMobileOpen(false)}>
Home
</Link>
{navItems.map((item) => (
<Link
key={item.href}
className="text-2xl font-[var(--font-display-lg)] text-on-surface"
href={item.href}
<div className="p-8 flex justify-between items-center border-b border-white/5 bg-[#222222]">
<span className="font-[var(--font-display-lg)] text-primary text-sm font-bold uppercase tracking-widest">
Menu
</span>
<button
type="button"
className="text-primary/70 hover:text-primary transition-colors"
onClick={() => setMobileOpen(false)}
>
{item.label}
</Link>
))}
<div className="mt-auto flex flex-col gap-6">
<div className="text-on-surface-variant/60 font-[var(--font-label-caps)] text-sm">Language: EN</div>
<Link
href="/registration"
className="gold-gradient-bg text-on-primary w-full py-5 font-[var(--font-body-base)] text-lg font-bold uppercase tracking-wider rounded-lg text-center"
onClick={() => setMobileOpen(false)}
>
Register Now
</Link>
<span className="material-symbols-outlined text-4xl">close</span>
</button>
</div>
<div className="flex-1 flex flex-col p-8 gap-8 overflow-y-auto">
<nav className="flex flex-col gap-6">
<Link
className={`text-xl font-[var(--font-display-lg)] ${isHome ? "text-primary" : "text-on-surface"} hover:text-primary transition-colors`}
href="/"
onClick={() => setMobileOpen(false)}
>
{backLabel}
</Link>
{navItems.map((item) => {
const active = pathname === item.href;
return (
<Link
key={item.href}
className={`text-xl font-[var(--font-display-lg)] transition-colors ${
active ? "text-primary" : "text-on-surface hover:text-primary"
}`}
href={item.href}
onClick={() => setMobileOpen(false)}
>
{item.label}
</Link>
);
})}
</nav>
<div className="mt-auto pt-10 border-t border-white/5 flex flex-col gap-6">
{/* 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)}
>
{registerNowLabel}
</Link>
</div>
</div>
</div>
</header>
</>
);
}
+80
View File
@@ -0,0 +1,80 @@
type SponsorContactData = {
id: string;
title: string;
description: string;
phone: { label: string; value: string };
email: { label: string; value: string };
cta: string;
image: { src: string; alt: string };
};
export default function SponsorContact({ data }: { data: SponsorContactData }) {
return (
<section id={data.id} className="py-16 lg:py-32 relative">
<div className="max-w-[1280px] mx-auto px-gutter">
{/* glass-panel gold-border — p-stack-lg lg:p-section-gap */}
<div className="glass-panel gold-border p-stack-lg lg:p-section-gap rounded-3xl overflow-hidden relative">
{/* Glow */}
<div className="absolute -bottom-24 -right-24 w-64 h-64 bg-primary/20 blur-[100px] rounded-full" />
<div className="relative z-10 grid grid-cols-1 lg:grid-cols-2 gap-stack-lg items-center">
{/* Left */}
<div>
<h2 className="font-display-lg text-display-lg mb-6">{data.title}</h2>
<p className="font-body-base text-body-base text-on-surface-variant/80 mb-8">
{data.description}
</p>
<div className="space-y-6 mb-12">
{/* Phone */}
<div className="flex items-center gap-4">
<div className="w-10 h-10 border border-primary/30 rounded-full flex items-center justify-center">
<span className="material-symbols-outlined text-primary">call</span>
</div>
<div>
<p className="text-label-caps opacity-60">{data.phone.label}</p>
<p className="font-title-sm">{data.phone.value}</p>
</div>
</div>
{/* Email */}
<div className="flex items-center gap-4">
<div className="w-10 h-10 border border-primary/30 rounded-full flex items-center justify-center">
<span className="material-symbols-outlined text-primary">mail</span>
</div>
<div>
<p className="text-label-caps opacity-60">{data.email.label}</p>
<p className="font-title-sm">{data.email.value}</p>
</div>
</div>
</div>
<button
type="button"
className="gold-gradient-bg text-on-primary font-title-sm px-12 py-5 rounded-lg hover:opacity-90 transition-all active:scale-95 flex items-center gap-3"
>
{data.cta}
<span className="material-symbols-outlined">arrow_forward</span>
</button>
</div>
{/* Right: image */}
<div className="hidden lg:block">
<div className="relative aspect-square">
{/* eslint-disable-next-line @next/next/no-img-element */}
<img
src={data.image.src}
alt={data.image.alt}
className="w-full h-full object-cover rounded-2xl opacity-60 mix-blend-luminosity"
/>
<div className="absolute inset-0 ring-1 ring-inset ring-white/10 rounded-2xl" />
</div>
</div>
</div>
</div>
</div>
</section>
);
}
+51
View File
@@ -0,0 +1,51 @@
import Link from "next/link";
type SponsorHeroData = {
eyebrow: string;
title: string;
subtitle: string;
description: string;
primaryCta: { label: string; href: string };
secondaryCta: { label: string; href: string };
};
export default function SponsorHero({ data }: { data: SponsorHeroData }) {
return (
<section className="relative py-16 lg:py-32 overflow-hidden">
<div className="absolute inset-0 bg-[radial-gradient(circle_at_50%_50%,rgba(197,160,89,0.08),transparent_70%)]" />
<div className="max-w-[1280px] mx-auto px-gutter relative z-10 text-center">
<span className="font-label-caps text-label-caps text-primary mb-4 block">
{data.eyebrow}
</span>
<h1 className="font-display-lg text-display-lg mb-6">
{data.title}
</h1>
<h2 className="font-headline-md text-headline-md text-on-surface-variant mb-8 max-w-3xl mx-auto">
{data.subtitle}
</h2>
<p className="font-body-base text-body-base text-on-surface-variant/80 max-w-2xl mx-auto mb-12">
{data.description}
</p>
<div className="flex flex-col sm:flex-row justify-center gap-stack-md">
<Link
href={data.primaryCta.href}
className="gold-gradient-bg text-on-primary font-title-sm px-10 py-4 rounded-lg hover:opacity-90 transition-all active:scale-95 text-center"
>
{data.primaryCta.label}
</Link>
<Link
href={data.secondaryCta.href}
className="border border-primary text-primary font-title-sm px-10 py-4 rounded-lg hover:bg-primary/5 transition-all active:scale-95 text-center"
>
{data.secondaryCta.label}
</Link>
</div>
</div>
</section>
);
}
@@ -0,0 +1,68 @@
type ItemCard = {
icon: string;
title: string;
status: string;
};
type Highlight = {
eyebrow: string;
title: string;
icon: string;
};
type SponsorItemBasedData = {
eyebrow: string;
title: string;
description: string;
items: ItemCard[];
highlights: Highlight[];
};
export default function SponsorItemBased({ data }: { data: SponsorItemBasedData }) {
return (
<section className="py-16 lg:py-32">
<div className="max-w-[1280px] mx-auto px-gutter">
{/* Header — text-center mb-stack-lg */}
<div className="text-center mb-stack-lg">
<h2 className="font-headline-md text-headline-md mb-2">{data.title}</h2>
<p className="text-on-surface-variant opacity-70">{data.description}</p>
</div>
{/* 4-col cards — gap-gutter = 24px, mb-stack-lg = 32px */}
<div className="grid grid-cols-2 lg:grid-cols-4 gap-gutter mb-stack-lg">
{data.items.map((item) => (
<div
key={item.title}
className="glass-panel p-stack-md rounded-lg text-center group hover:bg-surface-container transition-colors"
>
<div className="w-12 h-12 gold-gradient-bg rounded-full flex items-center justify-center mx-auto mb-4">
<span className="material-symbols-outlined text-on-primary">{item.icon}</span>
</div>
<h4 className="font-title-sm text-body-base mb-1">{item.title}</h4>
<p className="text-label-caps text-primary">{item.status}</p>
</div>
))}
</div>
{/* Highlights — gap-gutter = 24px */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-gutter">
{data.highlights.map((h) => (
<div
key={h.eyebrow}
className="bg-primary/5 border border-primary/20 p-stack-lg rounded-xl flex items-center justify-between"
>
<div>
<p className="font-label-caps text-primary mb-1">{h.eyebrow}</p>
<p className="font-title-sm">{h.title}</p>
</div>
<span className="material-symbols-outlined text-primary text-4xl">
{h.icon}
</span>
</div>
))}
</div>
</div>
</section>
);
}
@@ -0,0 +1,96 @@
type PackageItem = {
id: string;
tier: string;
badge: string | null;
price: string;
featured: boolean;
cta: string;
benefits: string[];
boldBenefit: string | null;
};
type SponsorPackagesData = {
eyebrow: string;
title: string;
items: PackageItem[];
};
export default function SponsorPackages({ data }: { data: SponsorPackagesData }) {
return (
<section className="py-16 lg:py-32 bg-surface-container-lowest">
<div className="max-w-[1280px] mx-auto px-gutter">
{/* Header — flex items-center justify-between mb-stack-lg */}
<div className="flex items-center justify-between mb-stack-lg">
<h2 className="font-headline-md text-headline-md">{data.title}</h2>
<div className="hidden lg:block h-px flex-grow mx-stack-lg bg-outline-variant/30" />
</div>
{/* Grid — gap-stack-lg = 32px */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-stack-lg">
{data.items.map((pkg) => (
<div
key={pkg.id}
className={`glass-panel p-stack-lg rounded-xl flex flex-col h-full ${
pkg.featured ? "gold-border gold-glow ring-2 ring-primary/20" : ""
}`}
>
<div className="mb-stack-lg">
{pkg.badge && (
<span className="bg-primary/20 text-primary px-3 py-1 rounded text-label-caps mb-4 inline-block">
{pkg.badge}
</span>
)}
<h3
className={`font-display-lg text-headline-md ${
pkg.featured ? "text-primary" : ""
}`}
>
{pkg.tier}
</h3>
<div className="text-on-surface font-display-lg text-title-sm mt-2">
{pkg.price}
</div>
</div>
<ul className="space-y-4 mb-stack-lg flex-grow">
{pkg.benefits.map((benefit) => (
<li key={benefit} className="flex items-start gap-3">
<span
className="material-symbols-outlined text-primary text-[20px]"
style={
pkg.featured
? { fontVariationSettings: "'FILL' 1" }
: undefined
}
>
check_circle
</span>
<span
className={`text-on-surface-variant ${
benefit === pkg.boldBenefit ? "font-bold" : ""
}`}
>
{benefit}
</span>
</li>
))}
</ul>
<button
type="button"
className={`w-full py-4 rounded font-title-sm transition-all active:scale-95 ${
pkg.featured
? "gold-gradient-bg text-on-primary hover:opacity-90"
: "border border-outline-variant text-on-surface hover:border-primary transition-colors"
}`}
>
{pkg.cta}
</button>
</div>
))}
</div>
</div>
</section>
);
}
@@ -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>
);
}
@@ -0,0 +1,72 @@
"use client";
import { useEffect, useRef } from "react";
type Props = {
open: boolean;
title: string;
message: string;
acknowledgeLabel?: string;
onAcknowledge: () => void;
};
export default function SubmissionSuccessModal({
open,
title,
message,
acknowledgeLabel = "OK",
onAcknowledge,
}: Props) {
const onAckRef = useRef(onAcknowledge);
onAckRef.current = onAcknowledge;
useEffect(() => {
if (!open) return;
const prevOverflow = document.body.style.overflow;
document.body.style.overflow = "hidden";
const onKeyDown = (e: KeyboardEvent) => {
if (e.key === "Escape") onAckRef.current();
};
window.addEventListener("keydown", onKeyDown);
return () => {
document.body.style.overflow = prevOverflow;
window.removeEventListener("keydown", onKeyDown);
};
}, [open]);
if (!open) return null;
return (
<div className="fixed inset-0 z-[200] flex items-center justify-center p-4">
<button
type="button"
className="absolute inset-0 cursor-default bg-black/75 backdrop-blur-sm"
aria-label="Close"
onClick={() => onAckRef.current()}
/>
<div
role="dialog"
aria-modal="true"
aria-labelledby="submission-success-title"
className="relative z-10 w-full max-w-md rounded-xl border border-primary/35 bg-[rgba(15,15,15,0.95)] backdrop-blur-xl px-8 py-10 shadow-[0_0_48px_rgba(197,160,89,0.18)] text-center border-t border-t-primary/20"
>
<div className="flex justify-center mb-5">
<span className="material-symbols-outlined text-primary text-[56px]" style={{ fontVariationSettings: "'FILL' 1" }}>
check_circle
</span>
</div>
<h2 id="submission-success-title" className="font-[var(--font-display-lg)] text-xl sm:text-2xl text-primary tracking-tight mb-3">
{title}
</h2>
<p className="text-on-surface-variant text-sm sm:text-[15px] leading-relaxed mb-8">{message}</p>
<button
type="button"
className="w-full sm:w-auto min-w-[200px] gold-gradient-bg px-10 py-3.5 rounded-lg text-on-primary font-[var(--font-display-lg)] text-sm font-bold uppercase tracking-wider shadow-[0_10px_30px_rgba(197,160,89,0.2)] hover:shadow-[0_14px_36px_rgba(197,160,89,0.3)] transition-all active:scale-[0.98]"
onClick={() => onAckRef.current()}
>
{acknowledgeLabel}
</button>
</div>
</div>
);
}
+116
View File
@@ -0,0 +1,116 @@
"use client";
import { useEffect, useRef, useState } from "react";
type ToastState = "idle" | "sending" | "ok" | "error";
type Props = {
state: ToastState;
successTitle?: string;
successMessage?: string;
onDismiss?: () => void;
};
/**
* Inline toast that appears below the submit button.
* - Slides up + fades in when visible
* - Auto-dismisses after 6 s on success
* - Shows a close button for manual dismiss
*/
export default function SubmitToast({
state,
successTitle = "Submitted successfully",
successMessage = "Your submission has been received.",
onDismiss,
}: Props) {
const [visible, setVisible] = useState(false);
const [mounted, setMounted] = useState(false);
const timerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
const isActive = state === "ok" || state === "error";
// Mount → trigger CSS enter transition
useEffect(() => {
if (isActive) {
setMounted(true);
// Tiny delay so the browser registers the initial state before animating
requestAnimationFrame(() => {
requestAnimationFrame(() => setVisible(true));
});
// Auto-dismiss success after 6 s
if (state === "ok") {
timerRef.current = setTimeout(() => handleDismiss(), 6000);
}
} else {
// Slide out then unmount
setVisible(false);
timerRef.current = setTimeout(() => setMounted(false), 400);
}
return () => {
if (timerRef.current) clearTimeout(timerRef.current);
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [isActive, state]);
const handleDismiss = () => {
if (timerRef.current) clearTimeout(timerRef.current);
setVisible(false);
setTimeout(() => {
setMounted(false);
onDismiss?.();
}, 400);
};
if (!mounted) return null;
const isSuccess = state === "ok";
return (
<div
role={isSuccess ? "status" : "alert"}
aria-live="polite"
style={{
opacity: visible ? 1 : 0,
transform: visible ? "translateY(0)" : "translateY(12px)",
transition: "opacity 0.4s cubic-bezier(0.16,1,0.3,1), transform 0.4s cubic-bezier(0.16,1,0.3,1)",
pointerEvents: visible ? "auto" : "none",
}}
className={[
"mt-5 w-full rounded-xl border px-5 py-4 flex items-start gap-4",
isSuccess
? "border-primary/40 bg-primary/10 text-primary"
: "border-red-500/40 bg-red-500/10 text-red-300",
].join(" ")}
>
{/* Icon */}
<span
className="material-symbols-outlined text-[28px] shrink-0 mt-0.5"
style={{ fontVariationSettings: "'FILL' 1" }}
>
{isSuccess ? "check_circle" : "error"}
</span>
{/* Text */}
<div className="flex-1 min-w-0">
<p className="font-[var(--font-display-lg)] font-semibold text-sm leading-snug">
{isSuccess ? successTitle : "Submission failed"}
</p>
<p className="mt-1 text-[13px] leading-relaxed opacity-80">
{isSuccess
? successMessage
: "Please check your connection and try again."}
</p>
</div>
{/* Close button */}
<button
type="button"
aria-label="Dismiss notification"
onClick={handleDismiss}
className="shrink-0 mt-0.5 opacity-60 hover:opacity-100 transition-opacity"
>
<span className="material-symbols-outlined text-[20px]">close</span>
</button>
</div>
);
}
+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": "Admin Console",
"subtitle": "IPv6 SUMMIT 2026 / Infrastructure Core",
"user": {
"name": "",
"role": "Systems Admin",
"avatarUrl": ""
},
"footerTag": "Systems Infrastructure Core v2.4.0"
},
"stats": [
{
"id": "total",
"label": "Total requests",
"value": "1,284",
"hint": "+12% vs last week",
"icon": "database",
"accent": "none"
},
{
"id": "pending",
"label": "Pending approval",
"value": "42",
"hint": "Critical threshold: 50",
"icon": "hourglass_empty",
"accent": "pending"
},
{
"id": "confirmed",
"label": "Confirmed",
"value": "856",
"hint": "Capacity at 74%",
"icon": "check_circle",
"accent": "confirmed"
},
{
"id": "conversion",
"label": "Conversion rate",
"value": "67%",
"hint": "",
"icon": "trending_up",
"accent": "progress",
"progressPercent": 67
}
],
"filters": {
"status": {
"label": "Status",
"options": [
{ "value": "all", "label": "All operations" },
{ "value": "pending", "label": "Pending" },
{ "value": "approved", "label": "Approved" },
{ "value": "rejected", "label": "Rejected" }
]
},
"companyGroup": {
"label": "Company",
"options": [
{ "value": "all", "label": "All segments" },
{ "value": "tier1", "label": "Global Tier 1" },
{ "value": "cloud", "label": "Cloud providers" },
{ "value": "enterprise", "label": "Enterprise" }
]
},
"searchPlaceholder": "Search by name, company, email, or ID…",
"exportLabel": "Export to Excel"
},
"table": {
"columns": {
"id": "ID",
"submitted": "Submit date",
"name": "Name",
"company": "Company",
"contact": "Email / phone",
"status": "Status",
"actions": "Actions"
},
"pageSize": 10
},
"statusMap": {
"approved": {
"label": "Approved",
"borderClass": "border-primary text-primary bg-primary/10",
"dotClass": "bg-primary"
},
"pending": {
"label": "Pending",
"borderClass": "border-secondary text-secondary bg-secondary/10",
"dotClass": "bg-secondary"
},
"rejected": {
"label": "Rejected",
"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 — requested front-row seating."
},
{
"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": "Awaiting employer approval letter."
},
{
"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": "Capacity exceeded — suggested waitlist."
},
{
"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": ["Privacy", "Terms", "Support"]
}
}
+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": "Event Feedback",
"subtitle": "Your insights drive the future of IPv6 infrastructure. Help us shape the 2027 experience.",
"sections": [
{
"id": "overall",
"title": "1. OVERALL EVENT EXPERIENCE",
"subtitle": "Overview of your summit experience",
"questions": [
{
"id": "overallExperience",
"type": "rating",
"label": "How would you rate your overall experience at IPv6 Summit 2026?",
"min": 1,
"max": 5
},
{
"id": "venueFacilities",
"type": "rating",
"label": "How satisfied were you with the venue, facilities, and catering services?",
"min": 1,
"max": 5
},
{
"id": "organisationProcess",
"type": "rating",
"label": "How would you rate the event organisation and registration process?",
"min": 1,
"max": 5
}
]
},
{
"id": "content",
"title": "2. CONTENT & PROGRAMME EVALUATION",
"subtitle": "Evaluation of program content and sessions",
"questions": [
{
"id": "mostValuableSession",
"type": "dropdown",
"label": "Which session did you find most valuable?",
"placeholder": "Select a session",
"options": [
{ "value": "opening", "label": "Opening Session" },
{ "value": "keynote1", "label": "Keynote 1" },
{ "value": "panel1", "label": "Panel Discussion 1" },
{ "value": "technical", "label": "Technical Track" },
{ "value": "policy", "label": "Policy Track" }
]
},
{
"id": "relevance",
"type": "scale",
"label": "How relevant was the conference content to your professional interests?",
"options": [
{ "value": "1", "label": "Not Relevant" },
{ "value": "2", "label": "Slightly Relevant" },
{ "value": "3", "label": "Moderately Relevant" },
{ "value": "4", "label": "Relevant" },
{ "value": "5", "label": "Highly Relevant" }
]
},
{
"id": "speakerRating",
"type": "rating_labeled",
"label": "How would you rate the keynote speakers and panel discussions?",
"options": [
{ "value": "1", "label": "1 Needs Improvement" },
{ "value": "2", "label": "2 Fair" },
{ "value": "3", "label": "3 Good" },
{ "value": "4", "label": "4 Strong" },
{ "value": "5", "label": "5 Outstanding" }
]
},
{
"id": "networkingUtility",
"type": "scale",
"label": "How useful were the networking opportunities provided during the Summit?",
"options": [
{ "value": "1", "label": "Not Useful" },
{ "value": "2", "label": "Slightly Useful" },
{ "value": "3", "label": "Useful" },
{ "value": "4", "label": "Very Useful" },
{ "value": "5", "label": "Extremely Valuable" }
]
},
{
"id": "futureTopics",
"type": "checkbox_group",
"label": "Which topics would you like to see more of in future editions?",
"hasOther": true,
"options": [
{ "value": "deployment", "label": "IPv6 Deployment Strategies" },
{ "value": "cybersecurity", "label": "Cybersecurity & Infrastructure" },
{ "value": "smart-cities", "label": "Smart Cities & IoT" },
{ "value": "ai-networks", "label": "AI & Future Networks" },
{ "value": "government", "label": "Government Policies" },
{ "value": "cloud-dc", "label": "Cloud & Data Centers" },
{ "value": "case-studies", "label": "Case Studies from Enterprises" }
]
}
]
},
{
"id": "future",
"title": "4. FUTURE PARTICIPATION",
"subtitle": "Your likelihood of attending future events",
"questions": [
{
"id": "attendAgain",
"type": "scale",
"label": "Would you attend IPv6 Summit again in 2027?",
"options": [
{ "value": "definitely-yes", "label": "Definitely Yes" },
{ "value": "probably-yes", "label": "Probably Yes" },
{ "value": "not-sure", "label": "Not Sure" },
{ "value": "probably-no", "label": "Probably No" },
{ "value": "definitely-no", "label": "Definitely No" }
]
},
{
"id": "recommend",
"type": "scale",
"label": "Would you recommend IPv6 Summit to colleagues or partners?",
"options": [
{ "value": "yes", "label": "Yes" },
{ "value": "maybe", "label": "Maybe" },
{ "value": "no", "label": "No" }
]
}
]
},
{
"id": "qualitative",
"title": "5. QUALITATIVE FEEDBACK",
"subtitle": "General opinions and suggestions",
"questions": [
{
"id": "enjoyedMost",
"type": "long_text",
"label": "What did you enjoy most about the Summit?"
},
{
"id": "improvementAreas",
"type": "long_text",
"label": "What areas should we improve for future events?"
},
{
"id": "additionalComments",
"type": "long_text",
"label": "Any additional comments, suggestions, or partnership interests?"
}
]
},
{
"id": "profile",
"title": "ATTENDEE PROFILE (OPTIONAL)",
"subtitle": "Information about your professional background",
"questions": [
{
"id": "industry",
"type": "checkbox_group",
"label": "Industry / Sector",
"hasOther": true,
"options": [
{ "value": "government", "label": "Government" },
{ "value": "telecom", "label": "Telecom" },
{ "value": "education", "label": "Education" },
{ "value": "enterprise", "label": "Enterprise" },
{ "value": "technology", "label": "Technology" },
{ "value": "startup", "label": "Startup" }
]
},
{
"id": "role",
"type": "checkbox_group",
"label": "Your role",
"hasOther": true,
"options": [
{ "value": "executive", "label": "Executive / Leadership" },
{ "value": "engineer", "label": "Engineer / Technical Specialist" },
{ "value": "researcher", "label": "Researcher / Academic" },
{ "value": "student", "label": "Student" },
{ "value": "bizdev", "label": "Business Development" }
]
}
]
}
],
"submit": {
"label": "Submit feedback",
"disclaimer": "THANK YOU FOR YOUR VALUABLE CONTRIBUTION. Your feedback helps us build a stronger and more impactful IPv6 ecosystem for the future."
}
}
+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": "OPPORTUNITY AWAITS",
"title": "Become a Sponsor",
"subtitle": "IPv6 for AI & Data Centre Summit 2026 — 18 June 2026, Ho Chi Minh City",
"description": "Join industry leaders at the forefront of the technological evolution. We invite innovative companies to co-sponsor an elite gathering focusing on AI, IPv6, Cloud Computing, Cybersecurity, Blockchain, and Data Centres. Align your brand with the architects of tomorrow's infrastructure.",
"primaryCta": { "label": "Download Prospectus", "href": "#" },
"secondaryCta": { "label": "Speak to an Agent", "href": "#contact" }
},
"packages": {
"eyebrow": "PACKAGES",
"title": "Sponsorship Packages & Benefits",
"items": [
{
"id": "diamond",
"tier": "DIAMOND SPONSOR",
"badge": "MOST PRESTIGIOUS",
"price": "80,000,000 VND",
"featured": true,
"cta": "Select Diamond",
"benefits": [
"Logo prominent on all branding materials",
"Logo on stage backdrop & LED screens",
"Premium exhibition standee space",
"1 dedicated speaking slot",
"07 VIP Delegate tickets"
],
"boldBenefit": "1 dedicated speaking slot"
},
{
"id": "gold",
"tier": "GOLD SPONSOR",
"badge": null,
"price": "50,000,000 VND",
"featured": false,
"cta": "Select Gold",
"benefits": [
"Logo on stage backdrop & LED screens",
"Exhibition standee placement",
"05 VIP Delegate tickets"
],
"boldBenefit": null
},
{
"id": "silver",
"tier": "SILVER SPONSOR",
"badge": null,
"price": "40,000,000 VND",
"featured": false,
"cta": "Select Silver",
"benefits": [
"Logo on stage backdrop & LED screens",
"Standard exhibition standee",
"04 VIP Delegate tickets"
],
"boldBenefit": null
},
{
"id": "partner",
"tier": "PARTNER SPONSOR",
"badge": null,
"price": "20,000,000 VND",
"featured": false,
"cta": "Select Partner",
"benefits": [
"Logo on official website & event communications",
"02 VIP Delegate tickets"
],
"boldBenefit": null
}
]
},
"itemBased": {
"eyebrow": "À LA CARTE",
"title": "Item-Based Sponsorship",
"description": "Targeted branding opportunities for specific summit pillars",
"items": [
{ "icon": "hotel", "title": "Hotel Accommodation", "status": "Limited Space" },
{ "icon": "meeting_room", "title": "Conference Hall", "status": "Sold Out" },
{ "icon": "groups", "title": "Networking Zone", "status": "Available" },
{ "icon": "newsmode", "title": "Media Partner", "status": "Apply Now" }
],
"highlights": [
{
"eyebrow": "STANDARD BENEFIT",
"title": "Logo on backdrop & LED screens",
"icon": "branding_watermark"
},
{
"eyebrow": "ACCESS PACK",
"title": "02 VIP Delegate tickets included",
"icon": "confirmation_number"
}
]
},
"contact": {
"id": "contact",
"title": "Partner With Us",
"description": "Elevate your brand presence in the Indo-Pacific tech corridor. Our summit offers unparalleled access to decision-makers, researchers, and government officials driving the next generation of data center infrastructure and AI development.",
"phone": {
"label": "PHONE ENQUIRIES",
"value": "+84 941 523 498"
},
"email": {
"label": "DIRECT EMAIL",
"value": "events@techvanguard.vn"
},
"cta": "Contact Us to Sponsor",
"image": {
"src": "https://lh3.googleusercontent.com/aida-public/AB6AXuATSk4rhRYAo9_OfX0cJwIC7rUdUmzD9kO7cJM24ECjPHwglU_s0wjBn6gdK0d7tk4ExLvG9DUJQ1L-PbP5ucVH2THO05fev6bWgyvpkElf_94JXJNeWj0Mo4-5-l2ofY-kHE6kPLhitbRzarR7u41favHCt_V9WM0yYb7tcZZQM8eAN5jIVT5OVKSGvfpZNYe05dJm_h3pW0qkWhX1XrUtg_xhhjdvTi-Mei7zHDUkwwRrydg7BHmO17W6ScfALH5rMXojTHub7UDN",
"alt": "A professional business meeting in a high-tech conference room"
}
}
}
+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"
}
}
}
+5 -1
View File
@@ -22,7 +22,7 @@
--font-mono: "JetBrains Mono", ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono",
"Courier New", monospace;
--spacing-gutter: clamp(1rem, 4vw, 3.75rem);
--spacing-gutter: clamp(1.25rem, 5vw, 5rem);
--spacing-container-max: 1800px;
}
@@ -82,3 +82,7 @@ textarea::placeholder {
display: none;
}
.status-led {
box-shadow: 0 0 8px currentColor;
}
+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,
};
}
+3 -7
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"),
@@ -29,15 +30,10 @@ export default function RootLayout({ children }: { children: React.ReactNode })
href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&display=swap"
rel="stylesheet"
/>
{/* Favicon */}
<link
rel="shortcut icon"
href="/assets/img/favicon.png"
type="image/x-icon"
/>
</head>
<body suppressHydrationWarning>
{children}
<LanguageProvider>{children}</LanguageProvider>
</body>
</html>
);
+2 -2
View File
@@ -1,8 +1,8 @@
export const imageUrl = (path?: string) => {
// Không có ảnh → ảnh mặc định
// No image → default image
if (!path) return "/_images/default.jpg";
// Đã là full URL
// Already a full URL
if (path.startsWith("http")) return path;
const base = (
View File
-357
View File
@@ -1,357 +0,0 @@
<!DOCTYPE html>
<html class="dark" lang="en"><head>
<meta charset="utf-8"/>
<meta content="width=device-width, initial-scale=1.0" name="viewport"/>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&amp;family=Space+Grotesk:wght@600;700&amp;family=Geist:wght@600&amp;display=swap" rel="stylesheet"/>
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&amp;display=swap" rel="stylesheet"/>
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&amp;display=swap" rel="stylesheet"/>
<script src="https://cdn.tailwindcss.com?plugins=forms,container-queries"></script>
<script id="tailwind-config">
tailwind.config = {
darkMode: "class",
theme: {
extend: {
"colors": {
"surface-tint": "#e9c176",
"on-secondary-container": "#d9b27c",
"surface-container-low": "#1c1b1b",
"primary-fixed-dim": "#e9c176",
"primary": "#e9c176",
"on-background": "#e5e2e1",
"surface-bright": "#3a3939",
"on-tertiary-fixed": "#1c1b20",
"tertiary-fixed-dim": "#c9c5cc",
"surface-container": "#201f1f",
"on-secondary-fixed-variant": "#5d4216",
"outline-variant": "#4e4639",
"on-secondary-fixed": "#291800",
"secondary-container": "#604418",
"secondary": "#e9c088",
"surface-container-lowest": "#0e0e0e",
"error": "#ffb4ab",
"error-container": "#93000a",
"on-error-container": "#ffdad6",
"inverse-primary": "#775a19",
"tertiary-container": "#a7a4ab",
"on-tertiary-fixed-variant": "#48464c",
"on-surface": "#e5e2e1",
"inverse-on-surface": "#313030",
"primary-container": "#c5a059",
"surface-container-high": "#2a2a2a",
"primary-fixed": "#ffdea5",
"secondary-fixed-dim": "#e9c088",
"on-tertiary": "#313035",
"surface": "#131313",
"secondary-fixed": "#ffddb2",
"background": "#131313",
"surface-container-highest": "#353534",
"tertiary": "#c9c5cc",
"on-primary": "#412d00",
"on-secondary": "#442b02",
"inverse-surface": "#e5e2e1",
"on-primary-container": "#4e3700",
"tertiary-fixed": "#e5e1e8",
"outline": "#9a8f80",
"on-surface-variant": "#d1c5b4",
"on-primary-fixed": "#261900",
"on-tertiary-container": "#3c3a40",
"on-primary-fixed-variant": "#5d4201",
"surface-dim": "#131313",
"surface-variant": "#353534",
"on-error": "#690005"
},
"borderRadius": {
"DEFAULT": "0.125rem",
"lg": "0.25rem",
"xl": "0.5rem",
"full": "0.75rem"
},
"spacing": {
"stack-lg": "32px",
"unit": "4px",
"stack-sm": "8px",
"margin-safe": "40px",
"stack-md": "16px",
"section-gap": "80px",
"gutter": "24px"
},
"fontFamily": {
"body-base": ["Inter"],
"display-lg": ["Space Grotesk"],
"title-sm": ["Inter"],
"label-caps": ["Geist"],
"headline-md": ["Space Grotesk"]
},
"fontSize": {
"body-base": ["16px", {"lineHeight": "1.6", "letterSpacing": "0", "fontWeight": "400"}],
"display-lg": ["48px", {"lineHeight": "1.1", "letterSpacing": "-0.02em", "fontWeight": "700"}],
"title-sm": ["20px", {"lineHeight": "1.4", "letterSpacing": "0.02em", "fontWeight": "600"}],
"label-caps": ["12px", {"lineHeight": "1", "letterSpacing": "0.1em", "fontWeight": "600"}],
"headline-md": ["32px", {"lineHeight": "1.2", "letterSpacing": "-0.01em", "fontWeight": "600"}]
}
},
},
}
</script>
<style>
.gold-gradient-bg {
background: linear-gradient(135deg, #e9c176 0%, #775a19 100%);
}
.hero-glow {
background: radial-gradient(circle at 50% 50%, rgba(233, 193, 118, 0.15) 0%, rgba(15, 15, 15, 0) 70%);
}
.glass-panel {
background: rgba(26, 25, 30, 0.8);
backdrop-filter: blur(20px);
border: 1px solid rgba(255, 255, 255, 0.05);
}
.input-focus-gold:focus {
outline: none;
border-color: #e9c176;
box-shadow: 0 0 0 1px #e9c176;
}
.material-symbols-outlined {
font-variation-settings: 'FILL' 0, 'wght' 400, 'GRAD' 0, 'opsz' 24;
}
</style>
</head>
<body class="bg-background text-on-background font-body-base overflow-x-hidden">
<!-- TopNavBar -->
<header class="fixed top-0 w-full z-50 bg-surface-glass backdrop-blur-md border-b border-border-subtle">
<div class="flex justify-between items-center w-full px-gutter py-4 max-w-container-max mx-auto">
<div class="font-h1 text-[24px] font-bold text-on-surface tracking-tighter uppercase">
<span class="text-primary">IPv6 SUMMIT 2026</span>
</div>
<nav class="hidden md:flex gap-stack-md items-center">
<a class="text-on-surface-variant font-medium hover:text-secondary-fixed transition-colors duration-300 font-label-caps text-label-caps uppercase" href="#">Agenda</a>
<a class="text-on-surface-variant font-medium hover:text-secondary-fixed transition-colors duration-300 font-label-caps text-label-caps uppercase" href="#">Speakers</a>
<a class="text-on-surface-variant font-medium hover:text-secondary-fixed transition-colors duration-300 font-label-caps text-label-caps uppercase" href="#">Stats</a>
<a class="text-on-surface-variant font-medium hover:text-secondary-fixed transition-colors duration-300 font-label-caps text-label-caps uppercase" href="#">Venue</a>
</nav>
<div class="flex gap-stack-sm items-center">
<span class="text-on-surface-variant font-label-caps text-label-caps mr-4 cursor-pointer"><br/></span>
<button class="gold-gradient-bg text-on-primary px-6 py-2 rounded-lg font-label-caps text-label-caps uppercase hover:scale-95 transition-all duration-200">Register</button>
</div>
</div>
</header>
<!-- Main Content & Hero Area -->
<main class="relative min-h-screen pt-32 pb-section-gap">
<!-- Background Hero Glow -->
<div class="absolute top-0 left-1/2 -translate-x-1/2 w-full h-[800px] hero-glow pointer-events-none -z-10"></div>
<div class="max-w-[800px] mx-auto px-gutter">
<!-- Header Title -->
<div class="text-center mb-section-gap">
<h1 class="font-display-lg text-[72px] leading-tight font-bold tracking-tighter mb-4 text-primary">Event Feedback</h1>
<p class="text-on-surface-variant text-title-sm font-title-sm max-w-2xl mx-auto">
Your insights drive the future of IPv6 infrastructure. Help us shape the 2027 experience.
<br/><br/></p>
</div>
<!-- Feedback Form -->
<form class="glass-panel p-stack-lg rounded-xl space-y-section-gap relative overflow-hidden">
<!-- Section 1: General Satisfaction -->
<section class="space-y-stack-lg">
<div class="border-l-2 border-primary pl-4">
<h2 class="font-headline-md text-headline-md text-on-surface mb-1 uppercase tracking-tight">1. GENERAL SATISFACTION</h2>
</div>
<div class="space-y-stack-md">
<label class="block font-label-caps text-label-caps text-primary uppercase">Overall Event Rating</label>
<div class="flex justify-between items-center gap-stack-sm max-w-md">
<span class="text-body-sm opacity-50">Poor</span>
<div class="flex gap-4">
<label class="cursor-pointer group">Overall Event Rating</label>
<label class="cursor-pointer group">
<input class="hidden peer" name="event-rating" type="radio" value="2"/>
<div class="w-12 h-12 flex items-center justify-center border border-white/10 rounded peer-checked:bg-primary peer-checked:text-on-primary hover:border-primary transition-all">2</div>
</label>
<label class="cursor-pointer group">
<input class="hidden peer" name="event-rating" type="radio" value="3"/>
<div class="w-12 h-12 flex items-center justify-center border border-white/10 rounded peer-checked:bg-primary peer-checked:text-on-primary hover:border-primary transition-all">3</div>
</label>
<label class="cursor-pointer group">
<input class="hidden peer" name="event-rating" type="radio" value="4"/>
<div class="w-12 h-12 flex items-center justify-center border border-white/10 rounded peer-checked:bg-primary peer-checked:text-on-primary hover:border-primary transition-all">4</div>
</label>
<label class="cursor-pointer group">
<input class="hidden peer" name="event-rating" type="radio" value="5"/>
<div class="w-12 h-12 flex items-center justify-center border border-white/10 rounded peer-checked:bg-primary peer-checked:text-on-primary hover:border-primary transition-all">5</div>
</label>
</div>
<span class="text-body-sm opacity-50">Excellent</span>
</div>
</div>
<div class="space-y-stack-md">
<label class="block font-label-caps text-label-caps text-primary uppercase">Overall Event Rating</label>
<div class="flex justify-between items-center gap-stack-sm max-w-md">
<span class="text-body-sm opacity-50">Dissatisfied</span>
<div class="flex gap-4">
<label class="cursor-pointer group">Overall Event Rating</label>
<label class="cursor-pointer group">
<input class="hidden peer" name="venue-rating" type="radio" value="2"/>
<div class="w-12 h-12 flex items-center justify-center border border-white/10 rounded peer-checked:bg-primary peer-checked:text-on-primary hover:border-primary transition-all">2</div>
</label>
<label class="cursor-pointer group">
<input class="hidden peer" name="venue-rating" type="radio" value="3"/>
<div class="w-12 h-12 flex items-center justify-center border border-white/10 rounded peer-checked:bg-primary peer-checked:text-on-primary hover:border-primary transition-all">3</div>
</label>
<label class="cursor-pointer group">
<input class="hidden peer" name="venue-rating" type="radio" value="4"/>
<div class="w-12 h-12 flex items-center justify-center border border-white/10 rounded peer-checked:bg-primary peer-checked:text-on-primary hover:border-primary transition-all">4</div>
</label>
<label class="cursor-pointer group">
<input class="hidden peer" name="venue-rating" type="radio" value="5"/>
<div class="w-12 h-12 flex items-center justify-center border border-white/10 rounded peer-checked:bg-primary peer-checked:text-on-primary hover:border-primary transition-all">5</div>
</label>
</div>
<span class="text-body-sm opacity-50">Very Satisfied</span>
</div>
</div>
</section>
<!-- Section 2: Content Evaluation -->
<section class="space-y-stack-lg">
<div class="border-l-2 border-primary pl-4">
<h2 class="font-headline-md text-headline-md text-on-surface mb-1 uppercase tracking-tight">2. CONTENT EVALUATION</h2>
</div>
<div class="space-y-stack-md">
<label class="block font-label-caps text-label-caps text-primary uppercase">Most valuable session </label>
<select class="w-full bg-surface-container-low border border-white/10 rounded px-4 py-3 text-on-surface input-focus-gold appearance-none"><option value="">Select a session</option><option value="ai">AI in Data Centres</option><option value="green">Green Tech</option><option value="cyber">Cybersecurity</option><option value="policy">Policy &amp; Scale</option><option value="local">Local Partner Session</option></select>
</div>
<div class="space-y-stack-md">
<label class="block font-label-caps text-label-caps text-primary uppercase">Keynote Speaker Rating </label>
<div class="flex justify-between items-center gap-stack-sm max-w-md">
<span class="text-body-sm opacity-50">Needs Improvement</span>
<div class="flex gap-4">
<label class="cursor-pointer group">
<input class="hidden peer" name="speaker-rating" type="radio" value="1"/>
<div class="w-12 h-12 flex items-center justify-center border border-white/10 rounded peer-checked:bg-primary peer-checked:text-on-primary hover:border-primary transition-all">1</div>
</label>
<label class="cursor-pointer group">
<input class="hidden peer" name="speaker-rating" type="radio" value="2"/>
<div class="w-12 h-12 flex items-center justify-center border border-white/10 rounded peer-checked:bg-primary peer-checked:text-on-primary hover:border-primary transition-all">2</div>
</label>
<label class="cursor-pointer group">
<input class="hidden peer" name="speaker-rating" type="radio" value="3"/>
<div class="w-12 h-12 flex items-center justify-center border border-white/10 rounded peer-checked:bg-primary peer-checked:text-on-primary hover:border-primary transition-all">3</div>
</label>
<label class="cursor-pointer group">
<input class="hidden peer" name="speaker-rating" type="radio" value="4"/>
<div class="w-12 h-12 flex items-center justify-center border border-white/10 rounded peer-checked:bg-primary peer-checked:text-on-primary hover:border-primary transition-all">4</div>
</label>
<label class="cursor-pointer group">
<input class="hidden peer" name="speaker-rating" type="radio" value="5"/>
<div class="w-12 h-12 flex items-center justify-center border border-white/10 rounded peer-checked:bg-primary peer-checked:text-on-primary hover:border-primary transition-all">5</div>
</label>
</div>
<span class="text-body-sm opacity-50">Exceptional</span>
</div>
</div>
</section>
<!-- Section 3: Qualitative Feedback -->
<section class="space-y-stack-lg">
<div class="border-l-2 border-primary pl-4">
<h2 class="font-headline-md text-headline-md text-on-surface mb-1 uppercase tracking-tight">3. Qualitative Feedback</h2>
</div>
<div class="space-y-stack-md">
<label class="block font-label-caps text-label-caps text-primary uppercase">What can we improve for 2027? </label>
<textarea class="w-full bg-surface-container-low border border-white/10 rounded px-4 py-3 text-on-surface input-focus-gold resize-none" placeholder="Your suggestions for next year..." rows="4"></textarea>
</div>
<div class="space-y-stack-md">
<label class="block font-label-caps text-label-caps text-primary uppercase">General Comments </label>
<textarea class="w-full bg-surface-container-low border border-white/10 rounded px-4 py-3 text-on-surface input-focus-gold resize-none" placeholder="Any additional thoughts..." rows="3"></textarea>
</div>
</section>
<!-- Submit Button Area -->
<!-- Section 4: Industry & Technical Insights -->
<section class="space-y-stack-lg">
<div class="border-l-2 border-primary pl-4">
<h2 class="font-headline-md text-headline-md text-on-surface mb-1 uppercase tracking-tight">4. INDUSTRY &amp; TECHNICAL INSIGHTS</h2>
</div>
<div class="space-y-stack-md">
<label class="block font-label-caps text-label-caps text-primary uppercase">IPv6 Readiness</label>
<p class="text-on-surface-variant text-sm mb-2">Your organization's current IPv6 deployment level</p>
<div class="flex justify-between items-center gap-stack-sm max-w-md">
<span class="text-body-sm opacity-50">1</span>
<div class="flex gap-4">
<label class="cursor-pointer group">IPv6 Readiness</label>
<label class="cursor-pointer group">IPv6 Readiness</label>
<label class="cursor-pointer group">IPv6 Readiness</label>
<label class="cursor-pointer group">IPv6 Readiness</label>
<label class="cursor-pointer group">IPv6 Readiness</label>
</div>
<span class="text-body-sm opacity-50">5</span>
</div>
</div>
<div class="space-y-stack-md">
<label class="block font-label-caps text-label-caps text-primary uppercase">Infrastructure Challenges</label>
<p class="text-on-surface-variant text-sm mb-2">What is the biggest challenge for your AI infrastructure?</p>
<select class="w-full bg-surface-container-low border border-white/10 rounded px-4 py-3 text-on-surface input-focus-gold appearance-none"><option value="">Select a challenge</option><option value="budget">Budget</option><option value="technical">Technical Expertise</option><option value="policy">Policy/Regulatory</option><option value="workforce">Workforce</option><option value="hardware">Hardware Availability</option></select>
</div>
</section>
<!-- Section 5: Networking & Future Cooperation -->
<section class="space-y-stack-lg">
<div class="border-l-2 border-primary pl-4">
<h2 class="font-headline-md text-headline-md text-on-surface mb-1 uppercase tracking-tight">5. NETWORKING &amp; FUTURE COOPERATION</h2>
</div>
<div class="space-y-stack-md">
<label class="block font-label-caps text-label-caps text-primary uppercase">Speaker Connection</label>
<p class="text-on-surface-variant text-sm mb-2">Are there any specific speakers or companies you wish to be introduced to?</p>
<input class="w-full bg-surface-container-low border border-white/10 rounded px-4 py-3 text-on-surface input-focus-gold" placeholder="Name of speaker or company..." type="text"/>
</div>
<div class="space-y-stack-md">
<label class="block font-label-caps text-label-caps text-primary uppercase">Collaboration Interests</label>
<p class="text-on-surface-variant text-sm mb-2">I am interested in:</p>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<label class="flex items-center gap-3 cursor-pointer group">Collaboration Interests</label>
<label class="flex items-center gap-3 cursor-pointer group">Collaboration Interests</label>
<label class="flex items-center gap-3 cursor-pointer group">Collaboration Interests</label>
<label class="flex items-center gap-3 cursor-pointer group">Collaboration Interests</label>
</div>
</div>
</section>
<!-- Section 6: Logistics Feedback -->
<section class="space-y-stack-lg">
<div class="border-l-2 border-primary pl-4">
<h2 class="font-headline-md text-headline-md text-on-surface mb-1 uppercase tracking-tight">6. LOGISTICS FEEDBACK</h2>
</div>
<div class="space-y-stack-md">
<label class="block font-label-caps text-label-caps text-primary uppercase">Venue Rating</label>
<p class="text-on-surface-variant text-sm mb-2">Venue Facilities &amp; Location (Ho Chi Minh City)</p>
<div class="flex justify-between items-center gap-stack-sm max-w-md">
<span class="text-body-sm opacity-50">Poor</span>
<div class="flex gap-4">
<label class="cursor-pointer group">Venue Rating</label>
<label class="cursor-pointer group">Venue Rating</label>
<label class="cursor-pointer group">Venue Rating</label>
<label class="cursor-pointer group">Venue Rating</label>
<label class="cursor-pointer group">Venue Rating</label>
</div>
<span class="text-body-sm opacity-50">Excellent</span>
</div>
</div>
</section><div class="pt-stack-lg border-t border-white/5 flex flex-col items-center gap-4">
<button class="gold-gradient-bg text-on-primary w-full md:w-auto px-12 py-4 rounded-lg font-headline-md text-[18px] uppercase tracking-wider hover:shadow-[0_0_20px_rgba(233,193,118,0.4)] transition-all duration-300 transform active:scale-95" type="submit">SUBMIT FEEDBACK</button>
<p class="text-on-surface-variant font-label-caps text-[10px] opacity-60 uppercase text-center">
Thank you for your valuable contribution to the IPv6 ecosystem.
</p>
</div>
</form>
</div>
</main>
<!-- Footer -->
<footer class="w-full block border-t border-border-subtle bg-surface-container-lowest">
<div class="flex flex-col md:flex-row justify-between items-center px-gutter py-12 max-w-container-max mx-auto gap-stack-md">
<div class="flex flex-col gap-2">
<div class="font-h2 text-h2 text-primary uppercase font-bold tracking-tighter">IPv6 SUMMIT 2026</div>
<p class="font-body-sm text-body-sm text-text-slate opacity-70">
© 2026 IPv6 for AI &amp; Data Centre Summit. Infrastructure for the Future.
</p>
</div>
<div class="flex flex-wrap gap-stack-md justify-center">
<a class="text-text-slate font-label-caps text-label-caps uppercase hover:text-accent-orange transition-opacity duration-300" href="#">Privacy </a>
<a class="text-text-slate font-label-caps text-label-caps uppercase hover:text-accent-orange transition-opacity duration-300" href="#">Terms </a>
<a class="text-text-slate font-label-caps text-label-caps uppercase hover:text-accent-orange transition-opacity duration-300" href="#">Contact </a>
<a class="text-text-slate font-label-caps text-label-caps uppercase hover:text-accent-orange transition-opacity duration-300" href="#">Sponsorship </a>
</div>
</div>
</footer>
</body></html>
-290
View File
@@ -1,290 +0,0 @@
# Kế hoạch dự án IPV6 (Next.js App Router) — Tạo mới & Chuyển đổi từ HTML
## 1) Mục tiêu & phạm vi
- **Mục tiêu**: Tạo project frontend mới tên **IPV6** dựa trên “format sẵn” của codebase Next.js hiện tại, chuyển các trang HTML tĩnh sang Next.js (TypeScript/TSX), tách component, tách static data (JSON), và chuẩn hoá routing theo nghiệp vụ doanh nghiệp.
- **Phạm vi**: Frontend בלבד (chưa làm backend). Các API gọi ra ngoài/CMS (nếu có trong codebase cũ) sẽ **loại bỏ hoặc chuyển thành static JSON** cho đúng scope.
- **Yêu cầu chung**:
- **Ngôn ngữ UI mặc định**: English (có thể để song ngữ EN/VI như UI toggle, nhưng content/label phải chuẩn hoá).
- **Design system thống nhất**: màu sắc, font chữ, spacing, width/height chuẩn (không lệch giữa trang).
- **Responsive**: hiển thị tốt trên desktop/tablet/mobile, kiểm tra layout với các breakpoint phổ biến.
## 2) Hiện trạng codebase (để bám “format sẵn”)
- Đang dùng **Next.js App Router**: có thư mục `app/`.
- Có layout sẵn `app/layout.tsx` đang import CSS global và render `Header` + `Footer`.
- Có nhiều phần liên quan LAMS (metadata, service/api, pages/route cũ…).
- Các file HTML nguồn cho IPV6 đang nằm ở root:
- `test.html` (Trang chủ)
- `registration.html` (Form đăng ký)
- `feedback.html` (Form feedback)
- `sponsor.html` (Kêu gọi đóng góp)
- `admin.html` (Trang quản lý request — nội bộ coder)
## 3) Outcome mong muốn (cấu trúc thư mục đề xuất)
> Mục tiêu: rõ ràng, tách bạch component/page/data/styles; dễ maintain; đúng App Router.
Đề xuất skeleton (có thể điều chỉnh theo format hiện tại):
- `app/`
- `(site)/`
- `layout.tsx` (site shell: Header/Footer dùng cho public pages)
- `page.tsx` (**Home** từ `test.html`)
- `registration/page.tsx`
- `feedback/page.tsx`
- `sponsor/page.tsx`
- `(internal)/admin/`
- `page.tsx` (**Admin** từ `admin.html`, route group để tách nội bộ)
- `globals.css` (CSS global/tokens)
- `not-found.tsx` (tuỳ chọn)
- `app/components/`
- `layout/` (`Header`, `Footer`, `Container`, `Section`, `Nav`, …)
- `home/` (các section của home)
- `forms/` (shared form fields/validation UI)
- `ui/` (Button, Input, Card, Modal, Badge…)
- `app/data/` (static JSON)
- `site.json` (brand, links, footer)
- `home.json` (hero, agenda, speakers, stats, venue, …)
- `sponsor.json`
- `registration.json` (copy text, options)
- `feedback.json`
- `admin.json` (cột bảng, status mapping…)
- `public/`
- `assets/` (img/icons/fonts nếu có)
## 4) Quy ước routing (đảm bảo nghiệp vụ doanh nghiệp)
### 4.1 Public pages (khách truy cập)
- `/`**Home** (từ `test.html`)
- `/registration`**Registration form** (từ `registration.html`)
- `/feedback`**Feedback form** (từ `feedback.html`)
- `/sponsor`**Sponsor/Donate** (từ `sponsor.html`)
### 4.2 Internal pages (chỉ coder nội bộ)
- `/(internal)/admin`**Admin request management** (từ `admin.html`)
- Yêu cầu “chỉ nội bộ”: vì không có backend, sẽ dùng **cơ chế chặn tối thiểu** (xem mục 9).
## 5) Chuyển đổi Component (HTML → TSX)
### 5.1 Nguyên tắc tách component
- Tách theo **layout shell** (Header/Footer) và theo **section** của từng page.
- Mỗi component:
- Props typed (TypeScript).
- Không hardcode text/data nếu có thể đưa vào JSON.
- Không nhúng `<script>` DOM manipulation kiểu HTML (sẽ chuyển sang state + event trong React).
### 5.2 Trang chủ — `test.html`
**Yêu cầu đặc biệt**: “Tách ra trang home riêng, header và footer có components được tách riêng”.
- Tạo `app/(site)/page.tsx` làm Home.
- Tách Header/Footer:
- `app/components/layout/Header/*`
- `app/components/layout/Footer/*`
- Tách các section của Home (đề xuất theo nội dung `test.html`):
- `HeroSection`
- `VisionGoalsSection`
- `StatsSection`
- `AgendaSection`
- `StakeholdersSection`
- `RegistrationCTASection` (nếu Home có form/CTA)
- (Nếu có `Speakers`, `Venue` trong HTML thì tách tương tự)
- Thay logic mobile menu:
- HTML đang dùng script toggle class `hidden` → chuyển sang `useState` và className theo state.
- Chuẩn hoá anchor navigation:
- Link `#agenda`, `#stats`, … giữ nguyên trong trang Home, đảm bảo scroll behavior trong CSS global.
### 5.3 Trang Registration — `registration.html`
- Tạo `app/(site)/registration/page.tsx`
- Tách:
- `RegistrationForm` (client component)
- Shared inputs: `TextField`, `SelectField`, `TextareaField`, `PhoneField`
- Form scope frontend-only:
- Submit tạm: `console.log` + toast/alert (hoặc disable submit) tuỳ yêu cầu.
- Chuẩn bị shape data để sau này nối backend.
### 5.4 Trang Feedback — `feedback.html`
- Tạo `app/(site)/feedback/page.tsx`
- Tách `FeedbackForm` tương tự Registration.
### 5.5 Trang Sponsor — `sponsor.html`
- Tạo `app/(site)/sponsor/page.tsx`
- Tách:
- `SponsorHero`
- `SponsorPackages`
- `SponsorCTA`
- `SponsorFAQ` (nếu có)
### 5.6 Trang Admin — `admin.html` (folder riêng)
- Tạo `app/(internal)/admin/page.tsx`
- Tách:
- `RequestsTable`
- `RequestFilters`
- `RequestDetailDrawer` (tuỳ)
- Vì chưa backend:
- Data bảng lấy từ `app/data/admin.json` (static).
- Chuẩn hoá model “Request”: id, createdAt, name, email, company, status…
## 6) JSON hoá static data (tách data ra file riêng)
### 6.1 Nguyên tắc
- Mọi text/cấu hình lặp lại (nav items, footer links, section headings, stats items, agenda items…) → đưa vào JSON.
- JSON đặt trong `app/data/` (hoặc `src/data/` nếu codebase đang dùng `src/`).
- Page/Component đọc JSON theo hướng server component:
- Page (server) import JSON → pass props xuống components.
- Form components là client, chỉ nhận props cần thiết.
### 6.2 Danh mục data đề xuất
- `site.json`: brandName, navLinks, footerLinks, social, legal, languages…
- `home.json`: hero, vision, stats, agenda, stakeholders, venue, contact…
- `registration.json`: copy + options.
- `feedback.json`: copy + categories.
- `sponsor.json`: tiers/packages + benefits.
- `admin.json`: columns + seed rows + status label/color mapping.
## 7) Styling — CSS chung & thống nhất design system
### 7.1 Quyết định kỹ thuật (theo codebase hiện tại)
Codebase hiện có:
- Đang import `./globals.css` và có pipeline SCSS build sang `public/assets/css/main.css`.
-`tailwindcss` trong devDependencies, nhưng HTML nguồn (`test.html`) đang dùng **Tailwind CDN** + inline `<style>`.
Kế hoạch:
- **Không dùng Tailwind CDN trong Next.js** (loại bỏ script CDN).
- Chọn 1 hướng (ưu tiên nhất quán):
- **Hướng A (khuyến nghị)**: cấu hình Tailwind nội bộ (tailwind.config + postcss) rồi chuyển class từ HTML qua TSX giữ nguyên nhiều utility class, đồng thời đưa token màu/font vào tailwind theme.
- **Hướng B**: giữ SCSS/CSS thuần, chuyển utility class sang class semantic (tốn công hơn).
> Chọn A sẽ giúp chuyển đổi nhanh nhất vì `test.html` đã viết theo Tailwind.
### 7.2 Design tokens cần chuẩn hoá
- **Colors**: primary (gold), background (dark), surface containers, on-surface… (đang có trong `test.html` tailwind config).
- **Typography**: font families (Space Grotesk, Inter, JetBrains Mono), size scale (fluid typography).
- **Layout**: container max width (HD 1920/1800), gutter responsive.
- **Border radius / shadows**: thống nhất.
### 7.3 Checklist responsive
- Breakpoints tối thiểu: 360, 390, 414, 768, 1024, 1280, 1536, 1920.
- Kiểm tra:
- Header: menu desktop vs mobile drawer, body scroll lock.
- Hero: text không tràn, CTA stack đúng.
- Grid sections: số cột thay đổi đúng.
- Form: label/input spacing, focus state, error state.
## 8) Routing & layout composition
### 8.1 Layout public
- Dùng `app/(site)/layout.tsx` để wrap Header/Footer cho toàn bộ public pages.
- `app/layout.tsx` giữ nhiệm vụ root (html/head, global css, metadata base).
### 8.2 Layout internal
- `app/(internal)/layout.tsx` (tuỳ) để internal khác theme nếu cần (ví dụ tối giản, ưu tiên bảng).
## 9) Bảo vệ trang Admin (nội bộ coder) khi chưa có backend
Vì chưa backend, không thể “đăng nhập chuẩn” theo doanh nghiệp. Kế hoạch chặn tối thiểu:
- **Phương án 1 (khuyến nghị)**: `middleware.ts` + Basic Auth (HTTP) dựa trên env server
- `ADMIN_BASIC_AUTH_USER`, `ADMIN_BASIC_AUTH_PASS`
- Chỉ áp cho path `/admin`
- **Phương án 2**: “secret query”/client guard (không an toàn) — chỉ dùng khi demo nội bộ.
> Mục tiêu: không cho public truy cập đường dẫn admin một cách dễ dàng trong môi trường triển khai.
## 10) Xoá toàn bộ phần không liên quan IPV6 (LAMS)
### 10.1 Nguyên tắc dọn dẹp
- Giữ lại:
- cấu trúc Next.js/TS/ESLint
- cấu trúc `app/` và các helper cần thiết
- pipeline CSS nếu dùng
- Xoá/đổi tên toàn bộ:
- Metadata/title/description liên quan LAMS
- Pages/routes/components/services/api types chỉ phục vụ LAMS
- Static assets LAMS (logo, favicon…) nếu không dùng
### 10.2 Checklist dọn dẹp cụ thể
- `package.json`:
- đổi `"name"``ipv6` (hoặc `ipv6-summit`)
- rà soát dependency không dùng (axios nếu không cần)
- `app/layout.tsx`:
- cập nhật metadata sang IPV6
- cập nhật favicon/logo/fonts đúng theo IPV6
- `app/api/**` (nếu đang gọi CMS cho LAMS):
- xoá toàn bộ nếu không dùng trong scope frontend-only
- `app/components/**`:
- thay Header/Footer LAMS bằng Header/Footer IPV6
- Các route/pages LAMS (ví dụ `/contact`, `/student-support`, `/request`, … nếu tồn tại trong `app/`):
- xoá để tránh rác routing
## 11) Quy trình thực hiện chuyển đổi (giống “lần trước”)
### 11.1 Chuẩn bị
- B1: Snapshot danh sách routes hiện tại (để biết thứ gì cần xoá).
- B2: Xác định style strategy (Tailwind nội bộ vs SCSS).
- B3: Tạo skeleton folders: `(site)`, `(internal)`, `components`, `data`.
### 11.2 Chuyển HTML → TSX theo từng trang
- B4: Home (`test.html`)
- Tách Header/Footer
- Tách sections
- Tách data ra `home.json` + `site.json`
- Loại bỏ `<script>` và chuyển sang state
- B5: Registration (`registration.html`)
- Tách form + fields
- Tách copy/options JSON
- B6: Feedback (`feedback.html`)
- B7: Sponsor (`sponsor.html`)
- B8: Admin (`admin.html`)
- Tạo route group `(internal)`
- Dựng table UI từ static JSON
- Thêm middleware bảo vệ
### 11.3 Chuẩn hoá
- B9: Đồng bộ typography/colors (global CSS + config).
- B10: Responsive pass + pixel polish (width/height, spacing).
- B11: Cleanup LAMS triệt để.
## 12) Test plan (frontend-only)
- **Routing**:
- `/` home load OK
- `/registration`, `/feedback`, `/sponsor` load OK
- `/admin` bị chặn nếu chưa auth (theo middleware)
- **Responsive**:
- Header: mobile menu hoạt động, không scroll nền khi mở drawer
- Sections: không overflow-x
- Forms: input full width mobile, label rõ ràng
- **Accessibility tối thiểu**:
- Label liên kết input
- Button type/aria cho menu toggle
- Focus ring rõ
## 13) Deliverables
- Next.js App Router pages đúng 5 trang yêu cầu.
- Component hoá toàn bộ, Header/Footer tách riêng.
- Static data tách JSON.
- CSS/design system thống nhất + responsive chuẩn.
- Xoá toàn bộ phần LAMS không liên quan, repo gọn chỉ còn IPV6.
+9
View File
@@ -0,0 +1,9 @@
import type { AdminData } from "@/app/components/admin/AdminConsole";
import adminJson from "@/app/data/admin.json";
import { listRequestsSorted } from "@/lib/summit-requests";
export async function loadAdminConsoleData(): Promise<AdminData> {
const base = adminJson as AdminData;
const requests = await listRequestsSorted();
return { ...base, requests };
}
+21
View File
@@ -0,0 +1,21 @@
import mongoose from "mongoose";
/** Cached connection for Next.js hot reload (global is preserved across HMR). */
const globalForMongoose = globalThis as typeof globalThis & {
mongooseConn?: typeof mongoose;
};
export default async function connectDB(): Promise<typeof mongoose> {
const uri = process.env.MONGODB_URI;
if (!uri) {
throw new Error("MONGODB_URI is not set. Add it to .env (e.g. mongodb://localhost:27017/ipv6_summit).");
}
if (globalForMongoose.mongooseConn?.connection?.readyState === 1) {
return globalForMongoose.mongooseConn;
}
const conn = await mongoose.connect(uri);
globalForMongoose.mongooseConn = conn;
return conn;
}
+54
View File
@@ -0,0 +1,54 @@
import type { AdminRequestRow, SubmissionStatus } from "@/types/admin-submission";
import connectDB from "@/lib/mongodb";
import SummitRequest, { type ISummitRequest } from "@/models/SummitRequest";
function docToRow(doc: ISummitRequest): AdminRequestRow {
return {
id: doc.publicId,
submittedAt: doc.submittedAt,
displayDate: doc.displayDate,
fullName: doc.fullName,
jobTitle: doc.jobTitle,
company: doc.company,
segment: doc.segment,
email: doc.email,
phone: doc.phone,
status: doc.status,
notes: doc.notes,
source: doc.source,
};
}
export async function listRequestsSorted(): Promise<AdminRequestRow[]> {
await connectDB();
const docs = await SummitRequest.find()
.sort({ submittedAt: -1, updatedAt: -1 })
.lean<ISummitRequest[]>();
return docs.map(docToRow);
}
export async function createSummitRequest(row: AdminRequestRow): Promise<void> {
await connectDB();
await SummitRequest.create({
publicId: row.id,
submittedAt: row.submittedAt,
displayDate: row.displayDate,
fullName: row.fullName,
jobTitle: row.jobTitle,
company: row.company,
segment: row.segment,
email: row.email,
phone: row.phone,
status: row.status,
notes: row.notes,
source: row.source,
});
}
export async function updateSummitRequestStatus(publicId: string, status: SubmissionStatus): Promise<void> {
await connectDB();
const updated = await SummitRequest.findOneAndUpdate({ publicId }, { $set: { status } }, { new: true });
if (!updated) {
throw new Error(`Request not found: ${publicId}`);
}
}
+46
View File
@@ -0,0 +1,46 @@
import type { NextRequest } 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) {
const user = process.env.ADMIN_BASIC_AUTH_USER;
const pass = process.env.ADMIN_BASIC_AUTH_PASS;
if (!user || !pass) {
return NextResponse.next();
}
const auth = request.headers.get("authorization");
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();
}
export const config = {
matcher: ["/admin", "/admin/:path*"],
};
+46
View File
@@ -0,0 +1,46 @@
import type { SubmissionStatus } from "@/types/admin-submission";
import { Schema, model, models } from "mongoose";
export type SummitRequestSource = "registration" | "feedback";
export interface ISummitRequest {
publicId: string;
submittedAt: string;
displayDate: string;
fullName: string;
jobTitle: string;
company: string;
segment: string;
email: string;
phone: string;
status: SubmissionStatus;
notes: string;
source?: SummitRequestSource;
}
const summitRequestSchema = new Schema<ISummitRequest>(
{
publicId: { type: String, required: true, unique: true, index: true },
submittedAt: { type: String, required: true, index: true },
displayDate: { type: String, required: true },
fullName: { type: String, required: true },
jobTitle: { type: String, default: "" },
company: { type: String, default: "" },
segment: { type: String, required: true },
email: { type: String, default: "" },
phone: { type: String, default: "" },
status: {
type: String,
enum: ["pending", "approved", "rejected"],
default: "pending",
index: true,
},
notes: { type: String, default: "" },
source: { type: String, enum: ["registration", "feedback"] },
},
{ timestamps: true },
);
const SummitRequest = models.SummitRequest ?? model<ISummitRequest>("SummitRequest", summitRequestSchema, "summit_requests");
export default SummitRequest;
+6 -1
View File
@@ -21,7 +21,12 @@ const nextConfig: NextConfig = {
hostname: "127.0.0.1",
port: "3001",
pathname: "/**",
}
},
{
protocol: "https",
hostname: "lh3.googleusercontent.com",
pathname: "/**",
},
],
},
};
+7795
View File
File diff suppressed because it is too large Load Diff
+4 -1
View File
@@ -7,11 +7,13 @@
"build": "next build",
"start": "dotenv -- next start",
"lint": "eslint",
"db:seed": "tsx scripts/seed-summit-requests.ts",
"scss:build": "sass public/assets/scss/main.scss public/assets/css/main.css --style=expanded --source-map",
"scss:watch": "sass --watch public/assets/scss/main.scss:public/assets/css/main.css --style=expanded --source-map"
},
"dependencies": {
"axios": "^1.13.4",
"mongoose": "^9.6.2",
"next": "16.1.6",
"react": "19.2.3",
"react-dom": "19.2.3"
@@ -26,6 +28,7 @@
"eslint-config-next": "16.1.6",
"sass": "^1.98.0",
"tailwindcss": "^4",
"tsx": "^4.21.0",
"typescript": "^5"
}
}
}
Binary file not shown.

Before

Width:  |  Height:  |  Size: 170 KiB

-284
View File
@@ -1,284 +0,0 @@
<!DOCTYPE html><html class="dark" lang="en"><head>
<meta charset="utf-8">
<meta content="width=device-width, initial-scale=1.0" name="viewport">
<title>Registration | IPv6 for AI &amp; Data Centre Summit 2026</title>
<script src="https://cdn.tailwindcss.com?plugins=forms,container-queries"></script>
<link href="https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@300;400;500;600;700&amp;family=Inter:wght@300;400;500;600;700&amp;family=Geist:wght@400;600&amp;display=swap" rel="stylesheet">
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&amp;display=swap" rel="stylesheet">
<script id="tailwind-config">
tailwind.config = {
darkMode: "class",
theme: {
extend: {
"colors": {
"on-primary-container": "#4e3700",
"on-tertiary-fixed-variant": "#48464c",
"secondary-fixed-dim": "#e9c088",
"secondary-fixed": "#ffddb2",
"on-secondary-container": "#d9b27c",
"secondary": "#e9c088",
"surface-container": "#201f1f",
"outline-variant": "#4e4639",
"surface": "#131313",
"surface-container-high": "#2a2a2a",
"secondary-container": "#604418",
"surface-dim": "#131313",
"primary-container": "#c5a059",
"on-tertiary": "#313035",
"outline": "#9a8f80",
"on-surface": "#e5e2e1",
"surface-container-low": "#1c1b1b",
"inverse-on-surface": "#313030",
"surface-container-highest": "#353534",
"on-error-container": "#ffdad6",
"on-background": "#e5e2e1",
"surface-variant": "#353534",
"on-primary": "#412d00",
"primary-fixed-dim": "#e9c176",
"on-error": "#690005",
"tertiary-fixed-dim": "#c9c5cc",
"background": "#131313",
"tertiary": "#c9c5cc",
"surface-tint": "#e9c176",
"primary": "#e9c176",
"surface-bright": "#3a3939",
"primary-fixed": "#ffdea5",
"on-tertiary-container": "#3c3a40",
"on-tertiary-fixed": "#1c1b20",
"on-secondary-fixed": "#291800",
"on-secondary-fixed-variant": "#5d4216",
"inverse-surface": "#e5e2e1",
"tertiary-container": "#a7a4ab",
"inverse-primary": "#775a19",
"on-surface-variant": "#d1c5b4",
"on-primary-fixed-variant": "#5d4201",
"on-secondary": "#442b02",
"error-container": "#93000a",
"error": "#ffb4ab",
"surface-container-lowest": "#0e0e0e",
"on-primary-fixed": "#261900",
"tertiary-fixed": "#e5e1e8"
},
"borderRadius": {
"DEFAULT": "0.125rem",
"lg": "0.25rem",
"xl": "0.5rem",
"full": "0.75rem"
},
"spacing": {
"section-gap": "80px",
"gutter": "24px",
"stack-md": "16px",
"stack-lg": "32px",
"margin-safe": "40px",
"unit": "4px",
"stack-sm": "8px"
},
"fontFamily": {
"title-sm": ["Inter"],
"body-base": ["Inter"],
"display-lg": ["Space Grotesk"],
"label-caps": ["Geist"],
"headline-md": ["Space Grotesk"]
},
"fontSize": {
"title-sm": ["20px", {"lineHeight": "1.4", "letterSpacing": "0.02em", "fontWeight": "600"}],
"body-base": ["16px", {"lineHeight": "1.6", "letterSpacing": "0", "fontWeight": "400"}],
"display-lg": ["48px", {"lineHeight": "1.1", "letterSpacing": "-0.02em", "fontWeight": "700"}],
"label-caps": ["12px", {"lineHeight": "1", "letterSpacing": "0.1em", "fontWeight": "600"}],
"headline-md": ["32px", {"lineHeight": "1.2", "letterSpacing": "-0.01em", "fontWeight": "600"}],
"hero-title": ["72px", {"lineHeight": "1.1", "letterSpacing": "-0.03em", "fontWeight": "700"}]
}
},
},
}
</script>
<style>
body {
background-color: #0F0F0F;
color: #E5E2E1;
overflow-x: hidden;
}
.gold-gradient-bg {
background: linear-gradient(135deg, #C5A059 0%, #E9C176 50%, #C5A059 100%);
}
.glass-panel {
background: rgba(26, 25, 30, 0.6);
backdrop-filter: blur(12px);
border: 1px solid rgba(255, 255, 255, 0.05);
}
.hero-glow {
position: fixed;
top: -20%;
right: -10%;
width: 60%;
height: 60%;
background: radial-gradient(circle, rgba(197, 160, 89, 0.08) 0%, rgba(15, 15, 15, 0) 70%);
z-index: -1;
pointer-events: none;
}
.hero-glow-bottom {
position: fixed;
bottom: -20%;
left: -10%;
width: 50%;
height: 50%;
background: radial-gradient(circle, rgba(197, 160, 89, 0.05) 0%, rgba(15, 15, 15, 0) 70%);
z-index: -1;
pointer-events: none;
}
.material-symbols-outlined {
font-variation-settings: 'FILL' 0, 'wght' 400, 'GRAD' 0, 'opsz' 24;
vertical-align: middle;
}
input:focus, select:focus, textarea:focus {
outline: none !important;
border-color: #C5A059 !important;
box-shadow: 0 0 0 1px rgba(197, 160, 89, 0.3) !important;
}
</style>
</head>
<body class="font-body-base text-body-base selection:bg-primary-container selection:text-on-primary-container">
<div class="hero-glow"></div>
<div class="hero-glow-bottom"></div>
<!-- TopNavBar -->
<header class="fixed top-0 w-full z-50 bg-surface/80 backdrop-blur-xl border-b border-white/5 shadow-sm">
<div class="max-w-[1280px] mx-auto px-gutter h-20 flex items-center justify-between">
<div class="flex items-center gap-stack-md">
<span class="font-headline-md text-headline-md font-bold text-primary tracking-tight">IPv6 SUMMIT 2026</span>
</div>
<nav class="hidden md:flex items-center gap-gutter">
<a class="font-body-base text-body-base text-on-surface-variant hover:text-primary transition-colors" href="#">Agenda</a>
<a class="font-body-base text-body-base text-on-surface-variant hover:text-primary transition-colors" href="#">Speakers</a>
<a class="font-body-base text-body-base text-on-surface-variant hover:text-primary transition-colors" href="#">Venue</a>
<a class="font-body-base text-body-base text-on-surface-variant hover:text-primary transition-colors" href="#">Sponsors</a>
</nav>
<div class="flex items-center gap-stack-md">
<button class="font-label-caps text-label-caps text-on-surface-variant hover:text-primary transition-colors flex items-center gap-unit">
<span class="material-symbols-outlined text-[18px]">language</span>
EN | VI
</button>
<a class="gold-gradient-bg px-6 py-2.5 rounded-lg text-on-primary font-title-sm text-[14px] font-bold active:scale-95 transition-all hover:shadow-[0_0_20px_rgba(197,160,89,0.3)]" href="#">
Register
</a>
</div>
</div>
</header>
<main class="pt-[160px] pb-section-gap px-gutter flex justify-center">
<div class="w-full max-w-[1280px]">
<div class="max-w-[800px] mx-auto">
<!-- Navigation Back -->
<div class="mb-stack-lg">
<a class="group flex items-center gap-stack-sm text-outline hover:text-primary transition-colors font-body-base" href="#">
<span class="material-symbols-outlined text-[20px] transition-transform group-hover:-translate-x-1">arrow_back</span>
Back to Home
</a>
</div>
<!-- Page Title -->
<div class="mb-stack-lg">
<h1 class="font-display-lg text-hero-title text-on-surface mb-stack-sm leading-[1.1]">Summit Registration</h1>
<p class="text-on-surface-variant font-body-base text-lg">Secure your presence at the forefront of IPv6 innovation for AI and Data Centres.</p>
</div>
<!-- Registration Card -->
<div class="glass-panel p-stack-lg md:p-12 rounded-xl shadow-2xl relative overflow-hidden">
<!-- Subtle internal accent -->
<div class="absolute top-0 left-0 w-full h-[1px] bg-gradient-to-r from-transparent via-primary/20 to-transparent"></div>
<form class="space-y-stack-lg">
<!-- Section 1: Personal Info -->
<div class="space-y-stack-md">
<h2 class="font-headline-md text-title-sm text-primary border-l-2 border-primary pl-3">Personal Info / Thông tin cá nhân</h2>
<div class="grid grid-cols-1 md:grid-cols-2 gap-stack-md">
<div class="space-y-unit">
<label class="font-body-base text-base text-outline uppercase tracking-wider">Full Name&nbsp;</label>
<input class="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 font-body-base" placeholder="John Doe" type="text">
</div>
<div class="space-y-unit">
<label class="font-body-base text-base text-outline uppercase tracking-wider">Phone Number</label>
<input class="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 font-body-base" placeholder="+84 ..." type="tel">
</div>
<div class="md:col-span-2 space-y-unit">
<label class="font-body-base text-base text-outline uppercase tracking-wider">Business Email / Email công việc</label>
<input class="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 font-body-base" placeholder="john.doe@company.com" type="email">
</div>
</div>
</div>
<hr class="border-white/5">
<!-- Section 2: Professional Info -->
<div class="space-y-stack-md">
<h2 class="font-headline-md text-title-sm text-primary border-l-2 border-primary pl-3">Professional Info&nbsp;</h2>
<div class="grid grid-cols-1 md:grid-cols-2 gap-stack-md">
<div class="space-y-unit">
<label class="font-body-base text-base text-outline uppercase tracking-wider">Company Name&nbsp;</label>
<input class="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 font-body-base" placeholder="Aurelian Cybernetics" type="text">
</div>
<div class="space-y-unit">
<label class="font-body-base text-base text-outline uppercase tracking-wider">Job Title&nbsp;</label>
<input class="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 font-body-base" placeholder="Network Architect" type="text">
</div>
</div>
</div>
<hr class="border-white/5">
<!-- Section 3: Industry -->
<div class="space-y-stack-md">
<h2 class="font-headline-md text-title-sm text-primary border-l-2 border-primary pl-3">Industry</h2>
<div class="space-y-unit">
<label class="font-body-base text-base text-outline uppercase tracking-wider">Select Industry / Chọn ngành</label>
<div class="relative">
<select class="w-full bg-surface-container-low border border-white/10 rounded-lg px-4 py-3 text-on-surface appearance-none transition-all font-body-base">
<option disabled="" selected="" value="">Please choose...</option>
<option value="government">Government / Chính phủ</option>
<option value="telecom">Telecom / Viễn thông</option>
<option value="cloud">Cloud/Data Centre / Đám mây/Trung tâm dữ liệu</option>
<option value="ai">AI/Software / AI/Phần mềm</option>
<option value="others">Others / Khác</option>
</select>
<span class="material-symbols-outlined absolute right-4 top-1/2 -translate-y-1/2 text-outline pointer-events-none">expand_more</span>
</div>
</div>
</div>
<hr class="border-white/5">
<!-- Section 4: Special requirements -->
<div class="space-y-stack-md">
<h2 class="font-headline-md text-title-sm text-primary border-l-2 border-primary pl-3">Additional Details&nbsp;</h2>
<div class="space-y-unit">
<label class="font-body-base text-base text-outline uppercase tracking-wider">Special requirements or Dietary restrictions&nbsp;</label>
<textarea class="w-full bg-surface-container-low border border-white/10 rounded-lg px-4 py-3 text-on-surface placeholder:text-outline/40 transition-all resize-none font-body-base" placeholder="Vegetarian options, accessibility needs, etc." rows="4"></textarea>
</div>
</div>
<!-- Submit Action -->
<div class="pt-stack-md">
<button class="w-full gold-gradient-bg py-5 rounded-lg text-on-primary font-display-lg text-title-sm font-bold shadow-[0_10px_30px_rgba(197,160,89,0.15)] hover:shadow-[0_15px_40px_rgba(197,160,89,0.25)] hover:-translate-y-0.5 active:translate-y-0 active:scale-[0.98] transition-all" type="submit">
Submit Registration&nbsp;</button>
<p class="mt-stack-md text-center text-outline font-body-base text-[11px] tracking-widest opacity-60 uppercase">
BY REGISTERING, YOU AGREE TO OUR PRIVACY POLICY AND TERMS OF SERVICE.
</p>
</div>
</form>
</div>
</div>
</div>
</main>
<!-- Footer -->
<footer class="w-full mt-section-gap border-t border-white/5 bg-surface-container-lowest">
<div class="max-w-[1280px] mx-auto px-gutter py-stack-lg flex flex-col md:flex-row justify-between items-center gap-stack-md">
<span class="font-headline-md text-title-sm text-on-surface">IPv6 Summit 2026</span>
<div class="flex flex-wrap justify-center gap-gutter">
<a class="font-body-base text-label-caps text-on-tertiary-fixed-variant hover:text-secondary transition-colors" href="#">Contact Us</a>
<a class="font-body-base text-label-caps text-on-tertiary-fixed-variant hover:text-secondary transition-colors" href="#">Sponsorship Prospectus</a>
<a class="font-body-base text-label-caps text-on-tertiary-fixed-variant hover:text-secondary transition-colors" href="#">Privacy Policy</a>
<a class="font-body-base text-label-caps text-on-tertiary-fixed-variant hover:text-secondary transition-colors" href="#">Terms of Service</a>
</div>
<p class="font-body-base text-label-caps text-on-tertiary-fixed-variant opacity-60">
© 2026 IPv6 for AI &amp; Data Centre Summit. All rights reserved.
</p>
</div>
</footer>
</body></html>
+89
View File
@@ -0,0 +1,89 @@
/**
* Idempotent seed: upserts rows from app/data/admin.json, then optional legacy data/user-submissions.json.
* Run: npm run db:seed
* Requires MONGODB_URI in .env or .env.local
*/
import { config } from "dotenv";
import { existsSync, readFileSync } from "fs";
import path from "path";
config({ path: path.join(process.cwd(), ".env.local") });
config({ path: path.join(process.cwd(), ".env") });
import connectDB from "../lib/mongodb";
import SummitRequest from "../models/SummitRequest";
import type { AdminRequestRow } from "../types/admin-submission";
type AdminJson = { requests: AdminRequestRow[] };
function loadJsonRequests(): AdminRequestRow[] {
const adminPath = path.join(process.cwd(), "app", "data", "admin.json");
const raw = JSON.parse(readFileSync(adminPath, "utf-8")) as AdminJson;
return raw.requests ?? [];
}
function loadLegacyFileRequests(): AdminRequestRow[] {
const p = path.join(process.cwd(), "data", "user-submissions.json");
if (!existsSync(p)) return [];
try {
const raw = JSON.parse(readFileSync(p, "utf-8")) as unknown;
return Array.isArray(raw) ? (raw as AdminRequestRow[]) : [];
} catch {
return [];
}
}
function rowToInsert(row: AdminRequestRow) {
return {
publicId: row.id,
submittedAt: row.submittedAt,
displayDate: row.displayDate,
fullName: row.fullName,
jobTitle: row.jobTitle ?? "",
company: row.company ?? "",
segment: row.segment,
email: row.email ?? "",
phone: row.phone ?? "",
status: row.status,
notes: row.notes ?? "",
source: row.source,
};
}
async function upsertRow(row: AdminRequestRow) {
return SummitRequest.updateOne(
{ publicId: row.id },
{ $setOnInsert: rowToInsert(row) },
{ upsert: true },
);
}
async function main() {
await connectDB();
let inserted = 0;
let skipped = 0;
const fromAdmin = loadJsonRequests();
for (const row of fromAdmin) {
const res = await upsertRow(row);
if (res.upsertedCount) inserted++;
else skipped++;
}
const fromLegacy = loadLegacyFileRequests();
for (const row of fromLegacy) {
const res = await upsertRow(row);
if (res.upsertedCount) inserted++;
else skipped++;
}
console.log(
`Seed finished. admin.json: ${fromAdmin.length} rows, legacy file: ${fromLegacy.length} rows. New documents: ${inserted}, already existed: ${skipped}.`,
);
}
main().catch((err) => {
console.error(err);
process.exit(1);
});
-379
View File
@@ -1,379 +0,0 @@
<!DOCTYPE html>
<html class="dark" lang="en"><head>
<meta charset="utf-8"/>
<meta content="width=device-width, initial-scale=1.0" name="viewport"/>
<title>Sponsorship | IPv6 Summit 2026</title>
<script src="https://cdn.tailwindcss.com?plugins=forms,container-queries"></script>
<link href="https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@300;400;500;600;700&amp;family=Inter:wght@300;400;500;600;700&amp;family=Geist:wght@300;400;500;600;700&amp;display=swap" rel="stylesheet"/>
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&amp;display=swap" rel="stylesheet"/>
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&amp;display=swap" rel="stylesheet"/>
<script id="tailwind-config">
tailwind.config = {
darkMode: "class",
theme: {
extend: {
"colors": {
"surface": "#131313",
"surface-container": "#201f1f",
"primary-container": "#c5a059",
"on-error": "#690005",
"surface-container-low": "#1c1b1b",
"primary-fixed": "#ffdea5",
"on-tertiary": "#313035",
"outline": "#9a8f80",
"on-tertiary-container": "#3c3a40",
"tertiary-fixed": "#e5e1e8",
"secondary-container": "#604418",
"primary-fixed-dim": "#e9c176",
"tertiary": "#c9c5cc",
"on-secondary-fixed": "#291800",
"on-error-container": "#ffdad6",
"on-primary-container": "#4e3700",
"surface-variant": "#353534",
"on-primary-fixed": "#261900",
"on-background": "#e5e2e1",
"on-surface": "#e5e2e1",
"on-surface-variant": "#d1c5b4",
"secondary-fixed-dim": "#e9c088",
"secondary-fixed": "#ffddb2",
"error": "#ffb4ab",
"tertiary-container": "#a7a4ab",
"primary": "#e9c176",
"inverse-primary": "#775a19",
"outline-variant": "#4e4639",
"surface-dim": "#131313",
"tertiary-fixed-dim": "#c9c5cc",
"on-tertiary-fixed": "#1c1b20",
"on-tertiary-fixed-variant": "#48464c",
"surface-container-high": "#2a2a2a",
"error-container": "#93000a",
"surface-bright": "#3a3939",
"on-secondary-fixed-variant": "#5d4216",
"on-secondary": "#442b02",
"background": "#131313",
"surface-container-highest": "#353534",
"inverse-on-surface": "#313030",
"surface-tint": "#e9c176",
"secondary": "#e9c088",
"on-primary-fixed-variant": "#5d4201",
"surface-container-lowest": "#0e0e0e",
"on-primary": "#412d00",
"on-secondary-container": "#d9b27c",
"inverse-surface": "#e5e2e1"
},
"borderRadius": {
"DEFAULT": "0.125rem",
"lg": "0.25rem",
"xl": "0.5rem",
"full": "0.75rem"
},
"spacing": {
"section-gap": "80px",
"stack-md": "16px",
"stack-sm": "8px",
"stack-lg": "32px",
"margin-safe": "40px",
"gutter": "24px",
"unit": "4px"
},
"fontFamily": {
"display-lg": ["Space Grotesk"],
"headline-md": ["Space Grotesk"],
"label-caps": ["Geist"],
"title-sm": ["Inter"],
"body-base": ["Inter"]
},
"fontSize": {
"display-lg": ["48px", {"lineHeight": "1.1", "letterSpacing": "-0.02em", "fontWeight": "700"}],
"headline-md": ["32px", {"lineHeight": "1.2", "letterSpacing": "-0.01em", "fontWeight": "600"}],
"label-caps": ["12px", {"lineHeight": "1", "letterSpacing": "0.1em", "fontWeight": "600"}],
"title-sm": ["20px", {"lineHeight": "1.4", "letterSpacing": "0.02em", "fontWeight": "600"}],
"body-base": ["16px", {"lineHeight": "1.6", "letterSpacing": "0", "fontWeight": "400"}]
}
},
},
}
</script>
<style>
body { background-color: #0F0F0F; color: #e5e2e1; }
.glass-panel {
background: #1A191E;
border: 1px solid rgba(255, 255, 255, 0.05);
position: relative;
overflow: hidden;
}
.glass-panel::before {
content: "";
position: absolute;
top: 0; left: 0; right: 0; height: 1px;
background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.1), transparent);
}
.gold-border {
border: 1px solid #c5a059;
}
.gold-glow {
box-shadow: 0 0 20px rgba(197, 160, 89, 0.15);
}
.metallic-gold-gradient {
background: linear-gradient(135deg, #e9c176 0%, #c5a059 50%, #775a19 100%);
}
</style>
</head>
<body class="font-body-base">
<!-- TopNavBar -->
<header class="fixed top-0 w-full z-50 bg-surface/80 backdrop-blur-xl border-b border-white/5 shadow-sm">
<div class="max-w-[1280px] mx-auto px-gutter h-20 flex items-center justify-between">
<div class="font-display-lg text-title-sm font-bold text-primary tracking-tight">IPv6 Summit 2026</div>
<nav class="hidden md:flex items-center gap-stack-lg">
<a class="font-body-base text-body-base text-on-surface-variant hover:text-primary transition-colors" href="#">Agenda</a>
<a class="font-body-base text-body-base text-on-surface-variant hover:text-primary transition-colors" href="#">Speakers</a>
<a class="font-body-base text-body-base text-on-surface-variant hover:text-primary transition-colors" href="#">Venue</a>
<a class="font-body-base text-body-base text-primary border-b-2 border-primary pb-1" href="#">Sponsors</a>
</nav>
<div class="flex items-center gap-stack-md">
<button class="font-body-base text-body-base text-on-surface-variant hover:opacity-80 transition-all active:scale-95">Language</button>
<button class="metallic-gold-gradient text-on-primary font-title-sm px-6 py-2 rounded-lg hover:opacity-90 transition-all active:scale-95">Register</button>
</div>
</div>
</header>
<main class="pt-20">
<!-- Hero Section -->
<section class="relative py-16 lg:py-32 overflow-hidden">
<div class="absolute inset-0 bg-[radial-gradient(circle_at_50%_50%,rgba(197,160,89,0.08),transparent_70%)]"></div>
<div class="max-w-[1280px] mx-auto px-gutter relative z-10 text-center">
<span class="font-label-caps text-label-caps text-primary mb-4 block">OPPORTUNITY AWAITS</span>
<h1 class="font-display-lg text-display-lg mb-6">Become a Sponsor</h1>
<h2 class="font-headline-md text-headline-md text-on-surface-variant mb-8 max-w-3xl mx-auto">IPv6 for AI &amp; Data Centre Summit 2026 — 18 June 2026, Ho Chi Minh City</h2>
<p class="font-body-base text-body-base text-on-surface-variant/80 max-w-2xl mx-auto mb-12">
Join industry leaders at the forefront of the technological evolution. We invite innovative companies to co-sponsor an elite gathering focusing on AI, IPv6, Cloud Computing, Cybersecurity, Blockchain, and Data Centres. Align your brand with the architects of tomorrow's infrastructure.
</p>
<div class="flex flex-col sm:flex-row justify-center gap-stack-md">
<button class="metallic-gold-gradient text-on-primary font-title-sm px-10 py-4 rounded-lg hover:opacity-90 transition-all active:scale-95">Download Prospectus</button>
<button class="border border-primary text-primary font-title-sm px-10 py-4 rounded-lg hover:bg-primary/5 transition-all active:scale-95">Speak to an Agent</button>
</div>
</div>
</section>
<!-- Sponsorship Packages Section -->
<section class="py-16 lg:py-32 bg-surface-container-lowest">
<div class="max-w-[1280px] mx-auto px-gutter">
<div class="flex items-center justify-between mb-stack-lg">
<h2 class="font-headline-md text-headline-md">Sponsorship Packages &amp; Benefits</h2>
<div class="hidden lg:block h-px flex-grow mx-stack-lg bg-outline-variant/30"></div>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-stack-lg">
<!-- DIAMOND SPONSOR -->
<div class="glass-panel gold-border gold-glow p-stack-lg rounded-xl flex flex-col h-full ring-2 ring-primary/20">
<div class="mb-stack-lg">
<span class="bg-primary/20 text-primary px-3 py-1 rounded text-label-caps mb-4 inline-block">MOST PRESTIGIOUS</span>
<h3 class="font-display-lg text-headline-md text-primary">DIAMOND SPONSOR</h3>
<div class="text-on-surface font-display-lg text-title-sm mt-2">80,000,000 VND</div>
</div>
<ul class="space-y-4 mb-stack-lg flex-grow">
<li class="flex items-start gap-3">
<span class="material-symbols-outlined text-primary text-[20px]" style="font-variation-settings: 'FILL' 1;">check_circle</span>
<span class="text-on-surface-variant">Logo prominent on all branding materials</span>
</li>
<li class="flex items-start gap-3">
<span class="material-symbols-outlined text-primary text-[20px]" style="font-variation-settings: 'FILL' 1;">check_circle</span>
<span class="text-on-surface-variant">Logo on stage backdrop &amp; LED screens</span>
</li>
<li class="flex items-start gap-3">
<span class="material-symbols-outlined text-primary text-[20px]" style="font-variation-settings: 'FILL' 1;">check_circle</span>
<span class="text-on-surface-variant">Premium exhibition standee space</span>
</li>
<li class="flex items-start gap-3">
<span class="material-symbols-outlined text-primary text-[20px]" style="font-variation-settings: 'FILL' 1;">check_circle</span>
<span class="text-on-surface-variant font-bold">1 dedicated speaking slot</span>
</li>
<li class="flex items-start gap-3">
<span class="material-symbols-outlined text-primary text-[20px]" style="font-variation-settings: 'FILL' 1;">check_circle</span>
<span class="text-on-surface-variant">07 VIP Delegate tickets</span>
</li>
</ul>
<button class="metallic-gold-gradient text-on-primary w-full py-4 rounded font-title-sm">Select Diamond</button>
</div>
<!-- GOLD SPONSOR -->
<div class="glass-panel p-stack-lg rounded-xl flex flex-col h-full">
<div class="mb-stack-lg">
<h3 class="font-display-lg text-headline-md">GOLD SPONSOR</h3>
<div class="text-on-surface font-display-lg text-title-sm mt-2">50,000,000 VND</div>
</div>
<ul class="space-y-4 mb-stack-lg flex-grow">
<li class="flex items-start gap-3">
<span class="material-symbols-outlined text-primary text-[20px]">check_circle</span>
<span class="text-on-surface-variant">Logo on stage backdrop &amp; LED screens</span>
</li>
<li class="flex items-start gap-3">
<span class="material-symbols-outlined text-primary text-[20px]">check_circle</span>
<span class="text-on-surface-variant">Exhibition standee placement</span>
</li>
<li class="flex items-start gap-3">
<span class="material-symbols-outlined text-primary text-[20px]">check_circle</span>
<span class="text-on-surface-variant">05 VIP Delegate tickets</span>
</li>
</ul>
<button class="border border-outline-variant text-on-surface w-full py-4 rounded font-title-sm hover:border-primary transition-colors">Select Gold</button>
</div>
<!-- SILVER SPONSOR -->
<div class="glass-panel p-stack-lg rounded-xl flex flex-col h-full">
<div class="mb-stack-lg">
<h3 class="font-display-lg text-headline-md">SILVER SPONSOR</h3>
<div class="text-on-surface font-display-lg text-title-sm mt-2">40,000,000 VND</div>
</div>
<ul class="space-y-4 mb-stack-lg flex-grow">
<li class="flex items-start gap-3">
<span class="material-symbols-outlined text-primary text-[20px]">check_circle</span>
<span class="text-on-surface-variant">Logo on stage backdrop &amp; LED screens</span>
</li>
<li class="flex items-start gap-3">
<span class="material-symbols-outlined text-primary text-[20px]">check_circle</span>
<span class="text-on-surface-variant">Standard exhibition standee</span>
</li>
<li class="flex items-start gap-3">
<span class="material-symbols-outlined text-primary text-[20px]">check_circle</span>
<span class="text-on-surface-variant">04 VIP Delegate tickets</span>
</li>
</ul>
<button class="border border-outline-variant text-on-surface w-full py-4 rounded font-title-sm hover:border-primary transition-colors">Select Silver</button>
</div>
<!-- PARTNER SPONSOR -->
<div class="glass-panel p-stack-lg rounded-xl flex flex-col h-full">
<div class="mb-stack-lg">
<h3 class="font-display-lg text-headline-md">PARTNER SPONSOR</h3>
<div class="text-on-surface font-display-lg text-title-sm mt-2">20,000,000 VND</div>
</div>
<ul class="space-y-4 mb-stack-lg flex-grow">
<li class="flex items-start gap-3">
<span class="material-symbols-outlined text-primary text-[20px]">check_circle</span>
<span class="text-on-surface-variant">Logo on official website &amp; event communications</span>
</li>
<li class="flex items-start gap-3">
<span class="material-symbols-outlined text-primary text-[20px]">check_circle</span>
<span class="text-on-surface-variant">02 VIP Delegate tickets</span>
</li>
</ul>
<button class="border border-outline-variant text-on-surface w-full py-4 rounded font-title-sm hover:border-primary transition-colors">Select Partner</button>
</div>
</div>
</div>
</section>
<!-- Item-Based Sponsorship -->
<section class="py-16 lg:py-32">
<div class="max-w-[1280px] mx-auto px-gutter">
<div class="text-center mb-stack-lg">
<h2 class="font-headline-md text-headline-md mb-2">Item-Based Sponsorship</h2>
<p class="text-on-surface-variant opacity-70">Targeted branding opportunities for specific summit pillars</p>
</div>
<div class="grid grid-cols-2 lg:grid-cols-4 gap-gutter mb-stack-lg">
<div class="glass-panel p-stack-md rounded-lg text-center group hover:bg-surface-container transition-colors">
<div class="w-12 h-12 metallic-gold-gradient rounded-full flex items-center justify-center mx-auto mb-4">
<span class="material-symbols-outlined text-on-primary">hotel</span>
</div>
<h4 class="font-title-sm text-body-base mb-1">Hotel Accommodation</h4>
<p class="text-label-caps text-primary">Limited Space</p>
</div>
<div class="glass-panel p-stack-md rounded-lg text-center group hover:bg-surface-container transition-colors">
<div class="w-12 h-12 metallic-gold-gradient rounded-full flex items-center justify-center mx-auto mb-4">
<span class="material-symbols-outlined text-on-primary">meeting_room</span>
</div>
<h4 class="font-title-sm text-body-base mb-1">Conference Hall</h4>
<p class="text-label-caps text-primary">Sold Out</p>
</div>
<div class="glass-panel p-stack-md rounded-lg text-center group hover:bg-surface-container transition-colors">
<div class="w-12 h-12 metallic-gold-gradient rounded-full flex items-center justify-center mx-auto mb-4">
<span class="material-symbols-outlined text-on-primary">groups</span>
</div>
<h4 class="font-title-sm text-body-base mb-1">Networking Zone</h4>
<p class="text-label-caps text-primary">Available</p>
</div>
<div class="glass-panel p-stack-md rounded-lg text-center group hover:bg-surface-container transition-colors">
<div class="w-12 h-12 metallic-gold-gradient rounded-full flex items-center justify-center mx-auto mb-4">
<span class="material-symbols-outlined text-on-primary">newsmode</span>
</div>
<h4 class="font-title-sm text-body-base mb-1">Media Partner</h4>
<p class="text-label-caps text-primary">Apply Now</p>
</div>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-gutter">
<div class="bg-primary/5 border border-primary/20 p-stack-lg rounded-xl flex items-center justify-between">
<div>
<p class="font-label-caps text-primary mb-1">STANDARD BENEFIT</p>
<p class="font-title-sm">Logo on backdrop &amp; LED screens</p>
</div>
<span class="material-symbols-outlined text-primary text-4xl">branding_watermark</span>
</div>
<div class="bg-primary/5 border border-primary/20 p-stack-lg rounded-xl flex items-center justify-between">
<div>
<p class="font-label-caps text-primary mb-1">ACCESS PACK</p>
<p class="font-title-sm">02 VIP Delegate tickets included</p>
</div>
<span class="material-symbols-outlined text-primary text-4xl">confirmation_number</span>
</div>
</div>
</div>
</section>
<!-- Contact Section -->
<section class="py-16 lg:py-32 relative">
<div class="max-w-[1280px] mx-auto px-gutter">
<div class="glass-panel gold-border p-stack-lg lg:p-section-gap rounded-3xl overflow-hidden relative">
<!-- Subtle Glow -->
<div class="absolute -bottom-24 -right-24 w-64 h-64 bg-primary/20 blur-[100px] rounded-full"></div>
<div class="relative z-10 grid grid-cols-1 lg:grid-cols-2 gap-stack-lg items-center">
<div>
<h2 class="font-display-lg text-display-lg mb-6">Partner With Us</h2>
<p class="font-body-base text-body-base text-on-surface-variant/80 mb-8">
Elevate your brand presence in the Indo-Pacific tech corridor. Our summit offers unparalleled access to decision-makers, researchers, and government officials driving the next generation of data center infrastructure and AI development.
</p>
<div class="space-y-6 mb-12">
<div class="flex items-center gap-4">
<div class="w-10 h-10 border border-primary/30 rounded-full flex items-center justify-center">
<span class="material-symbols-outlined text-primary">call</span>
</div>
<div>
<p class="text-label-caps opacity-60">PHONE ENQUIRIES</p>
<p class="font-title-sm">+84 941 523 498</p>
</div>
</div>
<div class="flex items-center gap-4">
<div class="w-10 h-10 border border-primary/30 rounded-full flex items-center justify-center">
<span class="material-symbols-outlined text-primary">mail</span>
</div>
<div>
<p class="text-label-caps opacity-60">DIRECT EMAIL</p>
<p class="font-title-sm">events@techvanguard.vn</p>
</div>
</div>
</div>
<button class="metallic-gold-gradient text-on-primary font-title-sm px-12 py-5 rounded-lg hover:opacity-90 transition-all active:scale-95 flex items-center gap-3">
Contact Us to Sponsor
<span class="material-symbols-outlined">arrow_forward</span>
</button>
</div>
<div class="hidden lg:block">
<div class="relative aspect-square">
<img alt="Sponsorship Meeting" class="w-full h-full object-cover rounded-2xl opacity-60 mix-blend-luminosity" data-alt="A professional business meeting in a high-tech conference room with glass walls overlooking a futuristic skyline at twilight. The lighting is low and sophisticated, featuring warm golden highlights and cool blue shadows. Three executives are discussing a strategic partnership over a digital tablet, reflecting the premium, high-tech aesthetic of the IPv6 Summit. The atmosphere is authoritative, cutting-edge, and elite." src="https://lh3.googleusercontent.com/aida-public/AB6AXuATSk4rhRYAo9_OfX0cJwIC7rUdUmzD9kO7cJM24ECjPHwglU_s0wjBn6gdK0d7tk4ExLvG9DUJQ1L-PbP5ucVH2THO05fev6bWgyvpkElf_94JXJNeWj0Mo4-5-l2ofY-kHE6kPLhitbRzarR7u41favHCt_V9WM0yYb7tcZZQM8eAN5jIVT5OVKSGvfpZNYe05dJm_h3pW0qkWhX1XrUtg_xhhjdvTi-Mei7zHDUkwwRrydg7BHmO17W6ScfALH5rMXojTHub7UDN"/>
<div class="absolute inset-0 ring-1 ring-inset ring-white/10 rounded-2xl"></div>
</div>
</div>
</div>
</div>
</div>
</section>
</main>
<!-- Footer -->
<footer class="w-full mt-section-gap bg-surface-container-lowest border-t border-outline-variant/20">
<div class="max-w-[1280px] mx-auto px-gutter py-stack-lg flex flex-col md:flex-row justify-between items-center gap-stack-md">
<div class="font-display-lg text-title-sm text-on-surface">IPv6 for AI &amp; Data Centre Summit</div>
<div class="flex flex-wrap justify-center gap-stack-lg">
<a class="font-label-caps text-label-caps text-on-tertiary-fixed-variant hover:text-secondary transition-colors" href="#">Contact Us</a>
<a class="font-label-caps text-label-caps text-on-tertiary-fixed-variant hover:text-secondary transition-colors" href="#">Sponsorship Prospectus</a>
<a class="font-label-caps text-label-caps text-on-tertiary-fixed-variant hover:text-secondary transition-colors" href="#">Privacy Policy</a>
<a class="font-label-caps text-label-caps text-on-tertiary-fixed-variant hover:text-secondary transition-colors" href="#">Terms of Service</a>
</div>
<div class="font-label-caps text-label-caps text-on-tertiary-fixed-variant">
© 2026 IPv6 for AI &amp; Data Centre Summit. All rights reserved.
</div>
</div>
</footer>
</body></html>
-419
View File
@@ -1,419 +0,0 @@
<!DOCTYPE html>
<html class="dark" lang="en"><head>
<meta charset="utf-8"/>
<meta content="width=device-width, initial-scale=1.0" name="viewport"/>
<title>IPv6 SUMMIT 2026 | Infrastructure for AI</title>
<script src="https://cdn.tailwindcss.com?plugins=forms,container-queries"></script>
<link href="https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@300;400;500;600;700&amp;family=Inter:wght@300;400;500;600&amp;family=JetBrains+Mono:wght@400;500&amp;display=swap" rel="stylesheet"/>
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&amp;display=swap" rel="stylesheet"/>
<style>
body {
background-color: #0F0F0F;
color: #E0E0E0;
scroll-behavior: smooth;
}
.material-symbols-outlined {
font-variation-settings: 'FILL' 0, 'wght' 400, 'GRAD' 0, 'opsz' 24;
}
.glass-panel {
background: rgba(15, 15, 15, 0.7);
backdrop-filter: blur(8px);
border: 1px solid rgba(197, 160, 89, 0.2);
}
@media (min-width: 1024px) {
.glass-panel {
backdrop-filter: blur(12px);
border: 1px solid rgba(197, 160, 89, 0.3);
}
}
.gold-gradient-bg {
background: linear-gradient(135deg, #C5A059 0%, #8E6D3D 100%);
}
.hero-glow {
background: radial-gradient(circle at 50% 40%, rgba(197, 160, 89, 0.08) 0%, rgba(15, 15, 15, 0) 70%);
}
@media (min-width: 1024px) {
.hero-glow {
background: radial-gradient(circle at 50% 50%, rgba(197, 160, 89, 0.12) 0%, rgba(15, 15, 15, 0) 80%);
}
}
input::placeholder {
color: rgba(224, 224, 224, 0.4);
}
.max-w-hd {
max-width: 1920px;
}
/* Fluid Typography and Utilities */
@layer utilities {
.fluid-hero-title {
font-size: clamp(2.25rem, 5vw + 1rem, 6.875rem); /* 36px to 110px */
}
.fluid-display-lg {
font-size: clamp(2rem, 4vw + 1rem, 4.5rem); /* 32px to 72px */
}
.fluid-body {
font-size: clamp(1rem, 0.5vw + 0.8rem, 1.25rem); /* 16px to 20px */
}
}
/* Mobile Menu Toggle logic */
#mobile-menu.hidden {
display: none;
}
#mobile-menu:not(.hidden) {
display: flex;
}
</style>
<script id="tailwind-config">
tailwind.config = {
darkMode: "class",
theme: {
extend: {
"colors": {
"primary": "#C5A059",
"on-primary": "#261900",
"surface": "#0F0F0F",
"surface-container-low": "#141414",
"surface-container-high": "#1F1F1F",
"surface-container-highest": "#292929",
"on-surface": "#FFFFFF",
"on-surface-variant": "#E0E0E0",
"outline": "#C5A059",
"background": "#0F0F0F",
"secondary": "#8E6D3D"
},
"borderRadius": {
"DEFAULT": "0.125rem",
"lg": "0.25rem",
"xl": "0.5rem",
"full": "0.75rem"
},
"spacing": {
"gutter": "clamp(1rem, 4vw, 3.75rem)",
"margin-safe": "clamp(1.5rem, 5vw, 5rem)",
"stack-lg": "clamp(2rem, 6vw, 4rem)",
"stack-md": "clamp(1rem, 3vw, 2rem)",
"section-gap": "clamp(4rem, 10vw, 11.25rem)",
"stack-sm": "1rem",
"unit": "8px",
"container-max": "1800px"
},
"fontFamily": {
"title-sm": ["Inter"],
"body-base": ["Inter"],
"label-caps": ["Geist"],
"display-lg": ["Space Grotesk"],
"headline-md": ["Space Grotesk"],
"h1": ["Space Grotesk"],
"h2": ["Space Grotesk"],
"hero-title": ["Space Grotesk"],
"stat-number": ["Space Grotesk"]
}
},
},
}
</script>
</head>
<body class="bg-background text-on-surface-variant font-body-base overflow-x-hidden">
<!-- TopAppBar -->
<header class="fixed top-0 w-full z-50 bg-surface/80 backdrop-blur-xl border-b border-white/5">
<div class="flex justify-between items-center px-gutter py-4 lg:py-8 w-full max-w-hd mx-auto">
<div class="font-display-lg text-[24px] lg:text-[36px] text-primary tracking-tighter uppercase font-bold">
IPv6 SUMMIT 2026
</div>
<!-- Desktop Nav -->
<nav class="hidden xl:flex items-center gap-10 2xl:gap-16">
<a class="text-primary border-b-2 border-primary pb-1 font-label-caps text-sm" href="#agenda">Agenda</a>
<a class="text-on-surface-variant font-label-caps text-sm hover:text-primary transition-colors duration-300" href="#speakers">Speakers</a>
<a class="text-on-surface-variant font-label-caps text-sm hover:text-primary transition-colors duration-300" href="#stats">Stats</a>
<a class="text-on-surface-variant font-label-caps text-sm hover:text-primary transition-colors duration-300" href="#venue">Venue</a>
</nav>
<div class="flex items-center gap-4 lg:gap-10">
<span class="hidden sm:block text-on-surface-variant/60 font-label-caps text-xs">EN | VI</span>
<button class="hidden sm:block gold-gradient-bg text-on-primary px-6 lg:px-10 py-3 lg:py-4 font-title-sm text-sm lg:text-[18px] font-bold uppercase tracking-wider rounded-lg active:scale-95 transition-transform shadow-lg shadow-primary/10">
Register
</button>
<!-- Hamburger Menu Button -->
<button class="xl:hidden text-primary p-2" id="menu-toggle">
<span class="material-symbols-outlined text-3xl">menu</span>
</button>
</div>
</div>
<!-- Mobile/Tablet Menu -->
<div class="hidden fixed inset-0 top-[64px] lg:top-[104px] bg-surface z-40 flex-col p-gutter gap-8 xl:hidden border-t border-white/5" id="mobile-menu">
<a class="text-2xl font-h2 text-on-surface" href="#agenda">Agenda</a>
<a class="text-2xl font-h2 text-on-surface" href="#speakers">Speakers</a>
<a class="text-2xl font-h2 text-on-surface" href="#stats">Stats</a>
<a class="text-2xl font-h2 text-on-surface" href="#venue">Venue</a>
<div class="mt-auto flex flex-col gap-6">
<div class="text-on-surface-variant/60 font-label-caps text-sm">Language: EN | VI</div>
<button class="gold-gradient-bg text-on-primary w-full py-5 font-title-sm text-lg font-bold uppercase tracking-wider rounded-lg">
Register Now
</button>
</div>
</div>
</header>
<!-- Hero Section -->
<section class="relative min-h-[90vh] lg:min-h-screen flex items-center pt-24 lg:pt-32 overflow-hidden bg-background hero-glow px-gutter">
<div class="absolute inset-0 z-0">
<div class="w-full h-full" style="background-image: url('https://lh3.googleusercontent.com/aida-public/AB6AXuC3vex0i2x5p-U68-wnSp7g7oJqwmNKU14F1Hbx6ImbAYK2BiIIhqE6LvalueLoqHQXB99RHZCLB4PecJtxz_QW5T2scRvSunIlJP6YGWnkkB8ck6Qge8nhA4Q4POVcCcMSEGEFdYOieKKEkHD_srUsO8op_b8W8423PyF4ANvDGyk3YmJFXEcGiuGlTsBiaQnyYopgEUSzBx3h2z9vgZSIjQIADDiGAQdY65HdYTkQAPMoSzm5ylL393Q9p7JvJZQ5pUOCMx-d3EMG'); background-size: cover; background-position: center; mix-blend-mode: luminosity; opacity: 0.15;"></div>
</div>
<div class="relative z-10 max-w-hd mx-auto w-full">
<div class="max-w-7xl">
<div class="inline-flex items-center gap-3 lg:gap-4 px-4 lg:px-8 py-2 lg:py-3 rounded-full bg-surface-container-high border border-primary/30 mb-8 lg:mb-16">
<span class="material-symbols-outlined text-primary text-[20px] lg:text-[26px]">calendar_today</span>
<span class="font-label-caps text-xs lg:text-[16px] text-on-surface">June 2026 | Ho Chi Minh City, Vietnam</span>
</div>
<h1 class="font-hero-title fluid-hero-title text-on-surface mb-6 lg:mb-10 leading-[1.1]">
IPv6 for AI &amp; Data Centre Summit 2026
</h1>
<p class="text-on-surface-variant text-lg lg:text-[28px] mb-10 lg:mb-20 max-w-4xl leading-relaxed opacity-90">
Unlocking the Potential of Next-Gen Infrastructure. Join global leaders in redefining ASEAN's digital connectivity for the age of AI.
</p>
<div class="flex flex-col sm:flex-row gap-4 lg:gap-10">
<button class="gold-gradient-bg text-on-primary px-8 lg:px-16 py-5 lg:py-7 font-h2 text-sm lg:text-[20px] font-bold uppercase tracking-widest rounded-xl hover:shadow-[0_0_50px_rgba(197,160,89,0.4)] transition-all">
Register / ĐĂNG KÝ
</button>
<button class="bg-surface-container-high border border-primary/40 text-on-surface px-8 lg:px-16 py-5 lg:py-7 font-h2 text-sm lg:text-[20px] font-bold uppercase tracking-widest rounded-xl hover:bg-surface-container-highest transition-all">
View Agenda
</button>
</div>
</div>
</div>
</section>
<!-- Vision Section -->
<section class="py-16 lg:py-32 max-w-hd mx-auto px-gutter">
<div class="grid grid-cols-1 lg:grid-cols-12 gap-12 lg:gap-32 items-start">
<div class="lg:col-span-5">
<span class="font-label-caps text-xs lg:text-sm text-primary mb-6 lg:mb-10 block tracking-[0.25em]">VISION &amp; GOALS</span>
<h2 class="font-h1 fluid-display-lg text-on-surface mb-8 lg:mb-12 leading-tight">ASEAN's Digital Future</h2>
<div class="space-y-6 lg:space-y-10 text-on-surface-variant text-lg lg:text-[22px] leading-relaxed">
<p>
Vietnam and the broader ASEAN region are at a critical juncture in digital transformation. The transition to IPv6 is the fundamental infrastructure for AI growth and massive data centre scaling.
</p>
<p class="opacity-70 italic border-l-2 border-primary/30 pl-6 lg:pl-8">
Hành trình chuyển đổi số của Việt Nam đang bước vào giai đoạn then chốt, nơi IPv6 đóng vai trò là hạ tầng xương sống cho sự phát triển của AI và các trung tâm dữ liệu thế hệ mới.
</p>
</div>
</div>
<div class="lg:col-span-7 grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-6 lg:gap-10">
<div class="glass-panel p-8 lg:p-12 rounded-2xl group transition-all duration-500 hover:lg:-translate-y-4 hover:shadow-2xl hover:shadow-primary/5">
<span class="material-symbols-outlined text-primary text-[40px] lg:text-[56px] mb-8 lg:mb-12 block" data-weight="fill">hub</span>
<h3 class="font-h2 text-xl lg:text-[28px] text-on-surface mb-4 lg:mb-6">Networking</h3>
<p class="text-sm lg:text-[18px] text-on-surface-variant/80 leading-relaxed">Connect with 500+ C-level executives. Kết nối với hơn 500 lãnh đạo cao cấp.</p>
</div>
<div class="glass-panel p-8 lg:p-12 rounded-2xl group transition-all duration-500 hover:lg:-translate-y-4 hover:shadow-2xl hover:shadow-primary/5">
<span class="material-symbols-outlined text-primary text-[40px] lg:text-[56px] mb-8 lg:mb-12 block" data-weight="fill">insights</span>
<h3 class="font-h2 text-xl lg:text-[28px] text-on-surface mb-4 lg:mb-6">Tech Insights</h3>
<p class="text-sm lg:text-[18px] text-on-surface-variant/80 leading-relaxed">Deep dives into AI workloads. Khám phá sâu về hạ tầng AI thế hệ mới.</p>
</div>
<div class="glass-panel p-8 lg:p-12 rounded-2xl group transition-all duration-500 hover:lg:-translate-y-4 hover:shadow-2xl hover:shadow-primary/5 sm:col-span-2 md:col-span-1">
<span class="material-symbols-outlined text-primary text-[40px] lg:text-[56px] mb-8 lg:mb-12 block" data-weight="fill">gavel</span>
<h3 class="font-h2 text-xl lg:text-[28px] text-on-surface mb-4 lg:mb-6">Policy</h3>
<p class="text-sm lg:text-[18px] text-on-surface-variant/80 leading-relaxed">National digital infrastructure standards. Thảo luận về hạ tầng số quốc gia.</p>
</div>
</div>
</div>
</section>
<!-- Stats Section -->
<section class="py-16 lg:py-32 bg-surface-container-low border-y border-primary/10" id="stats">
<div class="max-w-hd mx-auto px-gutter">
<div class="grid grid-cols-1 md:grid-cols-3 gap-12 lg:gap-32 text-center">
<div class="space-y-3 lg:space-y-6">
<div class="font-stat-number text-5xl lg:text-[96px] text-primary">130+</div>
<div>
<div class="font-label-caps text-sm lg:text-[18px] text-on-surface/80 tracking-widest uppercase">Global Speakers</div>
<div class="text-xs lg:text-[18px] text-on-surface-variant/50 mt-1 lg:mt-2">Diễn giả quốc tế</div>
</div>
</div>
<div class="space-y-3 lg:space-y-6 border-y md:border-y-0 md:border-x border-primary/5 py-8 md:py-0">
<div class="font-stat-number text-5xl lg:text-[96px] text-primary">14</div>
<div>
<div class="font-label-caps text-sm lg:text-[18px] text-on-surface/80 tracking-widest uppercase">ASEAN Nations</div>
<div class="text-xs lg:text-[18px] text-on-surface-variant/50 mt-1 lg:mt-2">Quốc gia khu vực</div>
</div>
</div>
<div class="space-y-3 lg:space-y-6">
<div class="font-stat-number text-5xl lg:text-[96px] text-primary">50+</div>
<div>
<div class="font-label-caps text-sm lg:text-[18px] text-on-surface/80 tracking-widest uppercase">Tech Partners</div>
<div class="text-xs lg:text-[18px] text-on-surface-variant/50 mt-1 lg:mt-2">Đối tác công nghệ</div>
</div>
</div>
</div>
</div>
</section>
<!-- Agenda Section -->
<section class="py-16 lg:py-32 max-w-hd mx-auto px-gutter" id="agenda">
<div class="flex flex-col xl:flex-row justify-between xl:items-end mb-12 lg:mb-24 gap-8">
<div class="max-w-3xl">
<span class="font-label-caps text-xs lg:text-sm text-primary block mb-4 lg:mb-8 tracking-[0.25em]">SCHEDULE</span>
<h2 class="font-h1 fluid-display-lg text-on-surface uppercase tracking-tight">Event Agenda / Lịch trình</h2>
</div>
<p class="text-on-surface-variant/60 text-base lg:text-[20px] max-w-md xl:text-right">A strategic overview of the technical and strategic sessions.</p>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-6 lg:gap-10">
<!-- Item 1 -->
<div class="glass-panel p-8 lg:p-12 rounded-3xl relative overflow-hidden group hover:border-primary/60 transition-colors">
<div class="absolute top-0 right-0 p-6 lg:p-8">
<span class="font-stat-number text-2xl lg:text-[40px] text-primary/20 group-hover:text-primary transition-colors">01</span>
</div>
<div class="mb-6 lg:mb-10">
<span class="font-stat-number text-2xl lg:text-[36px] text-primary">08:30</span>
</div>
<h3 class="font-h2 text-2xl lg:text-[32px] text-on-surface mb-4 lg:mb-6">Opening Ceremony</h3>
<p class="text-sm lg:text-[18px] text-on-surface-variant/80 leading-relaxed mb-6 lg:mb-8">Welcome address by MIC Vietnam and regional digital economy leaders.</p>
<div class="w-full h-1 bg-primary/10 group-hover:bg-primary transition-colors duration-500"></div>
</div>
<!-- Item 2 -->
<div class="glass-panel p-8 lg:p-12 rounded-3xl relative overflow-hidden group hover:border-primary/60 transition-colors">
<div class="absolute top-0 right-0 p-6 lg:p-8">
<span class="font-stat-number text-2xl lg:text-[40px] text-primary/20 group-hover:text-primary transition-colors">02</span>
</div>
<div class="mb-6 lg:mb-10">
<span class="font-stat-number text-2xl lg:text-[36px] text-primary">10:00</span>
</div>
<h3 class="font-h2 text-2xl lg:text-[32px] text-on-surface mb-4 lg:mb-6">AI in Data Centres</h3>
<p class="text-sm lg:text-[18px] text-on-surface-variant/80 leading-relaxed mb-6 lg:mb-8">How IPv6 enables hyperscale processing and low-latency fabric.</p>
<div class="w-full h-1 bg-primary/10 group-hover:bg-primary transition-colors duration-500"></div>
</div>
<!-- Item 3 -->
<div class="glass-panel p-8 lg:p-12 rounded-3xl relative overflow-hidden group hover:border-primary/60 transition-colors md:col-span-2 xl:col-span-1">
<div class="absolute top-0 right-0 p-6 lg:p-8">
<span class="font-stat-number text-2xl lg:text-[40px] text-primary/20 group-hover:text-primary transition-colors">03</span>
</div>
<div class="mb-6 lg:mb-10">
<span class="font-stat-number text-2xl lg:text-[36px] text-primary">14:00</span>
</div>
<h3 class="font-h2 text-2xl lg:text-[32px] text-on-surface mb-4 lg:mb-6">Forum: Policy &amp; Scale</h3>
<p class="text-sm lg:text-[18px] text-on-surface-variant/80 leading-relaxed mb-6 lg:mb-8">Closed-door discussion for regulators and industry giants.</p>
<div class="w-full h-1 bg-primary/10 group-hover:bg-primary transition-colors duration-500"></div>
</div>
</div>
</section>
<!-- Stakeholders Section -->
<section class="py-16 lg:py-32 bg-surface">
<div class="max-w-hd mx-auto px-gutter">
<div class="text-center mb-12 lg:mb-24 px-4">
<h2 class="font-h1 fluid-display-lg text-on-surface mb-4 lg:mb-6">Key Stakeholders</h2>
<p class="text-on-surface-variant text-lg lg:text-[24px] opacity-70">Bringing together the architects of the digital age.</p>
</div>
<div class="grid grid-cols-1 md:grid-cols-3 gap-8 lg:gap-24">
<div class="glass-panel p-10 lg:p-16 rounded-3xl space-y-8 lg:space-y-12 flex flex-col items-center text-center">
<h4 class="font-label-caps text-xs lg:text-[16px] text-primary tracking-[0.25em] border-b border-primary/20 pb-6 lg:pb-8 w-full uppercase">Government &amp; Regulators</h4>
<div class="flex flex-wrap justify-center gap-10 lg:gap-16 opacity-70 filter grayscale brightness-200 hover:grayscale-0 hover:brightness-100 transition-all duration-700">
<img alt="MIC Vietnam" class="h-12 lg:h-20 w-full object-contain" src="https://lh3.googleusercontent.com/aida-public/AB6AXuDhNobvtxMHNvZvxGODgUS3c0fA1_MPo_3uA9yeLNQsM3Ikcr8cvGbNsLlDXrv8Svb5lUk3xHIJtGdRuj0xhgA3B3ZV8buxROMSN3D9X0URDGHCU9e29PfXdu4tkgZ6txZHfsYDVyuXue7r567qjg2caBMZgKeZpRCnD_MLXUt_IntqDFz5hpShuDtoNQRDc1TqbIiuIJiJswgzMnx4U5O-mHtysv5TKR8FB3qYQJAJe05NNPtNzBJSILyejgqkWP7FED1E3NCzOKBf"/>
</div>
</div>
<div class="glass-panel p-10 lg:p-16 rounded-3xl space-y-8 lg:space-y-12 flex flex-col items-center text-center">
<h4 class="font-label-caps text-xs lg:text-[16px] text-primary tracking-[0.25em] border-b border-primary/20 pb-6 lg:pb-8 w-full uppercase">Cloud &amp; AI Giants</h4>
<div class="flex flex-wrap justify-center gap-10 lg:gap-16 opacity-70 filter grayscale brightness-200 hover:grayscale-0 hover:brightness-100 transition-all duration-700">
<img alt="Huawei" class="h-10 lg:h-16 w-full object-contain" src="https://lh3.googleusercontent.com/aida-public/AB6AXuBB9Xc7pLgZGRbpA7aSA6rry-Q3ld4zfynxem6bc3P6GbulvOf6wZyKIKwH48RSqkFXt5YBrHuFVSroy4GOdgRdswqKqviZANTcQ42T9ESLTomKfIRyV2tVRKaIlzPE5X-8nmmiXaBCDzyAxJMPpIn6rdiD4eqP-HMssJW_OQSytySjH3h_QKeRgsqfgREVSqE_y9Y-TzGln898m4FPgZ2LcOJ50Q3xj5Vjxlwg4sB9_q6Tqfr1U3ii28WPdxn1sVHsaEmmXq_SmUyh"/>
<img alt="AWS" class="h-8 lg:h-14 w-full object-contain" src="https://lh3.googleusercontent.com/aida-public/AB6AXuATUb5fi5MyPvCIYZR0ZXpfDUTA5sZcF4FkRY-HK4xqGTn2RD02Qr9W12Fb848PWucSQtdVSNqWmnOM1oeXLukRbkvU-angYear_Jvq8V-Rp7QMir-bjLoxAGn5U6tM9iWjr9apViqfBjwxrCiprCoQUzHK6iueA_j-2sNzV7qkc4YOVCKkIKYxMlH9szNkilxZzoN5cxaWbqwwJ-6M0lEsQPWPeZjgsjLjUjaaG-Iy5Foe-GGH-tvw0I5K919IWHg5B1_wgCcSDxWu"/>
</div>
</div>
<div class="glass-panel p-10 lg:p-16 rounded-3xl space-y-8 lg:space-y-12 flex flex-col items-center text-center">
<h4 class="font-label-caps text-xs lg:text-[16px] text-primary tracking-[0.25em] border-b border-primary/20 pb-6 lg:pb-8 w-full uppercase">Telecom Operators</h4>
<div class="flex flex-wrap justify-center gap-10 lg:gap-16 opacity-70 filter grayscale brightness-200 hover:grayscale-0 hover:brightness-100 transition-all duration-700">
<img alt="Viettel" class="h-10 lg:h-16 w-full object-contain" src="https://lh3.googleusercontent.com/aida-public/AB6AXuBeBSGYZD4s-iLE3P1hsH2Fu_uQKkTVIpDS1Pt_pXF4NxDUzLlRbTHuylc1NSbruhZIBnwyK1SnYZmpgH-PGrg-gwyBVdSoFFTsQme0qRWtH9R_gDLCA9MYNjnnoKxe7jqF69IzuB-woS82cRzVeL3hHNeEy2467WKyiz-vbpj3nG3R6T_3bbFXif1zqLQSW7hYDU9Q5IHhBS3Y1CWES0idZSRGgXbDK9vZV5sDaZYDRgYo86KgsaVIttVJIcB-EHZ9TzY3tOJ88pK6"/>
</div>
</div>
</div>
</div>
</section>
<!-- Registration Section -->
<section class="py-16 lg:py-32" id="register">
<div class="max-w-hd mx-auto px-gutter grid grid-cols-1 lg:grid-cols-2 gap-16 lg:gap-32 items-center">
<div>
<h2 class="font-h1 fluid-display-lg text-on-surface mb-8 lg:mb-12 leading-tight">Reserve Your Seat</h2>
<p class="text-xl lg:text-[26px] text-on-surface-variant mb-12 lg:mb-16 leading-relaxed opacity-90">
Limited executive passes available. Registration is subject to confirmation by the organizing committee.
</p>
<div class="space-y-8 lg:space-y-16">
<div class="flex items-start gap-4 lg:gap-8">
<div class="shrink-0 w-12 h-12 lg:w-16 lg:h-16 rounded-full bg-primary/10 flex items-center justify-center">
<span class="material-symbols-outlined text-primary text-[24px] lg:text-[36px]">mail</span>
</div>
<div>
<div class="font-label-caps text-xs lg:text-[16px] text-on-surface uppercase tracking-widest mb-1 lg:mb-2">Email Inquiry</div>
<div class="text-lg lg:text-[22px] text-on-surface-variant/80">contact@ipv6summit2026.vn</div>
</div>
</div>
<div class="flex items-start gap-4 lg:gap-8">
<div class="shrink-0 w-12 h-12 lg:w-16 lg:h-16 rounded-full bg-primary/10 flex items-center justify-center">
<span class="material-symbols-outlined text-primary text-[24px] lg:text-[36px]">location_on</span>
</div>
<div>
<div class="font-label-caps text-xs lg:text-[16px] text-on-surface uppercase tracking-widest mb-1 lg:mb-2">Venue</div>
<div class="text-lg lg:text-[22px] text-on-surface-variant/80">District 1, Ho Chi Minh City</div>
</div>
</div>
</div>
</div>
<div class="glass-panel p-8 lg:p-16 rounded-3xl border border-primary/30 shadow-2xl shadow-primary/5">
<form class="space-y-6 lg:space-y-10">
<div class="grid grid-cols-1 sm:grid-cols-2 gap-6 lg:gap-10">
<div>
<label class="font-label-caps text-xs text-on-surface/60 mb-2 lg:mb-4 block uppercase tracking-widest">Name / Họ tên</label>
<input class="w-full bg-surface-container-low border-white/10 rounded-xl p-4 lg:p-6 text-on-surface focus:border-primary focus:ring-0 transition-all text-base lg:text-[20px]" placeholder="John Doe" type="text"/>
</div>
<div>
<label class="font-label-caps text-xs text-on-surface/60 mb-2 lg:mb-4 block uppercase tracking-widest">Company / Công ty</label>
<input class="w-full bg-surface-container-low border-white/10 rounded-xl p-4 lg:p-6 text-on-surface focus:border-primary focus:ring-0 transition-all text-base lg:text-[20px]" placeholder="Tech Global" type="text"/>
</div>
</div>
<div>
<label class="font-label-caps text-xs text-on-surface/60 mb-2 lg:mb-4 block uppercase tracking-widest">Email / Thư điện tử</label>
<input class="w-full bg-surface-container-low border-white/10 rounded-xl p-4 lg:p-6 text-on-surface focus:border-primary focus:ring-0 transition-all text-base lg:text-[20px]" placeholder="john@company.com" type="email"/>
</div>
<div>
<label class="font-label-caps text-xs text-on-surface/60 mb-2 lg:mb-4 block uppercase tracking-widest">Phone / Số điện thoại</label>
<input class="w-full bg-surface-container-low border-white/10 rounded-xl p-4 lg:p-6 text-on-surface focus:border-primary focus:ring-0 transition-all text-base lg:text-[20px]" placeholder="+84 ..." type="tel"/>
</div>
<button class="w-full gold-gradient-bg text-on-primary py-5 lg:py-8 font-h2 text-lg lg:text-[22px] font-bold uppercase tracking-widest rounded-xl hover:shadow-[0_0_60px_rgba(197,160,89,0.5)] transition-all transform lg:hover:scale-[1.02]" type="submit">
Submit Registration
</button>
</form>
</div>
</div>
</section>
<!-- Footer -->
<footer class="w-full bg-surface-container-low border-t border-white/5 py-12 lg:py-20">
<div class="flex flex-col xl:flex-row justify-between items-center px-gutter gap-12 w-full max-w-hd mx-auto">
<div class="font-label-caps text-xl lg:text-[24px] text-primary tracking-[0.4em] font-bold">
IPv6 SUMMIT 2026
</div>
<div class="flex flex-wrap justify-center gap-8 lg:gap-16">
<a class="text-on-surface-variant font-label-caps text-xs lg:text-sm hover:text-primary transition-colors" href="#">Privacy</a>
<a class="text-on-surface-variant font-label-caps text-xs lg:text-sm hover:text-primary transition-colors" href="#">Terms</a>
<a class="text-on-surface-variant font-label-caps text-xs lg:text-sm hover:text-primary transition-colors" href="#">Press</a>
<a class="text-on-surface-variant font-label-caps text-xs lg:text-sm hover:text-primary transition-colors" href="#">Contact</a>
</div>
<div class="text-[14px] lg:text-[16px] text-on-surface-variant/40 text-center xl:text-right max-w-sm leading-relaxed uppercase tracking-widest">
© 2026 INTERNATIONAL TECH SUMMIT. IPV6 FOR AI EVOLUTION.
</div>
</div>
</footer>
<script>
const menuToggle = document.getElementById('menu-toggle');
const mobileMenu = document.getElementById('mobile-menu');
const menuIcon = menuToggle.querySelector('.material-symbols-outlined');
menuToggle.addEventListener('click', () => {
const isHidden = mobileMenu.classList.toggle('hidden');
menuIcon.textContent = isHidden ? 'menu' : 'close';
document.body.style.overflow = isHidden ? '' : 'hidden';
});
// Close menu when clicking links
mobileMenu.querySelectorAll('a').forEach(link => {
link.addEventListener('click', () => {
mobileMenu.classList.add('hidden');
menuIcon.textContent = 'menu';
document.body.style.overflow = '';
});
});
</script>
</body></html>
+18
View File
@@ -0,0 +1,18 @@
export type SubmissionStatus = "approved" | "pending" | "rejected";
/** One row in the admin requests table (registration or feedback). */
export type AdminRequestRow = {
id: string;
submittedAt: string;
displayDate: string;
fullName: string;
jobTitle: string;
company: string;
segment: string;
email: string;
phone: string;
status: SubmissionStatus;
notes: string;
/** Optional: where the row came from (not shown in table yet). */
source?: "registration" | "feedback";
};