nuxt-layer-core 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (85) hide show
  1. package/.claude/settings.local.json +8 -0
  2. package/app/app.config.ts +5 -0
  3. package/app/app.vue +11 -0
  4. package/app/assets/css/main.css +113 -0
  5. package/app/components/.gitkeep +0 -0
  6. package/app/components/ui/sonner/Sonner.vue +62 -0
  7. package/app/components/ui/sonner/index.ts +1 -0
  8. package/app/composables/.gitkeep +0 -0
  9. package/app/layers/solution/app/assets/proxmox-css/main.css +89 -0
  10. package/app/layers/solution/app/assets/proxmox-css/proxmox-utils.css +68 -0
  11. package/app/layers/solution/app/components/ClusterDiagram.vue +72 -0
  12. package/app/layers/solution/app/components/CoursesSection.vue +245 -0
  13. package/app/layers/solution/app/components/CtaSection.vue +347 -0
  14. package/app/layers/solution/app/components/DynamicPageSections.vue +103 -0
  15. package/app/layers/solution/app/components/FeaturesSection.vue +67 -0
  16. package/app/layers/solution/app/components/HeroSection.vue +144 -0
  17. package/app/layers/solution/app/components/PricingPlansSection.vue +396 -0
  18. package/app/layers/solution/app/components/RegistrationForm.vue +283 -0
  19. package/app/layers/solution/app/components/RegistrationSection.vue +24 -0
  20. package/app/layers/solution/app/components/RegistrationSidebar.vue +70 -0
  21. package/app/layers/solution/app/components/TestimonialsSection.vue +58 -0
  22. package/app/layers/solution/app/components/TheFooter.vue +61 -0
  23. package/app/layers/solution/app/components/TheHeader.vue +193 -0
  24. package/app/layers/solution/app/components/proxmox/AppIcon.vue +45 -0
  25. package/app/layers/solution/app/components/proxmox/ComparisonSection.vue +54 -0
  26. package/app/layers/solution/app/components/proxmox/FaqSection.vue +58 -0
  27. package/app/layers/solution/app/components/proxmox/HciOverviewSection.vue +37 -0
  28. package/app/layers/solution/app/components/proxmox/HeroSection.vue +74 -0
  29. package/app/layers/solution/app/components/proxmox/TestimonialsSection.vue +44 -0
  30. package/app/layers/solution/app/components/proxmox/TrainingBenefitsSection.vue +37 -0
  31. package/app/layers/solution/app/components/proxmox/VeFeaturesSection.vue +55 -0
  32. package/app/layers/solution/app/composables/useActiveSection.ts +47 -0
  33. package/app/layers/solution/app/composables/useAuthApi.ts +244 -0
  34. package/app/layers/solution/app/composables/useAuthState.ts +16 -0
  35. package/app/layers/solution/app/composables/useConsultationRequest.ts +8 -0
  36. package/app/layers/solution/app/composables/useCookie.ts +79 -0
  37. package/app/layers/solution/app/composables/useCookieHelper.ts +81 -0
  38. package/app/layers/solution/app/composables/useCrmApi.ts +32 -0
  39. package/app/layers/solution/app/composables/useDynamicCollection.ts +23 -0
  40. package/app/layers/solution/app/composables/usePlanNormalizer.ts +45 -0
  41. package/app/layers/solution/app/composables/useScrollAnimate.ts +40 -0
  42. package/app/layers/solution/app/composables/useScrollToSection.ts +13 -0
  43. package/app/layers/solution/app/composables/useSolutionApi.ts +73 -0
  44. package/app/layers/solution/app/composables/useToast.ts +14 -0
  45. package/app/layers/solution/app/data/pricingPlans.ts +150 -0
  46. package/app/layers/solution/app/data/proxmox-content.ts +158 -0
  47. package/app/layers/solution/app/images/Proxmox HCI.png +0 -0
  48. package/app/layers/solution/app/images/image_from_https_hercules_cdn.com_file_6dztkvkfnip0ud23tzzvu57c.png +0 -0
  49. package/app/layers/solution/app/layouts/default.vue +5 -0
  50. package/app/layers/solution/app/nuxt.config.ts +15 -0
  51. package/app/layers/solution/app/pages/[slug]/[slug2]/[slug3]/index.vue +22 -0
  52. package/app/layers/solution/app/pages/[slug]/[slug2]/index.vue +23 -0
  53. package/app/layers/solution/app/pages/[slug]/index.vue +22 -0
  54. package/app/layers/solution/app/pages/cap-nhat-thong-tin/index.vue.disabled +302 -0
  55. package/app/layers/solution/app/pages/dang-ky/index.vue.disabled +627 -0
  56. package/app/layers/solution/app/pages/dang-ky-tu-van/khoa-hoc-proxmox.vue.disabled +39 -0
  57. package/app/layers/solution/app/pages/dang-nhap/index.vue.disabled +208 -0
  58. package/app/layers/solution/app/pages/doi-mat-khau/index.vue.disabled +237 -0
  59. package/app/layers/solution/app/pages/giai-phap-proxmox/[slug]/index.vue.disabled +37 -0
  60. package/app/layers/solution/app/pages/giai-phap-proxmox/[slug]/proxmox-hci/index.vue.disabled +45 -0
  61. package/app/layers/solution/app/pages/giai-phap-proxmox/dang-ky-khoa-hoc/[slug].vue.disabled +56 -0
  62. package/app/layers/solution/app/pages/quen-mat-khau/index.vue.disabled +136 -0
  63. package/app/layers/solution/app/pages/xac-thuc/index.vue.disabled +320 -0
  64. package/app/layers/solution/app/plugins/reveal.ts +56 -0
  65. package/app/layers/solution/app/public/images/image_from_https_hercules_cdn.com_file_6dztkvkfnip0ud23tzzvu57c.png +0 -0
  66. package/app/layers/solution/app/public/images/logo-lvs-1300x330.png +0 -0
  67. package/app/layouts/auth.vue +71 -0
  68. package/app/lib/utils.ts +7 -0
  69. package/app/middleware/.gitkeep +0 -0
  70. package/app/modules/layer-route-priority.ts +60 -0
  71. package/app/pages/index.vue +10 -0
  72. package/app/pages/san-pham-proxmox.vue +8 -0
  73. package/app/plugins/.gitkeep +0 -0
  74. package/app/plugins/sdk.ts +29 -0
  75. package/app/stores/.gitkeep +0 -0
  76. package/app/types/sdk.d.ts +15 -0
  77. package/components.json +24 -0
  78. package/nuxt.config.ts +105 -0
  79. package/package.json +52 -0
  80. package/public/.gitkeep +0 -0
  81. package/server/api/.gitkeep +0 -0
  82. package/server/utils/.gitkeep +0 -0
  83. package/tailwind.config.js +96 -0
  84. package/tsconfig.base.json +18 -0
  85. package/tsconfig.json +18 -0
