create-nuxt-base 0.3.16 → 0.3.17

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 (41) hide show
  1. package/CHANGELOG.md +2 -0
  2. package/nuxt-base-template/.env.example +1 -1
  3. package/nuxt-base-template/CLAUDE.md +361 -0
  4. package/nuxt-base-template/README.md +127 -13
  5. package/nuxt-base-template/app/app.config.ts +67 -0
  6. package/nuxt-base-template/app/app.vue +10 -2
  7. package/nuxt-base-template/app/assets/css/tailwind.css +124 -84
  8. package/nuxt-base-template/app/components/Modal/ModalBase.vue +65 -0
  9. package/nuxt-base-template/app/components/Transition/TransitionSlide.vue +0 -2
  10. package/nuxt-base-template/app/components/Transition/TransitionSlideBottom.vue +0 -2
  11. package/nuxt-base-template/app/components/Transition/TransitionSlideRevert.vue +0 -2
  12. package/nuxt-base-template/app/composables/use-file.ts +19 -3
  13. package/nuxt-base-template/app/composables/use-share.ts +26 -10
  14. package/nuxt-base-template/app/error.vue +7 -43
  15. package/nuxt-base-template/app/layouts/default.vue +76 -4
  16. package/nuxt-base-template/app/layouts/slim.vue +5 -0
  17. package/nuxt-base-template/app/pages/auth/forgot-password.vue +64 -0
  18. package/nuxt-base-template/app/pages/auth/login.vue +71 -0
  19. package/nuxt-base-template/app/pages/auth/reset-password/[token].vue +110 -0
  20. package/nuxt-base-template/app/pages/index.vue +139 -2
  21. package/nuxt-base-template/app/public/favicon.ico +0 -0
  22. package/nuxt-base-template/docs/nuxt.config.ts +4 -0
  23. package/nuxt-base-template/docs/pages/docs.vue +663 -0
  24. package/nuxt-base-template/eslint.config.mjs +2 -1
  25. package/nuxt-base-template/nuxt.config.ts +72 -30
  26. package/nuxt-base-template/openapi-ts.config.ts +18 -0
  27. package/nuxt-base-template/package-lock.json +9781 -15157
  28. package/nuxt-base-template/package.json +30 -35
  29. package/nuxt-base-template/tsconfig.json +1 -1
  30. package/package.json +3 -3
  31. package/nuxt-base-template/app/composables/use-context-menu.ts +0 -19
  32. package/nuxt-base-template/app/composables/use-form-helper.ts +0 -41
  33. package/nuxt-base-template/app/composables/use-modal.ts +0 -84
  34. package/nuxt-base-template/app/composables/use-notification.ts +0 -29
  35. package/nuxt-base-template/app/middleware/admin.global.ts +0 -9
  36. package/nuxt-base-template/app/middleware/auth.global.ts +0 -9
  37. package/nuxt-base-template/app/middleware/logged-in.global.ts +0 -9
  38. package/nuxt-base-template/app/plugins/auth.server.ts +0 -72
  39. package/nuxt-base-template/app/plugins/form.plugin.ts +0 -21
  40. package/nuxt-base-template/app/plugins/pwa.plugin.ts +0 -114
  41. package/nuxt-base-template/tailwind.config.js +0 -21
@@ -9,64 +9,59 @@
9
9
  "scripts": {
10
10
  "init": "npm install",
11
11
  "reinit": "npx rimraf package-lock.json && npx nuxt cleanup && npx rimraf node_modules && npm cache clean --force && npm i",
12
+ "clean": "npx rimraf .nuxt .output",
13
+ "dev": "nuxt dev",
12
14
  "build": "nuxt build",
13
- "app:build": "npm run build",
14
- "build:develop": "nuxt build",
15
- "build:test": "nuxt build",
15
+ "build:develop": "npx cross-env NODE_ENV=development nuxt build",
16
+ "build:test": "npx cross-env NODE_ENV=test nuxt build",
17
+ "build:prod": "npx cross-env NODE_ENV=production nuxt build",
16
18
  "start": "nuxt dev",
17
19
  "start:develop": "node .output/server/index.mjs",
18
20
  "start:test": "node .output/server/index.mjs",
19
21
  "start:prod": "node .output/server/index.mjs",
20
22
  "start:tunnel": "nuxt dev --tunnel",
21
23
  "start:extern": "npx cross-env HOST=0.0.0.0 nuxt dev",
22
- "generate-types": "npx rimraf app/base && npx cross-env GENERATE_TYPES=1 nuxt dev",
23
- "dev": "nuxt dev",
24
24
  "generate": "nuxt generate",
25
+ "generate-types": "openapi-ts",
25
26
  "preview": "nuxt preview",
26
27
  "postinstall": "nuxt prepare",
27
- "app:e2e": "playwright test",
28
- "test": "echo 'No test specified' && exit 0",
28
+ "test": "npm run test:e2e",
29
+ "test:e2e": "playwright test",
29
30
  "lint": "eslint 'app/**/*.{ts,js,vue}'",
30
- "lint:fix": "eslint 'app/**/*.{ts,js,vue}' --fix"
31
+ "lint:fix": "eslint 'app/**/*.{ts,js,vue}' --fix",
32
+ "format": "prettier --write .",
33
+ "format:check": "prettier --check .",
34
+ "check": "npm run lint && npm run format:check",
35
+ "fix": "npm run lint:fix && npm run format"
31
36
  },
