@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,124 @@
1
+ import { ref } from 'vue'
2
+ import { defineStore } from 'pinia'
3
+ import StudioSDK from '@lilaquadrat/sdk';
4
+ import useMainStore from './main.store';
5
+ import { auth } from '../plugins/auth';
6
+ import logger from '../mixins/logger';
7
+ import { type CustomerPerson } from '@lilaquadrat/interfaces';
8
+
9
+ export const useUserStore = defineStore('user', () => {
10
+
11
+ const locked = ref<string>();
12
+ const customer = ref<{_id: string, id?: string}>();
13
+ const emailConfirmationCode = ref<string>();
14
+ const userData = ref<CustomerPerson>();
15
+ const isUser = ref<boolean>(false);
16
+ const isFullUser = ref<boolean>(false);
17
+
18
+ function setCustomer (customerData: {_id: string, id?: string}) {
19
+
20
+ customer.value = customerData;
21
+ localStorage.setItem('lila-customer', JSON.stringify(customerData));
22
+
23
+ }
24
+
25
+ async function initCustomer () {
26
+
27
+ const customerData = localStorage.getItem('lila-customer');
28
+
29
+ logger.userstore('init');
30
+
31
+ if(customerData) {
32
+
33
+ customer.value = JSON.parse(customerData);
34
+
35
+ }
36
+
37
+ if(auth.isAuth.value) {
38
+
39
+ await updateLock();
40
+ isUser.value = true;
41
+
42
+ if(locked.value === 'user-connect') {
43
+ isFullUser.value = false;
44
+ } else {
45
+ isFullUser.value = true;
46
+ }
47
+
48
+ }
49
+
50
+ logger.userstore('done');
51
+
52
+ }
53
+
54
+ async function updateLock () {
55
+
56
+ if(auth.isAuth.value) {
57
+
58
+ await getUser();
59
+
60
+ if(!userData.value) {
61
+
62
+ console.error('LOCK THE USE FOR MISSING CONNECTION');
63
+ locked.value = 'user-connect';
64
+
65
+ return;
66
+
67
+ }
68
+
69
+ /**
70
+ * check if a customerId is given and user is not connected
71
+ */
72
+ if (!userData.value?.emailConfirmed) {
73
+
74
+ console.error('LOCK THE USE FOR EMAIL VERIFICATION');
75
+ locked.value = 'email-verified';
76
+
77
+ return;
78
+
79
+ }
80
+
81
+ locked.value = undefined;
82
+ isFullUser.value = true;
83
+
84
+ }
85
+
86
+ }
87
+
88
+ async function getUser () {
89
+
90
+ const mainStore = useMainStore();
91
+ const sdk = new StudioSDK(mainStore.apiConfig);
92
+
93
+ try {
94
+
95
+ StudioSDK.flushCache('me');
96
+
97
+ const me = await sdk.members.me.get();
98
+
99
+ userData.value = me.data;
100
+
101
+ } catch (error) {
102
+
103
+ console.error(error);
104
+
105
+ }
106
+
107
+ }
108
+
109
+ return {
110
+ locked,
111
+ setCustomer,
112
+ initCustomer,
113
+ updateLock,
114
+ customer,
115
+ userData,
116
+ isUser,
117
+ isFullUser,
118
+ getUser,
119
+ emailConfirmationCode
120
+ }
121
+
122
+ })
123
+
124
+ export default useUserStore;
@@ -0,0 +1,138 @@
1
+ /**
2
+ * Framework translations shipped with @lilaquadrat/design-core.
3
+ *
4
+ * Only strings the package itself renders live here: validation, list state,
5
+ * file handling, the signup/order flow and the shared form labels.
6
+ *
7
+ * Module wording stays with the design and is merged on top through
8
+ * addMessages() - see plugins/translations.
9
+ */
10
+ export default {
11
+ 'home' : 'Home',
12
+ 'validation-error-type-number' : 'muss eine Zahl sein',
13
+ 'validation-error-type-string' : 'muss Text sein',
14
+ 'validation-error-minLength' : 'muss mindestens {0} Zeichen lang sein',
15
+ 'validation-error-maxLength' : 'darf maximal {0} Zeichen lang sein',
16
+ 'validation-error-additionalProperties' : 'Das zusätzliche Attribut {0} ist nicht erlaubt',
17
+ 'validation-error-maxItems' : 'darf maximal {0} Elemente besitzen',
18
+ 'validation-error-minItems' : 'muss mindestens {0} Elemente besitzen',
19
+ 'validation-error-contains' : 'muss mindestens {0} valide(s) Element(e) besitzen',
20
+ 'validation-error-required' : 'muss gesetzt sein',
21
+ 'validation-error-accepted' : 'Vereinbarungen müssen akzeptiert werden',
22
+ 'validation-error-invalidId' : 'ist keine valide ID',
23
+ 'validation-error-USER_ALREADY_CONNECTED' : 'Der Benutzer ist bereits mit einem Account verbunden',
24
+ 'city' : 'Stadt',
25
+ 'zipcode' : 'Postleitzahl',
26
+ 'street & number' : 'Straße & Hausnummer',
27
+ 'street' : 'Straße',
28
+ 'streetnumber' : 'Hausnummer',
29
+ 'prename' : 'Vorname',
30
+ 'name' : 'Name',
31
+ 'country' : 'Land',
32
+ 'country-aut' : 'Österreich',
33
+ 'country-che' : 'Schweiz',
34
+ 'country-deu' : 'Deutschland',
35
+ 'email' : 'E-Mail',
36
+ 'phone' : 'Telefon',
37
+ 'personal' : 'Persönliche Daten',
38
+ 'address' : 'Adresse',
39
+ 'addressAddition' : 'Adresszusatz',
40
+ 'contact' : 'Kontakt',
41
+ 'price_with_tax' : 'inkl. {0}% MwSt.',
42
+ 'category' : 'Kategorie',
43
+ 'message' : 'Nachricht',
44
+ 'order with payment' : 'Zahlungspflichtig bestellen',
45
+ 'back to the form' : 'Zurück zum Formular',
46
+ 'not available' : 'nicht verfügbar',
47
+ 'no charge' : 'kostenlos',
48
+ 'address addition' : 'Adresszusatz',
49
+ 'address search' : 'Adresssuche',
50
+ 'no matching addresses' : 'Keine passende Adresse gefunden',
51
+ 'provide at least your street and housenumber to find your address. city and zipcode increase accuracy.': 'Bitte geben Sie mindestens Ihre Straße und Hausnummer an, um Ihre Adresse zu finden. Stadt und Postleitzahl erhöhen die Genauigkeit.',
52
+ 'type your street and number' : 'Strasse und Hausnummer',
53
+ 'validation-error-LIST_CANNOT_JOIN' : 'Es ist leider ein Fehler aufgetreten. Bitte versuchen Sie es später erneut.',
54
+ 'validation-error-LIST_NOT_FOUND' : 'Es ist leider ein Fehler aufgetreten. Bitte versuchen Sie es später erneut.',
55
+ 'validation-error-LIST_NO_SPOT_AVAILABLE' : 'Es ist keine weiteren Plätze verfügbar.',
56
+ 'validation-error-LIST_UNIQUE_CUSTOMER_CONFIRMED' : 'Die maximale Anzahl wurde erreicht.',
57
+ 'validation-error-EMAIL_INVALID' : 'Bitte überprüfe deine E-Mail Adresse',
58
+ 'LIST_CANNOT_JOIN_reservation' : 'Es ist leider ein Fehler aufgetreten. Bitte versuchen Sie es später erneut.',
59
+ 'LIST_UNIQUE_CUSTOMER_CONFIRMED_reservation' : 'Sie sind bereits für eine Reservierung vorgemerkt.',
60
+ 'LIST_NOT_FOUND_reservation' : 'Es ist leider ein Fehler aufgetreten. Bitte versuchen Sie es später erneut.',
61
+ 'LIST_NO_SPOT_AVAILABLE_reservation' : 'Leider sind keine Plätze mehr verfügbar.',
62
+ 'LIST_CANNOT_JOIN_contact' : 'Es ist leider ein Fehler aufgetreten. Bitte versuchen Sie es später erneut.',
63
+ 'LIST_UNIQUE_CUSTOMER_CONFIRMED_contact' : 'Sie haben bereits die maximale Anzahl an Nachrichten gesendet.',
64
+ 'LIST_NOT_FOUND_contact' : 'Es ist leider ein Fehler aufgetreten. Bitte versuchen Sie es später erneut.',
65
+ 'LIST_NO_SPOT_AVAILABLE_contact' : 'Die maximale Anzahl an Nachrichten wurde erreicht.',
66
+ 'LIST_CATEGORY_LIMITED_AVAILABILITY_SINGULAR' : 'Nur einmalig verfügbar',
67
+ 'LIST_CATEGORY_LIMITED_AVAILABILITY_PLURAL' : 'Limitiert auf {0} Plätze.',
68
+ 'LIST_CATEGORY_LIMITED_AVAILABILITY' : 'Nur einmalig verfügbar | Limitiert auf {0} Plätze.',
69
+ 'LIST_LIMITED_AVAILABILITY_SINGULAR' : 'Für diese Veranstaltung ist maximal ein Platz verfügbar.',
70
+ 'LIST_LIMITED_AVAILABILITY_PLURAL' : 'Für diese Veranstaltung sind maximal {0} Plätze verfügbar.',
71
+ 'LIST_LIMITED_AVAILABILITY' : 'Für diese Veranstaltung ist maximal ein Platz verfügbar. | Für diese Veranstaltung sind maximal {0} Plätze verfügbar.',
72
+ 'LIST_LIMITED_AVAILABILITY_STATE' : 'Für diese Veranstaltung sind noch {0} von {1} Plätze verfügbar.',
73
+ 'LIST_CATEGORY_LAST_CHANCE_PERCENT' : 'Mehr als {0}% gebucht',
74
+ 'LIST_CATEGORY_LAST_CHANCE_SINGULAR' : 'Nur noch {0} Platz verfügbar',
75
+ 'LIST_CATEGORY_LAST_CHANCE_PLURAL' : 'Nur noch {0} Plätze verfügbar',
76
+ 'LIST_CATEGORY_LAST_CHANCE' : 'Nur noch {0} Platz verfügbar | Nur noch {0} Plätze verfügbar',
77
+ 'LIST_CATEGORY_SOLD_OUT' : 'AUSVERKAUFT',
78
+ 'LIST_SOLD_OUT' : 'Es sind keine weiteren Plätze verfügbar',
79
+ 'EMAIL_INVALID_contact' : 'Bitte überprüfe deine E-Mail Adresse',
80
+ 'EMAIL_INVALID_reservation' : 'Bitte überprüfe deine E-Mail Adresse',
81
+ 'EMAIL_INVALID_content' : 'Bitte überprüfe deine E-Mail Adresse',
82
+ 'euro' : '€',
83
+ 'required' : 'erforderlich',
84
+ 'disabled' : 'deaktiviert',
85
+ 'loading' : 'wird geladen',
86
+ 'Something went wrong' : 'Hier ist etwas schief gelaufen',
87
+ 'The content is missing' : 'Der Inhalt wurde nicht gefunden',
88
+ 'CONTENT_NOT_FOUND' : 'Inhalt wurde nicht gefunden',
89
+ 'CONTENT_LOADING' : 'Wird geladen',
90
+ 'customerId' : 'Kunden ID',
91
+ 'connect-to-user' : 'Account verbinden',
92
+ 'confirm-email' : 'E-Mail Adresse bestätigen',
93
+ 'signup-step-register' : 'Registrierung',
94
+ 'signup-step-account' : 'Konto erstellen',
95
+ 'signup-step-connect' : 'Account verbinden',
96
+ 'signup-step-verify' : 'E-Mail bestätigen',
97
+ 'ORDER_SUCCESS_TITLE' : 'Vielen Dank für deinen Kauf',
98
+ 'INVOICE_NOT_YET_READY_TITLE' : 'Deine Rechnung ist noch nicht verfügbar.',
99
+ 'INVOICE_NOT_YET_READY_DESCRIPTION' : 'Du kannst diese Seite neu laden um den Status zu aktualisieren.',
100
+ 'INVOICE_DOWNLOAD' : 'Rechnung herunterladen',
101
+ 'ORDER_ERROR_TITLE' : 'Ein Fehler ist aufgetreten',
102
+ 'ORDER_ERROR_DESCRIPTION' : 'Deine Bestellung konnte leider nicht gefunden werden. Bitte versuche es später erneut.',
103
+ 'confirmationCode' : 'Bestätigungscode',
104
+ 'EMAIL_CONFIRMATION_FAILED' : 'E-Mail Adresse konnte nicht bestaetigt werden.',
105
+ 'validation-error-EMAIL_CONFIRMATION_FAILED' : 'E-Mail Adresse konnte nicht bestaetigt werden.',
106
+ 'CONFIRM_BUTTON' : 'Aktion bestätigen',
107
+ 'files-add' : 'Dateien hinzufügen',
108
+ 'files-start-upload' : 'Upload starten',
109
+ 'files-clear-finished' : 'Abgeschlossene entfernen',
110
+ 'files-remove' : 'Entfernen',
111
+ 'files-delete' : 'Löschen',
112
+ 'files-download' : 'Herunterladen',
113
+ 'files-uploaded-at' : 'Hochgeladen',
114
+ 'files-loading' : 'Dateien werden geladen...',
115
+ 'files-empty' : 'Keine Dateien vorhanden',
116
+ 'files-list' : 'Dateien',
117
+ 'files-state-pending' : 'Wartet',
118
+ 'files-state-creating' : 'Erstelle...',
119
+ 'files-state-processing' : 'Verarbeitet',
120
+ 'files-state-uploading' : 'Lädt hoch',
121
+ 'files-state-done' : 'Fertig',
122
+ 'files-state-error' : 'Fehler',
123
+ 'files-asset' : 'Asset',
124
+ 'files-max-size' : 'Max. Dateigröße',
125
+ 'files-max-count' : 'Max. Anzahl',
126
+ 'files-allowed-types' : 'Erlaubte Dateitypen',
127
+ 'files-error-mime' : 'Dateityp nicht erlaubt',
128
+ 'files-error-too-large' : 'Datei überschreitet maximale Größe',
129
+ 'files-error-max-count' : 'Maximale Anzahl Dateien erreicht',
130
+ 'files-error-no-asset' : 'Asset auswählen vor dem Hochladen',
131
+ 'files-error-select-structure' : 'Bitte zuerst eine Kategorie auswählen',
132
+ 'files-select-structure' : 'Kategorie auswählen',
133
+ 'files-category-all' : 'Alle Dateien',
134
+ 'files-download-start' : 'Download starten',
135
+ 'files-error-no-url' : 'Download-URL konnte nicht erstellt werden',
136
+ 'FILE_LIMIT_EXCEEDED' : 'Maximale Anzahl Dateien überschritten',
137
+ 'UPLOAD_FAILED' : 'Upload fehlgeschlagen',
138
+ };
@@ -0,0 +1,219 @@
1
+ <script setup lang="ts">
2
+ import { computed, ref, watch } from 'vue';
3
+ import { distributeGenericData, generateDataWithContent, hardCopy, prepareContent } from '@lilaquadrat/studio';
4
+ import type { BasicData, Content, ContentWithPositions, GenericData } from '@lilaquadrat/interfaces';
5
+ import useMainStore from '../stores/main.store';
6
+ import { useRoute } from 'vue-router';
7
+ import { onBeforeMount } from 'vue';
8
+ import type { SDKResponse } from '@lilaquadrat/sdk';
9
+ import useUserStore from '../stores/user.store';
10
+ import { onServerPrefetch } from 'vue';
11
+ import useContentStore from '../stores/content.store';
12
+ import { HelpersPlugin } from '../plugins/filters';
13
+
14
+ const route = useRoute();
15
+ const userStore = useUserStore();
16
+ const mainStore = useMainStore();
17
+ const contentStore = useContentStore();
18
+ const dataMerged = ref<ContentWithPositions>();
19
+ const layout = ref<SDKResponse<BasicData<Content>> | undefined>();
20
+ const loading = ref<number>(0);
21
+ const error = ref<boolean>(false);
22
+
23
+ watch(route, async () => await getContent());
24
+ watch(() => userStore.locked, () => getContent());
25
+ onBeforeMount(async () => await getContent());
26
+ onServerPrefetch(() => getStoreContent(HelpersPlugin.getFilename(contentType.value, route.path)));
27
+
28
+ const linkMode = computed(() => {
29
+
30
+ if(mainStore.config?.dynamic === false) return 'link';
31
+
32
+ return undefined
33
+
34
+ });
35
+ const contentType = computed(() => route.meta.contentType as 'public' | 'members');
36
+ const isLocked = computed(() => contentType.value == 'members' && userStore.locked?.length);
37
+ const hint = computed(() => {
38
+
39
+ if(loading.value === 404) {
40
+ return 'error-404'
41
+ }
42
+
43
+ if(isLocked.value) {
44
+
45
+ return `error-${userStore.locked}-${contentType.value}`;
46
+
47
+ }
48
+
49
+ return undefined;
50
+
51
+ });
52
+
53
+ function getStoreContent (filename: string) {
54
+
55
+ const storeContent = mainStore.data;
56
+ const storeLayout = mainStore.layout;
57
+
58
+ if(!storeContent) return false;
59
+
60
+ /**
61
+ * if the current data matches the required filename or the target is mail, because emails dont have filenames
62
+ * we can use the store data
63
+ */
64
+ if(storeContent && (storeContent.settings?.filename?.includes(filename) || storeContent.target === 'mail' || filename === '__pdf')) {
65
+
66
+ dataMerged.value = mergeContent(storeContent, storeLayout);
67
+ loading.value = 200;
68
+ error.value = false;
69
+
70
+ updateContext();
71
+ return true;
72
+
73
+ } else {
74
+
75
+ dataMerged.value = undefined;
76
+ return false;
77
+
78
+ }
79
+
80
+ }
81
+
82
+ function updateContext () {
83
+
84
+ contentStore.setContext({
85
+ sitetitle : mainStore.data?.settings?.title,
86
+ description: mainStore.data?.settings?.description,
87
+ })
88
+
89
+ }
90
+
91
+ async function getContent () {
92
+
93
+ let content: SDKResponse<BasicData<Content>> | undefined;
94
+
95
+ if(isLocked.value) {
96
+
97
+ error.value = true;
98
+ loading.value = 403;
99
+ return;
100
+
101
+ }
102
+
103
+ const filename = HelpersPlugin.getFilename(contentType.value, route.path);
104
+
105
+ /**
106
+ * while hydration we need to check if the store has the content
107
+ * to avoid fetching it again from the server
108
+ */
109
+
110
+ if(getStoreContent(filename)) return;
111
+
112
+ // if(data.value?.data?.settings?.filename?.includes(filename)) return;
113
+
114
+ loading.value = 0;
115
+ error.value = false;
116
+
117
+ try {
118
+
119
+ content = await mainStore.getContent({filename}, contentType.value) as SDKResponse<BasicData<Content>>;
120
+
121
+ } catch (e) {
122
+
123
+ console.error(error);
124
+ loading.value = e.response.status;
125
+
126
+ }
127
+
128
+ if(content) {
129
+
130
+ mainStore.data = content.data;
131
+ loading.value = content.status;
132
+ updateContext();
133
+
134
+ if(loading.value === 200) {
135
+
136
+ error.value = false;
137
+
138
+ if(mainStore.data?.settings?.useLayout) {
139
+
140
+ // a layout explicitly assigned to the page always wins (also in dynamic mode)
141
+ layout.value = await mainStore.getContent({internalId: mainStore.data.settings.useLayout.toString()}, contentType.value) as SDKResponse<BasicData<Content>>;
142
+ mainStore.layout = layout.value.data;
143
+
144
+ } else if(mainStore.config?.dynamic) {
145
+
146
+ layout.value = await mainStore.getContent({id: `${contentType.value}-layout`}, contentType.value) as SDKResponse<BasicData<Content>>;
147
+ mainStore.layout = layout.value.data;
148
+
149
+ } else {
150
+
151
+ mainStore.layout = undefined;
152
+ layout.value = undefined;
153
+
154
+ }
155
+
156
+ }
157
+
158
+ }
159
+
160
+ if ([400, 401, 403].includes(loading.value)) {
161
+
162
+ error.value = true;
163
+
164
+ } else if ([204, 404].includes(loading.value) || !loading.value) {
165
+
166
+ error.value = true;
167
+ loading.value = 404;
168
+
169
+ }
170
+
171
+ if(mainStore.data) {
172
+
173
+ dataMerged.value = mergeContent(mainStore.data, mainStore.layout);
174
+
175
+ } else {
176
+
177
+ dataMerged.value = undefined;
178
+
179
+ }
180
+
181
+ }
182
+
183
+ function mergeContent (baseContent: Partial<BasicData<Content>>, layout?: Content) {
184
+
185
+ if(baseContent) {
186
+
187
+ const safeData = hardCopy(baseContent);
188
+
189
+ if(safeData.modules) {
190
+
191
+ /**
192
+ * content without generic data is the normal case for a static page, and
193
+ * generateDataWithContent reads Object.keys of what it is given - so it
194
+ * throws on undefined instead of returning nothing
195
+ */
196
+ if(safeData.genericData) {
197
+
198
+ distributeGenericData(safeData.modules, generateDataWithContent(safeData.genericData as GenericData));
199
+
200
+ }
201
+
202
+ }
203
+
204
+ return prepareContent(safeData, layout);
205
+
206
+ }
207
+
208
+ return prepareContent({})
209
+
210
+ }
211
+
212
+ </script>
213
+
214
+ <template>
215
+ <article class="content-screen screen">
216
+ <lila-error-partial v-if="error" :status="loading" :hint="hint" :type="contentType" />
217
+ <lila-content-module v-if="dataMerged && !error" :link-mode="linkMode" :content="dataMerged" />
218
+ </article>
219
+ </template>
@@ -0,0 +1,143 @@
1
+ <script setup lang="ts">
2
+ import { computed, onMounted, ref } from 'vue';
3
+ import { useRoute } from 'vue-router';
4
+ import useMainStore from '../stores/main.store';
5
+ import useFilesStore from '../stores/files.store';
6
+
7
+ const route = useRoute();
8
+ const mainStore = useMainStore();
9
+ const filesStore = useFilesStore();
10
+ const loading = ref(true);
11
+ const error = ref<string | undefined>();
12
+ const app = computed(() => route.params.app as string);
13
+ const filename = computed(() => route.params.filename as string);
14
+ const customer = computed(() => route.query.customer as string | undefined);
15
+ const list = computed(() => route.query.list as string | undefined);
16
+ const scope = computed(() => app.value === 'customers' ? 'company' : 'project');
17
+ const extension = computed(() => filename.value.split('.').pop()?.toLowerCase());
18
+ const imageExtensions = new Set(['jpg', 'jpeg', 'png', 'gif', 'webp', 'svg', 'bmp', 'avif']);
19
+ const videoExtensions = new Set(['mp4', 'webm', 'ogg', 'mov', 'avi', 'mkv']);
20
+ const isImage = computed(() => imageExtensions.has(extension.value ?? ''));
21
+ const isVideo = computed(() => videoExtensions.has(extension.value ?? ''));
22
+ const downloadUrl = computed(() => {
23
+
24
+ const { company, project } = mainStore.config ?? {};
25
+ const { secure } = mainStore.config?.cdn ?? {};
26
+
27
+ if (!secure || !company) return undefined;
28
+
29
+ const parts = [secure, app.value, company];
30
+
31
+ if (app.value !== 'customers' && project) {
32
+ parts.push(project);
33
+ if (list.value) parts.push(list.value);
34
+ }
35
+
36
+ parts.push(filename.value);
37
+
38
+ const queryParams = new URLSearchParams();
39
+
40
+ if (customer.value) queryParams.set('customer', customer.value);
41
+
42
+ const token = filesStore.token?.token;
43
+
44
+ if (token) queryParams.set('token', token);
45
+
46
+ const queryString = queryParams.toString();
47
+
48
+ return parts.join('/') + (queryString ? `?${queryString}` : '');
49
+
50
+ });
51
+
52
+ onMounted(async () => {
53
+
54
+ if (!filesStore.isTokenValid(app.value, scope.value)) {
55
+
56
+ try {
57
+
58
+ await filesStore.fetchToken(app.value, scope.value);
59
+
60
+ } catch (e) {
61
+
62
+ error.value = (e as Error)?.message || 'TOKEN_FETCH_FAILED';
63
+
64
+ }
65
+
66
+ }
67
+
68
+ loading.value = false;
69
+
70
+ });
71
+
72
+ </script>
73
+ <template>
74
+ <main class="download-view">
75
+ <article v-if="loading">
76
+ <p>{{ $t('files-loading') }}</p>
77
+ </article>
78
+ <article v-if="!loading && error" class="error">
79
+ <p>{{ $t(error) }}</p>
80
+ </article>
81
+ <template v-if="!loading && !error && downloadUrl">
82
+ <header class="download-header">
83
+ <h1>{{ filename }}</h1>
84
+ <lila-link-partial call-to-action :link="downloadUrl" button>
85
+ {{ $t('files-download-start') }}
86
+ </lila-link-partial>
87
+ </header>
88
+ <section class="preview-area">
89
+ <lila-picture-partial
90
+ v-if="isImage"
91
+ :src="downloadUrl"
92
+ :alt="filename"
93
+ />
94
+ <lila-video-partial
95
+ v-if="isVideo"
96
+ :src="downloadUrl"
97
+ preload="metadata"
98
+ />
99
+ </section>
100
+ </template>
101
+ <article v-if="!loading && !error && !downloadUrl" class="error">
102
+ <p>{{ $t('files-error-no-url') }}</p>
103
+ </article>
104
+ </main>
105
+ </template>
106
+ <style lang="less" scoped>
107
+ .download-view {
108
+ display: flex;
109
+ flex-direction: column;
110
+ min-height: 100vh;
111
+
112
+ article {
113
+ text-align: center;
114
+ display: grid;
115
+ justify-items: center;
116
+ gap: 20px;
117
+
118
+ &.error p {
119
+ color: red;
120
+ }
121
+ }
122
+
123
+ .download-header {
124
+ display: flex;
125
+ align-items: center;
126
+ justify-content: space-between;
127
+ padding: 16px 24px;
128
+ border-bottom: 1px solid @grey1;
129
+
130
+ h1 {
131
+ margin: 0;
132
+ font-size: @mobile_paragraph;
133
+ }
134
+ }
135
+
136
+ .preview-area {
137
+ flex: 1;
138
+ display: grid;
139
+ place-items: center;
140
+ }
141
+
142
+ }
143
+ </style>