@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.
Files changed (86) hide show
  1. package/README.md +236 -0
  2. package/html/index.html +25 -0
  3. package/html/index.mail.html +15 -0
  4. package/html/index.server.html +29 -0
  5. package/package.json +148 -0
  6. package/src/client-entry.ts +28 -0
  7. package/src/components/partials/action.partial.vue +30 -0
  8. package/src/components/partials/client-only.partial.vue +11 -0
  9. package/src/components/partials/error.partial.vue +73 -0
  10. package/src/components/partials/main-components.partial.vue +167 -0
  11. package/src/components/partials/mediadetection.partial.vue +71 -0
  12. package/src/components/partials/qrcode.partial.vue +35 -0
  13. package/src/customs.ts +12 -0
  14. package/src/env.d.ts +1 -0
  15. package/src/functions/PaymenyProviderFactory.ts +27 -0
  16. package/src/functions/lila-dom.ts +100 -0
  17. package/src/functions/shopify.provider.ts +378 -0
  18. package/src/functions/stripe.provider.ts +181 -0
  19. package/src/globals.d.ts +19 -0
  20. package/src/index.ts +47 -0
  21. package/src/interfaces/AppState.interface.ts +4 -0
  22. package/src/interfaces/EditorConfiguration.interface.ts +15 -0
  23. package/src/interfaces/EventDeclaration.interface.ts +19 -0
  24. package/src/interfaces/FrontendConfig.interface.ts +42 -0
  25. package/src/interfaces/GenericEvents.interface.ts +6 -0
  26. package/src/interfaces/GenericState.interface.ts +5 -0
  27. package/src/interfaces/IconsPartial.ts +9 -0
  28. package/src/interfaces/IdTokenExtended.interface.ts +7 -0
  29. package/src/interfaces/ModuleBaseProps.interface.ts +8 -0
  30. package/src/interfaces/PaymentProvider.interface.ts +13 -0
  31. package/src/libs/ActionNotice.ts +235 -0
  32. package/src/libs/Models.class.ts +482 -0
  33. package/src/main.ts +84 -0
  34. package/src/mixins/createCookieString.ts +78 -0
  35. package/src/mixins/createModelDeclaration.ts +29 -0
  36. package/src/mixins/createRouter.ts +30 -0
  37. package/src/mixins/date.ts +23 -0
  38. package/src/mixins/formatSize.ts +12 -0
  39. package/src/mixins/getAnchor.ts +14 -0
  40. package/src/mixins/getRoutes.ts +50 -0
  41. package/src/mixins/hasSlotContent.ts +32 -0
  42. package/src/mixins/hooks.ts +104 -0
  43. package/src/mixins/loadComponents.ts +107 -0
  44. package/src/mixins/logger.ts +32 -0
  45. package/src/mixins/replaceVariables.ts +19 -0
  46. package/src/mixins/scroll.ts +83 -0
  47. package/src/models/Address.model.ts +24 -0
  48. package/src/models/Contact.model.ts +22 -0
  49. package/src/models.ts +2 -0
  50. package/src/plugins/auth.ts +121 -0
  51. package/src/plugins/currency.ts +26 -0
  52. package/src/plugins/events.ts +156 -0
  53. package/src/plugins/filters.ts +43 -0
  54. package/src/plugins/inview.ts +351 -0
  55. package/src/plugins/replacer.ts +18 -0
  56. package/src/plugins/resize.ts +128 -0
  57. package/src/plugins/signupFlow.ts +89 -0
  58. package/src/plugins/traceable.ts +53 -0
  59. package/src/plugins/translations.ts +29 -0
  60. package/src/plugins/youtube.ts +80 -0
  61. package/src/routes.ts +132 -0
  62. package/src/server-entry.ts +113 -0
  63. package/src/stores/calls.store.ts +33 -0
  64. package/src/stores/cart.store.ts +186 -0
  65. package/src/stores/content.store.ts +83 -0
  66. package/src/stores/editor.store.ts +27 -0
  67. package/src/stores/files.store.ts +166 -0
  68. package/src/stores/main.store.ts +184 -0
  69. package/src/stores/user.store.ts +124 -0
  70. package/src/translations/de.ts +138 -0
  71. package/src/views/content.view.vue +219 -0
  72. package/src/views/download.view.vue +143 -0
  73. package/src/views/editor.view.vue +243 -0
  74. package/src/views/login.view.vue +82 -0
  75. package/src/views/signup-account.view.vue +64 -0
  76. package/tooling/cypress/config.ts +80 -0
  77. package/tooling/cypress/generate-manifest.js +5 -0
  78. package/tooling/cypress/manifest-utils.mjs +40 -0
  79. package/tooling/cypress/run-parallel.mjs +77 -0
  80. package/tooling/cypress/support/commands.ts +436 -0
  81. package/tooling/cypress/support/e2e.ts +17 -0
  82. package/tooling/eslint.config.js +223 -0
  83. package/tooling/preview.plugin.ts +134 -0
  84. package/tooling/ssr-test/index.mjs +391 -0
  85. package/tooling/stylelint.config.mjs +24 -0
  86. package/tooling/vite.ts +246 -0
