commit 6a48d6351c49714dade965fefcf2d75879c7c075 Author: DuongNguyendev Date: Mon May 11 16:24:14 2026 +0700 init diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..65da6ef --- /dev/null +++ b/.gitignore @@ -0,0 +1,50 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +/node_modules +/.pnp +.pnp.* +.yarn/* +!.yarn/patches +!.yarn/plugins +!.yarn/releases +!.yarn/versions + +# testing +/coverage + +# next.js +/.next/ +/out/ + +# production +/build + +# misc +.DS_Store +*.pem + +# debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* +.pnpm-debug.log* + +# env files (can opt-in for committing if needed) +.env + +# vercel +.vercel + +# typescript +*.tsbuildinfo +next-env.d.ts + +#vs code +/.vscode +package-lock.json +standalone-deploy.tar.gz + +#ai-agent +/.kiro/ +AGENTS.md diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..e69de29 diff --git a/README.md b/README.md new file mode 100644 index 0000000..1cc3745 --- /dev/null +++ b/README.md @@ -0,0 +1,218 @@ +# VisaService - Website Dịch Vụ Visa + +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%. + +## 🚀 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 + +## 🛠️ Công Nghệ Sử Dụng + +- **Framework**: Next.js 15 (App Router) +- **Language**: TypeScript +- **Styling**: Tailwind CSS +- **Icons**: Emoji icons +- **Deployment**: Docker support + +## 📁 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 +``` + +## 🚀 Hướng Dẫn Chạy Dự Án + +### Yêu Cầu Hệ Thống + +- Node.js 18+ +- npm, yarn, hoặc pnpm + +### Cài Đặt và Chạy + +1. **Clone repository** + +```bash +git clone +cd visa-service-website +``` + +2. **Cài đặt dependencies** + +```bash +npm install +# hoặc +yarn install +# hoặc +pnpm install +``` + +3. **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 + +### Scripts Có Sẵn + +```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 +``` + +## 🐳 Chạy Với Docker + +### Development với Docker + +```bash +# Build và chạy container +docker-compose up --build + +# Chạy ở background +docker-compose up -d + +# Dừng container +docker-compose down +``` + +### Production với Docker + +```bash +# Build production image +docker build -t visa-service . + +# Chạy container +docker run -p 3000:3000 visa-service +``` + +## 📝 Tùy Chỉnh Nội Dung + +### Cập Nhật Dữ Liệu + +Mỗi trang có file JSON riêng chứa dữ liệu: + +- `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 + +### Thêm Trang Mới + +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 + +- Sử dụng Tailwind CSS classes +- Tùy chỉnh theme trong `tailwind.config.js` +- Global styles trong `app/globals.css` + +## 🌐 Deployment + +### Vercel (Khuyến nghị) + +1. Push code lên GitHub +2. Kết nối repository với Vercel +3. Deploy tự động + +### Netlify + +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 +``` + +## 🤝 Đó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) + +--- + +**Phát triển bởi**: VisaService Team +**Phiên bản**: 1.0.0 +**Cập nhật**: January 2025 diff --git a/admin.html b/admin.html new file mode 100644 index 0000000..2b354b2 --- /dev/null +++ b/admin.html @@ -0,0 +1,400 @@ + + + + + +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/api/blogsApi.ts b/api/blogsApi.ts new file mode 100644 index 0000000..5c818ae --- /dev/null +++ b/api/blogsApi.ts @@ -0,0 +1,323 @@ +import { + BlogListResponse, + BlogDetailResponse, + BlogFeaturedResponse, + BlogRecentResponse, + CategoryListResponse, + CategoryDetailResponse, + TagListResponse, + TagDetailResponse, + BlogQueryParams, +} from '../types/blog'; + +/** + * Lấy API URL từ environment variable + */ +const getApiUrl = (): string | null => { + const apiUrl = process.env.NEXT_PUBLIC_API_URL; + // Nếu chưa cấu hình API URL, trả về null để skip việc gọi API (tránh lỗi fetch failed) + if (!apiUrl) { + return null; + } + return apiUrl; +}; + +/** + * Fetch blog list từ API, trả về null nếu thất bại (để caller fallback sang local JSON) + */ +export const fetchBlogList = async ( + params?: BlogQueryParams +): Promise => { + const apiUrl = getApiUrl(); + if (!apiUrl) return null; + + const queryParams = new URLSearchParams(); + + if (params?.page) queryParams.append('page', params.page.toString()); + if (params?.limit) queryParams.append('limit', params.limit.toString()); + if (params?.category) queryParams.append('category', params.category); + if (params?.tag) queryParams.append('tag', params.tag); + if (params?.search) queryParams.append('search', params.search); + + const queryString = queryParams.toString(); + const url = `${apiUrl}/api/blog${queryString ? `?${queryString}` : ''}`; + + try { + const response = await fetch(url, { + method: 'GET', + headers: { 'Content-Type': 'application/json' }, + cache: 'no-store', + }); + + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + + const data: BlogListResponse = await response.json(); + return data; + } catch (error) { + return null; + } +}; + +/** + * Fetch blog detail by slug từ API, trả về null nếu thất bại + */ +export const fetchBlogDetail = async ( + slug: string +): Promise => { + const apiUrl = getApiUrl(); + if (!apiUrl) return null; + + const url = `${apiUrl}/api/blog/${slug}`; + + try { + const response = await fetch(url, { + method: 'GET', + headers: { 'Content-Type': 'application/json' }, + cache: 'no-store', + }); + + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + + const data: BlogDetailResponse = await response.json(); + return data; + } catch (error) { + return null; + } +}; + +/** + * Fetch featured blogs từ API, trả về null nếu thất bại + */ +export const fetchFeaturedBlogs = async ( + limit: number = 5 +): Promise => { + const apiUrl = getApiUrl(); + if (!apiUrl) return null; + + const url = `${apiUrl}/api/blog/featured?limit=${limit}`; + + try { + const response = await fetch(url, { + method: 'GET', + headers: { 'Content-Type': 'application/json' }, + cache: 'no-store', + }); + + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + + const data: BlogFeaturedResponse = await response.json(); + return data; + } catch (error) { + return null; + } +}; + +/** + * Fetch recent blogs từ API, trả về null nếu thất bại + */ +export const fetchRecentBlogs = async ( + limit: number = 5 +): Promise => { + const apiUrl = getApiUrl(); + if (!apiUrl) return null; + + const url = `${apiUrl}/api/blog/recent?limit=${limit}`; + + try { + const response = await fetch(url, { + method: 'GET', + headers: { 'Content-Type': 'application/json' }, + cache: 'no-store', + }); + + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + + const data: BlogRecentResponse = await response.json(); + return data; + } catch (error) { + return null; + } +}; + +/** + * Fetch categories list từ API, trả về null nếu thất bại + */ +export const fetchCategories = async (): Promise => { + const apiUrl = getApiUrl(); + if (!apiUrl) return null; + + const url = `${apiUrl}/api/blog/categories`; + + try { + const response = await fetch(url, { + method: 'GET', + headers: { 'Content-Type': 'application/json' }, + cache: 'no-store', + }); + + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + + const data: CategoryListResponse = await response.json(); + return data; + } catch (error) { + return null; + } +}; + +/** + * Fetch category detail by slug từ API, trả về null nếu thất bại + */ +export const fetchCategoryDetail = async ( + slug: string +): Promise => { + const apiUrl = getApiUrl(); + if (!apiUrl) return null; + + const url = `${apiUrl}/api/blog/categories/${slug}`; + + try { + const response = await fetch(url, { + method: 'GET', + headers: { 'Content-Type': 'application/json' }, + cache: 'no-store', + }); + + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + + const data: CategoryDetailResponse = await response.json(); + return data; + } catch (error) { + return null; + } +}; + +/** + * Fetch tags list từ API, trả về null nếu thất bại + */ +export const fetchTags = async (): Promise => { + const apiUrl = getApiUrl(); + if (!apiUrl) return null; + + const url = `${apiUrl}/api/blog/tags`; + + try { + const response = await fetch(url, { + method: 'GET', + headers: { 'Content-Type': 'application/json' }, + cache: 'no-store', + }); + + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + + const data: TagListResponse = await response.json(); + return data; + } catch (error) { + return null; + } +}; + +/** + * Fetch popular tags từ API, trả về null nếu thất bại + */ +export const fetchPopularTags = async ( + limit: number = 10 +): Promise => { + const apiUrl = getApiUrl(); + if (!apiUrl) return null; + + const url = `${apiUrl}/api/blog/tags/popular?limit=${limit}`; + + try { + const response = await fetch(url, { + method: 'GET', + headers: { 'Content-Type': 'application/json' }, + cache: 'no-store', + }); + + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + + const data: TagListResponse = await response.json(); + return data; + } catch (error) { + return null; + } +}; + +/** + * Fetch tag detail by slug từ API, trả về null nếu thất bại + */ +export const fetchTagDetail = async ( + slug: string +): Promise => { + const apiUrl = getApiUrl(); + if (!apiUrl) return null; + + const url = `${apiUrl}/api/blog/tags/${slug}`; + + try { + const response = await fetch(url, { + method: 'GET', + headers: { 'Content-Type': 'application/json' }, + cache: 'no-store', + }); + + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + + const data: TagDetailResponse = await response.json(); + return data; + } catch (error) { + return null; + } +}; + +/** + * Fetch blogs by category từ API, trả về null nếu thất bại + */ +export const fetchBlogsByCategory = async ( + categorySlug: string, + params?: Omit +): Promise => { + const apiUrl = getApiUrl(); + if (!apiUrl) return null; + + const categoryResponse = await fetchCategoryDetail(categorySlug); + if (!categoryResponse) return null; + + const categoryName = categoryResponse.data.name; + return fetchBlogList({ ...params, category: categoryName }); +}; + +/** + * Fetch blogs by tag từ API, trả về null nếu thất bại + */ +export const fetchBlogsByTag = async ( + tagSlug: string, + params?: Omit +): Promise => { + const apiUrl = getApiUrl(); + if (!apiUrl) return null; + + const tagResponse = await fetchTagDetail(tagSlug); + if (!tagResponse) return null; + + const tagName = tagResponse.data.name; + return fetchBlogList({ ...params, tag: tagName }); +}; diff --git a/api/contentPagesApi.ts b/api/contentPagesApi.ts new file mode 100644 index 0000000..684a797 --- /dev/null +++ b/api/contentPagesApi.ts @@ -0,0 +1,12 @@ +import axiosInstance from "../lib/axios"; + +export const fetchContentPageData = async ( + endpoint: string, +): Promise => { + try { + const response = await axiosInstance.get(endpoint); + return response.data; + } catch { + return null; + } +}; diff --git a/api/headerApi.ts b/api/headerApi.ts new file mode 100644 index 0000000..37b600a --- /dev/null +++ b/api/headerApi.ts @@ -0,0 +1,39 @@ +import { HeaderData, HeaderMenuItem } from '@/types/header'; + +const CMS_API_URL = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:3001'; + +export async function fetchHeaderData(): Promise { + const res = await fetch(`${CMS_API_URL}/api/header`, { + next: { revalidate: 60 }, + }); + + if (!res.ok) { + throw new Error(`Failed to fetch header: ${res.status}`); + } + + const json = await res.json(); + + if (!json.success) { + throw new Error(json.message || 'Header API error'); + } + + return json.data as HeaderData; +} + +export async function fetchHeaderMenu(): Promise { + const res = await fetch(`${CMS_API_URL}/api/header-menu`, { + next: { revalidate: 60 }, + }); + + if (!res.ok) { + throw new Error(`Failed to fetch menu: ${res.status}`); + } + + const json = await res.json(); + + if (!json.success) { + throw new Error(json.message || 'Menu API error'); + } + + return json.data as HeaderMenuItem[]; +} diff --git a/api/homeApi.ts b/api/homeApi.ts new file mode 100644 index 0000000..c913c04 --- /dev/null +++ b/api/homeApi.ts @@ -0,0 +1,43 @@ +/** + * Lấy API URL từ environment variable + */ +const getApiUrl = (): string | null => { + const apiUrl = process.env.NEXT_PUBLIC_API_URL; + + if (!apiUrl) { + return null; + } + + return apiUrl; +}; + +/** + * Fetch home page data từ API + * @returns Promise + * @throws Error nếu fetch thất bại + */ +export const fetchHomeData = async (): Promise => { + const apiUrl = getApiUrl(); + if (!apiUrl) return null; + + const url = `${apiUrl}/api/home`; + + try { + const response = await fetch(url, { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + }, + cache: 'no-store', + }); + + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + + const data = await response.json(); + return data; + } catch (error) { + return null; // Trả về null để fallback sang dữ liệu local + } +}; diff --git a/api/newsletterApi.ts b/api/newsletterApi.ts new file mode 100644 index 0000000..cea7604 --- /dev/null +++ b/api/newsletterApi.ts @@ -0,0 +1,12 @@ +import axiosInstance from "@/lib/axios"; + +export interface NewsletterSubscribePayload { + email: string; + pageUrl?: string; +} + +export async function subscribeNewsletter( + payload: NewsletterSubscribePayload, +): Promise { + await axiosInstance.post("/api/newsletter/subscribe", payload); +} diff --git a/api/programmesApi.ts b/api/programmesApi.ts new file mode 100644 index 0000000..ee11b9f --- /dev/null +++ b/api/programmesApi.ts @@ -0,0 +1,101 @@ +import { FiltersData, Programme } from '../types/programme'; + +/** + * Lấy API URL từ environment variable + */ +const getApiUrl = (): string | null => { + const apiUrl = process.env.NEXT_PUBLIC_API_URL; + if (!apiUrl) return null; + return apiUrl; +}; + +/** + * Fetch tất cả programmes từ API, trả về null nếu thất bại + */ +export const fetchProgrammes = async (): Promise => { + const apiUrl = getApiUrl(); + if (!apiUrl) return null; + + const url = `${apiUrl}/api/programmes`; + + try { + const response = await fetch(url, { + method: 'GET', + headers: { 'Content-Type': 'application/json' }, + cache: 'no-store', + }); + + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + + const data: Programme[] = await response.json(); + return data; + } catch { + return null; + } +}; + +/** + * Fetch một programme theo id/slug, trả về null nếu thất bại + */ +export const fetchProgrammeById = async (id: string): Promise => { + const apiUrl = getApiUrl(); + if (!apiUrl) return null; + + const url = `${apiUrl}/api/programmes/${id}`; + + try { + const response = await fetch(url, { + method: 'GET', + headers: { 'Content-Type': 'application/json' }, + cache: 'no-store', + }); + + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + + const data: Programme = await response.json(); + return data; + } catch { + return null; + } +}; + +/** + * Fetch những programmes được đánh dấu featured (selected: true) + * trả về null nếu thất bại + */ +export const fetchFeaturedProgrammes = async (): Promise => { + const data = await fetchProgrammes(); + if (!data) return null; + return data.filter((p) => p.selected === true); +}; + +/** + * Fetch programme filter options from CMS. + */ +export const fetchProgrammeFilters = async (): Promise => { + const apiUrl = getApiUrl(); + if (!apiUrl) return null; + + const url = `${apiUrl}/api/programmes/filters`; + + try { + const response = await fetch(url, { + method: 'GET', + headers: { 'Content-Type': 'application/json' }, + cache: 'no-store', + }); + + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + + const data: FiltersData = await response.json(); + return data; + } catch { + return null; + } +}; diff --git a/api/submissionsApi.ts b/api/submissionsApi.ts new file mode 100644 index 0000000..73e3547 --- /dev/null +++ b/api/submissionsApi.ts @@ -0,0 +1,13 @@ +import axiosInstance from "@/lib/axios"; + +export type SubmissionSource = "home" | "request" | "contact" | "partnership"; + +export interface SubmitFormPayload { + source: SubmissionSource; + pageUrl: string; + payload: Record; +} + +export async function submitForm(payload: SubmitFormPayload): Promise { + await axiosInstance.post("/api/submissions", payload); +} diff --git a/app/(site)/layout.tsx b/app/(site)/layout.tsx new file mode 100644 index 0000000..67ab335 --- /dev/null +++ b/app/(site)/layout.tsx @@ -0,0 +1,13 @@ +import Header from "@/app/components/layout/Header/Header"; +import Footer from "@/app/components/layout/Footer/Footer"; + +export default function SiteLayout({ children }: { children: React.ReactNode }) { + return ( + <> +
+ {children} +