diff --git a/.env.example b/.env.example index c0c68b1..2fc80e3 100644 --- a/.env.example +++ b/.env.example @@ -1 +1 @@ -PORT=3000 \ No newline at end of file +PORT=3000 diff --git a/.gitignore b/.gitignore index 65da6ef..a2c881c 100644 --- a/.gitignore +++ b/.gitignore @@ -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 diff --git a/README.md b/README.md index 1cc3745..d03a4c5 100644 --- a/README.md +++ b/README.md @@ -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 và 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ữ Anh–Việ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 -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 diff --git a/admin.html b/admin.html deleted file mode 100644 index 2b354b2..0000000 --- a/admin.html +++ /dev/null @@ -1,400 +0,0 @@ - - - - - -IPv6 SUMMIT 2026 | Admin Console - - - - - - - - - - - -
- -
-
- -
-
-
-search - -
-
-
- -
-Admin Profile -
-
-
- -
- -
- -
-
-database -
-

Total Requests

-

1,284

-
-+12% vs last week -
-
- -
-

- - Pending Approval -

-

42

-
-Critical threshold: 50 -
-
- -
-

- - Confirmed -

-

856

-
-Capacity at 74% -
-
- -
-

Conversion Rate

-

67%

-
-
-
-
-
- -
-
-
- - -
-
- - -
-
- - -
-
- -
- -
-
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
IDSubmit DateNameCompanyEmail / PhoneStatusActions
#V6-482124 OCT 2025 -
Elena Rodriguez
-
Network Architect
-
Cloudflare, Inc. -
e.rodriguez@cloudflare.com
-
+1 (555) 012-9988
-
- - - Approved - - -
- - -
-
#V6-481923 OCT 2025 -
Marcus Thorne
-
Senior Devops
-
Akamai Technologies -
m.thorne@akamai.io
-
+44 20 7946 0123
-
- - - Pending - - -
- - - -
-
#V6-479022 OCT 2025 -
Kenji Yamamoto
-
CTO
-
NTT Data Japan -
yamamoto@ntt.jp
-
+81 3 1234 5678
-
- - - Rejected - - -
- - -
-
-
- -
-

Showing 1 to 10 of 1,284 entries

-
- - - - - -
-
-
-
- -
-
-IPv6 SUMMIT 2026 -| Systems Infrastructure Core v2.4.0 -
-

© 2026 IPv6 for AI & Data Centre Summit. Infrastructure for the Future.

