@lilaquadrat/design-core 0.2.2 → 0.3.0

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 (78) hide show
  1. package/README.md +71 -3
  2. package/html/index.server.html +1 -8
  3. package/package.json +53 -53
  4. package/src/client-entry.ts +8 -8
  5. package/src/components/partials/error.partial.vue +19 -19
  6. package/src/components/partials/main-components.partial.vue +75 -70
  7. package/src/components/partials/qrcode.partial.vue +8 -8
  8. package/src/functions/AuthProviderFactory.ts +30 -0
  9. package/src/functions/PaymenyProviderFactory.ts +11 -11
  10. package/src/functions/auth0.provider.ts +75 -0
  11. package/src/functions/keycloak.provider.ts +228 -0
  12. package/src/functions/lila-dom.ts +50 -50
  13. package/src/functions/shopify.provider.ts +176 -154
  14. package/src/functions/stripe.provider.ts +120 -108
  15. package/src/globals.d.ts +1 -2
  16. package/src/index.ts +9 -7
  17. package/src/interfaces/AuthCallbackResult.interface.ts +5 -0
  18. package/src/interfaces/AuthConfig.interface.ts +13 -0
  19. package/src/interfaces/AuthProvider.interface.ts +15 -0
  20. package/src/interfaces/FrontendConfig.interface.ts +11 -40
  21. package/src/interfaces/GenericEvents.interface.ts +1 -1
  22. package/src/interfaces/IdTokenExtended.interface.ts +13 -4
  23. package/src/interfaces/PaymentProvider.interface.ts +2 -2
  24. package/src/interfaces/RenderOptions.interface.ts +12 -0
  25. package/src/libs/ActionNotice.ts +125 -125
  26. package/src/libs/Models.class.ts +219 -221
  27. package/src/main.ts +41 -36
  28. package/src/mixins/createCookieString.ts +50 -36
  29. package/src/mixins/createModelDeclaration.ts +22 -19
  30. package/src/mixins/createRouter.ts +12 -12
  31. package/src/mixins/date.ts +4 -2
  32. package/src/mixins/formatSize.ts +5 -5
  33. package/src/mixins/getAnchor.ts +1 -1
  34. package/src/mixins/getRoutes.ts +13 -25
  35. package/src/mixins/hasSlotContent.ts +22 -16
  36. package/src/mixins/hooks.ts +101 -60
  37. package/src/mixins/loadComponents.ts +60 -58
  38. package/src/mixins/logger.ts +17 -13
  39. package/src/mixins/replaceVariables.ts +10 -8
  40. package/src/mixins/scroll.ts +61 -45
  41. package/src/models/Address.model.ts +7 -7
  42. package/src/models/Contact.model.ts +14 -14
  43. package/src/plugins/analytics.ts +198 -0
  44. package/src/plugins/auth.ts +74 -60
  45. package/src/plugins/currency.ts +10 -10
  46. package/src/plugins/events.ts +101 -97
  47. package/src/plugins/filters.ts +21 -18
  48. package/src/plugins/replacer.ts +7 -6
  49. package/src/plugins/signupFlow.ts +45 -43
  50. package/src/plugins/traceable.ts +28 -26
  51. package/src/plugins/translations.ts +9 -9
  52. package/src/plugins/youtube.ts +39 -34
  53. package/src/routes.ts +108 -108
  54. package/src/server-entry.ts +75 -77
  55. package/src/stores/calls.store.ts +14 -14
  56. package/src/stores/cart.store.ts +116 -107
  57. package/src/stores/content.store.ts +87 -45
  58. package/src/stores/editor.store.ts +16 -16
  59. package/src/stores/files.store.ts +72 -72
  60. package/src/stores/main.store.ts +155 -109
  61. package/src/stores/user.store.ts +72 -68
  62. package/src/translations/de.ts +127 -127
  63. package/src/views/content.view.vue +81 -143
  64. package/src/views/download.view.vue +25 -23
  65. package/src/views/editor.view.vue +150 -125
  66. package/src/views/login.view.vue +35 -36
  67. package/src/views/signup-account.view.vue +14 -14
  68. package/tooling/cypress/config.ts +68 -54
  69. package/tooling/cypress/manifest-utils.mjs +34 -22
  70. package/tooling/cypress/run-parallel.mjs +41 -35
  71. package/tooling/cypress/support/commands.ts +458 -300
  72. package/tooling/eslint.config.js +1 -223
  73. package/tooling/preview.plugin.js +68 -68
  74. package/tooling/ssr-test/index.mjs +205 -203
  75. package/tooling/vite.d.ts +0 -6
  76. package/tooling/vite.js +170 -162
  77. package/src/interfaces/EditorConfiguration.interface.ts +0 -15
  78. package/tooling/stylelint.config.mjs +0 -24