@@ -0,0 +1,74 @@
1
+ <script setup lang="ts">
2
+ import { heroStats } from "../../data/proxmox-content"
3
+ const { scrollTo } = useScrollToSection()
4
+ </script>
5
+
6
+ <template>
7
+ <section id="san-pham" class="relative overflow-hidden bg-gradient-to-b from-blue-50/60 via-white to-white">
8
+ <div
9
+ class="bg-grid pointer-events-none absolute inset-0 [mask-image:radial-gradient(ellipse_60%_60%_at_50%_0%,black,transparent)]" />
10
+
11
+ <div class="relative mx-auto max-w-7xl px-6 pt-20 pb-16 sm:pt-28 sm:pb-24 lg:px-8">
12
+ <div class="grid grid-cols-1 items-center gap-10 lg:grid-cols-2 lg:gap-12">
13
+ <!-- Left: copy -->
14
+ <div>
15
+ <div v-reveal
16
+ class="inline-flex items-center rounded-full bg-blue-50 px-4 py-1.5 text-sm font-medium text-blue-600 ring-1 ring-inset ring-blue-100">
17
+ Giải pháp Hạ tầng Siêu hội tụ hàng đầu
18
+ </div>
19
+
20
+ <h1 v-reveal="{ delay: 80 }"
21
+ class="mt-6 text-4xl font-extrabold tracking-tight text-slate-900 sm:text-5xl lg:text-[3.4rem] lg:leading-[1.1]">
22
+ Proxmox HCI —
23
+ <span class="block text-blue-600">Hạ tầng siêu hội tụ</span>
24
+ toàn diện
25
+ </h1>
26
+
27
+ <p v-reveal="{ delay: 160 }" class="mt-6 max-w-xl text-lg leading-relaxed text-slate-600">
28
+ Tích hợp KVM, Ceph Storage, SDN và HA Cluster vào một nền tảng duy nhất. Giải pháp hoàn hảo giúp doanh
29
+ nghiệp tối ưu chi phí, nâng cao hiệu suất và đảm bảo tính sẵn sàng cao.
30
+ </p>
31
+
32
+ <a v-reveal="{ delay: 220 }" href="#lien-he" @click.prevent="scrollTo('lien-he')"
33
+ class="mt-5 inline-flex items-center gap-2 text-[15px] font-medium text-blue-600 hover:text-blue-700">
34
+ <ProxmoxAppIcon name="calendar" class="h-4 w-4" />
35
+ Triển khai dễ dàng, mở rộng linh hoạt. Nhận tư vấn giải pháp từ chuyên gia Long Vân.
36
+ </a>
37
+
38
+ <div v-reveal="{ delay: 280 }" class="mt-8 flex flex-wrap items-center gap-4">
39
+ <a href="#lien-he" @click.prevent="scrollTo('lien-he')"
40
+ class="inline-flex items-center gap-2 rounded-[10px] bg-blue-600 px-6 py-3.5 text-[15px] font-semibold text-white shadow-sm shadow-blue-600/20 transition hover:bg-blue-700">
41
+ Nhận tư vấn triển khai
42
+ <ProxmoxAppIcon name="arrowRight" class="h-4 w-4" />
43
+ </a>
44
+
45
+ </div>
46
+
47
+ <dl class="mt-9 grid grid-cols-2 border-t border-slate-200 pt-6 sm:grid-cols-4">
48
+ <div v-for="(stat, i) in heroStats" :key="stat.label" class="px-3 first:pl-0 sm:px-5" :class="[
49
+ i === 2 ? 'mt-6 sm:mt-0' : '',
50
+ i === 3 ? 'mt-6 sm:mt-0' : '',
51
+ i > 0 ? 'sm:border-l sm:border-slate-200' : ''
52
+ ]">
53
+ <dt class="text-2xl font-extrabold tracking-tight text-slate-900">
54
+ {{ stat.value }}
55
+ </dt>
56
+
57
+ <dd class="mt-1 max-w-[110px] text-xs leading-5 text-slate-500">
58
+ {{ stat.label }}
59
+ </dd>
60
+ </div>
61
+ </dl>
62
+ </div>
63
+
64
+ <!-- Right: image with floating cards -->
65
+ <div v-reveal="{ type: 'right', delay: 160 }" class="relative flex h-full items-center">
66
+ <div class="w-full overflow-hidden rounded-3xl shadow-2xl shadow-blue-900/10 ring-1 ring-black/5">
67
+ <img src="../../images/Proxmox HCI.png" alt="Datacenter server racks"
68
+ class="block h-auto w-full object-contain" />
69
+ </div>
70
+ </div>
71
+ </div>
72
+ </div>
73
+ </section>
74
+ </template>
@@ -0,0 +1,44 @@
1
+ <script setup lang="ts">
2
+ import { testimonials } from "../../data/proxmox-content"
3
+ </script>
4
+
5
+ <template>
6
+ <section id="khach-hang" class="bg-white py-20 sm:py-28">
7
+ <div class="mx-auto max-w-6xl px-6 lg:px-8">
8
+ <div class="mx-auto max-w-3xl text-center">
9
+ <div v-reveal class="inline-flex items-center rounded-full bg-blue-50 px-4 py-1.5 text-sm font-medium text-blue-600 ring-1 ring-inset ring-blue-100">
10
+ Học viên nói gì
11
+ </div>
12
+ <h2 v-reveal="{ delay: 60 }" class="mt-5 text-3xl font-extrabold tracking-tight text-slate-900 sm:text-4xl">
13
+ Được tư vấn đúng khóa,
14
+ <span class="text-blue-600">học đúng thứ cần dùng</span>
15
+ </h2>
16
+ </div>
17
+
18
+ <div class="mt-14 grid grid-cols-1 gap-6 sm:grid-cols-2 lg:grid-cols-3">
19
+ <figure
20
+ v-for="(t, i) in testimonials"
21
+ :key="t.name + i"
22
+ v-reveal="{ delay: 100 + i * 90 }"
23
+ class="rounded-2xl border border-slate-200 bg-white p-7"
24
+ >
25
+ <div class="flex gap-0.5 text-amber-400">
26
+ <ProxmoxAppIcon v-for="n in 5" :key="n" name="star" class="h-4 w-4 fill-current" />
27
+ </div>
28
+ <blockquote class="mt-4 text-[15px] italic leading-relaxed text-slate-600">
29
+ “{{ t.quote }}”
30
+ </blockquote>
31
+ <figcaption class="mt-6 flex items-center gap-3 border-t border-slate-100 pt-5">
32
+ <span class="flex h-9 w-9 shrink-0 items-center justify-center rounded-full bg-blue-100 text-xs font-bold text-blue-600">
33
+ HV
34
+ </span>
35
+ <div class="text-sm">
36
+ <p class="font-semibold text-slate-900">{{ t.name }}</p>
37
+ <p class="text-slate-500">{{ t.role }}</p>
38
+ </div>
39
+ </figcaption>
40
+ </figure>
41
+ </div>
42
+ </div>
43
+ </section>
44
+ </template>
@@ -0,0 +1,37 @@
1
+ <script setup lang="ts">
2
+ import { trainingBenefits } from "../../data/proxmox-content"
3
+ </script>
4
+
5
+ <template>
6
+ <section class="bg-[#F7F9FB] py-20 sm:py-28">
7
+ <div class="mx-auto max-w-6xl px-6 lg:px-8">
8
+ <div class="mx-auto max-w-3xl text-center">
9
+ <div v-reveal class="inline-flex items-center rounded-full bg-blue-50 px-4 py-1.5 text-sm font-medium text-blue-600 ring-1 ring-inset ring-blue-100">
10
+ Lợi ích đào tạo
11
+ </div>
12
+ <h2 v-reveal="{ delay: 60 }" class="mt-5 text-3xl font-extrabold tracking-tight text-slate-900 sm:text-4xl">
13
+ Vì sao nên tham gia
14
+ <span class="text-blue-600">đào tạo Proxmox VE?</span>
15
+ </h2>
16
+ <p v-reveal="{ delay: 120 }" class="mt-5 leading-relaxed text-slate-600">
17
+ Đầu tư một lần vào đào tạo bài bản giúp bạn tối ưu năng lực vận hành, cắt giảm chi phí hạ tầng và tạo bước đệm bứt phá trên con đường sự nghiệp.
18
+ </p>
19
+ </div>
20
+
21
+ <div class="mt-14 grid grid-cols-1 gap-5 sm:grid-cols-2 lg:grid-cols-3">
22
+ <div
23
+ v-for="(item, i) in trainingBenefits"
24
+ :key="item.title"
25
+ v-reveal="{ delay: 100 + i * 70 }"
26
+ class="rounded-2xl border border-slate-200 bg-white p-7 transition hover:-translate-y-1 hover:border-blue-200 hover:shadow-lg hover:shadow-blue-900/5"
27
+ >
28
+ <span class="flex h-11 w-11 items-center justify-center rounded-xl bg-blue-50 text-blue-600">
29
+ <ProxmoxAppIcon :name="item.icon" class="h-5 w-5" />
30
+ </span>
31
+ <p class="mt-4 font-semibold text-slate-900">{{ item.title }}</p>
32
+ <p class="mt-2 text-sm leading-relaxed text-slate-500">{{ item.desc }}</p>
33
+ </div>
34
+ </div>
35
+ </div>
36
+ </section>
37
+ </template>
@@ -0,0 +1,55 @@
1
+ <script setup lang="ts">
2
+ import { veFeatureList, veFeatureCards } from "../../data/proxmox-content"
3
+ </script>
4
+
5
+ <template>
6
+ <section id="giai-phap" class="bg-white mx-auto max-w-7xl px-6 py-20 sm:py-28 lg:px-8">
7
+ <div class="grid grid-cols-1 items-start gap-16 lg:grid-cols-2">
8
+ <!-- Left copy -->
9
+ <div>
10
+ <div v-reveal class="inline-flex items-center rounded-full bg-blue-50 px-4 py-1.5 text-sm font-medium text-blue-600 ring-1 ring-inset ring-blue-100">
11
+ Nền tảng ảo hóa
12
+ </div>
13
+
14
+ <h2 v-reveal="{ delay: 60 }" class="mt-5 text-3xl font-extrabold tracking-tight text-slate-900 sm:text-4xl sm:leading-tight">
15
+ Proxmox VE — Nền tảng ảo hóa
16
+ <span class="text-blue-600">hàng đầu thế giới</span>
17
+ </h2>
18
+
19
+ <p v-reveal="{ delay: 120 }" class="mt-5 leading-relaxed text-slate-600">
20
+ Proxmox VE là nền tảng ảo hóa mã nguồn mở mạnh mẽ, cho phép doanh nghiệp triển khai và quản lý VM & Container tập trung trên một giao diện web duy nhất. Được hàng nghìn doanh nghiệp toàn cầu tin dùng thay thế VMware vSphere với chi phí gần như bằng 0.
21
+ </p>
22
+
23
+ <ul class="mt-7 space-y-4">
24
+ <li
25
+ v-for="(item, i) in veFeatureList"
26
+ :key="item.text"
27
+ v-reveal="{ delay: 160 + i * 60 }"
28
+ class="flex items-center gap-3"
29
+ >
30
+ <span class="flex h-8 w-8 shrink-0 items-center justify-center rounded-lg bg-blue-50 text-blue-600">
31
+ <ProxmoxAppIcon :name="item.icon" class="h-4 w-4" />
32
+ </span>
33
+ <span class="text-[15px] text-slate-700">{{ item.text }}</span>
34
+ </li>
35
+ </ul>
36
+ </div>
37
+
38
+ <!-- Right card grid -->
39
+ <div class="grid grid-cols-2 gap-5">
40
+ <div
41
+ v-for="(card, i) in veFeatureCards"
42
+ :key="card.title"
43
+ v-reveal="{ type: 'scale', delay: 100 + i * 70 }"
44
+ class="rounded-2xl border border-slate-200 bg-slate-50/60 p-6 transition hover:-translate-y-1 hover:border-blue-200 hover:bg-white hover:shadow-lg hover:shadow-blue-900/5"
45
+ >
46
+ <span class="flex h-11 w-11 items-center justify-center rounded-xl bg-white text-blue-600 shadow-sm ring-1 ring-slate-200">
47
+ <ProxmoxAppIcon :name="card.icon" class="h-5 w-5" />
48
+ </span>
49
+ <p class="mt-4 font-semibold text-slate-900">{{ card.title }}</p>
50
+ <p class="mt-1 text-sm text-slate-500">{{ card.desc }}</p>
51
+ </div>
52
+ </div>
53
+ </div>
54
+ </section>
55
+ </template>
@@ -0,0 +1,47 @@
1
+ export function useActiveSection(sectionIds: string[]) {
2
+ const activeSection = ref(sectionIds[0] || '')
3
+
4
+ onMounted(() => {
5
+ const sections = sectionIds
6
+ .map(id => document.getElementById(id))
7
+ .filter(Boolean) as HTMLElement[]
8
+
9
+ if (!sections.length) return
10
+
11
+ function update() {
12
+ let maxVisibleHeight = 0
13
+ let maxId = activeSection.value
14
+
15
+ const viewportHeight = window.innerHeight
16
+
17
+ for (let i = 0; i < sections.length; i++) {
18
+ const el = sections[i]
19
+ const rect = el.getBoundingClientRect()
20
+
21
+ const visibleTop = Math.max(0, rect.top)
22
+ const visibleBottom = Math.min(viewportHeight, rect.bottom)
23
+ const visibleHeight = Math.max(0, visibleBottom - visibleTop)
24
+
25
+ if (visibleHeight > maxVisibleHeight) {
26
+ maxVisibleHeight = visibleHeight
27
+ maxId = el.id
28
+ }
29
+ }
30
+
31
+ if (window.scrollY < 50 && sections[0]) {
32
+ activeSection.value = sections[0].id
33
+ return
34
+ }
35
+
36
+ if (maxVisibleHeight > 0) {
37
+ activeSection.value = maxId
38
+ }
39
+ }
40
+
41
+ update()
42
+ window.addEventListener('scroll', update, { passive: true })
43
+ onUnmounted(() => window.removeEventListener('scroll', update))
44
+ })
45
+
46
+ return activeSection
47
+ }
@@ -0,0 +1,244 @@
1
+ export const useAuthApi = () => {
2
+ const { $sdk } = useNuxtApp()
3
+
4
+ // ==========================
5
+ // LOGIN / REGISTER
6
+ // ==========================
7
+
8
+ const login = (data: {
9
+ username: string
10
+ password: string
11
+ type?: string
12
+ otpCode?: string
13
+ }) => {
14
+ return $sdk.auth.login(data)
15
+ }
16
+
17
+ const register = (data: {
18
+ username: string
19
+ fullName: string
20
+ }) => {
21
+ return $sdk.auth.register(data as any)
22
+ }
23
+
24
+ const logout = (accessToken: string, partyId: string) => {
25
+ return $sdk.auth.logout(accessToken, partyId)
26
+ }
27
+
28
+ const getUserByToken = (token: string) => {
29
+ return $sdk.auth.getUserDetail(token)
30
+ }
31
+
32
+ const getUserLoginByToken = (token: string) => {
33
+ return $sdk.auth.getUserLoginByToken(token)
34
+ }
35
+
36
+ const updateInfo = (
37
+ accessToken: string,
38
+ updateUserRequest: { fullName: string; phone: string; email?: string },
39
+ type: string,
40
+ password: string
41
+ ) => {
42
+ return $sdk.auth.updateInfo(accessToken, updateUserRequest, type, password)
43
+ }
44
+
45
+ const getAccessTokenByOTP = (
46
+ code: string,
47
+ phone: string,
48
+ method: string
49
+ ) => {
50
+ return $sdk.auth.getAccessTokenByOTP(code, phone, method)
51
+ }
52
+
53
+ // ==========================
54
+ // USER LOGIN
55
+ // ==========================
56
+
57
+ const getUserLoginByUserLoginId = (userLoginId: string) => {
58
+ return $sdk.auth.getUserLoginByUserLoginId(userLoginId)
59
+ }
60
+
61
+ const checkUsernameExisted = (username: string) => {
62
+ return $sdk.auth.checkUsernameExisted(username)
63
+ }
64
+
65
+ const createUserLogin = (phone: string) => {
66
+ return $sdk.auth.createUserLogin(phone)
67
+ }
68
+
69
+ const linkingUserLoginAndUserDetail = (
70
+ userLoginId: string,
71
+ partyId: string
72
+ ) => {
73
+ return $sdk.auth.linkingUserLoginAndUserDetail(
74
+ userLoginId,
75
+ partyId
76
+ )
77
+ }
78
+
79
+ const createUserDetail = (userLoginId: string) => {
80
+ return $sdk.auth.createUserDetail(userLoginId)
81
+ }
82
+
83
+ // ==========================
84
+ // OTP
85
+ // ==========================
86
+
87
+ const sendOTP = (
88
+ phone: string,
89
+ method: "sms" | "zalo"
90
+ ) => {
91
+ return $sdk.auth.sendOTP(phone, method)
92
+ }
93
+
94
+ const validateOTP = (
95
+ code: string,
96
+ phone: string,
97
+ method: string
98
+ ) => {
99
+ return $sdk.auth.validateOTP(code, phone, method)
100
+ }
101
+
102
+ // ==========================
103
+ // RESET PASSWORD
104
+ // ==========================
105
+
106
+ const createResetKey = (userLoginId: string) => {
107
+ return $sdk.auth.createResetKey(userLoginId)
108
+ }
109
+
110
+ const checkResetKey = (key: string) => {
111
+ return $sdk.auth.checkResetKey(key)
112
+ }
113
+
114
+ const resetPassword = (
115
+ username: string,
116
+ newPassword: string,
117
+ accessToken: string
118
+ ) => {
119
+ return $sdk.auth.resetPassword(username, newPassword, accessToken)
120
+ }
121
+
122
+ // ==========================
123
+ // USER
124
+ // ==========================
125
+
126
+ const updateCustomer = (
127
+ partyId: string,
128
+ body: any,
129
+ updatedBy: string
130
+ ) => {
131
+ return $sdk.user.updateCustomer(
132
+ partyId,
133
+ body,
134
+ updatedBy
135
+ )
136
+ }
137
+
138
+ const updateProfile = (
139
+ userLoginId: string,
140
+ fullName: string,
141
+ phone: string,
142
+ email: string
143
+ ) => {
144
+ return $sdk.auth.updateProfile(
145
+ userLoginId,
146
+ fullName,
147
+ phone,
148
+ email
149
+ )
150
+ }
151
+
152
+ const getPhoneByPartyId = (partyId: string) => {
153
+ return $sdk.auth.getPhoneByPartyId(partyId)
154
+ }
155
+
156
+ // ==========================
157
+ // SOCIAL LOGIN
158
+ // ==========================
159
+
160
+ const loginGoogle = (redirectUrl: string, redirectUrlError: string, serviceOperator: string, appId: string, deviceId: string) => {
161
+ return $sdk.auth.loginGoogle(redirectUrl, redirectUrlError, serviceOperator, appId, deviceId)
162
+ }
163
+
164
+ const loginFacebook = (redirectUrl: string, redirectUrlError: string, serviceOperator: string, appId: string, deviceId: string) => {
165
+ return $sdk.auth.loginFacebook(redirectUrl, redirectUrlError, serviceOperator, appId, deviceId)
166
+ }
167
+
168
+ const loginZalo = (redirectUrl: string, redirectUrlError: string, serviceOperator: string, appId: string, deviceId: string) => {
169
+ return $sdk.auth.loginZalo(redirectUrl, redirectUrlError, serviceOperator, appId, deviceId)
170
+ }
171
+
172
+ // ==========================
173
+ // ORDER
174
+ // ==========================
175
+
176
+ const setOrderToken = (token: string) => {
177
+ return $sdk.order.setToken(token)
178
+ }
179
+
180
+ const findCampaignActionGiftVoucher = () => {
181
+ return $sdk.order.findCampaignActionGiftVoucher()
182
+ }
183
+
184
+ // ==========================
185
+ // CRM CAMPAIGN
186
+ // ==========================
187
+
188
+ const setCampaignToken = (token: string) => {
189
+ return $sdk.crm_camping.setToken(token)
190
+ }
191
+
192
+ const addCustomerToVoucher = (
193
+ userId: string,
194
+ voucherCode: string
195
+ ) => {
196
+ return $sdk.crm_camping.addCustomerToVoucher(
197
+ userId,
198
+ voucherCode
199
+ )
200
+ }
201
+
202
+ return {
203
+ // login / register
204
+ login,
205
+ register,
206
+ logout,
207
+ getUserByToken,
208
+ getUserLoginByToken,
209
+ updateInfo,
210
+ getAccessTokenByOTP,
211
+
212
+ // user login
213
+ getUserLoginByUserLoginId,
214
+ checkUsernameExisted,
215
+ createUserLogin,
216
+ linkingUserLoginAndUserDetail,
217
+ createUserDetail,
218
+
219
+ // otp
220
+ sendOTP,
221
+ validateOTP,
222
+
223
+ // reset
224
+ createResetKey,
225
+ checkResetKey,
226
+ resetPassword,
227
+
228
+ // user
229
+ updateCustomer,
230
+ updateProfile,
231
+ getPhoneByPartyId,
232
+
233
+ // social
234
+ loginGoogle,
235
+ loginFacebook,
236
+ loginZalo,
237
+
238
+ // voucher
239
+ setOrderToken,
240
+ findCampaignActionGiftVoucher,
241
+ setCampaignToken,
242
+ addCustomerToVoucher,
243
+ }
244
+ }
@@ -0,0 +1,16 @@
1
+ export const useAuthState = () => {
2
+ const isLoggedIn = useState('auth-logged-in', () => false)
3
+ const userName = useState('auth-user-name', () => '')
4
+
5
+ function setUser(name: string) {
6
+ isLoggedIn.value = true
7
+ userName.value = name
8
+ }
9
+
10
+ function clearUser() {
11
+ isLoggedIn.value = false
12
+ userName.value = ''
13
+ }
14
+
15
+ return { isLoggedIn, userName, setUser, clearUser }
16
+ }
@@ -0,0 +1,8 @@
1
+ export type ConsultationRequest = {
2
+ category: 'hci' | 'workshop' | 'training'
3
+ planId: string
4
+ planName: string
5
+ } | null
6
+
7
+ export const useConsultationRequest = () =>
8
+ useState<ConsultationRequest>('consultation-request', () => null)
@@ -0,0 +1,79 @@
1
+ /**
2
+ * Extract root domain from hostname.
3
+ * e.g. "dev.longvan.net" → ".longvan.net"
4
+ * "longvan.net" → ".longvan.net"
5
+ * "localhost" → "localhost"
6
+ */
7
+ function getCookieDomain(): string {
8
+ if (typeof window === 'undefined') return ''
9
+ const hostname = window.location.hostname
10
+
11
+ // localhost / IP → không set domain (browser tự xử lý)
12
+ if (hostname === 'localhost' || /^\d{1,3}(\.\d{1,3}){3}$/.test(hostname)) {
13
+ return hostname
14
+ }
15
+
16
+ const parts = hostname.split('.')
17
+ // Lấy 2 phần cuối (root domain): longvan.net, example.com, ...
18
+ if (parts.length >= 2) {
19
+ return '.' + parts.slice(-2).join('.')
20
+ }
21
+ return hostname
22
+ }
23
+
24
+ /**
25
+ * Cookie helper composable — set, get, delete cookies theo domain hiện tại.
26
+ *
27
+ * Usage:
28
+ * const { setCookie, getCookie, deleteCookie } = useCookie()
29
+ * setCookie('token', 'abc123', 7) // expires in 7 days
30
+ * getCookie('token') // → 'abc123'
31
+ * deleteCookie('token')
32
+ */
33
+ export function useCookie() {
34
+ /**
35
+ * Lưu cookie.
36
+ * @param key - Tên cookie
37
+ * @param value - Giá trị cookie
38
+ * @param days - Số ngày hết hạn (mặc định 365)
39
+ */
40
+ function setCookie(key: string, value: string, days = 365): void {
41
+ const date = new Date()
42
+ date.setTime(date.getTime() + days * 24 * 60 * 60 * 1000)
43
+ const expires = 'expires=' + date.toUTCString()
44
+ const domain = getCookieDomain()
45
+ const domainPart = domain ? `; domain=${domain}` : ''
46
+ document.cookie = `${key}=${encodeURIComponent(value)}; ${expires}; path=/${domainPart}`
47
+ }
48
+
49
+ /**
50
+ * Lấy giá trị cookie theo key.
51
+ * @param key - Tên cookie
52
+ * @returns Giá trị cookie hoặc null nếu không tìm thấy
53
+ */
54
+ function getCookie(key: string): string | null {
55
+ const name = key + '='
56
+ const decodedCookie = decodeURIComponent(document.cookie)
57
+ const cookies = decodedCookie.split(';')
58
+
59
+ for (let cookie of cookies) {
60
+ cookie = cookie.trim()
61
+ if (cookie.startsWith(name)) {
62
+ return cookie.substring(name.length)
63
+ }
64
+ }
65
+ return null
66
+ }
67
+
68
+ /**
69
+ * Xóa cookie theo key.
70
+ * @param key - Tên cookie cần xóa
71
+ */
72
+ function deleteCookie(key: string): void {
73
+ const domain = getCookieDomain()
74
+ const domainPart = domain ? `; domain=${domain}` : ''
75
+ document.cookie = `${key}=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/${domainPart}`
76
+ }
77
+
78
+ return { setCookie, getCookie, deleteCookie }
79
+ }