@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.
@@ -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 config = useRuntimeConfig();
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 config = useRuntimeConfig();
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 config = useRuntimeConfig();
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 { ref, computed } from 'vue';
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: config.public.apiBase || 'http://localhost:3001/api',
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: config.public.apiBase || 'http://localhost:3001/api',
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: config.public.apiBase || 'http://localhost:3001/api',
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: config.public.apiBase || 'http://localhost:3001/api',
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: config.public.apiBase || 'http://localhost:3001/api',
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: config.public.apiBase || 'http://localhost:3001/api',
110
+ baseURL: getApiBaseUrl(),
110
111
  params,
111
112
  });
112
113
 
@@ -1,5 +1,6 @@
1
- import { ref, computed } from 'vue';
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
- // Get subdomain from current host or route
66
+ // Отримати subdomain з host працює і на SSR, і на клієнті
66
67
  const getSubdomain = () => {
67
- if (process.client) {
68
- const hostname = window.location.hostname;
69
- const parts = hostname.split('.');
70
- // Assuming format: subdomain.domain.com
71
- if (parts.length >= 3) {
72
- return parts[0];
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: config.public.apiBase || 'http://localhost:3001/api',
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 (process.client && colors) {
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 (process.client && fonts) {
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 (process.client && customCSS) {
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 (process.client) {
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@crm-market/template-shared",
3
- "version": "1.0.4",
3
+ "version": "1.0.6",
4
4
  "type": "module",
5
5
  "description": "Shared Nuxt 3 layer for CRM Market store templates (layouts, components, composables, pages)",
6
6
  "publishConfig": {
@@ -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 config = useRuntimeConfig();
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
- });