create-nuxt-base 0.1.19 → 0.1.21
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/CHANGELOG.md +9 -0
- package/README.md +1 -1
- package/index.js +22 -29
- package/nuxt-base-template/formkit-theme.js +137 -0
- package/nuxt-base-template/formkit.config.js +35 -0
- package/nuxt-base-template/nuxt.config.ts +34 -3
- package/nuxt-base-template/package-lock.json +21304 -0
- package/nuxt-base-template/package.json +37 -38
- package/nuxt-base-template/src/app.vue +8 -0
- package/nuxt-base-template/src/assets/css/tailwind.css +37 -2
- package/nuxt-base-template/src/components/ModalShare.vue +67 -0
- package/nuxt-base-template/src/components/SocialMediaBubble.vue +16 -0
- package/nuxt-base-template/src/components/base/BaseAccordion.vue +52 -0
- package/nuxt-base-template/src/components/base/BaseButton.vue +106 -0
- package/nuxt-base-template/src/components/base/BaseContainer.vue +5 -0
- package/nuxt-base-template/src/components/base/BaseInfinityList.vue +34 -0
- package/nuxt-base-template/src/components/base/BaseModalContainer.vue +7 -0
- package/nuxt-base-template/src/components/base/BaseNotification.vue +81 -0
- package/nuxt-base-template/src/components/base/BaseNotificationContainer.vue +34 -0
- package/nuxt-base-template/src/components/base/BaseProgressbar.vue +66 -0
- package/nuxt-base-template/src/components/base/BaseToggle.vue +20 -0
- package/nuxt-base-template/src/components/transition/TransitionFade.vue +24 -0
- package/nuxt-base-template/src/components/transition/TransitionFadeScale.vue +24 -0
- package/nuxt-base-template/src/components/transition/TransitionSlide.vue +14 -0
- package/nuxt-base-template/src/components/transition/TransitionSlideBottom.vue +14 -0
- package/nuxt-base-template/src/components/transition/TransitionSlideRevert.vue +14 -0
- package/nuxt-base-template/src/composables/use-auth-fetch.ts +19 -0
- package/nuxt-base-template/src/composables/use-file.ts +21 -0
- package/nuxt-base-template/src/composables/use-form-helper.ts +100 -0
- package/nuxt-base-template/src/composables/use-helper.ts +52 -0
- package/nuxt-base-template/src/composables/use-modal.ts +84 -0
- package/nuxt-base-template/src/composables/use-notification.ts +29 -0
- package/nuxt-base-template/src/composables/use-share.ts +22 -0
- package/nuxt-base-template/src/error.vue +53 -0
- package/nuxt-base-template/src/forms/inputs/InputCheckbox.vue +29 -0
- package/nuxt-base-template/src/forms/inputs/InputFreeTags.vue +98 -0
- package/nuxt-base-template/src/forms/inputs/InputImage.vue +65 -0
- package/nuxt-base-template/src/forms/inputs/InputTags.vue +112 -0
- package/nuxt-base-template/src/forms/inputs/InputToggle.vue +18 -0
- package/nuxt-base-template/src/forms/plugins/asterisk-plugin.ts +29 -0
- package/nuxt-base-template/src/forms/plugins/scroll-error-plugin.ts +36 -0
- package/nuxt-base-template/src/forms/plugins/value-changes-plugin.ts +13 -0
- package/nuxt-base-template/src/middleware/admin.global.ts +9 -0
- package/nuxt-base-template/src/middleware/auth.global.ts +5 -7
- package/nuxt-base-template/src/plugins/4.auth.server.ts +70 -0
- package/nuxt-base-template/src/tests/init.test.ts +12 -0
- package/nuxt-base-template/tailwind.config.js +42 -3
- package/nuxt-base-template/tsconfig.json +4 -1
- package/package.json +1 -1
- package/nuxt-base-template/cypress.config.ts +0 -54
- package/nuxt-base-template/plugins/index.js +0 -5
- package/nuxt-base-template/src/components/.gitkeep +0 -0
- package/nuxt-base-template/src/components/base/.gitkeep +0 -0
- package/nuxt-base-template/src/composables/.gitkeep +0 -0
- package/nuxt-base-template/src/forms/.gitkeep +0 -0
- package/nuxt-base-template/src/pages/.gitkeep +0 -0
- package/nuxt-base-template/src/tests/.gitkeep +0 -0
- package/nuxt-base-template/src/tests/hello-world.spec.ts +0 -11
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
const props = withDefaults(defineProps<{
|
|
3
|
+
startDuration?: number | `${number}`;
|
|
4
|
+
leaveDuration?: number | `${number}`;
|
|
5
|
+
}>(), {
|
|
6
|
+
startDuration: 100,
|
|
7
|
+
leaveDuration: 100,
|
|
8
|
+
});
|
|
9
|
+
</script>
|
|
10
|
+
|
|
11
|
+
<template>
|
|
12
|
+
<div :style="`--start-duration: ${props.startDuration}ms; --leave-duration: ${props.leaveDuration}ms;`">
|
|
13
|
+
<Transition
|
|
14
|
+
enter-active-class="transition ease-out duration-[--start-duration]"
|
|
15
|
+
enter-from-class="transform opacity-0"
|
|
16
|
+
enter-to-class="transform opacity-100"
|
|
17
|
+
leave-active-class="transition ease-in duration-[--leave-duration]"
|
|
18
|
+
leave-from-class="transform opacity-100"
|
|
19
|
+
leave-to-class="transform opacity-0"
|
|
20
|
+
>
|
|
21
|
+
<slot></slot>
|
|
22
|
+
</Transition>
|
|
23
|
+
</div>
|
|
24
|
+
</template>
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
const props = withDefaults(defineProps<{
|
|
3
|
+
startDuration?: number | `${number}`;
|
|
4
|
+
leaveDuration?: number | `${number}`;
|
|
5
|
+
}>(), {
|
|
6
|
+
startDuration: 100,
|
|
7
|
+
leaveDuration: 100,
|
|
8
|
+
});
|
|
9
|
+
</script>
|
|
10
|
+
|
|
11
|
+
<template>
|
|
12
|
+
<div :style="`--start-duration: ${props.startDuration}ms; --leave-duration: ${props.leaveDuration}ms;`">
|
|
13
|
+
<Transition
|
|
14
|
+
enter-active-class="transition ease-out duration-[--start-duration]"
|
|
15
|
+
enter-from-class="transform opacity-0 scale-95"
|
|
16
|
+
enter-to-class="transform opacity-100 scale-100"
|
|
17
|
+
leave-active-class="transition ease-in duration-[--leave-duration]"
|
|
18
|
+
leave-from-class="transform opacity-100 scale-100"
|
|
19
|
+
leave-to-class="transform opacity-0 scale-95"
|
|
20
|
+
>
|
|
21
|
+
<slot></slot>
|
|
22
|
+
</Transition>
|
|
23
|
+
</div>
|
|
24
|
+
</template>
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
<script setup lang="ts"></script>
|
|
2
|
+
|
|
3
|
+
<template>
|
|
4
|
+
<Transition
|
|
5
|
+
enter-active-class="transition ease-out duration-500"
|
|
6
|
+
enter-from-class="transform translate-x-full"
|
|
7
|
+
enter-to-class="transform translate-x-0"
|
|
8
|
+
leave-active-class="transition ease-in duration-500"
|
|
9
|
+
leave-from-class="transform translate-x-0"
|
|
10
|
+
leave-to-class="transform translate-x-full"
|
|
11
|
+
>
|
|
12
|
+
<slot></slot>
|
|
13
|
+
</Transition>
|
|
14
|
+
</template>
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
<script setup lang="ts"></script>
|
|
2
|
+
|
|
3
|
+
<template>
|
|
4
|
+
<Transition
|
|
5
|
+
enter-active-class="transition ease-out duration-500"
|
|
6
|
+
enter-from-class="transform translate-y-full"
|
|
7
|
+
enter-to-class="transform translate-y-0"
|
|
8
|
+
leave-active-class="transition ease-in duration-500"
|
|
9
|
+
leave-from-class="transform translate-y-0"
|
|
10
|
+
leave-to-class="transform translate-y-full"
|
|
11
|
+
>
|
|
12
|
+
<slot></slot>
|
|
13
|
+
</Transition>
|
|
14
|
+
</template>
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
<script setup lang="ts"></script>
|
|
2
|
+
|
|
3
|
+
<template>
|
|
4
|
+
<Transition
|
|
5
|
+
enter-active-class="transition ease-out duration-500"
|
|
6
|
+
enter-from-class="transform translate-x-[-100%]"
|
|
7
|
+
enter-to-class="transform translate-x-0"
|
|
8
|
+
leave-active-class="transition ease-in duration-500"
|
|
9
|
+
leave-from-class="transform translate-x-0"
|
|
10
|
+
leave-to-class="transform translate-x-[-100%]"
|
|
11
|
+
>
|
|
12
|
+
<slot></slot>
|
|
13
|
+
</Transition>
|
|
14
|
+
</template>
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { ExtractedRouteMethod, NitroFetchOptions, NitroFetchRequest, TypedInternalResponse } from 'nitropack';
|
|
2
|
+
|
|
3
|
+
export function useAuthFetch<DefaultT = unknown, DefaultR extends NitroFetchRequest = NitroFetchRequest, T = DefaultT, R extends NitroFetchRequest = DefaultR, O extends NitroFetchOptions<R> = NitroFetchOptions<R>>(request: R, opts?: O): Promise<TypedInternalResponse<R, T, ExtractedRouteMethod<R, O>>> {
|
|
4
|
+
const { requestNewToken } = useAuth();
|
|
5
|
+
const config = useRuntimeConfig();
|
|
6
|
+
|
|
7
|
+
// @ts-expect-error - because of nice types from ofetch <3
|
|
8
|
+
return $fetch(request, {
|
|
9
|
+
...opts,
|
|
10
|
+
baseURL: config.public.apiUrl,
|
|
11
|
+
retry: 3,
|
|
12
|
+
async onRequest({ request, options }) {
|
|
13
|
+
const { token } = await requestNewToken();
|
|
14
|
+
options.headers = {
|
|
15
|
+
'Authorization': 'Bearer ' + token,
|
|
16
|
+
};
|
|
17
|
+
},
|
|
18
|
+
});
|
|
19
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { useFetch } from '@vueuse/core';
|
|
2
|
+
import { useHelper } from '~/composables/use-helper';
|
|
3
|
+
|
|
4
|
+
export function useFile() {
|
|
5
|
+
const { isValidMongoID } = useHelper();
|
|
6
|
+
async function getFileInfo(id: string | undefined): Promise<any> {
|
|
7
|
+
const config = useRuntimeConfig();
|
|
8
|
+
|
|
9
|
+
if (!id) {
|
|
10
|
+
return null;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
if (!isValidMongoID(id)) {
|
|
14
|
+
return id;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
return useFetch<File>(config.public.apiUrl + '/files/info/' + id, { method: 'GET' });
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
return { getFileInfo };
|
|
21
|
+
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import type { FormKitEventListener, FormKitNode } from '@formkit/core';
|
|
2
|
+
import { getNode, submitForm as submitFormkit } from '@formkit/core';
|
|
3
|
+
|
|
4
|
+
export function useFormHelper() {
|
|
5
|
+
function prepareNode(node: FormKitNode | undefined, listener: FormKitEventListener, eventName: string = 'input') {
|
|
6
|
+
if (node?.on) {
|
|
7
|
+
node.on(eventName, listener);
|
|
8
|
+
}
|
|
9
|
+
node?.children?.forEach((childNode) => {
|
|
10
|
+
prepareNode(childNode as FormKitNode, listener, eventName);
|
|
11
|
+
});
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
async function valueChanges(nodeId: string, listener: FormKitEventListener, eventName: string = 'input') {
|
|
15
|
+
// Puffer to load form
|
|
16
|
+
await new Promise<void>((resolve) => setTimeout(() => {
|
|
17
|
+
resolve();
|
|
18
|
+
}, 1));
|
|
19
|
+
prepareNode(getNode(nodeId), listener, eventName);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function submitForm(formId: string) {
|
|
23
|
+
submitFormkit(formId);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function isDirty(formId: string): boolean {
|
|
27
|
+
const form = getNode(formId);
|
|
28
|
+
|
|
29
|
+
return !!form?.context?.state?.dirty;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
async function getChangedValue(formId: string): Promise<null | object> {
|
|
33
|
+
await new Promise(f => setTimeout(f, 100));
|
|
34
|
+
const form = getNode(formId);
|
|
35
|
+
|
|
36
|
+
if (!form) {
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
return getDifferences(form?.props?.initial, form?.value);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function getDifferences(obj1: any, obj2: any): any {
|
|
43
|
+
if (obj1 === obj2) {
|
|
44
|
+
return undefined;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if (typeof obj1 !== typeof obj2) {
|
|
48
|
+
return obj2;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (typeof obj1 !== 'object' || obj1 === null || obj2 === null) {
|
|
52
|
+
return obj2;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (Array.isArray(obj1) !== Array.isArray(obj2)) {
|
|
56
|
+
return obj2;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (Array.isArray(obj1)) {
|
|
60
|
+
if (obj1.length !== obj2.length) {
|
|
61
|
+
return obj2;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const diffArray: any[] = [];
|
|
65
|
+
|
|
66
|
+
for (let i = 0; i < obj1.length; i++) {
|
|
67
|
+
const itemDiff = getDifferences(obj1[i], obj2[i]);
|
|
68
|
+
if (itemDiff !== undefined) {
|
|
69
|
+
diffArray[i] = itemDiff;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (diffArray.length === 0) {
|
|
74
|
+
return undefined;
|
|
75
|
+
} else {
|
|
76
|
+
return diffArray;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const keys1 = Object.keys(obj1);
|
|
81
|
+
const keys2 = Object.keys(obj2);
|
|
82
|
+
|
|
83
|
+
const allKeys = new Set([...keys1, ...keys2]);
|
|
84
|
+
const diffObj: any = {};
|
|
85
|
+
for (const key of allKeys) {
|
|
86
|
+
const keyDiff = getDifferences(obj1[key], obj2[key]);
|
|
87
|
+
if (keyDiff !== undefined) {
|
|
88
|
+
diffObj[key] = keyDiff;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if (Object.keys(diffObj).length === 0) {
|
|
93
|
+
return undefined;
|
|
94
|
+
} else {
|
|
95
|
+
return diffObj;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return { getChangedValue, getDifferences, isDirty, submitForm, prepareNode, valueChanges };
|
|
100
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
export function useHelper() {
|
|
2
|
+
function removeFields(obj: any): any {
|
|
3
|
+
const ignoreFields = ['id', '__typename'];
|
|
4
|
+
if (Array.isArray(obj)) {
|
|
5
|
+
return obj.map(item => removeFields(item));
|
|
6
|
+
} else if (typeof obj === 'object' && obj !== null) {
|
|
7
|
+
const newObj: any = {};
|
|
8
|
+
for (const key in obj) {
|
|
9
|
+
if (!ignoreFields.includes(key)) {
|
|
10
|
+
newObj[key] = removeFields(obj[key]);
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
return newObj;
|
|
14
|
+
} else {
|
|
15
|
+
return obj;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function removeNullOrUndefined(obj: any): any {
|
|
20
|
+
if (Array.isArray(obj)) {
|
|
21
|
+
return obj.map(item => removeNullOrUndefined(item)).filter(item => item !== null && item !== undefined);
|
|
22
|
+
} else if (typeof obj === 'object' && obj !== null) {
|
|
23
|
+
const newObj: any = {};
|
|
24
|
+
for (const key in obj) {
|
|
25
|
+
const value = removeNullOrUndefined(obj[key]);
|
|
26
|
+
if (value !== null && value !== undefined) {
|
|
27
|
+
newObj[key] = value;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
return newObj;
|
|
31
|
+
} else {
|
|
32
|
+
return obj;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function isValidMongoID(id: string): boolean {
|
|
37
|
+
const validHexChars = /^[0-9a-fA-F]{24}$/;
|
|
38
|
+
|
|
39
|
+
return validHexChars.test(id);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function groupBy<T>(arr: T[], fn: (item: T) => any) {
|
|
43
|
+
return arr.reduce<Record<string, T[]>>((prev, curr) => {
|
|
44
|
+
const groupKey = fn(curr);
|
|
45
|
+
const group = prev[groupKey] || [];
|
|
46
|
+
group.push(curr);
|
|
47
|
+
return { ...prev, [groupKey]: group };
|
|
48
|
+
}, {});
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return { removeFields, removeNullOrUndefined, isValidMongoID, groupBy };
|
|
52
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
interface Modal<T> {
|
|
2
|
+
component: any;
|
|
3
|
+
data?: T;
|
|
4
|
+
size?: 'sm' | 'md' | 'lg' | 'auto';
|
|
5
|
+
closable?: boolean;
|
|
6
|
+
confirm?: (confirmed: boolean) => void;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export interface ModalContext<T = object> {
|
|
10
|
+
component: any;
|
|
11
|
+
data?: T;
|
|
12
|
+
show: boolean;
|
|
13
|
+
showInner: boolean;
|
|
14
|
+
size?: 'sm' | 'md' | 'lg' | 'auto';
|
|
15
|
+
closable?: boolean;
|
|
16
|
+
confirm?: (confirmed: boolean) => void;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const modalState = <T>() => useState<ModalContext<T> | null>(() => null);
|
|
20
|
+
|
|
21
|
+
export function useModal<T = object>() {
|
|
22
|
+
const modal = modalState<T>();
|
|
23
|
+
|
|
24
|
+
const open = (modalConfig: Modal<T>) => {
|
|
25
|
+
let delay = 0;
|
|
26
|
+
if (modal.value) {
|
|
27
|
+
delay = 130;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const data = Object.assign(modalConfig, { show: false, showInner: false }) as ModalContext<T>;
|
|
31
|
+
data.size ??= 'md';
|
|
32
|
+
data.closable ??= true;
|
|
33
|
+
|
|
34
|
+
setTimeout(() => {
|
|
35
|
+
modal.value = data;
|
|
36
|
+
}, delay);
|
|
37
|
+
|
|
38
|
+
// Disable scrolling of background
|
|
39
|
+
document.body.style.overflowY = 'hidden';
|
|
40
|
+
|
|
41
|
+
setTimeout(() => {
|
|
42
|
+
if (modal.value) {
|
|
43
|
+
modal.value.show = true;
|
|
44
|
+
}
|
|
45
|
+
}, 30 + delay);
|
|
46
|
+
|
|
47
|
+
setTimeout(() => {
|
|
48
|
+
if (modal.value) {
|
|
49
|
+
modal.value.showInner = true;
|
|
50
|
+
}
|
|
51
|
+
}, 100 + delay);
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
const close = (duration?: number) => {
|
|
55
|
+
let animationDuration = duration;
|
|
56
|
+
|
|
57
|
+
if (typeof animationDuration !== 'number') {
|
|
58
|
+
animationDuration = 100;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (modal.value) {
|
|
62
|
+
modal.value.showInner = false;
|
|
63
|
+
|
|
64
|
+
setTimeout(() => {
|
|
65
|
+
if (modal.value) {
|
|
66
|
+
modal.value.show = false;
|
|
67
|
+
}
|
|
68
|
+
}, 30 + animationDuration);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Deactivate scrolling of background
|
|
72
|
+
document.body.style.overflowY = '';
|
|
73
|
+
|
|
74
|
+
setTimeout(() => {
|
|
75
|
+
modal.value = null;
|
|
76
|
+
}, 130 + animationDuration);
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
return {
|
|
80
|
+
activeModal: modal,
|
|
81
|
+
open,
|
|
82
|
+
close,
|
|
83
|
+
};
|
|
84
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { randomUUID } from 'uncrypto';
|
|
2
|
+
|
|
3
|
+
interface Notification {
|
|
4
|
+
title: string;
|
|
5
|
+
text?: string;
|
|
6
|
+
type: 'success' | 'error' | 'warning' | 'info';
|
|
7
|
+
duration?: number;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const notificationState = () => useState<Array<Notification & { uuid: string }>>(() => []);
|
|
11
|
+
|
|
12
|
+
export function useNotification() {
|
|
13
|
+
const notifications = notificationState();
|
|
14
|
+
const notify = (message: Notification) => {
|
|
15
|
+
const data = Object.assign(message, { uuid: randomUUID() });
|
|
16
|
+
data.duration ??= 5000;
|
|
17
|
+
notifications.value.push(data);
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
const remove = (uuid: string) => {
|
|
21
|
+
notifications.value = notifications.value.filter(n => n.uuid !== uuid);
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
return {
|
|
25
|
+
notify,
|
|
26
|
+
remove,
|
|
27
|
+
notifications,
|
|
28
|
+
};
|
|
29
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { ModalShare } from '#components';
|
|
2
|
+
import { useModal } from '~/composables/use-modal';
|
|
3
|
+
|
|
4
|
+
export function useShare() {
|
|
5
|
+
const route = useRoute();
|
|
6
|
+
|
|
7
|
+
function share(title?: string, text?: string, url?: string) {
|
|
8
|
+
if (window?.navigator?.share) {
|
|
9
|
+
window.navigator.share({
|
|
10
|
+
url: url ?? route.fullPath,
|
|
11
|
+
title: title,
|
|
12
|
+
text: text ?? window.location.origin,
|
|
13
|
+
});
|
|
14
|
+
} else {
|
|
15
|
+
useModal().open({ component: ModalShare, size: 'md', data: { link: url ?? window.location.origin } });
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
return {
|
|
20
|
+
share,
|
|
21
|
+
};
|
|
22
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
const props = defineProps({
|
|
3
|
+
error: Object,
|
|
4
|
+
});
|
|
5
|
+
|
|
6
|
+
console.error(props.error);
|
|
7
|
+
const debugWord = ref<string>('');
|
|
8
|
+
const { current } = useMagicKeys();
|
|
9
|
+
|
|
10
|
+
watch(() => current.values(),
|
|
11
|
+
() => {
|
|
12
|
+
if (current.values().next().value === 'escape') {
|
|
13
|
+
debugWord.value = '';
|
|
14
|
+
return;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
if (debugWord.value === 'debug') {
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
if (current.values().next().value && !debugWord.value.includes(current.values().next().value)) {
|
|
22
|
+
debugWord.value += current.values().next().value;
|
|
23
|
+
}
|
|
24
|
+
},
|
|
25
|
+
);
|
|
26
|
+
|
|
27
|
+
const handleError = () => clearError({ redirect: '/' });
|
|
28
|
+
</script>
|
|
29
|
+
|
|
30
|
+
<template>
|
|
31
|
+
<NuxtLayout>
|
|
32
|
+
<div class="w-full min-h-screen flex flex-col justify-center items-center">
|
|
33
|
+
<div class="w-full flex flex-col items-center mb-20">
|
|
34
|
+
<h1 class="text-[12rem] font-extrabold text-transparent bg-clip-text bg-gradient-to-r from-primary-600 to-primary-400">
|
|
35
|
+
Oops!
|
|
36
|
+
</h1>
|
|
37
|
+
<h2 v-if="error?.statusCode" class="text-3xl text-gray-600">
|
|
38
|
+
{{ error?.statusCode }}
|
|
39
|
+
</h2>
|
|
40
|
+
</div>
|
|
41
|
+
|
|
42
|
+
<pre v-if="debugWord === 'debug'" class="w-full max-w-3xl mb-5 mx-auto bg-black/80 text-white font-mono p-2 rounded-lg overflow-x-scroll">
|
|
43
|
+
{{
|
|
44
|
+
{ error }
|
|
45
|
+
}}
|
|
46
|
+
</pre>
|
|
47
|
+
|
|
48
|
+
<BaseButton color="primary" appearance="outline" @click="handleError">
|
|
49
|
+
Zurück zur Startseite
|
|
50
|
+
</BaseButton>
|
|
51
|
+
</div>
|
|
52
|
+
</NuxtLayout>
|
|
53
|
+
</template>
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
const props = defineProps({
|
|
3
|
+
context: Object,
|
|
4
|
+
});
|
|
5
|
+
const name = props.context.name;
|
|
6
|
+
const label = props.context.attrs.text;
|
|
7
|
+
|
|
8
|
+
function setInput(e) {
|
|
9
|
+
props.context.node.input(e.target.checked);
|
|
10
|
+
}
|
|
11
|
+
</script>
|
|
12
|
+
|
|
13
|
+
<template>
|
|
14
|
+
<div :class="context.classes.container">
|
|
15
|
+
<label :id="props.context?.id" :class="context.classes.wrapper">
|
|
16
|
+
<div :class="context.classes.inner">
|
|
17
|
+
<input :class="context.classes.input" type="checkbox" :name="name" @input="setInput" />
|
|
18
|
+
<span :class="context.classes.decorator" aria-hidden="true">
|
|
19
|
+
<span :class="[context.classes.decoratorIcon, context.classes.icon]">
|
|
20
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 27">
|
|
21
|
+
<polygon fill="currentColor" points="26.99 0 10.13 17.17 4.69 11.63 0 16.41 10.4 27 15.05 22.27 15.09 22.31 32 5.1 26.99 0" />
|
|
22
|
+
</svg>
|
|
23
|
+
</span>
|
|
24
|
+
</span>
|
|
25
|
+
</div>
|
|
26
|
+
</label>
|
|
27
|
+
<span :class="context.classes.label" v-html="label"></span>
|
|
28
|
+
</div>
|
|
29
|
+
</template>
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
const props = defineProps({
|
|
3
|
+
context: Object,
|
|
4
|
+
});
|
|
5
|
+
|
|
6
|
+
const inputRef = ref();
|
|
7
|
+
const inputValue = ref<string>();
|
|
8
|
+
const name = props.context?.name;
|
|
9
|
+
const placeholder = props.context?.attrs?.placeholder;
|
|
10
|
+
const defaultSuggestions = [...props.context?.attrs?.suggestions] || [];
|
|
11
|
+
const suggestions = ref<string[]>([...props.context?.attrs?.suggestions]);
|
|
12
|
+
const tags = ref<string[]>([]);
|
|
13
|
+
patch();
|
|
14
|
+
|
|
15
|
+
function patch() {
|
|
16
|
+
const values = props.context?._value;
|
|
17
|
+
if (values && Array.isArray(values)) {
|
|
18
|
+
for (let item of values) {
|
|
19
|
+
addTag(item);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
function addTag(tag?: string) {
|
|
26
|
+
if (!inputValue.value && !tag) {
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
if (tag) {
|
|
31
|
+
tags.value.push(tag);
|
|
32
|
+
// remove tag from suggestions
|
|
33
|
+
if (suggestions.value.findIndex((e) => e === tag) >= 0) {
|
|
34
|
+
suggestions.value.splice(suggestions.value.findIndex((e) => e === tag), 1);
|
|
35
|
+
}
|
|
36
|
+
} else {
|
|
37
|
+
if (!tags.value.includes(inputValue.value as string)) {
|
|
38
|
+
tags.value.push(inputValue.value as string);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if (suggestions.value.includes(inputValue.value as string)) {
|
|
42
|
+
if (suggestions.value.findIndex((e) => e === tag) >= 0) {
|
|
43
|
+
suggestions.value.splice(suggestions.value.findIndex((e) => e === inputValue.value), 1);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
inputValue.value = '';
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
props.context?.node.input(tags.value);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function removeTag(tag: string) {
|
|
54
|
+
tags.value.splice(tags.value.findIndex((e) => e === tag), 1);
|
|
55
|
+
|
|
56
|
+
if (defaultSuggestions.includes(tag) && !suggestions.value.includes(tag)) {
|
|
57
|
+
suggestions.value.push(tag);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
props.context?.node.input(tags.value);
|
|
61
|
+
}
|
|
62
|
+
</script>
|
|
63
|
+
|
|
64
|
+
<template>
|
|
65
|
+
<div class="flex flex-col">
|
|
66
|
+
<div class="flex flex-row flex-wrap gap-2">
|
|
67
|
+
<span v-for="(tag, index) of tags" :key="index" :class="context?.classes.tag">{{ tag }} <span class="i-bi-x" :class="context?.classes.tagIcon" @click="removeTag(tag)"></span> </span>
|
|
68
|
+
</div>
|
|
69
|
+
<div :class="context?.classes.inputWrapper">
|
|
70
|
+
<input
|
|
71
|
+
:id="props.context?.id"
|
|
72
|
+
ref="inputRef"
|
|
73
|
+
v-model="inputValue"
|
|
74
|
+
:name="name"
|
|
75
|
+
type="text"
|
|
76
|
+
:placeholder="placeholder"
|
|
77
|
+
:class="context?.classes.input"
|
|
78
|
+
@keydown.enter.prevent="addTag()"
|
|
79
|
+
/>
|
|
80
|
+
<span :class="context?.classes.inputIcon"></span>
|
|
81
|
+
</div>
|
|
82
|
+
<div>
|
|
83
|
+
<button type="button" :disabled="!inputValue" :class="context?.classes.button" @click="addTag()">
|
|
84
|
+
{{ context?.attrs.buttonText ?? context?.attrs['button-text'] }}
|
|
85
|
+
</button>
|
|
86
|
+
</div>
|
|
87
|
+
<div v-if="suggestions?.length" :class="context?.classes.suggestionsWrapper">
|
|
88
|
+
<p :class="context?.classes.suggestionsHeadline">
|
|
89
|
+
Beliebte Skills
|
|
90
|
+
</p>
|
|
91
|
+
<div class="flex flex-row flex-wrap gap-2">
|
|
92
|
+
<div v-for="(tag, index) of suggestions" :key="index" :class="context?.classes.suggestionsTag">
|
|
93
|
+
{{ tag }} <span :class="context?.classes.suggestionsIcon" @click="addTag(tag)"></span>
|
|
94
|
+
</div>
|
|
95
|
+
</div>
|
|
96
|
+
</div>
|
|
97
|
+
</div>
|
|
98
|
+
</template>
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
const props = defineProps({
|
|
3
|
+
context: Object,
|
|
4
|
+
});
|
|
5
|
+
|
|
6
|
+
const value = ref('');
|
|
7
|
+
const label = props.context?.label;
|
|
8
|
+
const text = props.context?.attrs.text;
|
|
9
|
+
const name = props.context?.name;
|
|
10
|
+
const config = useRuntimeConfig();
|
|
11
|
+
await getPreviewUrl(props.context?._value);
|
|
12
|
+
|
|
13
|
+
watch(() => props.context?._value, async () => {
|
|
14
|
+
await getPreviewUrl(props.context?._value);
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
function handleInput(e: any) {
|
|
18
|
+
if (!e) {
|
|
19
|
+
props.context?.node.input(null);
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
props.context?.node.input(e.target.files[0]);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
async function getPreviewUrl(src: string | File) {
|
|
27
|
+
if (!src) {
|
|
28
|
+
value.value = '';
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const validHexChars = /^[0-9a-fA-F]{24}$/;
|
|
32
|
+
if (typeof src === 'object' && validHexChars.test(src?.id)) {
|
|
33
|
+
// return config.public.apiUrl + '/files/' + src.id;
|
|
34
|
+
const response = await useAuthFetch(config.public.apiUrl + '/files/' + src?.id, { method: 'GET' });
|
|
35
|
+
value.value = URL.createObjectURL(response as any);
|
|
36
|
+
}
|
|
37
|
+
else if (typeof src === 'string') {
|
|
38
|
+
value.value = src;
|
|
39
|
+
} else if (src instanceof File) {
|
|
40
|
+
value.value = URL.createObjectURL(src);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
</script>
|
|
44
|
+
|
|
45
|
+
<template>
|
|
46
|
+
<div class="image-container" :class="props?.context?.classes.container">
|
|
47
|
+
<div v-if="!props.context?._value" :class="props?.context?.classes.uploader">
|
|
48
|
+
<label :class="context?.classes.placeholder">{{ text }}</label>
|
|
49
|
+
<input
|
|
50
|
+
:id="props.context?.id"
|
|
51
|
+
:name="name"
|
|
52
|
+
type="file"
|
|
53
|
+
:class="props?.context?.classes.input"
|
|
54
|
+
@input="handleInput"
|
|
55
|
+
/>
|
|
56
|
+
</div>
|
|
57
|
+
<div v-else :class="props?.context.classes.fileItem">
|
|
58
|
+
<img :src="value" :class="props?.context?.classes.fileItemImage" />
|
|
59
|
+
<span :class="props?.context.classes.fileItemImageName">{{ props.context?._value?.name ?? props.context?._value?.filename }}</span>
|
|
60
|
+
<button type="button" :class="props?.context.classes.fileItemRemove" @click="handleInput(null)">
|
|
61
|
+
Entfernen
|
|
62
|
+
</button>
|
|
63
|
+
</div>
|
|
64
|
+
</div>
|
|
65
|
+
</template>
|