@crm-market/template-shared 1.0.4 → 1.0.6
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.
- package/components/Layout/MiddleHeader.vue +1 -2
- package/components/Layout/MiddleHeaderThree.vue +1 -2
- package/components/Layout/MiddleHeaderTwo.vue +1 -2
- package/composables/useCategories.ts +12 -7
- package/composables/useCheckout.ts +2 -2
- package/composables/useProducts.ts +4 -3
- package/composables/useSiteConfig.ts +32 -20
- package/nuxt.config.ts +2 -0
- package/package.json +1 -1
- package/plugins/site-init.ts +33 -0
- package/utils/api.ts +17 -0
- package/utils/image.ts +2 -2
- package/plugins/site-init.client.ts +0 -29
|
@@ -185,8 +185,7 @@ export default defineComponent({
|
|
|
185
185
|
const resolveImageUrl = (url: string) => {
|
|
186
186
|
if (!url) return '';
|
|
187
187
|
if (url.startsWith('http')) return url;
|
|
188
|
-
const
|
|
189
|
-
const apiBase = config.public.apiBase || 'http://localhost:3001/api';
|
|
188
|
+
const apiBase = getApiBaseUrl();
|
|
190
189
|
const baseUrl = apiBase.replace('/api', '');
|
|
191
190
|
return `${baseUrl}${url}`;
|
|
192
191
|
};
|
|
@@ -167,8 +167,7 @@ export default defineComponent({
|
|
|
167
167
|
const resolveImageUrl = (url: string) => {
|
|
168
168
|
if (!url) return '';
|
|
169
169
|
if (url.startsWith('http')) return url;
|
|
170
|
-
const
|
|
171
|
-
const apiBase = config.public.apiBase || 'http://localhost:3001/api';
|
|
170
|
+
const apiBase = getApiBaseUrl();
|
|
172
171
|
const baseUrl = apiBase.replace('/api', '');
|
|
173
172
|
return `${baseUrl}${url}`;
|
|
174
173
|
};
|
|
@@ -195,8 +195,7 @@ export default defineComponent({
|
|
|
195
195
|
const resolveImageUrl = (url: string) => {
|
|
196
196
|
if (!url) return '';
|
|
197
197
|
if (url.startsWith('http')) return url;
|
|
198
|
-
const
|
|
199
|
-
const apiBase = config.public.apiBase || 'http://localhost:3001/api';
|
|
198
|
+
const apiBase = getApiBaseUrl();
|
|
200
199
|
const baseUrl = apiBase.replace('/api', '');
|
|
201
200
|
return `${baseUrl}${url}`;
|
|
202
201
|
};
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { useRuntimeConfig } from '#imports';
|
|
1
|
+
import { computed } from 'vue';
|
|
2
|
+
import { useState, useRuntimeConfig } from '#imports';
|
|
3
|
+
import { getApiBaseUrl } from '../utils/api';
|
|
3
4
|
import { useSiteConfig } from './useSiteConfig';
|
|
4
5
|
|
|
5
6
|
interface Category {
|
|
@@ -14,16 +15,20 @@ interface Category {
|
|
|
14
15
|
order?: number;
|
|
15
16
|
}
|
|
16
17
|
|
|
17
|
-
const categories = ref<Category[]>([]);
|
|
18
|
-
const loading = ref(false);
|
|
19
|
-
const error = ref<string | null>(null);
|
|
20
|
-
|
|
21
18
|
export const useCategories = () => {
|
|
19
|
+
// useState — SSR-safe, дані передаються клієнту через гідрацію
|
|
20
|
+
const categories = useState<Category[]>('categories', () => []);
|
|
21
|
+
const loading = useState<boolean>('categories-loading', () => false);
|
|
22
|
+
const error = useState<string | null>('categories-error', () => null);
|
|
23
|
+
|
|
22
24
|
const config = useRuntimeConfig();
|
|
23
25
|
const { publicApiToken } = useSiteConfig();
|
|
24
26
|
|
|
25
27
|
// Fetch all categories
|
|
26
28
|
const fetchCategories = async () => {
|
|
29
|
+
// Якщо вже завантажено — не перезавантажуємо
|
|
30
|
+
if (categories.value.length > 0) return categories.value;
|
|
31
|
+
|
|
27
32
|
loading.value = true;
|
|
28
33
|
error.value = null;
|
|
29
34
|
|
|
@@ -34,7 +39,7 @@ export const useCategories = () => {
|
|
|
34
39
|
}
|
|
35
40
|
|
|
36
41
|
const response = await $fetch<{ categories: Category[] }>('/site-template/public/store/categories', {
|
|
37
|
-
baseURL:
|
|
42
|
+
baseURL: getApiBaseUrl(),
|
|
38
43
|
params: { token },
|
|
39
44
|
});
|
|
40
45
|
|
|
@@ -65,7 +65,7 @@ export const useCheckout = () => {
|
|
|
65
65
|
|
|
66
66
|
const response = await $fetch<{ id: string }>('/order', {
|
|
67
67
|
method: 'POST',
|
|
68
|
-
baseURL:
|
|
68
|
+
baseURL: getApiBaseUrl(),
|
|
69
69
|
body: orderData,
|
|
70
70
|
});
|
|
71
71
|
|
|
@@ -88,7 +88,7 @@ export const useCheckout = () => {
|
|
|
88
88
|
|
|
89
89
|
try {
|
|
90
90
|
const response = await $fetch(`/order/${id}`, {
|
|
91
|
-
baseURL:
|
|
91
|
+
baseURL: getApiBaseUrl(),
|
|
92
92
|
});
|
|
93
93
|
|
|
94
94
|
return response;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { ref, computed } from 'vue';
|
|
2
2
|
import { useRuntimeConfig } from '#imports';
|
|
3
|
+
import { getApiBaseUrl } from '../utils/api';
|
|
3
4
|
import { useSiteConfig } from './useSiteConfig';
|
|
4
5
|
|
|
5
6
|
interface Product {
|
|
@@ -52,7 +53,7 @@ export const useProducts = () => {
|
|
|
52
53
|
};
|
|
53
54
|
|
|
54
55
|
const response = await $fetch<{ products: Product[]; total: number }>('/site-template/public/store/products', {
|
|
55
|
-
baseURL:
|
|
56
|
+
baseURL: getApiBaseUrl(),
|
|
56
57
|
params,
|
|
57
58
|
});
|
|
58
59
|
|
|
@@ -79,7 +80,7 @@ export const useProducts = () => {
|
|
|
79
80
|
}
|
|
80
81
|
|
|
81
82
|
const response = await $fetch<{ product: Product }>(`/site-template/public/store/products/${productId}`, {
|
|
82
|
-
baseURL:
|
|
83
|
+
baseURL: getApiBaseUrl(),
|
|
83
84
|
params: { token },
|
|
84
85
|
});
|
|
85
86
|
|
|
@@ -106,7 +107,7 @@ export const useProducts = () => {
|
|
|
106
107
|
};
|
|
107
108
|
|
|
108
109
|
const response = await $fetch<{ products: Product[]; total: number }>('/site-template/public/store/products', {
|
|
109
|
-
baseURL:
|
|
110
|
+
baseURL: getApiBaseUrl(),
|
|
110
111
|
params,
|
|
111
112
|
});
|
|
112
113
|
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { useRuntimeConfig, useRoute } from '#imports';
|
|
1
|
+
import { computed } from 'vue';
|
|
2
|
+
import { useState, useRuntimeConfig, useRoute, useRequestHeaders } from '#imports';
|
|
3
|
+
import { getApiBaseUrl } from '../utils/api';
|
|
3
4
|
|
|
4
5
|
interface ColorScheme {
|
|
5
6
|
primary?: string;
|
|
@@ -54,29 +55,40 @@ interface SiteConfig {
|
|
|
54
55
|
};
|
|
55
56
|
}
|
|
56
57
|
|
|
57
|
-
const siteConfig = ref<SiteConfig | null>(null);
|
|
58
|
-
const loading = ref(false);
|
|
59
|
-
const error = ref<string | null>(null);
|
|
60
|
-
|
|
61
58
|
export const useSiteConfig = () => {
|
|
59
|
+
// useState — SSR-safe, дані передаються клієнту через гідрацію
|
|
60
|
+
const siteConfig = useState<SiteConfig | null>('site-config', () => null);
|
|
61
|
+
const loading = useState<boolean>('site-config-loading', () => false);
|
|
62
|
+
const error = useState<string | null>('site-config-error', () => null);
|
|
63
|
+
|
|
62
64
|
const config = useRuntimeConfig();
|
|
63
|
-
const route = useRoute();
|
|
64
65
|
|
|
65
|
-
//
|
|
66
|
+
// Отримати subdomain з host — працює і на SSR, і на клієнті
|
|
66
67
|
const getSubdomain = () => {
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
//
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
68
|
+
let hostname = '';
|
|
69
|
+
|
|
70
|
+
if (import.meta.server) {
|
|
71
|
+
// SSR: отримуємо host з request headers
|
|
72
|
+
const headers = useRequestHeaders(['host', 'x-forwarded-host']);
|
|
73
|
+
hostname = (headers['x-forwarded-host'] || headers['host'] || '').split(':')[0];
|
|
74
|
+
} else {
|
|
75
|
+
// Client: з window.location
|
|
76
|
+
hostname = window.location.hostname;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const parts = hostname.split('.');
|
|
80
|
+
// Формат: subdomain.domain.com або subdomain.domain.com.ua
|
|
81
|
+
if (parts.length >= 3) {
|
|
82
|
+
return parts[0];
|
|
74
83
|
}
|
|
75
84
|
return null;
|
|
76
85
|
};
|
|
77
86
|
|
|
78
87
|
// Fetch site configuration from API
|
|
79
88
|
const fetchConfig = async (subdomain?: string) => {
|
|
89
|
+
// Якщо вже завантажено — не перезавантажуємо
|
|
90
|
+
if (siteConfig.value && !subdomain) return siteConfig.value;
|
|
91
|
+
|
|
80
92
|
loading.value = true;
|
|
81
93
|
error.value = null;
|
|
82
94
|
|
|
@@ -89,7 +101,7 @@ export const useSiteConfig = () => {
|
|
|
89
101
|
const response = await $fetch<SiteConfig>(
|
|
90
102
|
`/site-template/public/config/subdomain/${sub}`,
|
|
91
103
|
{
|
|
92
|
-
baseURL:
|
|
104
|
+
baseURL: getApiBaseUrl(),
|
|
93
105
|
}
|
|
94
106
|
);
|
|
95
107
|
|
|
@@ -112,7 +124,7 @@ export const useSiteConfig = () => {
|
|
|
112
124
|
|
|
113
125
|
const { colors, fonts, customCSS } = siteConfig.value.customization;
|
|
114
126
|
|
|
115
|
-
if (
|
|
127
|
+
if (import.meta.client && colors) {
|
|
116
128
|
const root = document.documentElement;
|
|
117
129
|
|
|
118
130
|
if (colors.primary) {
|
|
@@ -131,7 +143,7 @@ export const useSiteConfig = () => {
|
|
|
131
143
|
}
|
|
132
144
|
}
|
|
133
145
|
|
|
134
|
-
if (
|
|
146
|
+
if (import.meta.client && fonts) {
|
|
135
147
|
const root = document.documentElement;
|
|
136
148
|
|
|
137
149
|
if (fonts.headingFont) {
|
|
@@ -149,7 +161,7 @@ export const useSiteConfig = () => {
|
|
|
149
161
|
}
|
|
150
162
|
|
|
151
163
|
// Inject custom CSS
|
|
152
|
-
if (
|
|
164
|
+
if (import.meta.client && customCSS) {
|
|
153
165
|
let styleEl = document.getElementById('custom-site-css');
|
|
154
166
|
if (!styleEl) {
|
|
155
167
|
styleEl = document.createElement('style');
|
|
@@ -167,7 +179,7 @@ export const useSiteConfig = () => {
|
|
|
167
179
|
const { siteName, siteDescription, logoUrl } = siteConfig.value;
|
|
168
180
|
const seo = siteConfig.value.customization?.content?.seo;
|
|
169
181
|
|
|
170
|
-
if (
|
|
182
|
+
if (import.meta.client) {
|
|
171
183
|
if (siteName) {
|
|
172
184
|
document.title = siteName;
|
|
173
185
|
}
|
package/nuxt.config.ts
CHANGED
|
@@ -21,6 +21,8 @@ export default defineNuxtConfig({
|
|
|
21
21
|
},
|
|
22
22
|
|
|
23
23
|
runtimeConfig: {
|
|
24
|
+
// SSR-only: внутрішній URL бекенду (Docker мережа або localhost)
|
|
25
|
+
apiBaseInternal: process.env.API_BASE_INTERNAL || '',
|
|
24
26
|
public: {
|
|
25
27
|
apiBase: process.env.API_BASE_URL || 'http://localhost:3001/api',
|
|
26
28
|
templateType: process.env.TEMPLATE_TYPE || 'electronics',
|
package/package.json
CHANGED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { defineNuxtPlugin } from '#app';
|
|
2
|
+
import { useSiteConfig } from '../composables/useSiteConfig';
|
|
3
|
+
import { useCategories } from '../composables/useCategories';
|
|
4
|
+
|
|
5
|
+
export default defineNuxtPlugin(async () => {
|
|
6
|
+
const { fetchConfig, siteConfig, error, applyCustomization } = useSiteConfig();
|
|
7
|
+
const { fetchCategories } = useCategories();
|
|
8
|
+
|
|
9
|
+
// SSR: завантажуємо конфіг і категорії — дані передаються клієнту через useState
|
|
10
|
+
// Client: якщо дані вже є від SSR (гідрація) — fetchConfig пропускає повторний запит
|
|
11
|
+
try {
|
|
12
|
+
await fetchConfig();
|
|
13
|
+
await fetchCategories();
|
|
14
|
+
} catch (e) {
|
|
15
|
+
console.error('Failed to initialize site:', e);
|
|
16
|
+
|
|
17
|
+
if (import.meta.client && error.value) {
|
|
18
|
+
console.error('Site configuration error:', error.value);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// Client-only: застосувати CSS кастомізацію після гідрації
|
|
23
|
+
if (import.meta.client && siteConfig.value) {
|
|
24
|
+
applyCustomization();
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// Client-only: завантажити кошик з localStorage
|
|
28
|
+
if (import.meta.client) {
|
|
29
|
+
const { useCart } = await import('../composables/useCart');
|
|
30
|
+
const { loadCart } = useCart();
|
|
31
|
+
loadCart();
|
|
32
|
+
}
|
|
33
|
+
});
|
package/utils/api.ts
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { useRuntimeConfig } from '#imports';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Повертає базовий URL для API-запитів.
|
|
5
|
+
* На сервері (SSR) використовує внутрішній URL Docker-мережі (якщо задано),
|
|
6
|
+
* на клієнті — завжди публічний URL.
|
|
7
|
+
*/
|
|
8
|
+
export const getApiBaseUrl = (): string => {
|
|
9
|
+
const config = useRuntimeConfig();
|
|
10
|
+
|
|
11
|
+
// На сервері: використовуємо внутрішній URL якщо він задано
|
|
12
|
+
if (import.meta.server && config.apiBaseInternal) {
|
|
13
|
+
return config.apiBaseInternal as string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
return (config.public.apiBase as string) || 'http://localhost:3001/api';
|
|
17
|
+
};
|
package/utils/image.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { useRuntimeConfig } from '#imports';
|
|
2
|
+
import { getApiBaseUrl } from './api';
|
|
2
3
|
|
|
3
4
|
/**
|
|
4
5
|
* Перетворює відносні шляхи до зображень (/media/...) у повні URL
|
|
@@ -7,8 +8,7 @@ export const resolveImageUrl = (url: string | undefined | null): string => {
|
|
|
7
8
|
if (!url) return '';
|
|
8
9
|
if (url.startsWith('http') || url.startsWith('data:') || url.startsWith('blob:')) return url;
|
|
9
10
|
|
|
10
|
-
const
|
|
11
|
-
const apiBase = (config.public.apiBase as string) || 'http://localhost:3001/api';
|
|
11
|
+
const apiBase = getApiBaseUrl();
|
|
12
12
|
// Видаляємо /api з кінця, щоб отримати базовий URL сервера
|
|
13
13
|
const baseUrl = apiBase.replace(/\/api\/?$/, '');
|
|
14
14
|
return `${baseUrl}${url.startsWith('/') ? '' : '/'}${url}`;
|
|
@@ -1,29 +0,0 @@
|
|
|
1
|
-
import { defineNuxtPlugin } from '#app';
|
|
2
|
-
import { useSiteConfig } from '../composables/useSiteConfig';
|
|
3
|
-
import { useCart } from '../composables/useCart';
|
|
4
|
-
import { useCategories } from '../composables/useCategories';
|
|
5
|
-
|
|
6
|
-
export default defineNuxtPlugin(async () => {
|
|
7
|
-
const { fetchConfig, error } = useSiteConfig();
|
|
8
|
-
const { loadCart } = useCart();
|
|
9
|
-
const { fetchCategories } = useCategories();
|
|
10
|
-
|
|
11
|
-
// Initialize cart from localStorage
|
|
12
|
-
loadCart();
|
|
13
|
-
|
|
14
|
-
// Fetch site configuration
|
|
15
|
-
try {
|
|
16
|
-
await fetchConfig();
|
|
17
|
-
|
|
18
|
-
// Load categories after config is loaded
|
|
19
|
-
await fetchCategories();
|
|
20
|
-
} catch (e) {
|
|
21
|
-
console.error('Failed to initialize site:', e);
|
|
22
|
-
|
|
23
|
-
// If site config fails to load, redirect to error page or show message
|
|
24
|
-
if (process.client && error.value) {
|
|
25
|
-
// You can customize this behavior
|
|
26
|
-
console.error('Site configuration error:', error.value);
|
|
27
|
-
}
|
|
28
|
-
}
|
|
29
|
-
});
|