- -
-
- -
-
- \ No newline at end of file diff --git a/app/(internal)/admin/actions.ts b/app/(internal)/admin/actions.ts new file mode 100644 index 0000000..a853804 --- /dev/null +++ b/app/(internal)/admin/actions.ts @@ -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); + } +} diff --git a/app/(internal)/admin/approved/page.tsx b/app/(internal)/admin/approved/page.tsx new file mode 100644 index 0000000..95c8e47 --- /dev/null +++ b/app/(internal)/admin/approved/page.tsx @@ -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 ; +} diff --git a/app/(internal)/admin/page.tsx b/app/(internal)/admin/page.tsx new file mode 100644 index 0000000..857955e --- /dev/null +++ b/app/(internal)/admin/page.tsx @@ -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 ; +} diff --git a/app/(internal)/admin/pending/page.tsx b/app/(internal)/admin/pending/page.tsx new file mode 100644 index 0000000..b37254f --- /dev/null +++ b/app/(internal)/admin/pending/page.tsx @@ -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 ; +} diff --git a/app/(internal)/admin/rejected/page.tsx b/app/(internal)/admin/rejected/page.tsx new file mode 100644 index 0000000..f69241d --- /dev/null +++ b/app/(internal)/admin/rejected/page.tsx @@ -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 ; +} diff --git a/app/(internal)/layout.tsx b/app/(internal)/layout.tsx new file mode 100644 index 0000000..2676a5b --- /dev/null +++ b/app/(internal)/layout.tsx @@ -0,0 +1,3 @@ +export default function InternalLayout({ children }: { children: React.ReactNode }) { + return children; +} diff --git a/app/(site)/feedback/page.tsx b/app/(site)/feedback/page.tsx new file mode 100644 index 0000000..2d7013a --- /dev/null +++ b/app/(site)/feedback/page.tsx @@ -0,0 +1,5 @@ +import FeedbackPageClient from "@/app/components/forms/FeedbackPageClient"; + +export default function FeedbackPage() { + return ; +} diff --git a/app/(site)/page.tsx b/app/(site)/page.tsx index 1b942a2..ad75f72 100644 --- a/app/(site)/page.tsx +++ b/app/(site)/page.tsx @@ -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 ( -
- - - - -
- ); + return ; } - diff --git a/app/(site)/registration/page.tsx b/app/(site)/registration/page.tsx index a99bd34..d593eb0 100644 --- a/app/(site)/registration/page.tsx +++ b/app/(site)/registration/page.tsx @@ -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 ( -
-
-
-
- - - arrow_back - - Back to Home - -
- -
-

- {data.title} -

-

{data.subtitle}

-
- -
-
- -
-
-
-
- ); + return ; } - diff --git a/app/(site)/sponsor/page.tsx b/app/(site)/sponsor/page.tsx new file mode 100644 index 0000000..75f8d6d --- /dev/null +++ b/app/(site)/sponsor/page.tsx @@ -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 ; +} diff --git a/app/api/submissions/route.ts b/app/api/submissions/route.ts new file mode 100644 index 0000000..5abbb27 --- /dev/null +++ b/app/api/submissions/route.ts @@ -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 = { + 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 }); +} diff --git a/app/components/admin/AdminConsole.tsx b/app/components/admin/AdminConsole.tsx new file mode 100644 index 0000000..8be6a5f --- /dev/null +++ b/app/components/admin/AdminConsole.tsx @@ -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; + 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 ( +
+ {label} + +
+ ); + + return ( +
+ {label} + {isStatus ? ( + + {value} + + ) : ( + + {value} + + )} +
+ ); +} + +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 ( +
+
+ {item.icon} +
+

+ {item.accent === "pending" || item.accent === "confirmed" ? ( + + ) : null} + {item.label} +

+

{item.value}

+ {item.accent === "progress" && typeof item.progressPercent === "number" ? ( +
+
+
+ ) : ( +
+ {item.hint ? ( + + {item.hint} + + ) : null} +
+ )} +
+ ); +} + +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(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 ( +
+ + +
+
+
+

+ {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")} +

+

+ {t.meta.subtitle} +

+
+
+
+ + search + + { + setSearch(e.target.value); + setPage(1); + }} + /> +
+
+
+ {/* Language Switcher — Admin Header */} +
+ + | + +
+
+ + {t.meta.user.role} + +
+
+
+ +
+
+ {liveStats.map((s) => ( + + ))} +
+ +
+
+ {view === "all" ? ( +
+ + +
+ ) : ( +
+ + {t.filters.status.label} + +

+ {t.statusMap[view as StatusKey]?.label ?? view} +

+
+ )} +
+ + +
+
+ + { + setAfterDate(e.target.value); + setPage(1); + }} + /> +
+
+ +
+ +
+
+ + + + + + + + + + + + + + {pageRows.map((row) => { + const sm = t.statusMap[row.status as StatusKey]; + return ( + + + + + + + + + + ); + })} + +
+ {t.table.columns.id} + + {t.table.columns.submitted} + + {t.table.columns.name} + + {t.table.columns.company} + + {t.table.columns.contact} + + {t.table.columns.status} + + {t.table.columns.actions} +
+ #{row.id} + + {row.displayDate} + +
+ {row.fullName} +
+
+ {row.jobTitle} +
+
+ {row.company || "—"} + +
{row.email || "—"}
+
{row.phone || "—"}
+
+ + + {sm.label} + + +
+ + {row.status === "pending" ? ( + <> + + + + + ) : ( + <> + + {row.status === "approved" ? ( + <> + + + + ) : ( + <> + + + + )} + + )} +
+
+
+
+

+ {lang === "vi" + ? `Hiển thị ${showingFrom}–${showingTo} / ${filtered.length} mục` + : `Showing ${showingFrom} to ${showingTo} of ${filtered.length} entries`} +

+
+ + {lang === "vi" ? `Trang ${safePage} / ${totalPages}` : `Page ${safePage} of ${totalPages}`} + + + +
+
+
+
+ +
+
+ IPv6 SUMMIT 2026 + | {t.meta.footerTag} +
+

+ {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."} +

+
+ {t.footer.legal.map((label) => ( + + ))} +
+
+
+ +
+
+ + {detail ? ( +
+ +
+ + {/* Modal Body */} +
+ {/* Primary Info Grid */} +
+
+

+ {lang === "vi" ? "Thông tin cá nhân" : "Identity & Professional"} +

+
+ + + + +
+
+ +
+

+ {lang === "vi" ? "Thông tin liên hệ" : "Contact Details"} +

+
+ + + +
+
+
+ + {/* Form Content / Notes Section */} + {detail.notes && ( +
+

+ {lang === "vi" ? "Nội dung phản hồi" : "Form Submission Content"} +

+
+ {detail.notes.startsWith("Source:") ? ( +
+ {detail.notes.split(/\s(?=[A-Z][a-z\s]+:)/).map((part, idx) => { + const [key, ...val] = part.split(":"); + if (!val.length) return

{part}

; + return ( +
+ + {key.replace("Source:", "").trim()} + + + {val.join(":").trim()} + +
+ ); + })} +
+ ) : ( +

+ {detail.notes} +

+ )} +
+
+ )} +
+ + {/* Modal Footer Actions */} +
+ + {detail.status === "pending" && ( + + )} +
+
+
+ ) : null} +
+ ); +} diff --git a/app/components/forms/FeedbackForm.tsx b/app/components/forms/FeedbackForm.tsx new file mode 100644 index 0000000..d0c5b8b --- /dev/null +++ b/app/components/forms/FeedbackForm.tsx @@ -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 ( +
+ {values.map((n) => ( + + ))} +
+ ); +} + +export default function FeedbackForm({ data }: { data: FeedbackJson }) { + const [form, setForm] = useState>({}); + 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 ( +
+ {data.sections.map((section) => ( +
+
+

+ {section.title} +

+

{section.subtitle}

+
+ +
+ {section.questions.map((q) => ( +
+
+ +
+ + {q.type === "rating" && ( + updateField(q.id, v)} + /> + )} + + {q.type === "dropdown" && ( +
+ + + expand_more + +
+ )} + + {q.type === "scale" && ( +
+ {q.options?.map((opt) => ( + + ))} +
+ )} + + {q.type === "rating_labeled" && ( +
+ {q.options?.map((opt) => ( + + ))} +
+ )} + + {q.type === "checkbox_group" && ( +
+
+ {q.options?.map((opt) => ( + + ))} +
+ {q.hasOther && ( +
+ Others: + updateField(`${q.id}_other`, e.target.value)} + /> +
+ )} +
+ )} + + {q.type === "long_text" && ( + -
-
- - -
-
- - -
-
-

4. INDUSTRY & TECHNICAL INSIGHTS

-
-
- -

Your organization's current IPv6 deployment level

-
-1 -
- - - - - -
-5 -
-
-
- -

What is the biggest challenge for your AI infrastructure?

- -
-
- -
-
-

5. NETWORKING & FUTURE COOPERATION

-
-
- -

Are there any specific speakers or companies you wish to be introduced to?

- -
-
- -

I am interested in:

-
- - - - -
-
-
- -
-
-

6. LOGISTICS FEEDBACK

-
-
- -

Venue Facilities & Location (Ho Chi Minh City)

-
-Poor -
- - - - - -
-Excellent -
-
-
- -

- Thank you for your valuable contribution to the IPv6 ecosystem. -

-
-
-
- - - - \ No newline at end of file diff --git a/ke_hoach_ipv6.md b/ke_hoach_ipv6.md deleted file mode 100644 index 779348f..0000000 --- a/ke_hoach_ipv6.md +++ /dev/null @@ -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 ` - - - - - - -
-
- -
-
-
-IPv6 SUMMIT 2026 -
- -
- - - Register - -
-
-
-
-
-
- - - -
-

Summit Registration

-

Secure your presence at the forefront of IPv6 innovation for AI and Data Centres.

-
- -
- -
-
- -
-

Personal Info / Thông tin cá nhân

-
-
- - -
-
- - -
-
- - -
-
-
-
- -
-

Professional Info 

-
-
- - -
-
- - -
-
-
-
- -
-

Industry

-
- -
- -expand_more -
-
-
-
- -
-

Additional Details 

-
- - -
-
- -
- -

- BY REGISTERING, YOU AGREE TO OUR PRIVACY POLICY AND TERMS OF SERVICE. -

-
-
-
-
-
-
- - - - - \ No newline at end of file diff --git a/scripts/seed-summit-requests.ts b/scripts/seed-summit-requests.ts new file mode 100644 index 0000000..991247a --- /dev/null +++ b/scripts/seed-summit-requests.ts @@ -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); +}); diff --git a/sponsor.html b/sponsor.html deleted file mode 100644 index e6f8ee6..0000000 --- a/sponsor.html +++ /dev/null @@ -1,379 +0,0 @@ - - - - - -Sponsorship | IPv6 Summit 2026 - - - - - - - - - -
-
-
IPv6 Summit 2026
- -
- - -
-
-
-
- -
-
-
-OPPORTUNITY AWAITS -

Become a Sponsor

-

IPv6 for AI & Data Centre Summit 2026 — 18 June 2026, Ho Chi Minh City

-

- 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. -

-
- - -
-
-
- -
-
-
-

Sponsorship Packages & Benefits

- -
-
- -
-
-MOST PRESTIGIOUS -

DIAMOND SPONSOR

-
80,000,000 VND
-
-
    -
  • -check_circle -Logo prominent on all branding materials -
  • -
  • -check_circle -Logo on stage backdrop & LED screens -
  • -
  • -check_circle -Premium exhibition standee space -
  • -
  • -check_circle -1 dedicated speaking slot -
  • -
  • -check_circle -07 VIP Delegate tickets -
  • -
- -
- -
-
-

GOLD SPONSOR

-
50,000,000 VND
-
-
    -
  • -check_circle -Logo on stage backdrop & LED screens -
  • -
  • -check_circle -Exhibition standee placement -
  • -
  • -check_circle -05 VIP Delegate tickets -
  • -
- -
- -
-
-

SILVER SPONSOR

-
40,000,000 VND
-
-
    -
  • -check_circle -Logo on stage backdrop & LED screens -
  • -
  • -check_circle -Standard exhibition standee -
  • -
  • -check_circle -04 VIP Delegate tickets -
  • -
- -
- -
-
-

PARTNER SPONSOR

-
20,000,000 VND
-
-
    -
  • -check_circle -Logo on official website & event communications -
  • -
  • -check_circle -02 VIP Delegate tickets -
  • -
- -
-
-
-
- -
-
-
-

Item-Based Sponsorship

-

Targeted branding opportunities for specific summit pillars

-
-
-
-
-hotel -
-

Hotel Accommodation

-

Limited Space

-
-
-
-meeting_room -
-

Conference Hall

-

Sold Out

-
-
-
-groups -
-

Networking Zone

-

Available

-
-
-
-newsmode -
-

Media Partner

-

Apply Now

-
-
-
-
-
-

STANDARD BENEFIT

-

Logo on backdrop & LED screens

-
-branding_watermark -
-
-
-

ACCESS PACK

-

02 VIP Delegate tickets included

-
-confirmation_number -
-
-
-
- -
-
-
- -
-
-
-

Partner With Us

-

- 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. -

-
-
-
-call -
-
-

PHONE ENQUIRIES

-

+84 941 523 498

-
-
-
-
-mail -
-
-

DIRECT EMAIL

-

events@techvanguard.vn

-
-
-
- -
- -
-
-
-
-
- - - \ No newline at end of file diff --git a/test.html b/test.html deleted file mode 100644 index 1db1be4..0000000 --- a/test.html +++ /dev/null @@ -1,419 +0,0 @@ - - - - - -IPv6 SUMMIT 2026 | Infrastructure for AI - - - - - - - - -
-
-
- IPv6 SUMMIT 2026 -
- - -
- - - - -
-
- - -
- -
-
-
-
-
-
-
-calendar_today -June 2026 | Ho Chi Minh City, Vietnam -
-

- IPv6 for AI & Data Centre Summit 2026 -

-

- Unlocking the Potential of Next-Gen Infrastructure. Join global leaders in redefining ASEAN's digital connectivity for the age of AI. -

-
- - -
-
-
-
- -
-
-
-VISION & GOALS -

ASEAN's Digital Future

-
-

- 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. -

-

- 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. -

-
-
-
-
-hub -

Networking

-

Connect with 500+ C-level executives. Kết nối với hơn 500 lãnh đạo cao cấp.

-
-
-insights -

Tech Insights

-

Deep dives into AI workloads. Khám phá sâu về hạ tầng AI thế hệ mới.

-
-
-gavel -

Policy

-

National digital infrastructure standards. Thảo luận về hạ tầng số quốc gia.

-
-
-
-
- -
-
-
-
-
130+
-
-
Global Speakers
-
Diễn giả quốc tế
-
-
-
-
14
-
-
ASEAN Nations
-
Quốc gia khu vực
-
-
-
-
50+
-
-
Tech Partners
-
Đối tác công nghệ
-
-
-
-
-
- -
-
-
-SCHEDULE -

Event Agenda / Lịch trình

-
-

A strategic overview of the technical and strategic sessions.

-
-
- -
-
-01 -
-
-08:30 -
-

Opening Ceremony

-

Welcome address by MIC Vietnam and regional digital economy leaders.

-
-
- -
-
-02 -
-
-10:00 -
-

AI in Data Centres

-

How IPv6 enables hyperscale processing and low-latency fabric.

-
-
- -
-
-03 -
-
-14:00 -
-

Forum: Policy & Scale

-

Closed-door discussion for regulators and industry giants.

-
-
-
-
- -
-
-
-

Key Stakeholders

-

Bringing together the architects of the digital age.

-
-
-
-

Government & Regulators

-
-MIC Vietnam -
-
-
-

Cloud & AI Giants

-
-Huawei -AWS -
-
-
-

Telecom Operators

-
-Viettel -
-
-
-
-
- -
-
-
-

Reserve Your Seat

-

- Limited executive passes available. Registration is subject to confirmation by the organizing committee. -

-
-
-
-mail -
-
-
Email Inquiry
-
contact@ipv6summit2026.vn
-
-
-
-
-location_on -
-
-
Venue
-
District 1, Ho Chi Minh City
-
-
-
-
-
-
-
-
- - -
-
- - -
-
-
- - -
-
- - -
- -
-
-
-
- -
-
-
- IPv6 SUMMIT 2026 -
- -
- © 2026 INTERNATIONAL TECH SUMMIT. IPV6 FOR AI EVOLUTION. -
-
-
- - \ No newline at end of file diff --git a/types/admin-submission.ts b/types/admin-submission.ts new file mode 100644 index 0000000..ee49f0d --- /dev/null +++ b/types/admin-submission.ts @@ -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"; +};