@lilaquadrat/design-core 0.1.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 +236 -0
- package/html/index.html +25 -0
- package/html/index.mail.html +15 -0
- package/html/index.server.html +29 -0
- package/package.json +148 -0
- package/src/client-entry.ts +28 -0
- package/src/components/partials/action.partial.vue +30 -0
- package/src/components/partials/client-only.partial.vue +11 -0
- package/src/components/partials/error.partial.vue +73 -0
- package/src/components/partials/main-components.partial.vue +167 -0
- package/src/components/partials/mediadetection.partial.vue +71 -0
- package/src/components/partials/qrcode.partial.vue +35 -0
- package/src/customs.ts +12 -0
- package/src/env.d.ts +1 -0
- package/src/functions/PaymenyProviderFactory.ts +27 -0
- package/src/functions/lila-dom.ts +100 -0
- package/src/functions/shopify.provider.ts +378 -0
- package/src/functions/stripe.provider.ts +181 -0
- package/src/globals.d.ts +19 -0
- package/src/index.ts +47 -0
- package/src/interfaces/AppState.interface.ts +4 -0
- package/src/interfaces/EditorConfiguration.interface.ts +15 -0
- package/src/interfaces/EventDeclaration.interface.ts +19 -0
- package/src/interfaces/FrontendConfig.interface.ts +42 -0
- package/src/interfaces/GenericEvents.interface.ts +6 -0
- package/src/interfaces/GenericState.interface.ts +5 -0
- package/src/interfaces/IconsPartial.ts +9 -0
- package/src/interfaces/IdTokenExtended.interface.ts +7 -0
- package/src/interfaces/ModuleBaseProps.interface.ts +8 -0
- package/src/interfaces/PaymentProvider.interface.ts +13 -0
- package/src/libs/ActionNotice.ts +235 -0
- package/src/libs/Models.class.ts +482 -0
- package/src/main.ts +84 -0
- package/src/mixins/createCookieString.ts +78 -0
- package/src/mixins/createModelDeclaration.ts +29 -0
- package/src/mixins/createRouter.ts +30 -0
- package/src/mixins/date.ts +23 -0
- package/src/mixins/formatSize.ts +12 -0
- package/src/mixins/getAnchor.ts +14 -0
- package/src/mixins/getRoutes.ts +50 -0
- package/src/mixins/hasSlotContent.ts +32 -0
- package/src/mixins/hooks.ts +104 -0
- package/src/mixins/loadComponents.ts +107 -0
- package/src/mixins/logger.ts +32 -0
- package/src/mixins/replaceVariables.ts +19 -0
- package/src/mixins/scroll.ts +83 -0
- package/src/models/Address.model.ts +24 -0
- package/src/models/Contact.model.ts +22 -0
- package/src/models.ts +2 -0
- package/src/plugins/auth.ts +121 -0
- package/src/plugins/currency.ts +26 -0
- package/src/plugins/events.ts +156 -0
- package/src/plugins/filters.ts +43 -0
- package/src/plugins/inview.ts +351 -0
- package/src/plugins/replacer.ts +18 -0
- package/src/plugins/resize.ts +128 -0
- package/src/plugins/signupFlow.ts +89 -0
- package/src/plugins/traceable.ts +53 -0
- package/src/plugins/translations.ts +29 -0
- package/src/plugins/youtube.ts +80 -0
- package/src/routes.ts +132 -0
- package/src/server-entry.ts +113 -0
- package/src/stores/calls.store.ts +33 -0
- package/src/stores/cart.store.ts +186 -0
- package/src/stores/content.store.ts +83 -0
- package/src/stores/editor.store.ts +27 -0
- package/src/stores/files.store.ts +166 -0
- package/src/stores/main.store.ts +184 -0
- package/src/stores/user.store.ts +124 -0
- package/src/translations/de.ts +138 -0
- package/src/views/content.view.vue +219 -0
- package/src/views/download.view.vue +143 -0
- package/src/views/editor.view.vue +243 -0
- package/src/views/login.view.vue +82 -0
- package/src/views/signup-account.view.vue +64 -0
- package/tooling/cypress/config.ts +80 -0
- package/tooling/cypress/generate-manifest.js +5 -0
- package/tooling/cypress/manifest-utils.mjs +40 -0
- package/tooling/cypress/run-parallel.mjs +77 -0
- package/tooling/cypress/support/commands.ts +436 -0
- package/tooling/cypress/support/e2e.ts +17 -0
- package/tooling/eslint.config.js +223 -0
- package/tooling/preview.plugin.ts +134 -0
- package/tooling/ssr-test/index.mjs +391 -0
- package/tooling/stylelint.config.mjs +24 -0
- package/tooling/vite.ts +246 -0
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import dayjs from 'dayjs';
|
|
2
|
+
import utc from 'dayjs/plugin/utc';
|
|
3
|
+
import timezone from 'dayjs/plugin/timezone';
|
|
4
|
+
import relativeTime from 'dayjs/plugin/relativeTime';
|
|
5
|
+
import 'dayjs/locale/de';
|
|
6
|
+
|
|
7
|
+
// Initialize dayjs plugins
|
|
8
|
+
dayjs.extend(utc);
|
|
9
|
+
dayjs.extend(timezone);
|
|
10
|
+
dayjs.extend(relativeTime);
|
|
11
|
+
dayjs.locale('de');
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Returns a dayjs instance with the timezone set to Berlin
|
|
15
|
+
*
|
|
16
|
+
* @param date Optional date parameter to initialize the dayjs instance with
|
|
17
|
+
* @returns A dayjs instance with Berlin timezone
|
|
18
|
+
*/
|
|
19
|
+
export function useDate (date?: string | number | Date | dayjs.Dayjs) {
|
|
20
|
+
return dayjs(date).tz('Europe/Berlin');
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export default useDate;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export function formatSize (size?: number): string {
|
|
2
|
+
|
|
3
|
+
if (!size) return '';
|
|
4
|
+
|
|
5
|
+
if (size < 1024) return `${size} B`;
|
|
6
|
+
if (size < 1024 * 1024) return `${(size / 1024).toFixed(1)} KB`;
|
|
7
|
+
|
|
8
|
+
return `${(size / 1024 / 1024).toFixed(1)} MB`;
|
|
9
|
+
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export default formatSize;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Converts the input string to a URL-friendly format.
|
|
3
|
+
*
|
|
4
|
+
* The function performs the following operations in order:
|
|
5
|
+
* 1. Converts all characters in the input string to lowercase.
|
|
6
|
+
* 2. Removes all non-word characters (anything except alphanumeric and underscores),
|
|
7
|
+
* whitespace, and hyphens.
|
|
8
|
+
* 3. Trims leading and trailing spaces from the string.
|
|
9
|
+
* 4. Replaces one or more spaces with a single hyphen.
|
|
10
|
+
* 5. Replaces multiple consecutive hyphens with a single hyphen.
|
|
11
|
+
*
|
|
12
|
+
*/
|
|
13
|
+
export default (input: string) => input?.toLowerCase().replace(/[^\w\s-]/g, '').trim().replace(/\s+/g, '-')
|
|
14
|
+
.replace(/-+/g, '-');
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import type { RouteRecordRaw } from 'vue-router';
|
|
2
|
+
import logger from './logger';
|
|
3
|
+
|
|
4
|
+
export interface RouteSets {
|
|
5
|
+
dynamicRoutes: readonly RouteRecordRaw[]
|
|
6
|
+
editorRoutes: readonly RouteRecordRaw[]
|
|
7
|
+
previewRoutes: readonly RouteRecordRaw[]
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export default (routeSets: RouteSets) => {
|
|
11
|
+
|
|
12
|
+
const config = __FRONTEND_CONFIG__;
|
|
13
|
+
const mode = import.meta.env.MODE;
|
|
14
|
+
const isIframe = window !== window.top;
|
|
15
|
+
const isDynamic = config.dynamic;
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* the shareable static build (yarn build:preview) is always the module
|
|
19
|
+
* gallery - it has no backend and no editor parent to talk to
|
|
20
|
+
*/
|
|
21
|
+
if(__PREVIEW__) {
|
|
22
|
+
|
|
23
|
+
logger.router('mode: preview routes (static build)');
|
|
24
|
+
return routeSets.previewRoutes;
|
|
25
|
+
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* running in an iframe indicates that we are in the STUDIO editor
|
|
30
|
+
*/
|
|
31
|
+
if(isIframe) {
|
|
32
|
+
|
|
33
|
+
logger.router('mode: editor routes');
|
|
34
|
+
return routeSets.editorRoutes;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* no iframe but development mode indicates that the design is started locally
|
|
39
|
+
*/
|
|
40
|
+
if(mode === 'development' && !isDynamic) {
|
|
41
|
+
|
|
42
|
+
logger.router('mode: preview routes');
|
|
43
|
+
return routeSets.previewRoutes
|
|
44
|
+
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
logger.router('mode: dynamic routes');
|
|
48
|
+
return routeSets.dynamicRoutes;
|
|
49
|
+
|
|
50
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
// slotUtils.ts
|
|
2
|
+
import type { Slot, VNode } from 'vue';
|
|
3
|
+
import { Comment } from 'vue';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Checks whether the provided slot function returns any meaningful content.
|
|
7
|
+
*
|
|
8
|
+
* @param slot - The slot function to check.
|
|
9
|
+
* @returns True if the slot has meaningful content, false otherwise.
|
|
10
|
+
*/
|
|
11
|
+
export function hasSlotContent (slot: Slot | undefined): boolean {
|
|
12
|
+
// If the slot function does not exist, consider the slot empty.
|
|
13
|
+
if (!slot) return false;
|
|
14
|
+
|
|
15
|
+
// Call the slot function to retrieve an array of VNodes.
|
|
16
|
+
const nodes: VNode[] = slot();
|
|
17
|
+
|
|
18
|
+
if (!nodes || nodes.length === 0) return false;
|
|
19
|
+
|
|
20
|
+
// Check if any of the VNodes contain meaningful content.
|
|
21
|
+
return nodes.some((node: VNode) => {
|
|
22
|
+
// Ignore comment nodes.
|
|
23
|
+
if (node.type === Comment) return false;
|
|
24
|
+
|
|
25
|
+
// If the node's children is a string, ignore it if it’s only whitespace.
|
|
26
|
+
if (typeof node.children === 'string' && node.children.trim() === '') {
|
|
27
|
+
return false;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
return true;
|
|
31
|
+
});
|
|
32
|
+
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { useMainStore } from '../stores/main.store'
|
|
2
|
+
import type { Router } from 'vue-router';
|
|
3
|
+
import logger from './logger';
|
|
4
|
+
import { auth } from '../plugins/auth';
|
|
5
|
+
import useUserStore from '../stores/user.store';
|
|
6
|
+
import { routeForLock } from '../plugins/signupFlow';
|
|
7
|
+
|
|
8
|
+
export default (router: Router, options?: {initAuth?: boolean}, target: 'server' | 'client' = 'server') => {
|
|
9
|
+
|
|
10
|
+
router.beforeEach(async (to, from, next) => {
|
|
11
|
+
|
|
12
|
+
const mainStore = useMainStore();
|
|
13
|
+
const userStore = useUserStore();
|
|
14
|
+
|
|
15
|
+
if (!mainStore.startupDone) {
|
|
16
|
+
|
|
17
|
+
logger.startup('start');
|
|
18
|
+
|
|
19
|
+
if(!mainStore.config) {
|
|
20
|
+
mainStore.config = __FRONTEND_CONFIG__;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
if(options?.initAuth !== false && !mainStore.isEditor){
|
|
24
|
+
if (mainStore.config?.auth0Options) {
|
|
25
|
+
|
|
26
|
+
await auth.init(mainStore.config.auth0Options);
|
|
27
|
+
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
await userStore.initCustomer();
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
mainStore.startupDone = true;
|
|
34
|
+
|
|
35
|
+
logger.startup('done');
|
|
36
|
+
|
|
37
|
+
} else {
|
|
38
|
+
|
|
39
|
+
if(target === 'client') {
|
|
40
|
+
|
|
41
|
+
if(!mainStore.clientOnlyStartupDone) {
|
|
42
|
+
|
|
43
|
+
if(options?.initAuth !== false) {
|
|
44
|
+
|
|
45
|
+
if (mainStore.config?.auth0Options) {
|
|
46
|
+
|
|
47
|
+
await auth.init(mainStore.config.auth0Options);
|
|
48
|
+
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
await userStore.initCustomer();
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
mainStore.clientOnlyStartupDone = true;
|
|
55
|
+
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if(mainStore.disabledLockUpdate) mainStore.disabledLockUpdate = false;
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* signup gate: locked users are forced through the signup steps before they can
|
|
66
|
+
* reach any members area. public pages stay reachable.
|
|
67
|
+
*/
|
|
68
|
+
const locked = userStore.locked;
|
|
69
|
+
const toName = typeof to.name === 'string' ? to.name : '';
|
|
70
|
+
|
|
71
|
+
if (locked) {
|
|
72
|
+
|
|
73
|
+
const stepRoute = routeForLock(locked);
|
|
74
|
+
|
|
75
|
+
// a locked user heading into members content (that isn't a signup step) → current step
|
|
76
|
+
if (to.meta?.contentType === 'members' && !toName.startsWith('signup') && toName !== stepRoute.name) {
|
|
77
|
+
|
|
78
|
+
return next(stepRoute);
|
|
79
|
+
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// a locked user on any signup screen that isn't their current step → that step
|
|
83
|
+
// (signup-account is the external account-creation trigger, leave it alone)
|
|
84
|
+
if (toName.startsWith('signup') && toName !== 'signup-account' && toName !== stepRoute.name) {
|
|
85
|
+
|
|
86
|
+
return next(stepRoute);
|
|
87
|
+
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
} else if (toName === 'signup' || toName === 'signup-connect' || toName === 'signup-verify') {
|
|
91
|
+
|
|
92
|
+
// not locked: a fully set up (authenticated) user has nothing to do here → app
|
|
93
|
+
if (auth.isAuth.value) return next({ name: 'members' });
|
|
94
|
+
|
|
95
|
+
// a guest can only start at the register step
|
|
96
|
+
if (toName !== 'signup') return next({ name: 'signup' });
|
|
97
|
+
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
next();
|
|
101
|
+
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
};
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { defineAsyncComponent, type App, type Component, type AsyncComponentLoader } from 'vue';
|
|
2
|
+
|
|
3
|
+
function getName (name: string, type: 'module' | 'partial', namespace?: string) {
|
|
4
|
+
|
|
5
|
+
return namespace ? `${namespace}-${name}-${type}` : `${name}-${type}`;
|
|
6
|
+
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function loadViaGlob (components: Record<string, Record<'default', Component>>, namespace: string | undefined, app: App<Element>) {
|
|
10
|
+
|
|
11
|
+
Object.entries(components).forEach(([filename, definition]) => {
|
|
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);
|
|
16
|
+
|
|
17
|
+
if (matches) {
|
|
18
|
+
|
|
19
|
+
const componentName = namespace ? `${namespace}-${matches[1]}-${matches[2]}` : `${matches[1]}-${matches[2]}`;
|
|
20
|
+
|
|
21
|
+
// Register component on this Vue instance
|
|
22
|
+
app.component(componentName, definition.default);
|
|
23
|
+
}
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function loadViaDeclaration (components: { name: string, component: AsyncComponentLoader<Component> }[], namespace: string | undefined, type: 'module' | 'partial', app: App<Element>) {
|
|
29
|
+
|
|
30
|
+
components.forEach((single) => {
|
|
31
|
+
|
|
32
|
+
const name = getName(single.name, type, namespace);
|
|
33
|
+
|
|
34
|
+
app.component(name, defineAsyncComponent(single.component));
|
|
35
|
+
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function loadViaDeclarationSync (components: { name: string, component: Component }[], namespace: string | undefined, type: 'module' | 'partial', app: App<Element>) {
|
|
41
|
+
|
|
42
|
+
return components.map(async (single) => {
|
|
43
|
+
|
|
44
|
+
const name = getName(single.name, type, namespace);
|
|
45
|
+
|
|
46
|
+
app.component(name, single.component);
|
|
47
|
+
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function getAvailableModules (modules: any[]) {
|
|
53
|
+
|
|
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
|
+
}));
|
|
60
|
+
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function createModulesArray (modules: any[]) {
|
|
64
|
+
|
|
65
|
+
const modulesArray: any[] = [];
|
|
66
|
+
|
|
67
|
+
modules.forEach((singleModule) => {
|
|
68
|
+
|
|
69
|
+
if (!singleModule.target) return;
|
|
70
|
+
|
|
71
|
+
const foundModule = modulesArray.find((single) => single.name === singleModule.name);
|
|
72
|
+
const useModule = foundModule
|
|
73
|
+
? foundModule
|
|
74
|
+
: {
|
|
75
|
+
name : singleModule.name,
|
|
76
|
+
targets: {}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
singleModule.target.forEach((target: string) => {
|
|
80
|
+
|
|
81
|
+
useModule.targets[target] = {
|
|
82
|
+
variants : singleModule.variants,
|
|
83
|
+
components: singleModule.component,
|
|
84
|
+
name : singleModule.name
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
if (!foundModule) {
|
|
90
|
+
|
|
91
|
+
modulesArray.push(useModule);
|
|
92
|
+
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
return modulesArray;
|
|
98
|
+
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export {
|
|
102
|
+
loadViaGlob,
|
|
103
|
+
loadViaDeclaration,
|
|
104
|
+
loadViaDeclarationSync,
|
|
105
|
+
getAvailableModules,
|
|
106
|
+
createModulesArray,
|
|
107
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
interface LoggerFunction {
|
|
2
|
+
(message: string, color?: string): void;
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
interface Logger {
|
|
6
|
+
[key: string]: LoggerFunction;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
// Predefined colors for categories, if needed
|
|
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
|
|
15
|
+
};
|
|
16
|
+
const logger = new Proxy<Logger>({}, {
|
|
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';
|
|
22
|
+
|
|
23
|
+
// Log the category with style and message without style
|
|
24
|
+
|
|
25
|
+
console.log(`%c[FRONTEND]%c%c[${category.toUpperCase()}]%c ${message}`, 'background-color: #3f2d56;', 'padding: 5px', categoryStyle, 'color: inherit');
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
return (message: string, color?: string) => log(propKey.toString(), message, color);
|
|
29
|
+
}
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
export default logger;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { Customers, List, Me } from '@lilaquadrat/interfaces';
|
|
2
|
+
import objectPath from 'object-path';
|
|
3
|
+
import useDate from './date';
|
|
4
|
+
import 'dayjs/locale/de';
|
|
5
|
+
|
|
6
|
+
const dateVariables = ['list.start', 'list.end'];
|
|
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 '';
|
|
11
|
+
|
|
12
|
+
const returnString = objectPath.get(variables as Record<string, string>, name);
|
|
13
|
+
|
|
14
|
+
if(!returnString) return '';
|
|
15
|
+
|
|
16
|
+
if(dateVariables.includes(name)) return useDate(returnString).locale('de').format('DD.MM.YYYY HH:mm');
|
|
17
|
+
|
|
18
|
+
return returnString;
|
|
19
|
+
});
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import type { RouteLocationNormalized, RouterScrollBehavior } from 'vue-router';
|
|
2
|
+
|
|
3
|
+
interface ScrollPosition {
|
|
4
|
+
left: number;
|
|
5
|
+
top: number;
|
|
6
|
+
behavior?: ScrollBehavior;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
// Add this constant at the top of the file, after the interfaces
|
|
10
|
+
const GENERAL_OFFSET = 20;
|
|
11
|
+
|
|
12
|
+
// Helper function to calculate the total height of fixed or overlaying elements.
|
|
13
|
+
function getFixedElementsOffset (): number {
|
|
14
|
+
const selectors: string[] = ['.anchor-calculation'];
|
|
15
|
+
|
|
16
|
+
return selectors.reduce((total, selector) => {
|
|
17
|
+
const el = document.querySelector(selector) as HTMLElement | null;
|
|
18
|
+
|
|
19
|
+
return el ? total + el.offsetHeight : total;
|
|
20
|
+
}, 0);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* scroll into view with calulated offset from fixed overlay elements
|
|
25
|
+
*/
|
|
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
|
+
}
|
|
40
|
+
|
|
41
|
+
resolve();
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* scroll into view with calulated offset from fixed overlay elements
|
|
46
|
+
* usage for vue-router scrollBehavior
|
|
47
|
+
*/
|
|
48
|
+
export const smartScrollBehavior: RouterScrollBehavior = (
|
|
49
|
+
to: RouteLocationNormalized,
|
|
50
|
+
_from: RouteLocationNormalized,
|
|
51
|
+
savedPosition: ScrollPosition | null | undefined
|
|
52
|
+
): ReturnType<RouterScrollBehavior> => {
|
|
53
|
+
if (savedPosition) {
|
|
54
|
+
return savedPosition;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (to.hash) {
|
|
58
|
+
|
|
59
|
+
return new Promise((resolve) => {
|
|
60
|
+
|
|
61
|
+
const targetEl = document.querySelector(to.hash) as HTMLElement | null;
|
|
62
|
+
|
|
63
|
+
if (targetEl) {
|
|
64
|
+
|
|
65
|
+
const offset = getFixedElementsOffset();
|
|
66
|
+
const elementTop = targetEl.getBoundingClientRect().top + window.scrollY;
|
|
67
|
+
|
|
68
|
+
resolve({
|
|
69
|
+
left : 0,
|
|
70
|
+
top : elementTop - offset - GENERAL_OFFSET,
|
|
71
|
+
behavior: 'smooth'
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
} else {
|
|
75
|
+
resolve({ left: 0, top: 0 });
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return { left: 0, top: 0 };
|
|
83
|
+
};
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
|
|
2
|
+
import Models, { type ModelDeclaration } from '../libs/Models.class';
|
|
3
|
+
|
|
4
|
+
export default interface Address {
|
|
5
|
+
osm_id: number
|
|
6
|
+
street: string
|
|
7
|
+
streetNumber: string
|
|
8
|
+
city: string
|
|
9
|
+
zipcode: string
|
|
10
|
+
country: string
|
|
11
|
+
addressAddition: string
|
|
12
|
+
}
|
|
13
|
+
|
|
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' },
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
Models.register('address', declaration);
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
|
|
2
|
+
import Models, { type ModelDeclaration } from '../libs/Models.class';
|
|
3
|
+
import type { Contact } from '@lilaquadrat/interfaces';
|
|
4
|
+
|
|
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' },
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
Models.register('contact', declaration);
|
package/src/models.ts
ADDED
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import type AppState from '../interfaces/AppState.interface';
|
|
2
|
+
import type IdTokenExtended from '../interfaces/IdTokenExtended.interface';
|
|
3
|
+
import logger from '../mixins/logger';
|
|
4
|
+
import { Auth0Client, type Auth0ClientOptions } from '@auth0/auth0-spa-js';
|
|
5
|
+
import { ref } from 'vue';
|
|
6
|
+
|
|
7
|
+
class Auth {
|
|
8
|
+
|
|
9
|
+
auth0!: Auth0Client;
|
|
10
|
+
|
|
11
|
+
options: { dev: boolean } = { dev: false };
|
|
12
|
+
|
|
13
|
+
isAuth = ref<boolean>(false);
|
|
14
|
+
|
|
15
|
+
token = ref<string>();
|
|
16
|
+
|
|
17
|
+
returnTo?: string;
|
|
18
|
+
|
|
19
|
+
async init (auth0: Auth0ClientOptions, options?: { dev: boolean }) {
|
|
20
|
+
|
|
21
|
+
logger.auth('init')
|
|
22
|
+
this.auth0 = new Auth0Client(auth0);
|
|
23
|
+
|
|
24
|
+
const redirectUri = auth0.authorizationParams?.redirect_uri;
|
|
25
|
+
|
|
26
|
+
if (redirectUri) this.returnTo = new URL(redirectUri).origin;
|
|
27
|
+
|
|
28
|
+
if (options) this.options = options;
|
|
29
|
+
|
|
30
|
+
try {
|
|
31
|
+
|
|
32
|
+
await this.updateAuthStatus();
|
|
33
|
+
|
|
34
|
+
} catch (error) {
|
|
35
|
+
|
|
36
|
+
console.error(error);
|
|
37
|
+
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
try {
|
|
41
|
+
|
|
42
|
+
if (this.isAuth.value && !this.token.value) await this.getToken();
|
|
43
|
+
|
|
44
|
+
} catch (error) {
|
|
45
|
+
|
|
46
|
+
console.error(error);
|
|
47
|
+
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
logger.auth('init done')
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
async updateAuthStatus () {
|
|
54
|
+
|
|
55
|
+
this.isAuth.value = await this.auth0.isAuthenticated();
|
|
56
|
+
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
async getToken () {
|
|
60
|
+
|
|
61
|
+
this.token.value = await this.auth0.getTokenSilently({ authorizationParams: { scope: 'openid profile email offline_access' } });
|
|
62
|
+
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
async refreshToken () {
|
|
66
|
+
|
|
67
|
+
this.token.value = await this.auth0.getTokenSilently({ authorizationParams: { scope: 'openid profile email offline_access' } });
|
|
68
|
+
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
getTokenContent (): Promise<IdTokenExtended | undefined> {
|
|
72
|
+
|
|
73
|
+
return this.auth0.getIdTokenClaims() as Promise<IdTokenExtended | undefined>;
|
|
74
|
+
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
triggerLogin (customerId?: string, emailConfirmationCode?: string) {
|
|
78
|
+
|
|
79
|
+
this.auth0.loginWithRedirect<AppState>({ appState: { customerId, emailConfirmationCode } });
|
|
80
|
+
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
triggerSignup (customerId?: string, emailConfirmationCode?: string) {
|
|
84
|
+
|
|
85
|
+
this.auth0.loginWithRedirect<AppState>({ appState: { customerId, emailConfirmationCode }, authorizationParams: { screen_hint: 'signup' } });
|
|
86
|
+
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
triggerLogout () {
|
|
90
|
+
|
|
91
|
+
this.auth0.logout(this.returnTo ? { logoutParams: { returnTo: this.returnTo } } : undefined);
|
|
92
|
+
localStorage.removeItem('lila-customer');
|
|
93
|
+
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
async handleCallback () {
|
|
97
|
+
|
|
98
|
+
const result = await this.auth0.handleRedirectCallback<AppState>();
|
|
99
|
+
|
|
100
|
+
await this.updateAuthStatus();
|
|
101
|
+
await this.getToken();
|
|
102
|
+
|
|
103
|
+
return result;
|
|
104
|
+
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const auth = new Auth;
|
|
110
|
+
const plugin = {
|
|
111
|
+
install: (): void => {
|
|
112
|
+
logger.plugins('auth installed')
|
|
113
|
+
},
|
|
114
|
+
};
|
|
115
|
+
const useAuth = () => ({ authToken: auth.token.value })
|
|
116
|
+
|
|
117
|
+
export default plugin;
|
|
118
|
+
export {
|
|
119
|
+
auth,
|
|
120
|
+
useAuth
|
|
121
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import logger from '../mixins/logger';
|
|
2
|
+
import type { App } from 'vue';
|
|
3
|
+
|
|
4
|
+
function formatCurrency (amount: number) {
|
|
5
|
+
|
|
6
|
+
return new Intl.NumberFormat(navigator?.language || 'en-US', {
|
|
7
|
+
style : 'currency',
|
|
8
|
+
currency: 'EUR',
|
|
9
|
+
}).format(amount);
|
|
10
|
+
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const plugin = {
|
|
14
|
+
install: (vue: App): void => {
|
|
15
|
+
|
|
16
|
+
vue.config.globalProperties.$currency = (amount: number) => formatCurrency(amount);
|
|
17
|
+
|
|
18
|
+
logger.plugins('currency installed');
|
|
19
|
+
|
|
20
|
+
},
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
export default plugin;
|
|
24
|
+
export {
|
|
25
|
+
formatCurrency
|
|
26
|
+
}
|