create-nuxt-base 0.1.23 → 0.2.1
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/.eslintignore +14 -0
- package/.eslintrc +3 -0
- package/.github/workflows/release.yml +1 -1
- package/.prettierignore +5 -0
- package/.prettierrc +6 -0
- package/.vscode/settings.json +10 -10
- package/CHANGELOG.md +22 -33
- package/README.md +1 -0
- package/index.js +30 -29
- package/nuxt-base-template/.env.example +8 -0
- package/nuxt-base-template/.eslintrc +1 -1
- package/nuxt-base-template/.vscode/settings.json +6 -16
- package/nuxt-base-template/e2e/init.spec.ts +18 -0
- package/nuxt-base-template/nuxt.config.ts +27 -12
- package/nuxt-base-template/package-lock.json +6050 -4606
- package/nuxt-base-template/package.json +47 -29
- package/nuxt-base-template/playwright.config.ts +77 -0
- package/nuxt-base-template/src/app.vue +4 -4
- package/nuxt-base-template/src/assets/css/tailwind.css +42 -33
- package/nuxt-base-template/src/components/SocialMediaBubble.vue +1 -1
- package/nuxt-base-template/src/components/base/BaseAccordion.vue +16 -13
- package/nuxt-base-template/src/components/base/BaseButton.vue +10 -13
- package/nuxt-base-template/src/components/base/BaseContainer.vue +1 -1
- package/nuxt-base-template/src/components/base/BaseContextMenuContainer.vue +61 -0
- package/nuxt-base-template/src/components/base/BaseInfinityList.vue +1 -1
- package/nuxt-base-template/src/components/base/BaseProgressbar.vue +28 -30
- package/nuxt-base-template/src/components/base/BaseToggle.vue +17 -10
- package/nuxt-base-template/src/components/form/FormInput.vue +34 -0
- package/nuxt-base-template/src/components/form/FormPassword.vue +47 -0
- package/nuxt-base-template/src/components/form/FormSelect.vue +40 -0
- package/nuxt-base-template/src/components/form/FormSubmit.vue +18 -0
- package/nuxt-base-template/src/components/form/FormTextarea.vue +36 -0
- package/nuxt-base-template/src/components/form/FormToggle.vue +27 -0
- package/nuxt-base-template/src/components/modal/Modal.vue +48 -0
- package/nuxt-base-template/src/components/modal/ModalConfirm.vue +38 -0
- package/nuxt-base-template/src/components/{base/BaseModalContainer.vue → modal/ModalContainer.vue} +1 -1
- package/nuxt-base-template/src/components/modal/ModalInfo.vue +22 -0
- package/nuxt-base-template/src/components/{ModalShare.vue → modal/ModalShare.vue} +7 -11
- package/nuxt-base-template/src/components/{base/BaseNotification.vue → notification/Notification.vue} +8 -7
- package/nuxt-base-template/src/components/{base/BaseNotificationContainer.vue → notification/NotificationContainer.vue} +11 -5
- package/nuxt-base-template/src/components/pwa/pwa-install-banner.vue +224 -0
- package/nuxt-base-template/src/components/transition/TransitionFade.vue +10 -7
- package/nuxt-base-template/src/components/transition/TransitionFadeScale.vue +10 -7
- package/nuxt-base-template/src/composables/use-auth-fetch.ts +22 -8
- package/nuxt-base-template/src/composables/use-context-menu.ts +19 -0
- package/nuxt-base-template/src/composables/use-file.ts +1 -2
- package/nuxt-base-template/src/composables/use-modal.ts +1 -1
- package/nuxt-base-template/src/composables/use-notification.ts +2 -2
- package/nuxt-base-template/src/composables/use-share.ts +3 -3
- package/nuxt-base-template/src/error.vue +10 -14
- package/nuxt-base-template/src/layouts/default.vue +13 -0
- package/nuxt-base-template/src/middleware/auth.global.ts +2 -2
- package/nuxt-base-template/src/pages/index.vue +31 -6
- package/nuxt-base-template/src/plugins/auth.server.ts +72 -0
- package/nuxt-base-template/src/plugins/form.plugin.ts +21 -0
- package/nuxt-base-template/src/plugins/pwa.plugin.ts +110 -0
- package/nuxt-base-template/src/tests/init.test.ts +1 -1
- package/nuxt-base-template/tailwind.config.js +33 -23
- package/nuxt-base-template/vitest.config.js +3 -3
- package/package.json +3 -1
- package/nuxt-base-template/formkit-theme.js +0 -137
- package/nuxt-base-template/formkit.config.js +0 -33
- package/nuxt-base-template/src/components/hello-world.vue +0 -10
- package/nuxt-base-template/src/composables/use-form-helper.ts +0 -100
- package/nuxt-base-template/src/composables/use-helper.ts +0 -52
- package/nuxt-base-template/src/forms/inputs/InputCheckbox.vue +0 -29
- package/nuxt-base-template/src/forms/inputs/InputFreeTags.vue +0 -98
- package/nuxt-base-template/src/forms/inputs/InputImage.vue +0 -65
- package/nuxt-base-template/src/forms/inputs/InputTags.vue +0 -112
- package/nuxt-base-template/src/forms/inputs/InputToggle.vue +0 -18
- package/nuxt-base-template/src/forms/plugins/asterisk-plugin.ts +0 -29
- package/nuxt-base-template/src/forms/plugins/scroll-error-plugin.ts +0 -36
- package/nuxt-base-template/src/forms/plugins/value-changes-plugin.ts +0 -13
- package/nuxt-base-template/src/plugins/4.auth.server.ts +0 -70
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
-
const props = withDefaults(
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
2
|
+
const props = withDefaults(
|
|
3
|
+
defineProps<{
|
|
4
|
+
startDuration?: number | `${number}`;
|
|
5
|
+
leaveDuration?: number | `${number}`;
|
|
6
|
+
}>(),
|
|
7
|
+
{
|
|
8
|
+
startDuration: 100,
|
|
9
|
+
leaveDuration: 100,
|
|
10
|
+
}
|
|
11
|
+
);
|
|
9
12
|
</script>
|
|
10
13
|
|
|
11
14
|
<template>
|
|
@@ -1,19 +1,33 @@
|
|
|
1
1
|
import type { ExtractedRouteMethod, NitroFetchOptions, NitroFetchRequest, TypedInternalResponse } from 'nitropack';
|
|
2
2
|
|
|
3
|
-
export function useAuthFetch<
|
|
3
|
+
export function useAuthFetch<
|
|
4
|
+
DefaultT = unknown,
|
|
5
|
+
DefaultR extends NitroFetchRequest = NitroFetchRequest,
|
|
6
|
+
T = DefaultT,
|
|
7
|
+
R extends NitroFetchRequest = DefaultR,
|
|
8
|
+
O extends NitroFetchOptions<R> = NitroFetchOptions<R>
|
|
9
|
+
>(request: R, opts?: O): Promise<TypedInternalResponse<R, T, ExtractedRouteMethod<R, O>>> {
|
|
4
10
|
const { requestNewToken } = useAuth();
|
|
11
|
+
const { accessTokenState } = useAuthState();
|
|
5
12
|
const config = useRuntimeConfig();
|
|
6
13
|
|
|
7
14
|
// @ts-expect-error - because of nice types from ofetch <3
|
|
8
15
|
return $fetch(request, {
|
|
9
16
|
...opts,
|
|
10
17
|
baseURL: config.public.apiUrl,
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
18
|
+
async onRequest(data: any) {
|
|
19
|
+
if (accessTokenState.value) {
|
|
20
|
+
data.options.headers = {
|
|
21
|
+
...data.options.headers,
|
|
22
|
+
Authorization: `Bearer ${accessTokenState.value}`,
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
},
|
|
26
|
+
onResponseError: async () => {
|
|
27
|
+
await requestNewToken();
|
|
17
28
|
},
|
|
29
|
+
retry: 3,
|
|
30
|
+
retryDelay: 500,
|
|
31
|
+
retryStatusCodes: [401],
|
|
18
32
|
});
|
|
19
|
-
}
|
|
33
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
const contextMenuState = <T>() => useState<any | null>(() => null);
|
|
2
|
+
|
|
3
|
+
export function useContextMenu<T = object>() {
|
|
4
|
+
const menu = contextMenuState<T>();
|
|
5
|
+
|
|
6
|
+
const open = (config: { items: any[] }) => {
|
|
7
|
+
menu.value = Object.assign(config, { show: true });
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
const close = () => {
|
|
11
|
+
menu.value = null;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
return {
|
|
15
|
+
activeMenu: menu,
|
|
16
|
+
close,
|
|
17
|
+
open,
|
|
18
|
+
};
|
|
19
|
+
}
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { useFetch } from '@vueuse/core';
|
|
2
|
-
import { useHelper } from '~/composables/use-helper';
|
|
3
2
|
|
|
4
3
|
export function useFile() {
|
|
5
4
|
const { isValidMongoID } = useHelper();
|
|
@@ -18,4 +17,4 @@ export function useFile() {
|
|
|
18
17
|
}
|
|
19
18
|
|
|
20
19
|
return { getFileInfo };
|
|
21
|
-
}
|
|
20
|
+
}
|
|
@@ -18,7 +18,7 @@ export function useNotification() {
|
|
|
18
18
|
};
|
|
19
19
|
|
|
20
20
|
const remove = (uuid: string) => {
|
|
21
|
-
notifications.value = notifications.value.filter(n => n.uuid !== uuid);
|
|
21
|
+
notifications.value = notifications.value.filter((n) => n.uuid !== uuid);
|
|
22
22
|
};
|
|
23
23
|
|
|
24
24
|
return {
|
|
@@ -26,4 +26,4 @@ export function useNotification() {
|
|
|
26
26
|
remove,
|
|
27
27
|
notifications,
|
|
28
28
|
};
|
|
29
|
-
}
|
|
29
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { ModalShare } from '#components';
|
|
2
1
|
import { useModal } from '~/composables/use-modal';
|
|
2
|
+
import ModalShare from '~/components/ModalShare.vue';
|
|
3
3
|
|
|
4
4
|
export function useShare() {
|
|
5
5
|
const route = useRoute();
|
|
@@ -12,11 +12,11 @@ export function useShare() {
|
|
|
12
12
|
text: text ?? window.location.origin,
|
|
13
13
|
});
|
|
14
14
|
} else {
|
|
15
|
-
useModal().open({ component: ModalShare, size: 'md', data: { link: url ?? window.location.origin } });
|
|
15
|
+
useModal().open({ component: ModalShare, size: 'md', data: { link: url ?? window.location.origin, name: window.name } });
|
|
16
16
|
}
|
|
17
17
|
}
|
|
18
18
|
|
|
19
19
|
return {
|
|
20
20
|
share,
|
|
21
21
|
};
|
|
22
|
-
}
|
|
22
|
+
}
|
|
@@ -7,7 +7,8 @@ console.error(props.error);
|
|
|
7
7
|
const debugWord = ref<string>('');
|
|
8
8
|
const { current } = useMagicKeys();
|
|
9
9
|
|
|
10
|
-
watch(
|
|
10
|
+
watch(
|
|
11
|
+
() => current.values(),
|
|
11
12
|
() => {
|
|
12
13
|
if (current.values().next().value === 'escape') {
|
|
13
14
|
debugWord.value = '';
|
|
@@ -21,7 +22,7 @@ watch(() => current.values(),
|
|
|
21
22
|
if (current.values().next().value && !debugWord.value.includes(current.values().next().value)) {
|
|
22
23
|
debugWord.value += current.values().next().value;
|
|
23
24
|
}
|
|
24
|
-
}
|
|
25
|
+
}
|
|
25
26
|
);
|
|
26
27
|
|
|
27
28
|
const handleError = () => clearError({ redirect: '/' });
|
|
@@ -31,23 +32,18 @@ const handleError = () => clearError({ redirect: '/' });
|
|
|
31
32
|
<NuxtLayout>
|
|
32
33
|
<div class="w-full min-h-screen flex flex-col justify-center items-center">
|
|
33
34
|
<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>
|
|
35
|
+
<h1 class="text-[12rem] font-extrabold text-transparent bg-clip-text bg-gradient-to-r from-primary-600 to-primary-400">Oops!</h1>
|
|
37
36
|
<h2 v-if="error?.statusCode" class="text-3xl text-gray-600">
|
|
38
37
|
{{ error?.statusCode }}
|
|
39
38
|
</h2>
|
|
40
39
|
</div>
|
|
41
40
|
|
|
42
41
|
<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
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
<BaseButton color="primary" appearance="outline" @click="handleError">
|
|
49
|
-
Zurück zur Startseite
|
|
50
|
-
</BaseButton>
|
|
42
|
+
{{ { error } }}
|
|
43
|
+
</pre
|
|
44
|
+
>
|
|
45
|
+
|
|
46
|
+
<BaseButton color="primary" appearance="outline" @click="handleError"> Zurück zur Startseite </BaseButton>
|
|
51
47
|
</div>
|
|
52
48
|
</NuxtLayout>
|
|
53
|
-
</template>
|
|
49
|
+
</template>
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { useGlobalMutationLoading, useGlobalQueryLoading } from '@vue/apollo-composable';
|
|
3
|
+
|
|
4
|
+
const queryLoading = useGlobalQueryLoading();
|
|
5
|
+
const mutationLoading = useGlobalMutationLoading();
|
|
6
|
+
</script>
|
|
7
|
+
|
|
8
|
+
<template>
|
|
9
|
+
<div>
|
|
10
|
+
<NuxtLoadingIndicator :loading="queryLoading || mutationLoading" />
|
|
11
|
+
<slot></slot>
|
|
12
|
+
</div>
|
|
13
|
+
</template>
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
export default defineNuxtRouteMiddleware((to, from) => {
|
|
2
|
-
const {
|
|
2
|
+
const { accessTokenState } = useAuthState();
|
|
3
3
|
|
|
4
4
|
if (to.fullPath.startsWith('/app')) {
|
|
5
|
-
if (!
|
|
5
|
+
if (!accessTokenState?.value) {
|
|
6
6
|
return navigateTo('/auth/login');
|
|
7
7
|
}
|
|
8
8
|
}
|
|
@@ -1,10 +1,35 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import FormTextarea from '~/components/form/FormTextarea.vue';
|
|
3
|
+
|
|
4
|
+
const isSubmitting = ref(false);
|
|
5
|
+
</script>
|
|
6
|
+
|
|
1
7
|
<template>
|
|
2
8
|
<div class="flex h-screen items-center justify-center flex-col">
|
|
3
|
-
<h1
|
|
4
|
-
|
|
5
|
-
>
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
<
|
|
9
|
+
<h1 class="font-extrabold text-transparent text-8xl bg-clip-text bg-gradient-to-r from-purple-400 to-pink-600">Lenne Nuxt Starter</h1>
|
|
10
|
+
|
|
11
|
+
<h2 class="mt-5">DEMO</h2>
|
|
12
|
+
<FormInput name="email" type="email" label="E-Mail" placeholder="max.mustermann@test.de" />
|
|
13
|
+
<FormTextarea name="description" label="Description" placeholder="..." />
|
|
14
|
+
<FormPassword name="password" label="Password" placeholder="Password" />
|
|
15
|
+
<FormSelect
|
|
16
|
+
name="select"
|
|
17
|
+
label="Select"
|
|
18
|
+
placeholder="placeholder"
|
|
19
|
+
:options="[
|
|
20
|
+
{ label: 'Option 1', value: 1 },
|
|
21
|
+
{ label: 'Option 2', value: 2 },
|
|
22
|
+
{ label: 'Option 3', value: 3 },
|
|
23
|
+
]"
|
|
24
|
+
/>
|
|
25
|
+
<FormToggle name="toggle" label="Toggle" />
|
|
26
|
+
<div class="flex items-center gap-3">
|
|
27
|
+
<FormSubmit label="Save" :is-submitting="isSubmitting" @click="isSubmitting = true" />
|
|
28
|
+
<BaseButton size="sm" :loading="isSubmitting" @click="isSubmitting = true">Submit</BaseButton>
|
|
29
|
+
<BaseButton size="lg" :loading="isSubmitting" @click="isSubmitting = true">Submit</BaseButton>
|
|
30
|
+
<BaseButton size="md" :loading="isSubmitting" appearance="outline" @click="isSubmitting = true">Submit</BaseButton>
|
|
31
|
+
<BaseButton size="md" :loading="isSubmitting" appearance="none" @click="isSubmitting = true">Submit</BaseButton>
|
|
32
|
+
<BaseButton size="md" color="danger" @click="isSubmitting = false">Reset</BaseButton>
|
|
33
|
+
</div>
|
|
9
34
|
</div>
|
|
10
35
|
</template>
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { callWithNuxt, defineNuxtPlugin, useNuxtApp, useRuntimeConfig } from 'nuxt/app';
|
|
2
|
+
import { ofetch } from 'ofetch';
|
|
3
|
+
|
|
4
|
+
export default defineNuxtPlugin({
|
|
5
|
+
enforce: 'post',
|
|
6
|
+
name: 'auth-server',
|
|
7
|
+
async setup() {
|
|
8
|
+
const _nuxt = useNuxtApp();
|
|
9
|
+
const config = await callWithNuxt(_nuxt, useRuntimeConfig);
|
|
10
|
+
const { accessTokenState, currentUserState, refreshTokenState } = await callWithNuxt(_nuxt, useAuthState);
|
|
11
|
+
const { clearSession, getDecodedAccessToken, isTokenExpired, setCurrentUser, setTokens } = await callWithNuxt(_nuxt, useAuth);
|
|
12
|
+
const payload = accessTokenState.value ? getDecodedAccessToken(accessTokenState.value) : null;
|
|
13
|
+
|
|
14
|
+
if (!accessTokenState.value || !refreshTokenState.value || currentUserState.value) {
|
|
15
|
+
return;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
let token = accessTokenState.value;
|
|
19
|
+
if (isTokenExpired(accessTokenState.value)) {
|
|
20
|
+
const refreshTokenResult = await ofetch(config.public.host, {
|
|
21
|
+
body: JSON.stringify({
|
|
22
|
+
query: 'mutation refreshToken {refreshToken {token, refreshToken}}',
|
|
23
|
+
variables: {},
|
|
24
|
+
}),
|
|
25
|
+
headers: {
|
|
26
|
+
Authorization: `Bearer ${refreshTokenState.value}`,
|
|
27
|
+
},
|
|
28
|
+
method: 'POST',
|
|
29
|
+
}).catch((err) => {
|
|
30
|
+
console.error('2.auth.server.ts::refreshToken::catch', err.data);
|
|
31
|
+
clearSession();
|
|
32
|
+
navigateTo('/auth/login');
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
const data = refreshTokenResult?.data?.refreshToken;
|
|
36
|
+
if (data) {
|
|
37
|
+
setTokens(data.token, data.refreshToken);
|
|
38
|
+
token = data?.token;
|
|
39
|
+
} else {
|
|
40
|
+
clearSession();
|
|
41
|
+
await navigateTo('/auth/login');
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if (token && payload?.id) {
|
|
46
|
+
const userResult = await ofetch(config.public.host, {
|
|
47
|
+
body: JSON.stringify({
|
|
48
|
+
query: 'query getUser($id: String!){' + 'getUser(id: $id){' + 'id ' + 'avatar ' + 'firstName ' + 'lastName ' + 'email ' + 'gender ' + 'roles ' + '}}',
|
|
49
|
+
variables: {
|
|
50
|
+
id: payload.id,
|
|
51
|
+
},
|
|
52
|
+
}),
|
|
53
|
+
headers: {
|
|
54
|
+
Authorization: `Bearer ${token}`,
|
|
55
|
+
},
|
|
56
|
+
method: 'POST',
|
|
57
|
+
}).catch((err) => {
|
|
58
|
+
console.error('2.auth.server.ts::getUser::catch', err);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
if (userResult?.errors) {
|
|
62
|
+
clearSession();
|
|
63
|
+
navigateTo('/auth/login');
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (userResult?.data) {
|
|
68
|
+
setCurrentUser(userResult?.data?.getUser);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
},
|
|
72
|
+
});
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { setLocale } from 'yup';
|
|
2
|
+
|
|
3
|
+
export default defineNuxtPlugin(async (_nuxtApp) => {
|
|
4
|
+
setLocale({
|
|
5
|
+
// use constant translation keys for messages without values
|
|
6
|
+
mixed: {
|
|
7
|
+
required: 'Dieses Feld ist erforderlich.',
|
|
8
|
+
default: 'Dieses Feld ist ungültig.',
|
|
9
|
+
notType: 'Dieses Feld ist ungültig.',
|
|
10
|
+
},
|
|
11
|
+
string: {
|
|
12
|
+
email: 'Bitte geben Sie eine gültige E-Mail-Adresse ein.',
|
|
13
|
+
},
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
// Custom validation rules
|
|
17
|
+
// https://github.com/jquense/yup?tab=readme-ov-file#addmethodschematype-schema-name-string-method--schema-void
|
|
18
|
+
// addMethod(string, 'plz', () => {
|
|
19
|
+
// return
|
|
20
|
+
// });
|
|
21
|
+
});
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
export default defineNuxtPlugin(async (_nuxtApp) => {
|
|
2
|
+
const applicationServerKey = useRuntimeConfig().public.webPushKey as string;
|
|
3
|
+
const permissionState = ref<PermissionState>('prompt');
|
|
4
|
+
const subscription = ref<PushSubscription | null>(null);
|
|
5
|
+
|
|
6
|
+
if (process.client) {
|
|
7
|
+
const iosPWASplash = (await import('ios-pwa-splash')).default;
|
|
8
|
+
iosPWASplash('/notification.png', '#FFFFFF');
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
async function subscribe() {
|
|
12
|
+
const sw = await navigator.serviceWorker.ready;
|
|
13
|
+
const push = await sw.pushManager.subscribe({
|
|
14
|
+
applicationServerKey,
|
|
15
|
+
userVisibleOnly: true,
|
|
16
|
+
});
|
|
17
|
+
await refreshSubscription();
|
|
18
|
+
await refreshPermissionState();
|
|
19
|
+
const body = {
|
|
20
|
+
payload: push,
|
|
21
|
+
};
|
|
22
|
+
try {
|
|
23
|
+
await useAuthFetch('/web-push/subscribe', {
|
|
24
|
+
baseURL: process.env.API_URL,
|
|
25
|
+
body,
|
|
26
|
+
method: 'POST',
|
|
27
|
+
});
|
|
28
|
+
} catch (error) {}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
async function refreshPermissionState() {
|
|
32
|
+
try {
|
|
33
|
+
const sw = await navigator.serviceWorker.ready;
|
|
34
|
+
permissionState.value = await sw.pushManager.permissionState({
|
|
35
|
+
applicationServerKey,
|
|
36
|
+
userVisibleOnly: true,
|
|
37
|
+
});
|
|
38
|
+
} catch (error) {
|
|
39
|
+
console.error(error);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
async function refreshSubscription() {
|
|
44
|
+
try {
|
|
45
|
+
const sw = await navigator.serviceWorker.ready;
|
|
46
|
+
subscription.value = await sw.pushManager.getSubscription();
|
|
47
|
+
} catch (e) {
|
|
48
|
+
console.error(e);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
async function unsubscribe() {
|
|
53
|
+
if (subscription.value) {
|
|
54
|
+
await subscription.value.unsubscribe();
|
|
55
|
+
const body = {
|
|
56
|
+
payload: subscription.value,
|
|
57
|
+
};
|
|
58
|
+
await refreshPermissionState();
|
|
59
|
+
try {
|
|
60
|
+
await useAuthFetch('/web-push/', {
|
|
61
|
+
baseURL: process.env.API_URL,
|
|
62
|
+
body,
|
|
63
|
+
method: 'DELETE',
|
|
64
|
+
});
|
|
65
|
+
} catch (error) {}
|
|
66
|
+
subscription.value = null;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const route = useRoute();
|
|
71
|
+
const pwa = ref(false);
|
|
72
|
+
const isPwa = computed(() => route.query.standalone === 'true' || pwa.value);
|
|
73
|
+
|
|
74
|
+
if (process.client) {
|
|
75
|
+
if (window && window.matchMedia && document && window.matchMedia('(display-mode: standalone)').matches) {
|
|
76
|
+
if (navigator && navigator.serviceWorker) {
|
|
77
|
+
await refreshSubscription();
|
|
78
|
+
await refreshPermissionState();
|
|
79
|
+
}
|
|
80
|
+
pwa.value = true;
|
|
81
|
+
document.body.classList.add('select-none', 'overscroll-y-none', 'no-scrollbar', 'bg-rm-gray-1');
|
|
82
|
+
} else {
|
|
83
|
+
pwa.value = false;
|
|
84
|
+
document.body.classList.remove('select-none', 'overscroll-y-none', 'no-scrollbar', 'bg-rm-gray-1');
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
useHead({
|
|
89
|
+
htmlAttrs: { class: isPwa.value ? 'pwa' : '' },
|
|
90
|
+
meta: [
|
|
91
|
+
{
|
|
92
|
+
content: 'width=device-width, initial-scale=1, viewport-fit=cover, user-scalable=no',
|
|
93
|
+
hid: 'viewport',
|
|
94
|
+
name: 'viewport',
|
|
95
|
+
},
|
|
96
|
+
],
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
return {
|
|
100
|
+
provide: {
|
|
101
|
+
pwa: {
|
|
102
|
+
isPwa: () => isPwa.value,
|
|
103
|
+
permissionState,
|
|
104
|
+
subscribe,
|
|
105
|
+
subscription,
|
|
106
|
+
unsubscribe,
|
|
107
|
+
},
|
|
108
|
+
},
|
|
109
|
+
};
|
|
110
|
+
});
|
|
@@ -3,11 +3,10 @@ const defaultTheme = require('tailwindcss/defaultTheme');
|
|
|
3
3
|
const { iconsPlugin, getIconCollections } = require('@egoist/tailwindcss-icons');
|
|
4
4
|
const typography = require('@tailwindcss/typography');
|
|
5
5
|
const forms = require('@tailwindcss/forms');
|
|
6
|
-
const
|
|
6
|
+
const plugin = require('tailwindcss/plugin');
|
|
7
7
|
|
|
8
8
|
module.exports = {
|
|
9
|
-
darkMode:
|
|
10
|
-
content: ['./formkit-theme.js'],
|
|
9
|
+
darkMode: 'class',
|
|
11
10
|
theme: {
|
|
12
11
|
// fontFamily: {
|
|
13
12
|
// montserrat: ['Montserrat'],
|
|
@@ -15,34 +14,45 @@ module.exports = {
|
|
|
15
14
|
// serif: ['Work Sans', 'Montserrat', ...defaultTheme.fontFamily.serif],
|
|
16
15
|
// },
|
|
17
16
|
extend: {
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
17
|
+
colors: {
|
|
18
|
+
primary: {
|
|
19
|
+
DEFAULT: '#57B39A',
|
|
20
|
+
50: '#f3faf7',
|
|
21
|
+
100: '#d6f1e7',
|
|
22
|
+
200: '#ade2d0',
|
|
23
|
+
300: '#7cccb3',
|
|
24
|
+
400: '#57b39a',
|
|
25
|
+
500: '#37957d',
|
|
26
|
+
600: '#2a7765',
|
|
27
|
+
700: '#256052',
|
|
28
|
+
800: '#224d45',
|
|
29
|
+
900: '#20413a',
|
|
30
|
+
950: '#0d2621',
|
|
31
|
+
},
|
|
32
|
+
background: '#FFFFFF',
|
|
33
|
+
foreground: '#000000',
|
|
34
|
+
border: 'hsl(0 0% 0% / 0.5)',
|
|
35
|
+
hover: 'hsl(0 0% 100% / 0.2)',
|
|
36
|
+
active: 'hsl(0 0% 100% / 0.2)',
|
|
37
|
+
},
|
|
34
38
|
screens: {
|
|
35
|
-
'3xl': '2400px'
|
|
36
|
-
}
|
|
37
|
-
}
|
|
39
|
+
'3xl': '2400px',
|
|
40
|
+
},
|
|
41
|
+
},
|
|
38
42
|
},
|
|
39
43
|
plugins: [
|
|
40
44
|
typography,
|
|
41
|
-
formkit,
|
|
42
45
|
forms,
|
|
43
46
|
iconsPlugin({
|
|
44
47
|
// Select the icon collections you want to use
|
|
45
48
|
collections: getIconCollections(['bi']),
|
|
46
49
|
}),
|
|
50
|
+
plugin(({ addVariant, e }) => {
|
|
51
|
+
addVariant('pwa', ({ modifySelectors, separator }) => {
|
|
52
|
+
modifySelectors(({ className }) => {
|
|
53
|
+
return `.pwa .${e(`pwa${separator}${className}`)}`;
|
|
54
|
+
});
|
|
55
|
+
});
|
|
56
|
+
}),
|
|
47
57
|
],
|
|
48
58
|
};
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import vue from '@vitejs/plugin-vue'
|
|
2
|
-
import { defineConfig } from 'vite'
|
|
1
|
+
import vue from '@vitejs/plugin-vue';
|
|
2
|
+
import { defineConfig } from 'vite';
|
|
3
3
|
|
|
4
4
|
export default defineConfig({
|
|
5
5
|
plugins: [vue()],
|
|
@@ -7,4 +7,4 @@ export default defineConfig({
|
|
|
7
7
|
globals: true,
|
|
8
8
|
environment: 'jsdom',
|
|
9
9
|
},
|
|
10
|
-
})
|
|
10
|
+
});
|
package/package.json
CHANGED
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "create-nuxt-base",
|
|
3
|
-
"version": "0.1
|
|
3
|
+
"version": "0.2.1",
|
|
4
4
|
"description": "Starter to generate a configured environment with VueJS, Nuxt, Tailwind, Eslint, @lenne.tech/nuxt-base, Unit Tests, Cypress etc.",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"bin": {
|
|
7
7
|
"create-nuxt-base": "./index.js"
|
|
8
8
|
},
|
|
9
9
|
"scripts": {
|
|
10
|
+
"prettier": "prettier \"*.{js,json,yml,md,html,ts}\" .",
|
|
11
|
+
"format": "npm run prettier -- --write",
|
|
10
12
|
"release": "standard-version && git push --follow-tags origin main",
|
|
11
13
|
"release:minor": "standard-version --release-as minor && git push --follow-tags origin main",
|
|
12
14
|
"release:major": "standard-version --release-as major && git push --follow-tags origin main"
|