From 6a48d6351c49714dade965fefcf2d75879c7c075 Mon Sep 17 00:00:00 2001 From: DuongNguyendev Date: Mon, 11 May 2026 16:24:14 +0700 Subject: [PATCH] init --- .gitignore | 50 + Dockerfile | 0 README.md | 218 + admin.html | 400 + api/blogsApi.ts | 323 + api/contentPagesApi.ts | 12 + api/headerApi.ts | 39 + api/homeApi.ts | 43 + api/newsletterApi.ts | 12 + api/programmesApi.ts | 101 + api/submissionsApi.ts | 13 + app/(site)/layout.tsx | 13 + app/(site)/page.tsx | 17 + app/(site)/registration/page.tsx | 38 + app/components/forms/RegistrationForm.tsx | 167 + app/components/home/AgendaSection.tsx | 57 + app/components/home/HeroSection.tsx | 52 + app/components/home/StatsSection.tsx | 28 + app/components/home/VisionSection.tsx | 49 + app/components/layout/Footer/Footer.tsx | 28 + app/components/layout/Header/Header.tsx | 6 + app/components/layout/Header/HeaderClient.tsx | 113 + app/data/home.json | 70 + app/data/registration.json | 27 + app/data/site.json | 22 + app/globals.css | 84 + app/home.json | 130 + app/layout.tsx | 44 + app/not-found.tsx | 33 + app/utils/editorjsToHtml.ts | 146 + app/utils/image.ts | 13 + docker-compose.yml | 0 docker-entrypoint.sh | 0 eslint.config.mjs | 18 + feedback.html | 357 + ke_hoach_ipv6.md | 290 + lib/axios.ts | 20 + next.config.ts | 29 + package.json | 30 + postcss.config.mjs | 7 + public/_images/default.jpg | Bin 0 -> 2220 bytes public/assets/css/all.min.css | 6 + public/assets/css/animate.css | 2745 ++++ public/assets/css/bootstrap.min.css | 12633 ++++++++++++++++ public/assets/css/color.css | 0 public/assets/css/icomon.css | 94 + public/assets/css/magnific-popup.css | 351 + public/assets/css/main.css | 8717 +++++++++++ public/assets/css/main.css.map | 1 + public/assets/css/meanmenu.css | 148 + public/assets/css/nice-select.css | 144 + public/assets/css/odometer.css | 100 + public/assets/css/swiper-bundle.min.css | 13 + public/assets/img/accreditations/QAHE.png | Bin 0 -> 30029 bytes public/assets/img/accreditations/head.png | Bin 0 -> 12481 bytes public/assets/img/accreditations/ico.png | Bin 0 -> 16800 bytes public/assets/img/accreditations/ukrlp.png | Bin 0 -> 48588 bytes public/assets/img/blog/OnlineMeeting.png | Bin 0 -> 43298 bytes public/assets/img/blog/StudentStudying.png | Bin 0 -> 39113 bytes public/assets/img/blog/UniversityCampus.png | Bin 0 -> 40177 bytes .../img/blog/blog-details/OnlineMeeting.png | Bin 0 -> 43298 bytes .../img/blog/blog-details/StudentStudying.png | Bin 0 -> 39113 bytes .../blog/blog-details/UniversityCampus.png | Bin 0 -> 618738 bytes public/assets/img/close.png | Bin 0 -> 232 bytes public/assets/img/favicon.png | Bin 0 -> 174569 bytes public/assets/img/header/home-1.jpg | Bin 0 -> 1405 bytes public/assets/img/header/home-2.jpg | Bin 0 -> 1405 bytes public/assets/img/header/home-3.jpg | Bin 0 -> 1405 bytes public/assets/img/hero.png | Bin 0 -> 1262359 bytes public/assets/img/history/2023.png | Bin 0 -> 202182 bytes public/assets/img/history/2024.png | Bin 0 -> 201813 bytes public/assets/img/history/2025.png | Bin 0 -> 169529 bytes public/assets/img/history/2026.png | Bin 0 -> 199718 bytes public/assets/img/home-1/about/Vector.png | Bin 0 -> 15182 bytes public/assets/img/home-1/about/about-02.jpg | Bin 0 -> 2603 bytes public/assets/img/home-1/about/about-1.jpg | Bin 0 -> 2353 bytes public/assets/img/home-1/about/globe.png | Bin 0 -> 14711 bytes public/assets/img/home-1/about/plane.png | Bin 0 -> 22727 bytes public/assets/img/home-1/about/shape.png | Bin 0 -> 11118 bytes public/assets/img/home-1/brand/01.png | Bin 0 -> 3095 bytes public/assets/img/home-1/brand/02.png | Bin 0 -> 3396 bytes public/assets/img/home-1/brand/03.png | Bin 0 -> 3232 bytes public/assets/img/home-1/brand/04.png | Bin 0 -> 2610 bytes public/assets/img/home-1/brand/05.png | Bin 0 -> 2239 bytes public/assets/img/home-1/feature/Vector.png | Bin 0 -> 11266 bytes public/assets/img/home-1/feature/bg-2.jpg | Bin 0 -> 294404 bytes public/assets/img/home-1/feature/bg.png | Bin 0 -> 845644 bytes public/assets/img/home-1/feature/icon-1.png | Bin 0 -> 3403 bytes public/assets/img/home-1/feature/icon-2.png | Bin 0 -> 3147 bytes public/assets/img/home-1/feature/icon-3.png | Bin 0 -> 2693 bytes public/assets/img/home-1/feature/icon-4.png | Bin 0 -> 2309 bytes public/assets/img/home-1/feature/shape-2.png | Bin 0 -> 1477 bytes public/assets/img/home-1/feature/shape.png | Bin 0 -> 58494 bytes public/assets/img/home-1/feature/text-2.png | Bin 0 -> 8978 bytes public/assets/img/home-1/feature/text.png | Bin 0 -> 103045 bytes public/assets/img/home-1/feature/video-bg.jpg | Bin 0 -> 16196 bytes public/assets/img/home-1/footer-bg.jpg | Bin 0 -> 191388 bytes public/assets/img/home-1/hero/bg.jpg | Bin 0 -> 400378 bytes public/assets/img/home-1/hero/man.png | Bin 0 -> 7636 bytes public/assets/img/home-1/hero/sape-2.png | Bin 0 -> 1904 bytes public/assets/img/home-1/hero/shape-3.png | Bin 0 -> 11838 bytes public/assets/img/home-1/hero/shape-4.png | Bin 0 -> 2066 bytes public/assets/img/home-1/hero/shape.png | Bin 0 -> 20164 bytes public/assets/img/home-1/hover-bg.jpg | Bin 0 -> 2042 bytes public/assets/img/home-1/icon/01.svg | 10 + public/assets/img/home-1/news/client.png | Bin 0 -> 227 bytes public/assets/img/home-1/news/news-1.jpg | Bin 0 -> 2501 bytes public/assets/img/home-1/news/news-10.jpg | Bin 0 -> 2501 bytes public/assets/img/home-1/news/news-11.jpg | Bin 0 -> 2655 bytes public/assets/img/home-1/news/news-12.jpg | Bin 0 -> 2501 bytes public/assets/img/home-1/news/news-13.jpg | Bin 0 -> 6741 bytes public/assets/img/home-1/news/news-14.jpg | Bin 0 -> 6741 bytes public/assets/img/home-1/news/news-15.jpg | Bin 0 -> 6741 bytes public/assets/img/home-1/news/news-2.jpg | Bin 0 -> 2501 bytes public/assets/img/home-1/news/news-3.jpg | Bin 0 -> 2501 bytes public/assets/img/home-1/news/news-4.jpg | Bin 0 -> 2501 bytes public/assets/img/home-1/news/news-5.jpg | Bin 0 -> 2655 bytes public/assets/img/home-1/news/news-6.jpg | Bin 0 -> 2501 bytes public/assets/img/home-1/news/news-7.jpg | Bin 0 -> 2501 bytes public/assets/img/home-1/news/news-8.jpg | Bin 0 -> 2655 bytes public/assets/img/home-1/news/news-9.jpg | Bin 0 -> 2501 bytes public/assets/img/home-1/testimonial/01.jpg | Bin 0 -> 3564 bytes .../img/home-1/testimonial/client-2.png | Bin 0 -> 329 bytes .../assets/img/home-1/testimonial/client.png | Bin 0 -> 329 bytes public/assets/img/home-2/Vector.png | Bin 0 -> 15166 bytes public/assets/img/home-2/bg.jpg | Bin 0 -> 17783 bytes public/assets/img/home-2/feature/01.png | Bin 0 -> 4273 bytes public/assets/img/home-2/feature/02.png | Bin 0 -> 5800 bytes public/assets/img/home-2/feature/03.png | Bin 0 -> 5045 bytes public/assets/img/home-2/feature/Icon.png | Bin 0 -> 7990 bytes public/assets/img/home-2/feature/Years.png | Bin 0 -> 1285 bytes public/assets/img/home-2/feature/bg-shape.png | Bin 0 -> 68458 bytes public/assets/img/home-2/hero/hero-bg.jpg | Bin 0 -> 17771 bytes public/assets/img/home-2/hero/hero.png | Bin 0 -> 9462 bytes public/assets/img/home-2/hero/shape.png | Bin 0 -> 17345 bytes public/assets/img/home-2/icon/01.png | Bin 0 -> 1252 bytes public/assets/img/home-2/icon/02.svg | 4 + public/assets/img/home-2/news/01.jpg | Bin 0 -> 1874 bytes public/assets/img/home-2/news/02.jpg | Bin 0 -> 1874 bytes public/assets/img/home-2/news/03.jpg | Bin 0 -> 1874 bytes public/assets/img/home-2/service.jpg | Bin 0 -> 2071 bytes public/assets/img/home-2/testimonial/01.jpg | Bin 0 -> 3256 bytes .../img/home-2/testimonial/client-1.png | Bin 0 -> 377 bytes .../img/home-2/testimonial/client-2.png | Bin 0 -> 424 bytes .../img/home-2/testimonial/client-3.jpg | Bin 0 -> 424 bytes .../img/home-2/testimonial/client-4.jpg | Bin 0 -> 424 bytes public/assets/img/home-2/visa/01.png | Bin 0 -> 3785 bytes public/assets/img/home-2/visa/02.png | Bin 0 -> 3591 bytes public/assets/img/home-2/visa/03.png | Bin 0 -> 1265 bytes public/assets/img/home-2/visa/04.png | Bin 0 -> 5498 bytes public/assets/img/home-2/visa/05.png | Bin 0 -> 10256 bytes public/assets/img/home-2/visa/06.png | Bin 0 -> 8280 bytes public/assets/img/home-2/visa/07.png | Bin 0 -> 6156 bytes public/assets/img/home-2/visa/08.png | Bin 0 -> 7400 bytes public/assets/img/home-2/visa/09.png | Bin 0 -> 2347 bytes public/assets/img/home-2/visa/10.png | Bin 0 -> 1913 bytes public/assets/img/home-2/visa/11.png | Bin 0 -> 3690 bytes public/assets/img/home-2/visa/12.png | Bin 0 -> 931 bytes public/assets/img/home-2/visa/13.png | Bin 0 -> 2742 bytes public/assets/img/home-2/visa/14.png | Bin 0 -> 2765 bytes public/assets/img/home-2/visa/15.png | Bin 0 -> 2015 bytes public/assets/img/home-2/visa/16.png | Bin 0 -> 2412 bytes public/assets/img/home-2/visa/17.png | Bin 0 -> 1623 bytes public/assets/img/home-2/visa/18.png | Bin 0 -> 1586 bytes public/assets/img/home-3/about/01.png | Bin 0 -> 4261 bytes public/assets/img/home-3/about/bg.jpg | Bin 0 -> 24868 bytes public/assets/img/home-3/choose-us/01.jpg | Bin 0 -> 1432 bytes public/assets/img/home-3/choose-us/02.jpg | Bin 0 -> 1432 bytes public/assets/img/home-3/choose-us/03.jpg | Bin 0 -> 1325 bytes public/assets/img/home-3/choose-us/04.jpg | Bin 0 -> 1432 bytes public/assets/img/home-3/choose-us/05.jpg | Bin 0 -> 1432 bytes public/assets/img/home-3/choose-us/06.png | Bin 0 -> 1672 bytes public/assets/img/home-3/choose-us/07.png | Bin 0 -> 1783 bytes public/assets/img/home-3/choose-us/bg.png | Bin 0 -> 263816 bytes public/assets/img/home-3/choose-us/icon-1.png | Bin 0 -> 1365 bytes public/assets/img/home-3/choose-us/icon-2.png | Bin 0 -> 1163 bytes public/assets/img/home-3/choose-us/icon-3.png | Bin 0 -> 534 bytes .../img/home-3/choose-us/pricing-bg.jpg | Bin 0 -> 147579 bytes public/assets/img/home-3/footer.jpg | Bin 0 -> 104583 bytes public/assets/img/home-3/hero/bg.jpg | Bin 0 -> 18995 bytes public/assets/img/home-3/hero/flag.png | Bin 0 -> 65897 bytes public/assets/img/home-3/hero/man.png | Bin 0 -> 6302 bytes public/assets/img/home-3/news/01.jpg | Bin 0 -> 1465 bytes public/assets/img/home-3/news/02.jpg | Bin 0 -> 1465 bytes public/assets/img/home-3/news/03.jpg | Bin 0 -> 1465 bytes public/assets/img/home-3/news/04.jpg | Bin 0 -> 1465 bytes public/assets/img/home-3/news/client.png | Bin 0 -> 227 bytes public/assets/img/home-3/service/01.jpg | Bin 0 -> 2220 bytes public/assets/img/home-3/service/02.jpg | Bin 0 -> 2220 bytes public/assets/img/home-3/service/03.jpg | Bin 0 -> 2220 bytes public/assets/img/home-3/service/04.jpg | Bin 0 -> 2220 bytes public/assets/img/home-3/test-thumb.jpg | Bin 0 -> 2401 bytes public/assets/img/home/pic1.png | Bin 0 -> 107682 bytes public/assets/img/home/pic2.png | Bin 0 -> 145077 bytes public/assets/img/inner-page/404.png | Bin 0 -> 9463 bytes public/assets/img/inner-page/breadcrumb.jpg | Bin 0 -> 184952 bytes .../img/inner-page/country-details/01.png | Bin 0 -> 2039 bytes .../img/inner-page/country-details/02.png | Bin 0 -> 1839 bytes .../img/inner-page/country-details/03.png | Bin 0 -> 856 bytes .../img/inner-page/country-details/04.png | Bin 0 -> 1492 bytes .../img/inner-page/country-details/05.png | Bin 0 -> 1927 bytes .../img/inner-page/country-details/06.png | Bin 0 -> 1633 bytes .../img/inner-page/country-details/07.png | Bin 0 -> 1750 bytes .../img/inner-page/country-details/08.png | Bin 0 -> 953 bytes .../img/inner-page/country-details/bg.jpg | Bin 0 -> 2565 bytes .../inner-page/country-details/details-1.jpg | Bin 0 -> 6597 bytes .../inner-page/country-details/details-2.jpg | Bin 0 -> 2347 bytes .../inner-page/country-details/details-3.png | Bin 0 -> 2347 bytes public/assets/img/inner-page/intro.jpg | Bin 0 -> 12102 bytes .../img/inner-page/news-details/comment-1.png | Bin 0 -> 524 bytes .../img/inner-page/news-details/comment-2.png | Bin 0 -> 524 bytes .../img/inner-page/news-details/comment-3.png | Bin 0 -> 524 bytes .../img/inner-page/news-details/details-1.jpg | Bin 0 -> 6741 bytes .../img/inner-page/news-details/details-2.jpg | Bin 0 -> 2324 bytes .../img/inner-page/news-details/details-3.jpg | Bin 0 -> 2324 bytes .../img/inner-page/news-details/post-1.jpg | Bin 0 -> 534 bytes .../img/inner-page/news-details/post-2.jpg | Bin 0 -> 534 bytes .../img/inner-page/news-details/post-3.jpg | Bin 0 -> 534 bytes .../inner-page/service-details/details-1.jpg | Bin 0 -> 10791 bytes .../inner-page/service-details/details-2.jpg | Bin 0 -> 4620 bytes .../inner-page/service-details/details-3.jpg | Bin 0 -> 4598 bytes public/assets/img/inner-page/shape.png | Bin 0 -> 1963 bytes public/assets/img/logo/Logo_HaiLearning.jpg | Bin 0 -> 60990 bytes public/assets/img/logo/black-logo.svg | 18 + public/assets/img/logo/logo.jpg | Bin 0 -> 139348 bytes public/assets/img/logo/white-logo.svg | 18 + .../ENG_main_2022-05-20-070029_kstp.jpg | Bin 0 -> 189821 bytes public/assets/img/partnerships/horizons.jpg | Bin 0 -> 14825 bytes .../img/partnerships/royalcosmetics.png | Bin 0 -> 59191 bytes public/assets/img/partnerships/swiss.jpg | Bin 0 -> 15529 bytes public/assets/img/partnerships/tech-logo.png | Bin 0 -> 117908 bytes public/assets/img/partnerships/uldp.jpg | Bin 0 -> 25116 bytes public/assets/js/ScrollTrigger.min.js | 10 + public/assets/js/SplitText.min.js | 11 + public/assets/js/ajax-mail.js | 48 + public/assets/js/bootstrap.bundle.min.js | 7 + public/assets/js/circle-progress.js | 215 + public/assets/js/countdowncustom.js | 48 + public/assets/js/gsap.js | 10 + public/assets/js/jquery-3.7.1.min.js | 2 + public/assets/js/jquery.counterup.min.js | 1 + public/assets/js/jquery.magnific-popup.min.js | 4 + public/assets/js/jquery.meanmenu.min.js | 90 + public/assets/js/jquery.nice-select.min.js | 4 + public/assets/js/jquery.waypoints.js | 662 + public/assets/js/lenis.min.js | 1 + public/assets/js/main.js | 793 + public/assets/js/odometer.min.js | 2 + public/assets/js/swiper-bundle.min.js | 14 + public/assets/js/viewport.jquery.js | 71 + public/assets/js/wow.min.js | 3 + public/assets/scss/_about.scss | 280 + public/assets/scss/_animation.scss | 915 ++ public/assets/scss/_brand.scss | 70 + public/assets/scss/_buttons.scss | 58 + public/assets/scss/_contact.scss | 362 + public/assets/scss/_cta.scss | 0 public/assets/scss/_faq.scss | 93 + public/assets/scss/_feature.scss | 1036 ++ public/assets/scss/_footer.scss | 683 + public/assets/scss/_header.scss | 1264 ++ public/assets/scss/_helping.scss | 501 + public/assets/scss/_hero.scss | 622 + public/assets/scss/_marquee.scss | 0 public/assets/scss/_meanmenu.scss | 95 + public/assets/scss/_mixins.scss | 130 + public/assets/scss/_news.scss | 886 ++ public/assets/scss/_preloader.scss | 446 + public/assets/scss/_pricing.scss | 309 + public/assets/scss/_section.scss | 126 + public/assets/scss/_service.scss | 673 + public/assets/scss/_testimonial.scss | 370 + public/assets/scss/_typography.scss | 174 + public/assets/scss/_variables.scss | 31 + public/assets/scss/_visa.scss | 565 + public/assets/scss/main.scss | 102 + public/assets/webfonts/fa-brands-400.ttf | Bin 0 -> 180684 bytes public/assets/webfonts/fa-brands-400.woff2 | Bin 0 -> 104740 bytes public/assets/webfonts/fa-duotone-900.ttf | Bin 0 -> 1132808 bytes public/assets/webfonts/fa-duotone-900.woff2 | Bin 0 -> 402000 bytes public/assets/webfonts/fa-light-300.ttf | Bin 0 -> 999704 bytes public/assets/webfonts/fa-light-300.woff2 | Bin 0 -> 392136 bytes public/assets/webfonts/fa-regular-400.ttf | Bin 0 -> 891584 bytes public/assets/webfonts/fa-regular-400.woff2 | Bin 0 -> 357732 bytes public/assets/webfonts/fa-solid-900.ttf | Bin 0 -> 785900 bytes public/assets/webfonts/fa-solid-900.woff2 | Bin 0 -> 303544 bytes public/assets/webfonts/fa-thin-100.ttf | Bin 0 -> 1100824 bytes public/assets/webfonts/fa-thin-100.woff2 | Bin 0 -> 426420 bytes public/assets/webfonts/fa-v4compatibility.ttf | Bin 0 -> 10556 bytes .../assets/webfonts/fa-v4compatibility.woff2 | Bin 0 -> 4964 bytes public/file.svg | 1 + public/globe.svg | 1 + public/next.svg | 1 + public/vercel.svg | 1 + public/window.svg | 1 + registration.html | 284 + sponsor.html | 379 + standalone-build.sh | 15 + test.html | 419 + tsconfig.json | 34 + types/about.ts | 57 + types/blog.ts | 136 + types/footer.ts | 27 + types/header-menu.ts | 7 + types/header.ts | 42 + types/home.ts | 73 + types/index.ts | 5 + types/programme.ts | 63 + utils/date.ts | 21 + utils/editorjsToHtml.ts | 401 + utils/image.ts | 57 + utils/index.ts | 3 + utils/slugify.ts | 20 + 313 files changed, 41936 insertions(+) create mode 100644 .gitignore create mode 100644 Dockerfile create mode 100644 README.md create mode 100644 admin.html create mode 100644 api/blogsApi.ts create mode 100644 api/contentPagesApi.ts create mode 100644 api/headerApi.ts create mode 100644 api/homeApi.ts create mode 100644 api/newsletterApi.ts create mode 100644 api/programmesApi.ts create mode 100644 api/submissionsApi.ts create mode 100644 app/(site)/layout.tsx create mode 100644 app/(site)/page.tsx create mode 100644 app/(site)/registration/page.tsx create mode 100644 app/components/forms/RegistrationForm.tsx create mode 100644 app/components/home/AgendaSection.tsx create mode 100644 app/components/home/HeroSection.tsx create mode 100644 app/components/home/StatsSection.tsx create mode 100644 app/components/home/VisionSection.tsx create mode 100644 app/components/layout/Footer/Footer.tsx create mode 100644 app/components/layout/Header/Header.tsx create mode 100644 app/components/layout/Header/HeaderClient.tsx create mode 100644 app/data/home.json create mode 100644 app/data/registration.json create mode 100644 app/data/site.json create mode 100644 app/globals.css create mode 100644 app/home.json create mode 100644 app/layout.tsx create mode 100644 app/not-found.tsx create mode 100644 app/utils/editorjsToHtml.ts create mode 100644 app/utils/image.ts create mode 100644 docker-compose.yml create mode 100644 docker-entrypoint.sh create mode 100644 eslint.config.mjs create mode 100644 feedback.html create mode 100644 ke_hoach_ipv6.md create mode 100644 lib/axios.ts create mode 100644 next.config.ts create mode 100644 package.json create mode 100644 postcss.config.mjs create mode 100644 public/_images/default.jpg create mode 100644 public/assets/css/all.min.css create mode 100644 public/assets/css/animate.css create mode 100644 public/assets/css/bootstrap.min.css create mode 100644 public/assets/css/color.css create mode 100644 public/assets/css/icomon.css create mode 100644 public/assets/css/magnific-popup.css create mode 100644 public/assets/css/main.css create mode 100644 public/assets/css/main.css.map create mode 100644 public/assets/css/meanmenu.css create mode 100644 public/assets/css/nice-select.css create mode 100644 public/assets/css/odometer.css create mode 100644 public/assets/css/swiper-bundle.min.css create mode 100644 public/assets/img/accreditations/QAHE.png create mode 100644 public/assets/img/accreditations/head.png create mode 100644 public/assets/img/accreditations/ico.png create mode 100644 public/assets/img/accreditations/ukrlp.png create mode 100644 public/assets/img/blog/OnlineMeeting.png create mode 100644 public/assets/img/blog/StudentStudying.png create mode 100644 public/assets/img/blog/UniversityCampus.png create mode 100644 public/assets/img/blog/blog-details/OnlineMeeting.png create mode 100644 public/assets/img/blog/blog-details/StudentStudying.png create mode 100644 public/assets/img/blog/blog-details/UniversityCampus.png create mode 100644 public/assets/img/close.png create mode 100644 public/assets/img/favicon.png create mode 100644 public/assets/img/header/home-1.jpg create mode 100644 public/assets/img/header/home-2.jpg create mode 100644 public/assets/img/header/home-3.jpg create mode 100644 public/assets/img/hero.png create mode 100644 public/assets/img/history/2023.png create mode 100644 public/assets/img/history/2024.png create mode 100644 public/assets/img/history/2025.png create mode 100644 public/assets/img/history/2026.png create mode 100644 public/assets/img/home-1/about/Vector.png create mode 100644 public/assets/img/home-1/about/about-02.jpg create mode 100644 public/assets/img/home-1/about/about-1.jpg create mode 100644 public/assets/img/home-1/about/globe.png create mode 100644 public/assets/img/home-1/about/plane.png create mode 100644 public/assets/img/home-1/about/shape.png create mode 100644 public/assets/img/home-1/brand/01.png create mode 100644 public/assets/img/home-1/brand/02.png create mode 100644 public/assets/img/home-1/brand/03.png create mode 100644 public/assets/img/home-1/brand/04.png create mode 100644 public/assets/img/home-1/brand/05.png create mode 100644 public/assets/img/home-1/feature/Vector.png create mode 100644 public/assets/img/home-1/feature/bg-2.jpg create mode 100644 public/assets/img/home-1/feature/bg.png create mode 100644 public/assets/img/home-1/feature/icon-1.png create mode 100644 public/assets/img/home-1/feature/icon-2.png create mode 100644 public/assets/img/home-1/feature/icon-3.png create mode 100644 public/assets/img/home-1/feature/icon-4.png create mode 100644 public/assets/img/home-1/feature/shape-2.png create mode 100644 public/assets/img/home-1/feature/shape.png create mode 100644 public/assets/img/home-1/feature/text-2.png create mode 100644 public/assets/img/home-1/feature/text.png create mode 100644 public/assets/img/home-1/feature/video-bg.jpg create mode 100644 public/assets/img/home-1/footer-bg.jpg create mode 100644 public/assets/img/home-1/hero/bg.jpg create mode 100644 public/assets/img/home-1/hero/man.png create mode 100644 public/assets/img/home-1/hero/sape-2.png create mode 100644 public/assets/img/home-1/hero/shape-3.png create mode 100644 public/assets/img/home-1/hero/shape-4.png create mode 100644 public/assets/img/home-1/hero/shape.png create mode 100644 public/assets/img/home-1/hover-bg.jpg create mode 100644 public/assets/img/home-1/icon/01.svg create mode 100644 public/assets/img/home-1/news/client.png create mode 100644 public/assets/img/home-1/news/news-1.jpg create mode 100644 public/assets/img/home-1/news/news-10.jpg create mode 100644 public/assets/img/home-1/news/news-11.jpg create mode 100644 public/assets/img/home-1/news/news-12.jpg create mode 100644 public/assets/img/home-1/news/news-13.jpg create mode 100644 public/assets/img/home-1/news/news-14.jpg create mode 100644 public/assets/img/home-1/news/news-15.jpg create mode 100644 public/assets/img/home-1/news/news-2.jpg create mode 100644 public/assets/img/home-1/news/news-3.jpg create mode 100644 public/assets/img/home-1/news/news-4.jpg create mode 100644 public/assets/img/home-1/news/news-5.jpg create mode 100644 public/assets/img/home-1/news/news-6.jpg create mode 100644 public/assets/img/home-1/news/news-7.jpg create mode 100644 public/assets/img/home-1/news/news-8.jpg create mode 100644 public/assets/img/home-1/news/news-9.jpg create mode 100644 public/assets/img/home-1/testimonial/01.jpg create mode 100644 public/assets/img/home-1/testimonial/client-2.png create mode 100644 public/assets/img/home-1/testimonial/client.png create mode 100644 public/assets/img/home-2/Vector.png create mode 100644 public/assets/img/home-2/bg.jpg create mode 100644 public/assets/img/home-2/feature/01.png create mode 100644 public/assets/img/home-2/feature/02.png create mode 100644 public/assets/img/home-2/feature/03.png create mode 100644 public/assets/img/home-2/feature/Icon.png create mode 100644 public/assets/img/home-2/feature/Years.png create mode 100644 public/assets/img/home-2/feature/bg-shape.png create mode 100644 public/assets/img/home-2/hero/hero-bg.jpg create mode 100644 public/assets/img/home-2/hero/hero.png create mode 100644 public/assets/img/home-2/hero/shape.png create mode 100644 public/assets/img/home-2/icon/01.png create mode 100644 public/assets/img/home-2/icon/02.svg create mode 100644 public/assets/img/home-2/news/01.jpg create mode 100644 public/assets/img/home-2/news/02.jpg create mode 100644 public/assets/img/home-2/news/03.jpg create mode 100644 public/assets/img/home-2/service.jpg create mode 100644 public/assets/img/home-2/testimonial/01.jpg create mode 100644 public/assets/img/home-2/testimonial/client-1.png create mode 100644 public/assets/img/home-2/testimonial/client-2.png create mode 100644 public/assets/img/home-2/testimonial/client-3.jpg create mode 100644 public/assets/img/home-2/testimonial/client-4.jpg create mode 100644 public/assets/img/home-2/visa/01.png create mode 100644 public/assets/img/home-2/visa/02.png create mode 100644 public/assets/img/home-2/visa/03.png create mode 100644 public/assets/img/home-2/visa/04.png create mode 100644 public/assets/img/home-2/visa/05.png create mode 100644 public/assets/img/home-2/visa/06.png create mode 100644 public/assets/img/home-2/visa/07.png create mode 100644 public/assets/img/home-2/visa/08.png create mode 100644 public/assets/img/home-2/visa/09.png create mode 100644 public/assets/img/home-2/visa/10.png create mode 100644 public/assets/img/home-2/visa/11.png create mode 100644 public/assets/img/home-2/visa/12.png create mode 100644 public/assets/img/home-2/visa/13.png create mode 100644 public/assets/img/home-2/visa/14.png create mode 100644 public/assets/img/home-2/visa/15.png create mode 100644 public/assets/img/home-2/visa/16.png create mode 100644 public/assets/img/home-2/visa/17.png create mode 100644 public/assets/img/home-2/visa/18.png create mode 100644 public/assets/img/home-3/about/01.png create mode 100644 public/assets/img/home-3/about/bg.jpg create mode 100644 public/assets/img/home-3/choose-us/01.jpg create mode 100644 public/assets/img/home-3/choose-us/02.jpg create mode 100644 public/assets/img/home-3/choose-us/03.jpg create mode 100644 public/assets/img/home-3/choose-us/04.jpg create mode 100644 public/assets/img/home-3/choose-us/05.jpg create mode 100644 public/assets/img/home-3/choose-us/06.png create mode 100644 public/assets/img/home-3/choose-us/07.png create mode 100644 public/assets/img/home-3/choose-us/bg.png create mode 100644 public/assets/img/home-3/choose-us/icon-1.png create mode 100644 public/assets/img/home-3/choose-us/icon-2.png create mode 100644 public/assets/img/home-3/choose-us/icon-3.png create mode 100644 public/assets/img/home-3/choose-us/pricing-bg.jpg create mode 100644 public/assets/img/home-3/footer.jpg create mode 100644 public/assets/img/home-3/hero/bg.jpg create mode 100644 public/assets/img/home-3/hero/flag.png create mode 100644 public/assets/img/home-3/hero/man.png create mode 100644 public/assets/img/home-3/news/01.jpg create mode 100644 public/assets/img/home-3/news/02.jpg create mode 100644 public/assets/img/home-3/news/03.jpg create mode 100644 public/assets/img/home-3/news/04.jpg create mode 100644 public/assets/img/home-3/news/client.png create mode 100644 public/assets/img/home-3/service/01.jpg create mode 100644 public/assets/img/home-3/service/02.jpg create mode 100644 public/assets/img/home-3/service/03.jpg create mode 100644 public/assets/img/home-3/service/04.jpg create mode 100644 public/assets/img/home-3/test-thumb.jpg create mode 100644 public/assets/img/home/pic1.png create mode 100644 public/assets/img/home/pic2.png create mode 100644 public/assets/img/inner-page/404.png create mode 100644 public/assets/img/inner-page/breadcrumb.jpg create mode 100644 public/assets/img/inner-page/country-details/01.png create mode 100644 public/assets/img/inner-page/country-details/02.png create mode 100644 public/assets/img/inner-page/country-details/03.png create mode 100644 public/assets/img/inner-page/country-details/04.png create mode 100644 public/assets/img/inner-page/country-details/05.png create mode 100644 public/assets/img/inner-page/country-details/06.png create mode 100644 public/assets/img/inner-page/country-details/07.png create mode 100644 public/assets/img/inner-page/country-details/08.png create mode 100644 public/assets/img/inner-page/country-details/bg.jpg create mode 100644 public/assets/img/inner-page/country-details/details-1.jpg create mode 100644 public/assets/img/inner-page/country-details/details-2.jpg create mode 100644 public/assets/img/inner-page/country-details/details-3.png create mode 100644 public/assets/img/inner-page/intro.jpg create mode 100644 public/assets/img/inner-page/news-details/comment-1.png create mode 100644 public/assets/img/inner-page/news-details/comment-2.png create mode 100644 public/assets/img/inner-page/news-details/comment-3.png create mode 100644 public/assets/img/inner-page/news-details/details-1.jpg create mode 100644 public/assets/img/inner-page/news-details/details-2.jpg create mode 100644 public/assets/img/inner-page/news-details/details-3.jpg create mode 100644 public/assets/img/inner-page/news-details/post-1.jpg create mode 100644 public/assets/img/inner-page/news-details/post-2.jpg create mode 100644 public/assets/img/inner-page/news-details/post-3.jpg create mode 100644 public/assets/img/inner-page/service-details/details-1.jpg create mode 100644 public/assets/img/inner-page/service-details/details-2.jpg create mode 100644 public/assets/img/inner-page/service-details/details-3.jpg create mode 100644 public/assets/img/inner-page/shape.png create mode 100644 public/assets/img/logo/Logo_HaiLearning.jpg create mode 100644 public/assets/img/logo/black-logo.svg create mode 100644 public/assets/img/logo/logo.jpg create mode 100644 public/assets/img/logo/white-logo.svg create mode 100644 public/assets/img/partnerships/ENG_main_2022-05-20-070029_kstp.jpg create mode 100644 public/assets/img/partnerships/horizons.jpg create mode 100644 public/assets/img/partnerships/royalcosmetics.png create mode 100644 public/assets/img/partnerships/swiss.jpg create mode 100644 public/assets/img/partnerships/tech-logo.png create mode 100644 public/assets/img/partnerships/uldp.jpg create mode 100644 public/assets/js/ScrollTrigger.min.js create mode 100644 public/assets/js/SplitText.min.js create mode 100644 public/assets/js/ajax-mail.js create mode 100644 public/assets/js/bootstrap.bundle.min.js create mode 100644 public/assets/js/circle-progress.js create mode 100644 public/assets/js/countdowncustom.js create mode 100644 public/assets/js/gsap.js create mode 100644 public/assets/js/jquery-3.7.1.min.js create mode 100644 public/assets/js/jquery.counterup.min.js create mode 100644 public/assets/js/jquery.magnific-popup.min.js create mode 100644 public/assets/js/jquery.meanmenu.min.js create mode 100644 public/assets/js/jquery.nice-select.min.js create mode 100644 public/assets/js/jquery.waypoints.js create mode 100644 public/assets/js/lenis.min.js create mode 100644 public/assets/js/main.js create mode 100644 public/assets/js/odometer.min.js create mode 100644 public/assets/js/swiper-bundle.min.js create mode 100644 public/assets/js/viewport.jquery.js create mode 100644 public/assets/js/wow.min.js create mode 100644 public/assets/scss/_about.scss create mode 100644 public/assets/scss/_animation.scss create mode 100644 public/assets/scss/_brand.scss create mode 100644 public/assets/scss/_buttons.scss create mode 100644 public/assets/scss/_contact.scss create mode 100644 public/assets/scss/_cta.scss create mode 100644 public/assets/scss/_faq.scss create mode 100644 public/assets/scss/_feature.scss create mode 100644 public/assets/scss/_footer.scss create mode 100644 public/assets/scss/_header.scss create mode 100644 public/assets/scss/_helping.scss create mode 100644 public/assets/scss/_hero.scss create mode 100644 public/assets/scss/_marquee.scss create mode 100644 public/assets/scss/_meanmenu.scss create mode 100644 public/assets/scss/_mixins.scss create mode 100644 public/assets/scss/_news.scss create mode 100644 public/assets/scss/_preloader.scss create mode 100644 public/assets/scss/_pricing.scss create mode 100644 public/assets/scss/_section.scss create mode 100644 public/assets/scss/_service.scss create mode 100644 public/assets/scss/_testimonial.scss create mode 100644 public/assets/scss/_typography.scss create mode 100644 public/assets/scss/_variables.scss create mode 100644 public/assets/scss/_visa.scss create mode 100644 public/assets/scss/main.scss create mode 100644 public/assets/webfonts/fa-brands-400.ttf create mode 100644 public/assets/webfonts/fa-brands-400.woff2 create mode 100644 public/assets/webfonts/fa-duotone-900.ttf create mode 100644 public/assets/webfonts/fa-duotone-900.woff2 create mode 100644 public/assets/webfonts/fa-light-300.ttf create mode 100644 public/assets/webfonts/fa-light-300.woff2 create mode 100644 public/assets/webfonts/fa-regular-400.ttf create mode 100644 public/assets/webfonts/fa-regular-400.woff2 create mode 100644 public/assets/webfonts/fa-solid-900.ttf create mode 100644 public/assets/webfonts/fa-solid-900.woff2 create mode 100644 public/assets/webfonts/fa-thin-100.ttf create mode 100644 public/assets/webfonts/fa-thin-100.woff2 create mode 100644 public/assets/webfonts/fa-v4compatibility.ttf create mode 100644 public/assets/webfonts/fa-v4compatibility.woff2 create mode 100644 public/file.svg create mode 100644 public/globe.svg create mode 100644 public/next.svg create mode 100644 public/vercel.svg create mode 100644 public/window.svg create mode 100644 registration.html create mode 100644 sponsor.html create mode 100644 standalone-build.sh create mode 100644 test.html create mode 100644 tsconfig.json create mode 100644 types/about.ts create mode 100644 types/blog.ts create mode 100644 types/footer.ts create mode 100644 types/header-menu.ts create mode 100644 types/header.ts create mode 100644 types/home.ts create mode 100644 types/index.ts create mode 100644 types/programme.ts create mode 100644 utils/date.ts create mode 100644 utils/editorjsToHtml.ts create mode 100644 utils/image.ts create mode 100644 utils/index.ts create mode 100644 utils/slugify.ts 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} +