32
37
  "dependencies": {
33
- "@egoist/tailwindcss-icons": "1.9.0",
34
- "@iconify-json/bi": "1.2.6",
38
+ "@hey-api/client-fetch": "0.13.1",
35
39
  "@lenne.tech/bug.lt": "latest",
36
- "@lenne.tech/nuxt-base": "latest",
37
40
  "@nuxt/image": "1.11.0",
38
- "@vee-validate/yup": "4.15.1",
39
- "@vueuse/core": "13.9.0",
40
- "@vueuse/integrations": "13.9.0",
41
+ "@nuxt/ui": "4.0.1",
42
+ "@pinia/nuxt": "0.11.2",
41
43
  "@vueuse/nuxt": "13.9.0",
42
- "ios-pwa-splash": "1.0.0",
43
- "rimraf": "6.0.1",
44
- "tailwind-merge": "3.3.1",
45
- "tailwindcss": "4.1.13",
46
- "vee-validate": "4.15.1"
44
+ "valibot": "1.1.0"
47
45
  },
48
46
  "devDependencies": {
49
- "@lenne.tech/eslint-config-vue": "2.1.3",
50
- "@nuxt/devtools": "2.6.3",
47
+ "@hey-api/openapi-ts": "0.85.2",
48
+ "@lenne.tech/eslint-config-vue": "2.1.4",
49
+ "@nuxt/devtools": "2.6.5",
51
50
  "@nuxt/test-utils": "3.19.2",
52
51
  "@nuxtjs/color-mode": "3.5.2",
53
- "@nuxtjs/google-fonts": "3.2.0",
54
52
  "@nuxtjs/plausible": "2.0.1",
55
- "@nuxtjs/seo": "3.1.0",
56
- "@nuxtjs/tailwindcss": "6.14.0",
57
- "@playwright/test": "1.55.0",
58
- "@tailwindcss/forms": "0.5.10",
59
- "@tailwindcss/typography": "0.5.16",
60
- "@tailwindcss/vite": "4.1.13",
61
- "@types/node": "24.3.1",
62
- "@vitejs/plugin-vue": "6.0.1",
63
- "@vue/test-utils": "2.4.6",
53
+ "@nuxtjs/seo": "3.2.2",
54
+ "@playwright/test": "1.56.1",
55
+ "@tailwindcss/typography": "0.5.19",
56
+ "@tailwindcss/vite": "4.1.14",
57
+ "@types/node": "24.8.1",
64
58
  "dayjs-nuxt": "2.1.11",
65
- "eslint": "9.34.0",
59
+ "eslint": "9.37.0",
66
60
  "jsdom": "26.1.0",
67
- "nuxt": "4.1.0",
68
- "ts-loader": "9.5.4",
69
- "typescript": "5.9.2"
61
+ "nuxt": "4.1.3",
62
+ "rimraf": "6.0.1",
63
+ "tailwindcss": "4.1.14",
64
+ "typescript": "5.9.3"
70
65
  },
