@lilaquadrat/design-core 0.2.1 → 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.
- package/README.md +76 -3
- package/html/index.server.html +1 -8
- package/package.json +54 -54
- package/src/client-entry.ts +8 -8
- package/src/components/partials/error.partial.vue +19 -19
- package/src/components/partials/main-components.partial.vue +75 -70
- package/src/components/partials/qrcode.partial.vue +8 -8
- package/src/functions/AuthProviderFactory.ts +30 -0
- package/src/functions/PaymenyProviderFactory.ts +11 -11
- package/src/functions/auth0.provider.ts +75 -0
- package/src/functions/keycloak.provider.ts +228 -0
- package/src/functions/lila-dom.ts +50 -50
- package/src/functions/shopify.provider.ts +176 -154
- package/src/functions/stripe.provider.ts +120 -108
- package/src/globals.d.ts +1 -2
- package/src/index.ts +9 -7
- package/src/interfaces/AuthCallbackResult.interface.ts +5 -0
- package/src/interfaces/AuthConfig.interface.ts +13 -0
- package/src/interfaces/AuthProvider.interface.ts +15 -0
- package/src/interfaces/FrontendConfig.interface.ts +11 -40
- package/src/interfaces/GenericEvents.interface.ts +1 -1
- package/src/interfaces/IdTokenExtended.interface.ts +13 -4
- package/src/interfaces/PaymentProvider.interface.ts +2 -2
- package/src/interfaces/RenderOptions.interface.ts +12 -0
- package/src/libs/ActionNotice.ts +125 -125
- package/src/libs/Models.class.ts +219 -221
- package/src/main.ts +41 -36
- package/src/mixins/createCookieString.ts +50 -36
- package/src/mixins/createModelDeclaration.ts +22 -19
- package/src/mixins/createRouter.ts +12 -12
- package/src/mixins/date.ts +4 -2
- package/src/mixins/formatSize.ts +5 -5
- package/src/mixins/getAnchor.ts +1 -1
- package/src/mixins/getRoutes.ts +13 -25
- package/src/mixins/hasSlotContent.ts +22 -16
- package/src/mixins/hooks.ts +101 -60
- package/src/mixins/loadComponents.ts +60 -58
- package/src/mixins/logger.ts +17 -13
- package/src/mixins/replaceVariables.ts +10 -8
- package/src/mixins/scroll.ts +61 -45
- package/src/models/Address.model.ts +7 -7
- package/src/models/Contact.model.ts +14 -14
- package/src/plugins/analytics.ts +198 -0
- package/src/plugins/auth.ts +74 -60
- package/src/plugins/currency.ts +10 -10
- package/src/plugins/events.ts +101 -97
- package/src/plugins/filters.ts +21 -18
- package/src/plugins/replacer.ts +7 -6
- package/src/plugins/signupFlow.ts +45 -43
- package/src/plugins/traceable.ts +28 -26
- package/src/plugins/translations.ts +9 -9
- package/src/plugins/youtube.ts +39 -34
- package/src/routes.ts +108 -108
- package/src/server-entry.ts +75 -77
- package/src/stores/calls.store.ts +14 -14
- package/src/stores/cart.store.ts +116 -107
- package/src/stores/content.store.ts +87 -45
- package/src/stores/editor.store.ts +16 -16
- package/src/stores/files.store.ts +72 -72
- package/src/stores/main.store.ts +155 -109
- package/src/stores/user.store.ts +72 -68
- package/src/translations/de.ts +127 -127
- package/src/views/content.view.vue +81 -143
- package/src/views/download.view.vue +25 -23
- package/src/views/editor.view.vue +150 -125
- package/src/views/login.view.vue +35 -36
- package/src/views/signup-account.view.vue +14 -14
- package/tooling/cypress/config.ts +68 -54
- package/tooling/cypress/manifest-utils.mjs +34 -22
- package/tooling/cypress/run-parallel.mjs +41 -35
- package/tooling/cypress/support/commands.ts +458 -300
- package/tooling/eslint.config.js +1 -223
- package/tooling/preview.plugin.js +132 -0
- package/tooling/ssr-test/index.mjs +205 -203
- package/tooling/vite.d.ts +36 -0
- package/tooling/vite.js +234 -0
- package/src/interfaces/EditorConfiguration.interface.ts +0 -15
- package/tooling/preview.plugin.ts +0 -134
- package/tooling/stylelint.config.mjs +0 -24
- package/tooling/vite.ts +0 -260
|
@@ -1,107 +1,109 @@
|
|
|
1
1
|
import { defineAsyncComponent, type App, type Component, type AsyncComponentLoader } from 'vue';
|
|
2
2
|
|
|
3
|
-
function getName
|
|
3
|
+
function getName(name: string, type: 'module' | 'partial', namespace?: string) {
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
return namespace ? `${namespace}-${name}-${type}` : `${name}-${type}`;
|
|
6
6
|
|
|
7
7
|
}
|
|
8
8
|
|
|
9
|
-
function loadViaGlob
|
|
9
|
+
function loadViaGlob(components: Record<string, Record<'default', Component>>, namespace: string | undefined, app: App<Element>) {
|
|
10
10
|
|
|
11
|
-
|
|
11
|
+
Object.entries(components).forEach(([filename, definition]) => {
|
|
12
12
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
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
|
-
|
|
17
|
+
if (matches) {
|
|
18
18
|
|
|
19
|
-
|
|
19
|
+
const componentName = namespace ? `${namespace}-${matches[1]}-${matches[2]}` : `${matches[1]}-${matches[2]}`;
|
|
20
20
|
|
|
21
|
-
|
|
22
|
-
|
|
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
|
|
30
|
+
function loadViaDeclaration(components: { name: string, component: AsyncComponentLoader<Component> }[], namespace: string | undefined, type: 'module' | 'partial', app: App<Element>) {
|
|
29
31
|
|
|
30
|
-
|
|
32
|
+
components.forEach((single) => {
|
|
31
33
|
|
|
32
|
-
|
|
34
|
+
const name = getName(single.name, type, namespace);
|
|
33
35
|
|
|
34
|
-
|
|
36
|
+
app.component(name, defineAsyncComponent(single.component));
|
|
35
37
|
|
|
36
|
-
|
|
38
|
+
});
|
|
37
39
|
|
|
38
40
|
}
|
|
39
41
|
|
|
40
|
-
function loadViaDeclarationSync
|
|
42
|
+
function loadViaDeclarationSync(components: { name: string, component: Component }[], namespace: string | undefined, type: 'module' | 'partial', app: App<Element>) {
|
|
41
43
|
|
|
42
|
-
|
|
44
|
+
return components.map(async (single) => {
|
|
43
45
|
|
|
44
|
-
|
|
46
|
+
const name = getName(single.name, type, namespace);
|
|
45
47
|
|
|
46
|
-
|
|
48
|
+
app.component(name, single.component);
|
|
47
49
|
|
|
48
|
-
|
|
50
|
+
});
|
|
49
51
|
|
|
50
52
|
}
|
|
51
53
|
|
|
52
|
-
function getAvailableModules
|
|
54
|
+
function getAvailableModules(modules: any[]) {
|
|
53
55
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
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
|
|
65
|
+
function createModulesArray(modules: any[]) {
|
|
64
66
|
|
|
65
|
-
|
|
67
|
+
const modulesArray: any[] = [];
|
|
66
68
|
|
|
67
|
-
|
|
69
|
+
modules.forEach((singleModule) => {
|
|
68
70
|
|
|
69
|
-
|
|
71
|
+
if (!singleModule.target) return;
|
|
70
72
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
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
|
-
|
|
81
|
+
singleModule.target.forEach((target: string) => {
|
|
80
82
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
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
|
-
|
|
91
|
+
if (!foundModule) {
|
|
90
92
|
|
|
91
|
-
|
|
93
|
+
modulesArray.push(useModule);
|
|
92
94
|
|
|
93
|
-
|
|
95
|
+
}
|
|
94
96
|
|
|
95
|
-
|
|
97
|
+
});
|
|
96
98
|
|
|
97
|
-
|
|
99
|
+
return modulesArray;
|
|
98
100
|
|
|
99
101
|
}
|
|
100
102
|
|
|
101
103
|
export {
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
104
|
+
loadViaGlob,
|
|
105
|
+
loadViaDeclaration,
|
|
106
|
+
loadViaDeclarationSync,
|
|
107
|
+
getAvailableModules,
|
|
108
|
+
createModulesArray,
|
|
107
109
|
}
|
package/src/mixins/logger.ts
CHANGED
|
@@ -8,25 +8,29 @@ interface Logger {
|
|
|
8
8
|
|
|
9
9
|
// Predefined colors for categories, if needed
|
|
10
10
|
const categoryColors: { [key: string]: string } = {
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
26
|
-
};
|
|
23
|
+
const categoryStyle = color ? `color: ${color}` : categoryColors[category.toLowerCase()] || 'color: #FFF';
|
|
27
24
|
|
|
28
|
-
|
|
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
|
-
|
|
10
|
+
// Check if the placeholder's name exists in the variables object
|
|
11
|
+
if(!variables) return '';
|
|
13
12
|
|
|
14
|
-
|
|
13
|
+
const returnString = objectPath.get(variables as Record<string, string>, name);
|
|
15
14
|
|
|
16
|
-
|
|
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
|
});
|
package/src/mixins/scroll.ts
CHANGED
|
@@ -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
|
|
14
|
-
const selectors: string[] = ['.anchor-calculation'];
|
|
13
|
+
function getFixedElementsOffset(): number {
|
|
15
14
|
|
|
16
|
-
|
|
17
|
-
|
|
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
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
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
|
-
|
|
50
|
-
|
|
51
|
-
|
|
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
|
-
|
|
64
|
+
if (savedPosition) {
|
|
58
65
|
|
|
59
|
-
|
|
66
|
+
return savedPosition;
|
|
67
|
+
|
|
68
|
+
}
|
|
60
69
|
|
|
61
|
-
|
|
70
|
+
if (to.hash) {
|
|
62
71
|
|
|
63
|
-
|
|
72
|
+
return new Promise((resolve) => {
|
|
64
73
|
|
|
65
|
-
|
|
66
|
-
const elementTop = targetEl.getBoundingClientRect().top + window.scrollY;
|
|
74
|
+
const targetEl = document.querySelector(to.hash) as HTMLElement | null;
|
|
67
75
|
|
|
68
|
-
|
|
69
|
-
left : 0,
|
|
70
|
-
top : elementTop - offset - GENERAL_OFFSET,
|
|
71
|
-
behavior: 'smooth'
|
|
72
|
-
});
|
|
76
|
+
if (targetEl) {
|
|
73
77
|
|
|
74
|
-
|
|
75
|
-
|
|
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
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
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
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
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
|
+
}
|