@@ -1,107 +1,109 @@
1
1
  import { defineAsyncComponent, type App, type Component, type AsyncComponentLoader } from 'vue';
2
2
 
3
- function getName (name: string, type: 'module' | 'partial', namespace?: string) {
3
+ function getName(name: string, type: 'module' | 'partial', namespace?: string) {
4
4
 
5
- return namespace ? `${namespace}-${name}-${type}` : `${name}-${type}`;
5
+ return namespace ? `${namespace}-${name}-${type}` : `${name}-${type}`;
6
6
 
7
7
  }
8
8
 
9
- function loadViaGlob (components: Record<string, Record<'default', Component>>, namespace: string | undefined, app: App<Element>) {
9
+ function loadViaGlob(components: Record<string, Record<'default', Component>>, namespace: string | undefined, app: App<Element>) {
10
10
 
11
- Object.entries(components).forEach(([filename, definition]) => {
11
+ Object.entries(components).forEach(([filename, definition]) => {
12
12
 
13
- // Get name of component, based on filename
14
- // "./components/Fruits.vue" will become "Fruits"
15
- const matches = filename.match(/([a-z-]+)\.(module|partial).(vue|ts)$/i);
13
+ // Get name of component, based on filename
14
+ // "./components/Fruits.vue" will become "Fruits"
15
+ const matches = filename.match(/([a-z-]+)\.(module|partial).(vue|ts)$/i);
16
16
 
17
- if (matches) {
17
+ if (matches) {
18
18
 
19
- const componentName = namespace ? `${namespace}-${matches[1]}-${matches[2]}` : `${matches[1]}-${matches[2]}`;
19
+ const componentName = namespace ? `${namespace}-${matches[1]}-${matches[2]}` : `${matches[1]}-${matches[2]}`;
20
20
 
21
- // Register component on this Vue instance
22
- app.component(componentName, definition.default);
23
- }
24
- });
21
+ // Register component on this Vue instance
22
+ app.component(componentName, definition.default);
23
+
24
+ }
25
+
26
+ });
25
27
 
26
28
  }
27
29
 
28
- function loadViaDeclaration (components: { name: string, component: AsyncComponentLoader<Component> }[], namespace: string | undefined, type: 'module' | 'partial', app: App<Element>) {
30
+ function loadViaDeclaration(components: { name: string, component: AsyncComponentLoader<Component> }[], namespace: string | undefined, type: 'module' | 'partial', app: App<Element>) {
29
31
 
30
- components.forEach((single) => {
32
+ components.forEach((single) => {
31
33
 
32
- const name = getName(single.name, type, namespace);
34
+ const name = getName(single.name, type, namespace);
33
35
 
34
- app.component(name, defineAsyncComponent(single.component));
36
+ app.component(name, defineAsyncComponent(single.component));
35
37
 
36
- });
38
+ });
37
39
 
38
40
  }
39
41
 
40
- function loadViaDeclarationSync (components: { name: string, component: Component }[], namespace: string | undefined, type: 'module' | 'partial', app: App<Element>) {
42
+ function loadViaDeclarationSync(components: { name: string, component: Component }[], namespace: string | undefined, type: 'module' | 'partial', app: App<Element>) {
41
43
 
42
- return components.map(async (single) => {
44
+ return components.map(async (single) => {
43
45
 
44
- const name = getName(single.name, type, namespace);
46
+ const name = getName(single.name, type, namespace);
45
47
 
46
- app.component(name, single.component);
48
+ app.component(name, single.component);
47
49
 
48
- });
50
+ });
49
51
 
50
52
  }
51
53
 
52
- function getAvailableModules (modules: any[]) {
54
+ function getAvailableModules(modules: any[]) {
53
55
 
54
- return modules.filter((single) => single.availableInEditor !== false).map((single) => ({
55
- name : `${single.name}-module`,
56
- variants : single.variants || [],
57
- editor : single.editor || {},
58
- additionalData: single.additionalData || {}
59
- }));
56
+ return modules.filter((single) => single.availableInEditor !== false).map((single) => ({
57
+ name: `${single.name}-module`,
58
+ variants: single.variants || [],
59
+ editor: single.editor || {},
60
+ additionalData: single.additionalData || {}
61
+ }));
60
62
 
61
63
  }
62
64
 
63
- function createModulesArray (modules: any[]) {
65
+ function createModulesArray(modules: any[]) {
64
66
 
65
- const modulesArray: any[] = [];
67
+ const modulesArray: any[] = [];
66
68
 
67
- modules.forEach((singleModule) => {
69
+ modules.forEach((singleModule) => {
68
70
 
69
- if (!singleModule.target) return;
71
+ if (!singleModule.target) return;
70
72
 
71
- const foundModule = modulesArray.find((single) => single.name === singleModule.name);
72
- const useModule = foundModule
73
- ? foundModule
74
- : {
75
- name : singleModule.name,
76
- targets: {}
77
- }
73
+ const foundModule = modulesArray.find((single) => single.name === singleModule.name);
74
+ const useModule = foundModule
75
+ ? foundModule
76
+ : {
77
+ name: singleModule.name,
78
+ targets: {}
79
+ }
78
80
 
79
- singleModule.target.forEach((target: string) => {
81
+ singleModule.target.forEach((target: string) => {
80
82
 
81
- useModule.targets[target] = {
82
- variants : singleModule.variants,
83
- components: singleModule.component,
84
- name : singleModule.name
85
- };
83
+ useModule.targets[target] = {
84
+ variants: singleModule.variants,
85
+ components: singleModule.component,
86
+ name: singleModule.name
87
+ };
86
88
 
87
- });
89
+ });
88
90
 
89
- if (!foundModule) {
91
+ if (!foundModule) {
90
92
 
91
- modulesArray.push(useModule);
93
+ modulesArray.push(useModule);
92
94
 
93
- }
95
+ }
94
96
 
95
- });
97
+ });
96
98
 
97
- return modulesArray;
99
+ return modulesArray;
98
100
 
99
101
  }
100
102
 
101
103
  export {
102
- loadViaGlob,
103
- loadViaDeclaration,
104
- loadViaDeclarationSync,
105
- getAvailableModules,
106
- createModulesArray,
104
+ loadViaGlob,
105
+ loadViaDeclaration,
106
+ loadViaDeclarationSync,
107
+ getAvailableModules,
108
+ createModulesArray,
107
109
  }
@@ -8,25 +8,29 @@ interface Logger {
8
8
 
9
9
  // Predefined colors for categories, if needed
10
10
  const categoryColors: { [key: string]: string } = {
11
- plugins: 'background-color: #3f2d56; color: #FFF',
12
- startup: 'background-color: #5A8C99; color: #FFF',
13
- auth : 'background-color: #5A8C99; color: #FFF',
14
- // Add other predefined categories and colors as needed
11
+ plugins: 'background-color: #3f2d56; color: #FFF',
12
+ startup: 'background-color: #5A8C99; color: #FFF',
13
+ auth: 'background-color: #5A8C99; color: #FFF',
14
+ // Add other predefined categories and colors as needed
15
15
  };
16
16
  const logger = new Proxy<Logger>({}, {
17
17
 
18
- get: function (target, propKey, receiver) {
19
- // Method to log messages with dynamic category-based styling and optional color for the category
20
- const log = (category: string, message: string, color?: string): void => {
21
- const categoryStyle = color ? `color: ${color}` : categoryColors[category.toLowerCase()] || 'color: #FFF';
18
+ get: function(target, propKey, receiver) {
22
19
 
23
- // Log the category with style and message without style
20
+ // Method to log messages with dynamic category-based styling and optional color for the category
21
+ const log = (category: string, message: string, color?: string): void => {
24
22
 
25
- console.log(`%c[FRONTEND]%c%c[${category.toUpperCase()}]%c ${message}`, 'background-color: #3f2d56;', 'padding: 5px', categoryStyle, 'color: inherit');
26
- };
23
+ const categoryStyle = color ? `color: ${color}` : categoryColors[category.toLowerCase()] || 'color: #FFF';
27
24
 
28
- return (message: string, color?: string) => log(propKey.toString(), message, color);
29
- }
25
+ // Log the category with style and message without style
26
+
27
+ console.log(`%c[FRONTEND]%c%c[${category.toUpperCase()}]%c ${message}`, 'background-color: #3f2d56;', 'padding: 5px', categoryStyle, 'color: inherit');
28
+
29
+ };
30
+
31
+ return (message: string, color?: string) => log(propKey.toString(), message, color);
32
+
33
+ }
30
34
  });
31
35
 
32
36
  export default logger;
@@ -1,19 +1,21 @@
1
- import type { Customers, List, Me } from '@lilaquadrat/interfaces';
1
+ import type { Cart, Customers, List, Me } from '@lilaquadrat/interfaces';
2
2
  import objectPath from 'object-path';
3
3
  import useDate from './date';
4
4
  import 'dayjs/locale/de';
5
5
 
6
6
  const dateVariables = ['list.start', 'list.end'];
7
7
 
8
- export default (template?: string, variables?: Record<string, string | undefined | Record<string, string> | Customers | Me | List>) => template?.replace(/\$([\w.]+)/g, (match, name) => {
9
- // Check if the placeholder's name exists in the variables object
10
- if(!variables) return '';
8
+ export default (template?: string, variables?: Record<string, string | undefined | Record<string, string> | Customers | Me | List | Cart>) => template?.replace(/\$([\w.]+)/g, (match, name) => {
11
9
 
12
- const returnString = objectPath.get(variables as Record<string, string>, name);
10
+ // Check if the placeholder's name exists in the variables object
11
+ if(!variables) return '';
13
12
 
14
- if(!returnString) return '';
13
+ const returnString = objectPath.get(variables as Record<string, string>, name);
15
14
 
16
- if(dateVariables.includes(name)) return useDate(returnString).locale('de').format('DD.MM.YYYY HH:mm');
15
+ if(!returnString) return '';
16
+
17
+ if(dateVariables.includes(name)) return useDate(returnString).locale('de').format('DD.MM.YYYY HH:mm');
18
+
19
+ return returnString;
17
20
 
18
- return returnString;
19
21
  });
@@ -10,74 +10,90 @@ interface ScrollPosition {
10
10
  const GENERAL_OFFSET = 20;
11
11
 
12
12
  // Helper function to calculate the total height of fixed or overlaying elements.
13
- function getFixedElementsOffset (): number {
14
- const selectors: string[] = ['.anchor-calculation'];
13
+ function getFixedElementsOffset(): number {
15
14
 
16
- return selectors.reduce((total, selector) => {
17
- const el = document.querySelector(selector) as HTMLElement | null;
15
+ const selectors: string[] = ['.anchor-calculation'];
16
+
17
+ return selectors.reduce((total, selector) => {
18
+
19
+ const el = document.querySelector(selector) as HTMLElement | null;
20
+
21
+ return el ? total + el.offsetHeight : total;
22
+
23
+ }, 0);
18
24
 
19
- return el ? total + el.offsetHeight : total;
20
- }, 0);
21
25
  }
22
26
 
23
27
  /**
24
28
  * scroll into view with calulated offset from fixed overlay elements
25
29
  */
26
- export function scrollToSelector (selector: string): Promise<void> {
27
- return new Promise((resolve) => {
28
- const targetEl = document.querySelector(selector) as HTMLElement | null;
29
-
30
- if (targetEl) {
31
- const offset = getFixedElementsOffset();
32
- const elementTop = targetEl.getBoundingClientRect().top + window.scrollY;
33
-
34
- window.scrollTo({
35
- left : 0,
36
- top : elementTop - offset - GENERAL_OFFSET,
37
- behavior: 'smooth'
38
- });
39
- }
30
+ export function scrollToSelector(selector: string): Promise<void> {
31
+
32
+ return new Promise((resolve) => {
33
+
34
+ const targetEl = document.querySelector(selector) as HTMLElement | null;
35
+
36
+ if (targetEl) {
37
+
38
+ const offset = getFixedElementsOffset();
39
+ const elementTop = targetEl.getBoundingClientRect().top + window.scrollY;
40
+
41
+ window.scrollTo({
42
+ left: 0,
43
+ top: elementTop - offset - GENERAL_OFFSET,
44
+ behavior: 'smooth'
45
+ });
46
+
47
+ }
48
+
49
+ resolve();
50
+
51
+ });
40
52
 
41
- resolve();
42
- });
43
53
  }
44
54
  /**
45
55
  * scroll into view with calulated offset from fixed overlay elements
46
56
  * usage for vue-router scrollBehavior
47
57
  */
48
58
  export const smartScrollBehavior: RouterScrollBehavior = (
49
- to: RouteLocationNormalized,
50
- _from: RouteLocationNormalized,
51
- savedPosition: ScrollPosition | null | undefined
59
+ to: RouteLocationNormalized,
60
+ _from: RouteLocationNormalized,
61
+ savedPosition: ScrollPosition | null | undefined
52
62
  ): ReturnType<RouterScrollBehavior> => {
53
- if (savedPosition) {
54
- return savedPosition;
55
- }
56
63
 
57
- if (to.hash) {
64
+ if (savedPosition) {
58
65
 
59
- return new Promise((resolve) => {
66
+ return savedPosition;
67
+
68
+ }
60
69
 
61
- const targetEl = document.querySelector(to.hash) as HTMLElement | null;
70
+ if (to.hash) {
62
71
 
63
- if (targetEl) {
72
+ return new Promise((resolve) => {
64
73
 
65
- const offset = getFixedElementsOffset();
66
- const elementTop = targetEl.getBoundingClientRect().top + window.scrollY;
74
+ const targetEl = document.querySelector(to.hash) as HTMLElement | null;
67
75
 
68
- resolve({
69
- left : 0,
70
- top : elementTop - offset - GENERAL_OFFSET,
71
- behavior: 'smooth'
72
- });
76
+ if (targetEl) {
73
77
 
74
- } else {
75
- resolve({ left: 0, top: 0 });
76
- }
78
+ const offset = getFixedElementsOffset();
79
+ const elementTop = targetEl.getBoundingClientRect().top + window.scrollY;
77
80
 
78
- });
81
+ resolve({
82
+ left: 0,
83
+ top: elementTop - offset - GENERAL_OFFSET,
84
+ behavior: 'smooth'
85
+ });
86
+
87
+ } else {
88
+
89
+ resolve({ left: 0, top: 0 });
90
+
91
+ }
92
+
93
+ });
79
94
 
80
- }
95
+ }
96
+
97
+ return { left: 0, top: 0 };
81
98
 
82
- return { left: 0, top: 0 };
83
99
  };
@@ -12,13 +12,13 @@ export default interface Address {
12
12
  }
13
13
 
14
14
  const declaration: ModelDeclaration<Address> = {
15
- osm_id : { type: 'number' },
16
- street : { type: 'string' },
17
- streetNumber : { type: 'string' },
18
- city : { type: 'string' },
19
- zipcode : { type: 'string' },
20
- country : { type: 'string' },
21
- addressAddition: { type: 'string' },
15
+ osm_id: { type: 'number' },
16
+ street: { type: 'string' },
17
+ streetNumber: { type: 'string' },
18
+ city: { type: 'string' },
19
+ zipcode: { type: 'string' },
20
+ country: { type: 'string' },
21
+ addressAddition: { type: 'string' },
22
22
  };
23
23
 
24
24
  Models.register('address', declaration);
@@ -3,20 +3,20 @@ import Models, { type ModelDeclaration } from '../libs/Models.class';
3
3
  import type { Contact } from '@lilaquadrat/interfaces';
4
4
 
5
5
  const declaration: ModelDeclaration<Omit<Contact, 'emailConfirmed' | 'emailConfirmationCode'>> = {
6
- prename : { type: 'string' },
7
- name : { type: 'string' },
8
- email : { type: 'string' },
9
- phone : { type: 'string' },
10
- message : { type: 'string' },
11
- type : { type: 'string' },
12
- category : { type: 'string' },
13
- osm_id : { type: 'number' },
14
- street : { type: 'string' },
15
- streetNumber : { type: 'string' },
16
- city : { type: 'string' },
17
- zipcode : { type: 'string' },
18
- country : { type: 'string' },
19
- addressAddition: { type: 'string' },
6
+ prename: { type: 'string' },
7
+ name: { type: 'string' },
8
+ email: { type: 'string' },
9
+ phone: { type: 'string' },
10
+ message: { type: 'string' },
11
+ type: { type: 'string' },
12
+ category: { type: 'string' },
13
+ osm_id: { type: 'number' },
14
+ street: { type: 'string' },
15
+ streetNumber: { type: 'string' },
16
+ city: { type: 'string' },
17
+ zipcode: { type: 'string' },
18
+ country: { type: 'string' },
19
+ addressAddition: { type: 'string' },
20
20
  };
21
21
 
22
22
  Models.register('contact', declaration);
@@ -0,0 +1,198 @@
1
+ import { START_LOCATION, type Router } from 'vue-router';
2
+ import { nextTick } from 'vue';
3
+ import type { FrontendConfigAnalyticsGoogle, FrontendConfigAnalyticsMatomo } from '@lilaquadrat/interfaces';
4
+
5
+ declare global {
6
+
7
+ interface Window {
8
+ _paq?: unknown[][]
9
+ dataLayer?: unknown[][]
10
+ gtag?: (...args: unknown[]) => void
11
+ }
12
+
13
+ }
14
+
15
+ const initialized = false;
16
+
17
+ /**
18
+ * the cookie module stores the accepted categories in this cookie. it is read
19
+ * here as well because the tracker starts before the module is mounted.
20
+ */
21
+ function hasAnalyticsConsent() {
22
+
23
+ const accepted = document.cookie
24
+ .split(';')
25
+ .map((single) => single.trim())
26
+ .find((single) => single.startsWith('lila-cookies-accepted='));
27
+
28
+ return !!accepted && accepted.split('=')[1].split(',').includes('analytics');
29
+
30
+ }
31
+
32
+ /**
33
+ * the auth0 login callback carries a one-time authorization code and its state
34
+ * in the query. they must never end up in the analytics log.
35
+ */
36
+ function trackableUrl(fullPath: string) {
37
+
38
+ const url = new URL(fullPath, window.location.origin);
39
+
40
+ url.searchParams.delete('code');
41
+ url.searchParams.delete('state');
42
+
43
+ return url.toString();
44
+
45
+ }
46
+
47
+ function matomoPush(command: unknown[]) {
48
+
49
+ window._paq = window._paq || [];
50
+ window._paq.push(command);
51
+
52
+ }
53
+
54
+ /**
55
+ * starts matomo for the current target. visits are tracked without cookies
56
+ * until the visitor accepts analytics, comparable to google consent mode.
57
+ */
58
+ function initMatomo(router: Router, config: FrontendConfigAnalyticsMatomo) {
59
+
60
+ // with a trackerPath the edge proxies script and hits under neutral names on
61
+ // the page's own origin. the names are fixed in the edge Caddyfile.
62
+ const proxyBase = config.trackerPath ? `${window.location.origin}${config.trackerPath}` : undefined;
63
+ const trackerUrl = proxyBase ? `${proxyBase}/c` : `${config.url}/matomo.php`;
64
+ const scriptUrl = proxyBase ? `${proxyBase}/v.js` : `${config.url}/matomo.js`;
65
+
66
+ if (config.requireConsent !== false) matomoPush(['requireCookieConsent']);
67
+
68
+ matomoPush(['setCookieNamePrefix', 'lq_']);
69
+
70
+ if (hasAnalyticsConsent()) matomoPush(['rememberCookieConsentGiven']);
71
+
72
+ matomoPush(['setTrackerUrl', trackerUrl]);
73
+ matomoPush(['setSiteId', config.siteId]);
74
+ matomoPush(['enableLinkTracking']);
75
+
76
+ router.afterEach((to, from, failure) => {
77
+
78
+ if (failure) return;
79
+
80
+ // the title is set by the page after the navigation has resolved
81
+ nextTick(() => {
82
+
83
+ if (from !== START_LOCATION) matomoPush(['setReferrerUrl', trackableUrl(from.fullPath)]);
84
+
85
+ matomoPush(['setCustomUrl', trackableUrl(to.fullPath)]);
86
+ matomoPush(['setDocumentTitle', document.title]);
87
+ matomoPush(['trackPageView']);
88
+
89
+ });
90
+
91
+ });
92
+
93
+ const script = document.createElement('script');
94
+
95
+ script.async = true;
96
+ script.src = scriptUrl;
97
+ document.head.appendChild(script);
98
+
99
+ }
100
+
101
+ function googlePush(...args: unknown[]) {
102
+
103
+ window.dataLayer = window.dataLayer || [];
104
+ window.dataLayer.push(args);
105
+
106
+ }
107
+
108
+ /**
109
+ * loads gtag.js and starts google analytics. hits are sent without cookies
110
+ * until the visitor grants consent (consent mode default is denied).
111
+ */
112
+ function initGoogleAnalytics(router: Router, config: FrontendConfigAnalyticsGoogle) {
113
+
114
+ window.dataLayer = window.dataLayer || [];
115
+
116
+ const gtag: (...args: unknown[]) => void = window.gtag || ((...args: unknown[]) => googlePush(...args));
117
+
118
+ window.gtag = gtag;
119
+
120
+ if (config.requireConsent) {
121
+
122
+ gtag('consent', 'default', { ad_storage: 'denied', analytics_storage: 'denied' });
123
+
124
+ }
125
+
126
+ gtag('js', new Date());
127
+ gtag('config', config.measurementId, { send_page_view: false });
128
+
129
+ router.afterEach((to, from, failure) => {
130
+
131
+ if (failure) return;
132
+
133
+ // the title is set by the page after the navigation has resolved
134
+ nextTick(() => {
135
+
136
+ window.gtag?.('event', 'page_view', {
137
+ page_path: trackableUrl(to.fullPath),
138
+ page_location: trackableUrl(to.fullPath),
139
+ page_title: document.title,
140
+ });
141
+
142
+ });
143
+
144
+ });
145
+
146
+ const script = document.createElement('script');
147
+
148
+ script.async = true;
149
+ script.src = `https://www.googletagmanager.com/gtag/js?id=${config.measurementId}`;
150
+ document.head.appendChild(script);
151
+
152
+ }
153
+
154
+ /**
155
+ * starts the analytics provider configured in `__FRONTEND_CONFIG__.analytics`.
156
+ * the provider owns the consent-gated cookie behaviour: either matomo's cookie
157
+ * consent or google's consent mode.
158
+ */
159
+ export function initAnalytics(router: Router): void {
160
+
161
+ // const config = __FRONTEND_CONFIG__.analytics;
162
+ //
163
+ // if (!config || initialized || typeof window === 'undefined') return;
164
+ //
165
+ // initialized = true;
166
+ //
167
+ // if (config.provider === 'matomo') {
168
+ //
169
+ // initMatomo(router, config);
170
+ //
171
+ // } else {
172
+ //
173
+ // initGoogleAnalytics(router, config);
174
+ //
175
+ // }
176
+
177
+ }
178
+
179
+ /**
180
+ * matomo forgetCookieConsentGiven makes matomo delete its own tracking cookies,
181
+ * so no manual cleanup is needed on opt-out. google uses consent mode update.
182
+ */
183
+ export function setAnalyticsConsent(granted: boolean): void {
184
+
185
+ if (!initialized || typeof window === 'undefined') return;
186
+
187
+ if (window._paq) {
188
+
189
+ matomoPush([granted ? 'rememberCookieConsentGiven' : 'forgetCookieConsentGiven']);
190
+
191
+ }
192
+
193
+ window.gtag?.('consent', 'update', {
194
+ ad_storage: granted ? 'granted' : 'denied',
195
+ analytics_storage: granted ? 'granted' : 'denied',
196
+ });
197
+
198
+ }