71
66
  "exports": {
72
67
  ".": {
@@ -2,6 +2,6 @@
2
2
  // https://nuxt.com/docs/guide/concepts/typescript
3
3
  "extends": "./.nuxt/tsconfig.json",
4
4
  "compilerOptions": {
5
- "target": "ES2017"
5
+ "target": "ES2022"
6
6
  }
7
7
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "create-nuxt-base",
3
- "version": "0.3.16",
4
- "description": "Starter to generate a configured environment with VueJS, Nuxt, Tailwind, Eslint, @lenne.tech/nuxt-base, Unit Tests, Cypress etc.",
3
+ "version": "0.3.17",
4
+ "description": "Starter to generate a configured environment with VueJS, Nuxt, Tailwind, Eslint, Unit Tests, Playwright etc.",
5
5
  "main": "index.js",
6
6
  "engines": {
7
7
  "node": ">=22",
@@ -21,7 +21,7 @@
21
21
  "license": "MIT",
22
22
  "dependencies": {
23
23
  "cross-spawn": "7.0.6",
24
- "fs-extra": "11.3.1"
24
+ "fs-extra": "11.3.2"
25
25
  },
26
26
  "devDependencies": {
27
27
  "prettier": "3.6.2",
@@ -1,19 +0,0 @@
1
- const contextMenuState = () => useState<any | null>(() => null);
2
-
3
- export function useContextMenu() {
4
- const menu = contextMenuState();
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,41 +0,0 @@
1
- export function useFormHelper() {
2
- const { close, open } = useModal();
3
-
4
- function onReload(dirty: boolean, event: BeforeUnloadEvent) {
5
- if (dirty) {
6
- event.preventDefault();
7
- event.returnValue = '';
8
- }
9
- }
10
-
11
- async function onNavigate(dirty: boolean) {
12
- if (dirty) {
13
- const answer = await openConfirmModal();
14
- if (!answer) {
15
- return false;
16
- }
17
- }
18
- return true;
19
- }
20
-
21
- function openConfirmModal() {
22
- return new Promise((resolve) => {
23
- open({
24
- component: 'ModalConfirm', // TODO: Replace with correct component
25
- confirm: (confirmed) => {
26
- resolve(confirmed);
27
- close();
28
- },
29
- data: {
30
- text: 'Sind Sie sich sicher, dass Sie den Vorgang abbrechen möchten?\n' + 'Es befinden sich ungesicherte Daten auf dieser Seite.',
31
- title: 'Vorsicht, ungesicherte Daten!',
32
- },
33
- });
34
- });
35
- }
36
-
37
- return {
38
- onNavigate,
39
- onReload,
40
- };
41
- }
@@ -1,84 +0,0 @@
1
- export interface ModalContext<T = object> {
2
- closable?: boolean;
3
- component: any;
4
- confirm?: (confirmed: boolean) => void;
5
- data?: T;
6
- show: boolean;
7
- showInner: boolean;
8
- size?: 'auto' | 'lg' | 'md' | 'sm';
9
- }
10
-
11
- interface Modal<T> {
12
- closable?: boolean;
13
- component: any;
14
- confirm?: (confirmed: boolean) => void;
15
- data?: T;
16
- size?: 'auto' | 'lg' | 'md' | 'sm';
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
- close,
82
- open,
83
- };
84
- }
@@ -1,29 +0,0 @@
1
- import { randomUUID } from 'uncrypto';
2
-
3
- interface Notification {
4
- duration?: number;
5
- text?: string;
6
- title: string;
7
- type: 'error' | 'info' | 'success' | 'warning';
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
- notifications,
26
- notify,
27
- remove,
28
- };
29
- }
@@ -1,9 +0,0 @@
1
- export default defineNuxtRouteMiddleware((to) => {
2
- const { currentUserState } = useAuthState();
3
-
4
- if (to.fullPath.startsWith('/app/admin')) {
5
- if (!currentUserState?.value?.roles?.includes('admin')) {
6
- return navigateTo('/app');
7
- }
8
- }
9
- });
@@ -1,9 +0,0 @@
1
- export default defineNuxtRouteMiddleware((to) => {
2
- const { accessTokenState } = useAuthState();
3
-
4
- if (to.fullPath.startsWith('/app')) {
5
- if (!accessTokenState?.value) {
6
- return navigateTo('/auth/login');
7
- }
8
- }
9
- });
@@ -1,9 +0,0 @@
1
- export default defineNuxtRouteMiddleware((to) => {
2
- const { accessTokenState } = useAuthState();
3
-
4
- if (to.fullPath === '/auth' || to.fullPath === '/auth/register') {
5
- if (accessTokenState?.value) {
6
- return navigateTo('/cms');
7
- }
8
- }
9
- });
@@ -1,72 +0,0 @@
1
- import { callWithNuxt, defineNuxtPlugin, useNuxtApp, useRuntimeConfig } from 'nuxt/app';
2
- import { ofetch } from 'ofetch';
3
-
4
- export default defineNuxtPlugin({
5
- dependsOn: ['cookies', 'graphql-meta'], // from nuxt-base
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.gqlHost, {
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');
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');
42
- }
43
- }
44
-
45
- if (token && payload?.id) {
46
- const userResult = await ofetch(config.public.gqlHost, {
47
- body: JSON.stringify({
48
- query: 'query getUser($id: String!){' + 'getUser(id: $id){' + 'id ' + 'firstName ' + 'lastName ' + 'email ' + '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');
64
- return;
65
- }
66
-
67
- if (userResult?.data) {
68
- setCurrentUser(userResult?.data?.getUser);
69
- }
70
- }
71
- },
72
- });
@@ -1,21 +0,0 @@
1
- import { setLocale } from 'yup';
2
-
3
- export default defineNuxtPlugin(async () => {
4
- setLocale({
5
- // use constant translation keys for messages without values
6
- mixed: {
7
- default: 'Dieses Feld ist ungültig.',
8
- notType: 'Dieses Feld ist ungültig.',
9
- required: 'Dieses Feld ist erforderlich.',
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
- });
@@ -1,114 +0,0 @@
1
- export default defineNuxtPlugin(async () => {
2
- const applicationServerKey = useRuntimeConfig().public.webPushKey as string;
3
- const permissionState = ref<PermissionState>('prompt');
4
- const subscription = ref<null | PushSubscription>(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 (e) {
29
- console.error(e);
30
- }
31
- }
32
-
33
- async function refreshPermissionState() {
34
- try {
35
- const sw = await navigator.serviceWorker.ready;
36
- permissionState.value = await sw.pushManager.permissionState({
37
- applicationServerKey,
38
- userVisibleOnly: true,
39
- });
40
- } catch (e) {
41
- console.error(e);
42
- }
43
- }
44
-
45
- async function refreshSubscription() {
46
- try {
47
- const sw = await navigator.serviceWorker.ready;
48
- subscription.value = await sw.pushManager.getSubscription();
49
- } catch (e) {
50
- console.error(e);
51
- }
52
- }
53
-
54
- async function unsubscribe() {
55
- if (subscription.value) {
56
- await subscription.value.unsubscribe();
57
- const body = {
58
- payload: subscription.value,
59
- };
60
- await refreshPermissionState();
61
- try {
62
- await useAuthFetch('/web-push/', {
63
- baseURL: process.env.API_URL,
64
- body,
65
- method: 'DELETE',
66
- });
67
- } catch (e) {
68
- console.error(e);
69
- }
70
- subscription.value = null;
71
- }
72
- }
73
-
74
- const route = useRoute();
75
- const pwa = ref(false);
76
- const isPwa = computed(() => route.query.standalone === 'true' || pwa.value);
77
-
78
- if (process.client) {
79
- if (window && window.matchMedia && document && window.matchMedia('(display-mode: standalone)').matches) {
80
- if (navigator && navigator.serviceWorker) {
81
- await refreshSubscription();
82
- await refreshPermissionState();
83
- }
84
- pwa.value = true;
85
- document.body.classList.add('select-none', 'overscroll-y-none', 'no-scrollbar', 'bg-rm-gray-1');
86
- } else {
87
- pwa.value = false;
88
- document.body.classList.remove('select-none', 'overscroll-y-none', 'no-scrollbar', 'bg-rm-gray-1');
89
- }
90
- }
91
-
92
- useHead({
93
- htmlAttrs: { class: isPwa.value ? 'pwa' : '' },
94
- meta: [
95
- {
96
- content: 'width=device-width, initial-scale=1, viewport-fit=cover, user-scalable=no',
97
- hid: 'viewport',
98
- name: 'viewport',
99
- },
100
- ],
101
- });
102
-
103
- return {
104
- provide: {
105
- pwa: {
106
- isPwa: () => isPwa.value,
107
- permissionState,
108
- subscribe,
109
- subscription,
110
- unsubscribe,
111
- },
112
- },
113
- };
114
- });
@@ -1,21 +0,0 @@
1
- /** @type {import('tailwindcss').Config} */
2
- // eslint-disable-next-line @typescript-eslint/no-require-imports
3
- const { iconsPlugin, getIconCollections } = require('@egoist/tailwindcss-icons');
4
- // eslint-disable-next-line @typescript-eslint/no-require-imports
5
- const plugin = require('tailwindcss/plugin');
6
-
7
- module.exports = {
8
- plugins: [
9
- iconsPlugin({
10
- // Select the icon collections you want to use
11
- collections: getIconCollections(['bi']),
12
- }),
13
- plugin(({ addVariant, e }) => {
14
- addVariant('pwa', ({ modifySelectors, separator }) => {
15
- modifySelectors(({ className }) => {
16
- return `.pwa .${e(`pwa${separator}${className}`)}`;
17
- });
18
- });
19
- }),
20
- ],
21
- };