@@ -0,0 +1,89 @@
1
+ import { computed } from 'vue';
2
+ import { useRoute } from 'vue-router';
3
+ import useUserStore from '../stores/user.store';
4
+ import { auth } from './auth';
5
+
6
+ export type SignupStepKey = 'register' | 'account' | 'connect' | 'verify';
7
+ export type StepStatus = 'done' | 'current' | 'upcoming';
8
+
9
+ export interface SignupStep {
10
+ key: SignupStepKey
11
+ label: string
12
+ status: StepStatus
13
+ }
14
+
15
+ const STEP_ORDER: SignupStepKey[] = ['register', 'account', 'connect', 'verify'];
16
+
17
+ /**
18
+ * maps a lock state to the route the user has to complete next.
19
+ * keep this the single place that knows about the lock string values.
20
+ */
21
+ function routeForLock (locked?: string): { name: string } {
22
+
23
+ if (locked === 'user-connect') return { name: 'signup-connect' };
24
+ if (locked === 'email-verified') return { name: 'signup-verify' };
25
+
26
+ return { name: 'members' };
27
+
28
+ }
29
+
30
+ function useSignupFlow () {
31
+
32
+ const userStore = useUserStore();
33
+ const route = useRoute();
34
+ // DEBUG: ?step=<1|2|3|4> forces the active step (dev only)
35
+ const debugStep = computed<SignupStepKey | undefined>(() => {
36
+
37
+ const step = route?.query?.step;
38
+
39
+ if (import.meta.env.DEV && typeof step === 'string') {
40
+
41
+ const index = Number(step) - 1;
42
+
43
+ if (index >= 0 && index < STEP_ORDER.length) return STEP_ORDER[index];
44
+
45
+ }
46
+
47
+ return undefined;
48
+
49
+ });
50
+ // the furthest step the user has reached, derived from auth + lock state.
51
+ // being authenticated implies register + account are done, even when there
52
+ // is no local customer (e.g. connecting from a different browser).
53
+ const currentKey = computed<SignupStepKey>(() => {
54
+
55
+ if (debugStep.value) return debugStep.value;
56
+
57
+ if (!auth.isAuth.value) {
58
+ return (userStore.customer || userStore.userData) ? 'account' : 'register';
59
+ }
60
+
61
+ if (userStore.locked === 'user-connect') return 'connect';
62
+ if (userStore.locked === 'email-verified') return 'verify';
63
+
64
+ return 'verify';
65
+
66
+ });
67
+ const isComplete = computed(() => !debugStep.value && auth.isAuth.value && !userStore.locked);
68
+ // steps are monotonic: everything before the current step counts as done
69
+ const steps = computed<SignupStep[]>(() => {
70
+
71
+ const currentIndex = STEP_ORDER.indexOf(currentKey.value);
72
+
73
+ return STEP_ORDER.map((key, index) => ({
74
+ key,
75
+ label : `signup-step-${key}`,
76
+ status: isComplete.value || index < currentIndex
77
+ ? 'done'
78
+ : index === currentIndex
79
+ ? 'current'
80
+ : 'upcoming',
81
+ }));
82
+
83
+ });
84
+
85
+ return { steps, currentKey, isComplete, routeForLock };
86
+
87
+ }
88
+
89
+ export { useSignupFlow, routeForLock };
@@ -0,0 +1,53 @@
1
+ import logger from '../mixins/logger';
2
+ import useCallStore from '../stores/calls.store';
3
+ import type { Ref } from 'vue';
4
+
5
+ const plugin = {
6
+ install: (): void => {
7
+ logger.plugins('traceable installed')
8
+ },
9
+ };
10
+ const useTraceable = () => {
11
+
12
+ const { addCall, updateCall } = useCallStore();
13
+
14
+ async function traceable<T> (promise: Promise<T>, traceId?: Ref<string|undefined>, options: {time: number} = {time: 3000}) {
15
+
16
+ // add the new call to vuex and fetch the id
17
+ const id = addCall();
18
+
19
+ if(traceId) traceId.value = id;
20
+
21
+ const minimum = new Promise<void>((resolve) => {
22
+
23
+ setTimeout(() => resolve(), options.time);
24
+
25
+ });
26
+ // the minimum promise will hold the execution of allsettled
27
+ const results = await Promise.allSettled([minimum, promise]);
28
+
29
+ // no error will be thrown, we need to check the result of the given promise
30
+ if (results[1].status === 'rejected') {
31
+
32
+ updateCall(id, 'rejected');
33
+ throw results[1].reason;
34
+
35
+ } else {
36
+
37
+ updateCall(id, 'resolved');
38
+ return results[1].value;
39
+
40
+ }
41
+
42
+ }
43
+
44
+ return {
45
+ traceable
46
+ }
47
+
48
+ };
49
+
50
+ export default plugin;
51
+ export {
52
+ useTraceable
53
+ }
@@ -0,0 +1,29 @@
1
+ import { createI18n } from 'vue-i18n';
2
+ import de from '../translations/de';
3
+
4
+ const i18n = createI18n({
5
+ legacy : false,
6
+ locale : 'de',
7
+ fallbackLocale: 'de',
8
+ missingWarn : false,
9
+ fallbackWarn : false,
10
+ missing : (_locale, key) => key,
11
+ messages : { de },
12
+ });
13
+ const { t } = i18n.global;
14
+
15
+ /**
16
+ * merges project owned messages on top of the framework base.
17
+ *
18
+ * the design ships the wording for its own modules, the package only carries
19
+ * the strings it renders itself - so a project calls this once at startup with
20
+ * its own translation file.
21
+ */
22
+ function addMessages (locale: string, messages: Record<string, unknown>) {
23
+
24
+ i18n.global.mergeLocaleMessage(locale, messages);
25
+
26
+ }
27
+
28
+ export default i18n;
29
+ export { t, addMessages };
@@ -0,0 +1,80 @@
1
+ import logger from '../mixins/logger';
2
+ import { ref } from 'vue';
3
+
4
+ class Youtube {
5
+
6
+ youtubeApiState = ref<boolean>(false);
7
+
8
+ loadState = ref<boolean>(false);
9
+
10
+ loadApi () {
11
+
12
+ if(this.loadState.value) return Promise.resolve();
13
+
14
+ return new Promise<void>((resolve) => {
15
+
16
+ const script = document.createElement('script');
17
+
18
+ script.src = 'https://www.youtube.com/iframe_api';
19
+
20
+ script.onload = () => {
21
+ resolve();
22
+ };
23
+
24
+ document.body.appendChild(script);
25
+ this.loadState.value = true;
26
+
27
+ });
28
+ }
29
+
30
+ /**
31
+ * load the youtube iframe api and changes the youtubeApiState to true when all is ready
32
+ */
33
+ addYoutube () {
34
+
35
+ if(!this.youtubeApiState.value) {
36
+
37
+ this.loadApi()
38
+
39
+ }
40
+
41
+ }
42
+
43
+ }
44
+
45
+ const youtubePlugin = new Youtube();
46
+ const plugin = {
47
+ install: (): void => {
48
+ // this exists only for initialization purposes
49
+ logger.plugins('youtube installed');
50
+ },
51
+ };
52
+
53
+ if(typeof window !== 'undefined') {
54
+
55
+ window.onYouTubeIframeAPIReady = () => {
56
+
57
+ youtubePlugin.youtubeApiState.value = true;
58
+
59
+ };
60
+
61
+ }
62
+
63
+ function useYoutube () {
64
+
65
+ return {
66
+ youtubeApiState: youtubePlugin.youtubeApiState,
67
+ /**
68
+ * loads the youtube iframe api and changes the youtubeApiState to true when all is ready
69
+ */
70
+ addYoutube : () => youtubePlugin.addYoutube()
71
+ };
72
+
73
+ }
74
+
75
+ export default plugin;
76
+
77
+ export {
78
+ useYoutube
79
+ };
80
+
package/src/routes.ts ADDED
@@ -0,0 +1,132 @@
1
+ import type { RouteRecordRaw } from 'vue-router';
2
+ import type { Component } from 'vue';
3
+ import ContentView from './views/content.view.vue';
4
+ import EditorView from './views/editor.view.vue';
5
+ import LoginView from './views/login.view.vue';
6
+ import SignupAccountView from './views/signup-account.view.vue';
7
+ import DownloadView from './views/download.view.vue';
8
+
9
+ const dynamicRoutes: readonly RouteRecordRaw[] = [
10
+ {
11
+ path : '/m/:pathMatch(.*)?',
12
+ name : 'members',
13
+ component: ContentView,
14
+ meta : {
15
+ contentType: 'members'
16
+ }
17
+ },
18
+ {
19
+ path : '/login-callback',
20
+ name : 'login-callback',
21
+ component: LoginView
22
+ },
23
+ {
24
+ path : '/signup/account',
25
+ name : 'signup-account',
26
+ component: SignupAccountView
27
+ },
28
+ {
29
+ path : '/signup',
30
+ name : 'signup',
31
+ component: ContentView,
32
+ meta : {
33
+ contentType: 'public'
34
+ }
35
+ },
36
+ {
37
+ path : '/signup/connect',
38
+ name : 'signup-connect',
39
+ component: ContentView,
40
+ meta : {
41
+ contentType: 'public'
42
+ }
43
+ },
44
+ {
45
+ path : '/signup/verify',
46
+ name : 'signup-verify',
47
+ component: ContentView,
48
+ meta : {
49
+ contentType: 'public'
50
+ }
51
+ },
52
+ {
53
+ path : '/signup/done',
54
+ name : 'signup-done',
55
+ component: ContentView,
56
+ meta : {
57
+ contentType: 'public'
58
+ }
59
+ },
60
+ {
61
+ path : '/download/:app/:filename',
62
+ name : 'download',
63
+ component: DownloadView,
64
+ meta : {
65
+ contentType: 'members'
66
+ }
67
+ },
68
+ {
69
+ path : '',
70
+ name : 'public-home',
71
+ component: ContentView,
72
+ meta : {
73
+ contentType: 'public'
74
+ }
75
+ },
76
+ {
77
+ path : '/:pathMatch(.*)*',
78
+ name : 'public-content',
79
+ component: ContentView,
80
+ meta : {
81
+ contentType: 'public'
82
+ }
83
+ },
84
+ ];
85
+ const editorRoutes: readonly RouteRecordRaw[] = [
86
+ {
87
+ path : '/:pathMatch(.*)*',
88
+ name : 'editor',
89
+ component: EditorView
90
+ },
91
+ ];
92
+
93
+ /**
94
+ * the module gallery is the design itself, so the project provides the views.
95
+ * testView is dev only and is left out of a production build.
96
+ */
97
+ function createPreviewRoutes (previewView: Component, testView?: Component): readonly RouteRecordRaw[] {
98
+
99
+ const testRoutes: RouteRecordRaw[] = testView && import.meta.env.DEV
100
+ ? [
101
+ {
102
+ path : '/test/:name',
103
+ name : 'test-view',
104
+ component: testView,
105
+ meta : {
106
+ testOnly: true,
107
+ },
108
+ },
109
+ ]
110
+ : [];
111
+
112
+ return [
113
+ ...testRoutes,
114
+ {
115
+ path : '/:pathMatch(.*)*',
116
+ name : 'preview',
117
+ component: previewView
118
+ },
119
+ {
120
+ path : '',
121
+ name : 'preview-home',
122
+ component: previewView
123
+ },
124
+ ];
125
+
126
+ }
127
+
128
+ export {
129
+ dynamicRoutes,
130
+ editorRoutes,
131
+ createPreviewRoutes
132
+ };
@@ -0,0 +1,113 @@
1
+ import { renderToString } from 'vue/server-renderer';
2
+ import { getAppInstance, type AppComponents } from './main';
3
+
4
+ import { basename } from 'path';
5
+ import type { Content, Customers, List } from '@lilaquadrat/interfaces';
6
+ import type { RouteRecordRaw } from 'vue-router';
7
+ import useContentStore from './stores/content.store';
8
+ import { dynamicRoutes } from './routes';
9
+ import hooks from './mixins/hooks';
10
+
11
+ export async function render (url: string, context: any, contextData: Content[], recipientData: Customers, list: List, manifest: Record<string, string[]>, options: {cdnUrl: string, components: AppComponents, routes?: readonly RouteRecordRaw[]}) {
12
+ const { app, router, pinia } = getAppInstance(context, options.routes ?? dynamicRoutes, false, options.components);
13
+
14
+ hooks(router, { initAuth: false });
15
+
16
+ await router.push(url);
17
+ await router.isReady();
18
+
19
+ const contentStore = useContentStore();
20
+
21
+ /**
22
+ * for pdfs all images must be preloaded
23
+ * because there will be no hydration
24
+ */
25
+ if(context.renderTarget === 'pdf') {
26
+
27
+ context.settings.preloadImages = true;
28
+
29
+ }
30
+
31
+ if(contextData) contentStore.addMulti(contextData);
32
+ if(recipientData) contentStore.recipient = recipientData;
33
+ if(list) contentStore.list = list;
34
+
35
+ // passing SSR context object which will be available via useSSRContext()
36
+ // @vitejs/plugin-vue injects code into a component's setup() that registers
37
+ // itself on ctx.modules. After the render, ctx.modules would contain all the
38
+ // components that have been instantiated during this render call.
39
+ const ctx: { modules?: Set<string> } = {};
40
+ const html = await renderToString(app, ctx);
41
+ const initialState = JSON.stringify(pinia.state.value).replace(/</g, '\\u003c');
42
+ // the SSR manifest generated by Vite contains module -> chunk/asset mapping
43
+ // which we can then use to determine what files need to be preloaded for this
44
+ // request.
45
+ const preloadLinks = renderPreloadLinks(ctx.modules as Set<string>, manifest, options);
46
+
47
+ return { html, preloadLinks, initialState, url };
48
+ }
49
+
50
+ function renderPreloadLinks (modulesSet: Set<string>, manifest: Record<string, string[]>, options: {cdnUrl: string}) {
51
+ let links = '';
52
+ const seen = new Set();
53
+
54
+ modulesSet?.forEach((id) => {
55
+
56
+ const files = manifest[id];
57
+
58
+ if (files) {
59
+
60
+ files.forEach((file: string) => {
61
+
62
+ if (!seen.has(file)) {
63
+ seen.add(file);
64
+
65
+ const filename = basename(file);
66
+
67
+ if (manifest[filename]) {
68
+
69
+ for (const depFile of manifest[filename]) {
70
+ links += renderPreloadLink(depFile, options.cdnUrl);
71
+ seen.add(depFile);
72
+ }
73
+
74
+ }
75
+
76
+ links += renderPreloadLink(file, options.cdnUrl);
77
+ }
78
+
79
+ });
80
+ }
81
+
82
+ });
83
+
84
+ return links;
85
+
86
+ }
87
+
88
+ function renderPreloadLink (file: string, base: string) {
89
+ if (!file) return '';
90
+
91
+ const fileExtMap: Record<string, { rel: string, as?: string, type?: string, crossorigin?: boolean }> = {
92
+ '.js' : { rel: 'modulepreload', crossorigin: true },
93
+ '.css' : { rel: 'stylesheet' },
94
+ '.woff' : { rel: 'preload', as: 'font', type: 'font/woff', crossorigin: true },
95
+ '.woff2': { rel: 'preload', as: 'font', type: 'font/woff2', crossorigin: true },
96
+ '.gif' : { rel: 'preload', as: 'image', type: 'image/gif' },
97
+ '.jpg' : { rel: 'preload', as: 'image', type: 'image/jpeg' },
98
+ '.jpeg' : { rel: 'preload', as: 'image', type: 'image/jpeg' },
99
+ '.png' : { rel: 'preload', as: 'image', type: 'image/png' }
100
+ };
101
+ const ext = Object.keys(fileExtMap).find(ext => file.endsWith(ext));
102
+
103
+ if (!ext) return '';
104
+
105
+ const { rel, as, type, crossorigin } = fileExtMap[ext];
106
+ let link = `<link rel="${rel}" href="${base}${file}"`;
107
+
108
+ if (as) link += ` as="${as}"`;
109
+ if (type) link += ` type="${type}"`;
110
+ if (crossorigin) link += ' crossorigin';
111
+
112
+ return `${link}>`;
113
+ }
@@ -0,0 +1,33 @@
1
+ import { ref } from 'vue'
2
+ import { defineStore } from 'pinia'
3
+ import { v4 as uuid } from 'uuid';
4
+
5
+ export const useCallStore = defineStore('calls', () => {
6
+
7
+ const calls = ref<Record<string, 'pending' | 'resolved' | 'rejected'>>({});
8
+
9
+ function addCall () {
10
+
11
+ const id = uuid();
12
+
13
+ calls.value[id] = 'pending';
14
+
15
+ return id;
16
+
17
+ }
18
+
19
+ function updateCall (id: string, state: 'pending' | 'resolved' | 'rejected') {
20
+
21
+ calls.value[id] = state;
22
+
23
+ }
24
+
25
+ return {
26
+ calls,
27
+ addCall,
28
+ updateCall
29
+ }
30
+
31
+ })
32
+
33
+ export default useCallStore;