create-nuxt-base 0.3.16 → 1.0.2

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 (64) hide show
  1. package/.github/workflows/publish.yml +4 -2
  2. package/.oxfmtrc.jsonc +7 -0
  3. package/CHANGELOG.md +22 -8
  4. package/nuxt-base-template/.dockerignore +44 -0
  5. package/nuxt-base-template/.env.example +1 -1
  6. package/nuxt-base-template/.nuxtrc +1 -0
  7. package/nuxt-base-template/.oxfmtrc.jsonc +8 -0
  8. package/nuxt-base-template/Dockerfile.dev +23 -0
  9. package/nuxt-base-template/README.md +127 -13
  10. package/nuxt-base-template/app/app.config.ts +67 -0
  11. package/nuxt-base-template/app/app.vue +10 -2
  12. package/nuxt-base-template/app/assets/css/tailwind.css +124 -84
  13. package/nuxt-base-template/app/components/Modal/ModalBackupCodes.vue +117 -0
  14. package/nuxt-base-template/app/components/Modal/ModalBase.vue +65 -0
  15. package/nuxt-base-template/app/components/Transition/TransitionSlide.vue +0 -2
  16. package/nuxt-base-template/app/components/Transition/TransitionSlideBottom.vue +0 -2
  17. package/nuxt-base-template/app/components/Transition/TransitionSlideRevert.vue +0 -2
  18. package/nuxt-base-template/app/components/Upload/TusFileUpload.vue +302 -0
  19. package/nuxt-base-template/app/composables/use-better-auth.ts +25 -0
  20. package/nuxt-base-template/app/composables/use-file.ts +57 -6
  21. package/nuxt-base-template/app/composables/use-share.ts +26 -10
  22. package/nuxt-base-template/app/composables/use-tus-upload.ts +278 -0
  23. package/nuxt-base-template/app/error.vue +7 -43
  24. package/nuxt-base-template/app/interfaces/upload.interface.ts +58 -0
  25. package/nuxt-base-template/app/interfaces/user.interface.ts +12 -0
  26. package/nuxt-base-template/app/layouts/default.vue +76 -4
  27. package/nuxt-base-template/app/layouts/slim.vue +5 -0
  28. package/nuxt-base-template/app/lib/auth-client.ts +135 -0
  29. package/nuxt-base-template/app/middleware/admin.global.ts +20 -6
  30. package/nuxt-base-template/app/middleware/auth.global.ts +15 -6
  31. package/nuxt-base-template/app/middleware/guest.global.ts +18 -0
  32. package/nuxt-base-template/app/pages/app/settings/security.vue +409 -0
  33. package/nuxt-base-template/app/pages/auth/2fa.vue +120 -0
  34. package/nuxt-base-template/app/pages/auth/forgot-password.vue +115 -0
  35. package/nuxt-base-template/app/pages/auth/login.vue +135 -0
  36. package/nuxt-base-template/app/pages/auth/register.vue +184 -0
  37. package/nuxt-base-template/app/pages/auth/reset-password.vue +153 -0
  38. package/nuxt-base-template/app/pages/index.vue +139 -2
  39. package/nuxt-base-template/app/utils/crypto.ts +13 -0
  40. package/nuxt-base-template/docker-entrypoint.sh +21 -0
  41. package/nuxt-base-template/docs/nuxt.config.ts +4 -0
  42. package/nuxt-base-template/docs/pages/docs.vue +663 -0
  43. package/nuxt-base-template/nuxt.config.ts +75 -30
  44. package/nuxt-base-template/openapi-ts.config.ts +18 -0
  45. package/nuxt-base-template/oxlint.json +14 -0
  46. package/nuxt-base-template/package-lock.json +11414 -15883
  47. package/nuxt-base-template/package.json +48 -50
  48. package/nuxt-base-template/tests/iam.spec.ts +247 -0
  49. package/nuxt-base-template/tsconfig.json +1 -1
  50. package/package.json +15 -12
  51. package/.eslintignore +0 -14
  52. package/.eslintrc +0 -3
  53. package/.prettierignore +0 -5
  54. package/.prettierrc +0 -6
  55. package/nuxt-base-template/app/composables/use-context-menu.ts +0 -19
  56. package/nuxt-base-template/app/composables/use-form-helper.ts +0 -41
  57. package/nuxt-base-template/app/composables/use-modal.ts +0 -84
  58. package/nuxt-base-template/app/composables/use-notification.ts +0 -29
  59. package/nuxt-base-template/app/middleware/logged-in.global.ts +0 -9
  60. package/nuxt-base-template/app/plugins/auth.server.ts +0 -72
  61. package/nuxt-base-template/app/plugins/form.plugin.ts +0 -21
  62. package/nuxt-base-template/app/plugins/pwa.plugin.ts +0 -114
  63. package/nuxt-base-template/eslint.config.mjs +0 -3
  64. package/nuxt-base-template/tailwind.config.js +0 -21
@@ -0,0 +1,135 @@
1
+ <script setup lang="ts">
2
+ // ============================================================================
3
+ // Imports
4
+ // ============================================================================
5
+ import type { AuthFormField, FormSubmitEvent } from '@nuxt/ui';
6
+ import type { InferOutput } from 'valibot';
7
+
8
+ import * as v from 'valibot';
9
+
10
+ import { authClient } from '~/lib/auth-client';
11
+
12
+ // ============================================================================
13
+ // Composables
14
+ // ============================================================================
15
+ const toast = useToast();
16
+
17
+ // ============================================================================
18
+ // Page Meta
19
+ // ============================================================================
20
+ definePageMeta({
21
+ layout: 'slim',
22
+ });
23
+
24
+ // ============================================================================
25
+ // Variables
26
+ // ============================================================================
27
+ const loading = ref<boolean>(false);
28
+ const passkeyLoading = ref<boolean>(false);
29
+
30
+ const fields: AuthFormField[] = [
31
+ {
32
+ label: 'E-Mail',
33
+ name: 'email',
34
+ placeholder: 'E-Mail eingeben',
35
+ required: true,
36
+ type: 'email',
37
+ },
38
+ {
39
+ label: 'Passwort',
40
+ name: 'password',
41
+ placeholder: 'Passwort eingeben',
42
+ required: true,
43
+ type: 'password',
44
+ },
45
+ ];
46
+
47
+ const schema = v.object({
48
+ email: v.pipe(v.string('E-Mail ist erforderlich'), v.email('Bitte eine gültige E-Mail eingeben')),
49
+ password: v.pipe(v.string('Passwort ist erforderlich'), v.minLength(5, 'Mindestens 5 Zeichen erforderlich')),
50
+ });
51
+
52
+ type Schema = InferOutput<typeof schema>;
53
+
54
+ async function onPasskeyLogin(): Promise<void> {
55
+ passkeyLoading.value = true;
56
+
57
+ try {
58
+ const { error } = await authClient.signIn.passkey();
59
+
60
+ if (error) {
61
+ toast.add({
62
+ color: 'error',
63
+ description: error.message || 'Passkey-Anmeldung fehlgeschlagen',
64
+ title: 'Fehler',
65
+ });
66
+ return;
67
+ }
68
+
69
+ await navigateTo('/app');
70
+ } finally {
71
+ passkeyLoading.value = false;
72
+ }
73
+ }
74
+
75
+ // ============================================================================
76
+ // Functions
77
+ // ============================================================================
78
+ async function onSubmit(payload: FormSubmitEvent<Schema>): Promise<void> {
79
+ loading.value = true;
80
+
81
+ try {
82
+ const { error } = await authClient.signIn.email({
83
+ email: payload.data.email,
84
+ password: payload.data.password,
85
+ });
86
+
87
+ if (error) {
88
+ toast.add({
89
+ color: 'error',
90
+ description: error.message || 'Anmeldung fehlgeschlagen',
91
+ title: 'Fehler',
92
+ });
93
+ return;
94
+ }
95
+
96
+ await navigateTo('/app');
97
+ } finally {
98
+ loading.value = false;
99
+ }
100
+ }
101
+ </script>
102
+
103
+ <template>
104
+ <UPageCard class="w-md" variant="naked">
105
+ <UAuthForm
106
+ :schema="schema"
107
+ title="Anmelden"
108
+ icon="i-lucide-user"
109
+ :fields="fields"
110
+ :loading="loading"
111
+ :submit="{
112
+ label: 'Anmelden',
113
+ block: true,
114
+ }"
115
+ @submit="onSubmit"
116
+ >
117
+ <template #password-hint>
118
+ <ULink to="/auth/forgot-password" class="text-primary font-medium" tabindex="-1">Passwort vergessen?</ULink>
119
+ </template>
120
+
121
+ <template #footer>
122
+ <div class="flex flex-col gap-4">
123
+ <USeparator label="oder" />
124
+
125
+ <UButton block color="neutral" variant="outline" icon="i-lucide-key" :loading="passkeyLoading" @click="onPasskeyLogin"> Mit Passkey anmelden </UButton>
126
+
127
+ <p class="text-center text-sm text-muted">
128
+ Noch kein Konto?
129
+ <ULink to="/auth/register" class="text-primary font-medium">Registrieren</ULink>
130
+ </p>
131
+ </div>
132
+ </template>
133
+ </UAuthForm>
134
+ </UPageCard>
135
+ </template>
@@ -0,0 +1,184 @@
1
+ <script setup lang="ts">
2
+ // ============================================================================
3
+ // Imports
4
+ // ============================================================================
5
+ import type { AuthFormField, FormSubmitEvent } from '@nuxt/ui';
6
+ import type { InferOutput } from 'valibot';
7
+
8
+ import * as v from 'valibot';
9
+
10
+ import { authClient } from '~/lib/auth-client';
11
+
12
+ // ============================================================================
13
+ // Composables
14
+ // ============================================================================
15
+ const toast = useToast();
16
+
17
+ // ============================================================================
18
+ // Page Meta
19
+ // ============================================================================
20
+ definePageMeta({
21
+ layout: 'slim',
22
+ });
23
+
24
+ // ============================================================================
25
+ // Variables
26
+ // ============================================================================
27
+ const loading = ref<boolean>(false);
28
+ const showPasskeyPrompt = ref<boolean>(false);
29
+ const passkeyLoading = ref<boolean>(false);
30
+
31
+ const fields: AuthFormField[] = [
32
+ {
33
+ label: 'Name',
34
+ name: 'name',
35
+ placeholder: 'Name eingeben',
36
+ required: true,
37
+ type: 'text',
38
+ },
39
+ {
40
+ label: 'E-Mail',
41
+ name: 'email',
42
+ placeholder: 'E-Mail eingeben',
43
+ required: true,
44
+ type: 'email',
45
+ },
46
+ {
47
+ label: 'Passwort',
48
+ name: 'password',
49
+ placeholder: 'Passwort eingeben',
50
+ required: true,
51
+ type: 'password',
52
+ },
53
+ {
54
+ label: 'Passwort bestätigen',
55
+ name: 'confirmPassword',
56
+ placeholder: 'Passwort wiederholen',
57
+ required: true,
58
+ type: 'password',
59
+ },
60
+ ];
61
+
62
+ const schema = v.pipe(
63
+ v.object({
64
+ confirmPassword: v.pipe(v.string('Passwortbestätigung ist erforderlich'), v.minLength(8, 'Mindestens 8 Zeichen erforderlich')),
65
+ email: v.pipe(v.string('E-Mail ist erforderlich'), v.email('Bitte eine gültige E-Mail eingeben')),
66
+ name: v.pipe(v.string('Name ist erforderlich'), v.minLength(2, 'Mindestens 2 Zeichen erforderlich')),
67
+ password: v.pipe(v.string('Passwort ist erforderlich'), v.minLength(8, 'Mindestens 8 Zeichen erforderlich')),
68
+ }),
69
+ v.forward(
70
+ v.partialCheck([['password'], ['confirmPassword']], (input) => input.password === input.confirmPassword, 'Passwörter stimmen nicht überein'),
71
+ ['confirmPassword'],
72
+ ),
73
+ );
74
+
75
+ type Schema = InferOutput<typeof schema>;
76
+
77
+ async function addPasskey(): Promise<void> {
78
+ passkeyLoading.value = true;
79
+
80
+ try {
81
+ const { error } = await authClient.passkey.addPasskey({
82
+ name: 'Mein Gerät',
83
+ });
84
+
85
+ if (error) {
86
+ toast.add({
87
+ color: 'error',
88
+ description: error.message || 'Passkey konnte nicht hinzugefügt werden',
89
+ title: 'Fehler',
90
+ });
91
+ return;
92
+ }
93
+
94
+ toast.add({
95
+ color: 'success',
96
+ description: 'Passkey wurde erfolgreich hinzugefügt',
97
+ title: 'Erfolg',
98
+ });
99
+
100
+ await navigateTo('/app');
101
+ } finally {
102
+ passkeyLoading.value = false;
103
+ }
104
+ }
105
+
106
+ // ============================================================================
107
+ // Functions
108
+ // ============================================================================
109
+ async function onSubmit(payload: FormSubmitEvent<Schema>): Promise<void> {
110
+ loading.value = true;
111
+
112
+ try {
113
+ const { error } = await authClient.signUp.email({
114
+ email: payload.data.email,
115
+ name: payload.data.name,
116
+ password: payload.data.password,
117
+ });
118
+
119
+ if (error) {
120
+ toast.add({
121
+ color: 'error',
122
+ description: error.message || 'Registrierung fehlgeschlagen',
123
+ title: 'Fehler',
124
+ });
125
+ return;
126
+ }
127
+
128
+ toast.add({
129
+ color: 'success',
130
+ description: 'Dein Konto wurde erfolgreich erstellt',
131
+ title: 'Willkommen!',
132
+ });
133
+
134
+ showPasskeyPrompt.value = true;
135
+ } finally {
136
+ loading.value = false;
137
+ }
138
+ }
139
+
140
+ async function skipPasskey(): Promise<void> {
141
+ await navigateTo('/app');
142
+ }
143
+ </script>
144
+
145
+ <template>
146
+ <UPageCard class="w-md" variant="naked">
147
+ <template v-if="!showPasskeyPrompt">
148
+ <UAuthForm
149
+ :schema="schema"
150
+ title="Registrieren"
151
+ icon="i-lucide-user-plus"
152
+ :fields="fields"
153
+ :loading="loading"
154
+ :submit="{
155
+ label: 'Konto erstellen',
156
+ block: true,
157
+ }"
158
+ @submit="onSubmit"
159
+ >
160
+ <template #footer>
161
+ <p class="text-center text-sm text-muted">
162
+ Bereits ein Konto?
163
+ <ULink to="/auth/login" class="text-primary font-medium">Anmelden</ULink>
164
+ </p>
165
+ </template>
166
+ </UAuthForm>
167
+ </template>
168
+
169
+ <template v-else>
170
+ <div class="flex flex-col items-center gap-6">
171
+ <UIcon name="i-lucide-key" class="size-16 text-primary" />
172
+ <div class="text-center">
173
+ <h2 class="text-xl font-semibold">Passkey hinzufügen?</h2>
174
+ <p class="mt-2 text-sm text-muted">Mit einem Passkey kannst du dich schnell und sicher ohne Passwort anmelden.</p>
175
+ </div>
176
+
177
+ <div class="flex w-full flex-col gap-3">
178
+ <UButton block :loading="passkeyLoading" @click="addPasskey"> Passkey hinzufügen </UButton>
179
+ <UButton block variant="outline" color="neutral" @click="skipPasskey"> Später einrichten </UButton>
180
+ </div>
181
+ </div>
182
+ </template>
183
+ </UPageCard>
184
+ </template>
@@ -0,0 +1,153 @@
1
+ <script setup lang="ts">
2
+ // ============================================================================
3
+ // Imports
4
+ // ============================================================================
5
+ import type { AuthFormField, FormSubmitEvent } from '@nuxt/ui';
6
+ import type { InferOutput } from 'valibot';
7
+
8
+ import * as v from 'valibot';
9
+
10
+ import { authClient } from '~/lib/auth-client';
11
+
12
+ // ============================================================================
13
+ // Composables
14
+ // ============================================================================
15
+ const route = useRoute();
16
+ const toast = useToast();
17
+
18
+ // ============================================================================
19
+ // Page Meta
20
+ // ============================================================================
21
+ definePageMeta({
22
+ layout: 'slim',
23
+ });
24
+
25
+ // ============================================================================
26
+ // Variables
27
+ // ============================================================================
28
+ const token = computed<string>(() => {
29
+ const queryToken = route.query.token;
30
+ return typeof queryToken === 'string' ? queryToken : '';
31
+ });
32
+
33
+ const isTokenValid = computed<boolean>(() => token.value.length > 0);
34
+ const loading = ref<boolean>(false);
35
+ const resetSuccess = ref<boolean>(false);
36
+
37
+ const fields: AuthFormField[] = [
38
+ {
39
+ label: 'Neues Passwort',
40
+ name: 'password',
41
+ placeholder: 'Neues Passwort eingeben',
42
+ required: true,
43
+ type: 'password',
44
+ },
45
+ {
46
+ label: 'Passwort bestätigen',
47
+ name: 'confirmPassword',
48
+ placeholder: 'Passwort wiederholen',
49
+ required: true,
50
+ type: 'password',
51
+ },
52
+ ];
53
+
54
+ const schema = v.pipe(
55
+ v.object({
56
+ confirmPassword: v.pipe(v.string('Passwortbestätigung ist erforderlich'), v.minLength(8, 'Mindestens 8 Zeichen erforderlich')),
57
+ password: v.pipe(v.string('Passwort ist erforderlich'), v.minLength(8, 'Mindestens 8 Zeichen erforderlich')),
58
+ }),
59
+ v.forward(
60
+ v.partialCheck([['password'], ['confirmPassword']], (input) => input.password === input.confirmPassword, 'Passwörter stimmen nicht überein'),
61
+ ['confirmPassword'],
62
+ ),
63
+ );
64
+
65
+ type Schema = InferOutput<typeof schema>;
66
+
67
+ // ============================================================================
68
+ // Lifecycle Hooks
69
+ // ============================================================================
70
+ onMounted(() => {
71
+ if (!isTokenValid.value) {
72
+ toast.add({
73
+ color: 'error',
74
+ description: 'Der Link zum Zurücksetzen des Passworts ist ungültig oder fehlt.',
75
+ title: 'Ungültiger Link',
76
+ });
77
+ }
78
+ });
79
+
80
+ // ============================================================================
81
+ // Functions
82
+ // ============================================================================
83
+ async function onSubmit(payload: FormSubmitEvent<Schema>): Promise<void> {
84
+ loading.value = true;
85
+
86
+ try {
87
+ const { error } = await authClient.resetPassword({
88
+ newPassword: payload.data.password,
89
+ token: token.value,
90
+ });
91
+
92
+ if (error) {
93
+ toast.add({
94
+ color: 'error',
95
+ description: error.message || 'Passwort konnte nicht zurückgesetzt werden',
96
+ title: 'Fehler',
97
+ });
98
+ return;
99
+ }
100
+
101
+ resetSuccess.value = true;
102
+ } finally {
103
+ loading.value = false;
104
+ }
105
+ }
106
+ </script>
107
+
108
+ <template>
109
+ <UPageCard class="w-md" variant="naked">
110
+ <template v-if="resetSuccess">
111
+ <div class="flex flex-col items-center gap-6">
112
+ <UIcon name="i-lucide-check-circle" class="size-16 text-success" />
113
+ <div class="text-center">
114
+ <h2 class="text-xl font-semibold">Passwort zurückgesetzt</h2>
115
+ <p class="mt-2 text-sm text-muted">Dein Passwort wurde erfolgreich geändert. Du kannst dich jetzt mit deinem neuen Passwort anmelden.</p>
116
+ </div>
117
+
118
+ <UButton to="/auth/login" block> Zur Anmeldung </UButton>
119
+ </div>
120
+ </template>
121
+
122
+ <template v-else>
123
+ <UAlert
124
+ v-if="!isTokenValid"
125
+ color="error"
126
+ description="Der Link zum Zurücksetzen des Passworts ist ungültig oder fehlt. Bitte fordere einen neuen Link an."
127
+ icon="i-lucide-alert-triangle"
128
+ title="Ungültiger Link"
129
+ class="mb-4"
130
+ />
131
+
132
+ <UAuthForm
133
+ :schema="schema"
134
+ title="Neues Passwort"
135
+ icon="i-lucide-shield-check"
136
+ :fields="fields"
137
+ :loading="loading"
138
+ :submit="{
139
+ label: 'Passwort speichern',
140
+ block: true,
141
+ disabled: !isTokenValid,
142
+ }"
143
+ @submit="onSubmit"
144
+ >
145
+ <template #footer>
146
+ <p class="text-center text-sm text-muted">
147
+ <ULink to="/auth/forgot-password" class="text-primary font-medium"> Neuen Link anfordern </ULink>
148
+ </p>
149
+ </template>
150
+ </UAuthForm>
151
+ </template>
152
+ </UPageCard>
153
+ </template>
@@ -1,5 +1,142 @@
1
+ <script lang="ts" setup>
2
+ // ============================================================================
3
+ // Composables
4
+ // ============================================================================
5
+ const config = useRuntimeConfig();
6
+
7
+ // ============================================================================
8
+ // Variables
9
+ // ============================================================================
10
+ const features: Array<{
11
+ color: 'info' | 'primary' | 'secondary' | 'success';
12
+ description: string;
13
+ icon: string;
14
+ title: string;
15
+ }> = [
16
+ {
17
+ color: 'primary',
18
+ description: 'Built with Vue 3 Composition API, TypeScript, and Nuxt 4 for modern development.',
19
+ icon: 'i-lucide-rocket',
20
+ title: 'Modern Stack',
21
+ },
22
+ {
23
+ color: 'secondary',
24
+ description: 'Pre-configured with Nuxt UI components, Tailwind CSS v4, and customizable themes.',
25
+ icon: 'i-lucide-palette',
26
+ title: 'UI Components',
27
+ },
28
+ {
29
+ color: 'success',
30
+ description: 'Built-in support for REST API integration with auto-generated types.',
31
+ icon: 'i-lucide-database',
32
+ title: 'API Ready',
33
+ },
34
+ {
35
+ color: 'info',
36
+ description: 'Form validation with VeeValidate and Valibot and state management with Pinia.',
37
+ icon: 'i-lucide-check-circle',
38
+ title: 'Production Ready',
39
+ },
40
+ ];
41
+
42
+ // ============================================================================
43
+ // Computed Properties
44
+ // ============================================================================
45
+ const isDevelopment = computed<boolean>(() => config.public.appEnv !== 'production');
46
+ </script>
47
+
1
48
  <template>
2
- <div class="flex h-screen items-center justify-center flex-col">
3
- <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>
49
+ <div>
50
+ <!-- Hero Section -->
51
+ <div class="relative overflow-hidden">
52
+ <div class="mx-auto max-w-7xl px-6 py-24 sm:py-32 lg:px-8">
53
+ <div class="mx-auto max-w-3xl text-center">
54
+ <div class="mb-8 flex items-center justify-center gap-x-3">
55
+ <UBadge color="primary" variant="subtle" size="lg"> v4 </UBadge>
56
+ <UBadge color="neutral" variant="outline" size="lg"> Nuxt Starter Template </UBadge>
57
+ </div>
58
+ <h1 class="text-5xl font-bold tracking-tight text-neutral-900 dark:text-white sm:text-6xl lg:text-7xl">
59
+ Lenne Nuxt
60
+ <span class="block text-primary mt-2">Base Template</span>
61
+ </h1>
62
+ <p class="mt-8 text-lg leading-8 text-neutral-600 dark:text-neutral-400 max-w-2xl mx-auto">
63
+ A modern Nuxt 4 starter template with TypeScript, Nuxt UI components, and production-ready tools. Start building your next application with confidence.
64
+ </p>
65
+ <div class="mt-10 flex flex-col sm:flex-row items-center justify-center gap-4">
66
+ <UButton to="https://github.com/lenneTech/nuxt-base-starter" target="_blank" size="xl" icon="i-lucide-github" color="primary"> View on GitHub </UButton>
67
+ <UButton to="https://ui.nuxt.com" target="_blank" size="xl" variant="outline" color="neutral" trailing-icon="i-lucide-arrow-right"> Nuxt UI Docs </UButton>
68
+ </div>
69
+ </div>
70
+ </div>
71
+ </div>
72
+
73
+ <!-- Features Section -->
74
+ <div class="mx-auto max-w-7xl px-6 py-16 lg:px-8">
75
+ <div class="mx-auto max-w-2xl text-center mb-16">
76
+ <h2 class="text-3xl font-bold tracking-tight text-neutral-900 dark:text-white sm:text-4xl">Features</h2>
77
+ <p class="mt-4 text-lg text-neutral-600 dark:text-neutral-400">Everything you need to build modern web applications</p>
78
+ </div>
79
+
80
+ <div class="grid grid-cols-1 gap-8 sm:grid-cols-2 lg:grid-cols-4">
81
+ <UCard v-for="feature in features" :key="feature.title" class="hover:shadow-lg transition-shadow">
82
+ <template #header>
83
+ <div class="flex items-center gap-3">
84
+ <UBadge :icon="feature.icon" :color="feature.color" size="lg" square class="p-2" />
85
+ <h3 class="font-semibold text-neutral-900 dark:text-white">
86
+ {{ feature.title }}
87
+ </h3>
88
+ </div>
89
+ </template>
90
+ <p class="text-sm text-neutral-600 dark:text-neutral-400">
91
+ {{ feature.description }}
92
+ </p>
93
+ </UCard>
94
+ </div>
95
+ </div>
96
+
97
+ <!-- Getting Started Section -->
98
+ <div class="mx-auto max-w-7xl px-6 py-16 lg:px-8">
99
+ <div class="space-y-8">
100
+ <UAlert
101
+ title="Getting Started"
102
+ description="Run 'npm run dev' to start the development server on port 3001. Check CLAUDE.md for detailed documentation."
103
+ icon="i-lucide-info"
104
+ color="primary"
105
+ variant="subtle"
106
+ />
107
+
108
+ <!-- Dev Examples Link (only in development) -->
109
+ <UCard v-if="isDevelopment" variant="outline" class="border-2 border-primary/20">
110
+ <template #header>
111
+ <div class="flex items-center gap-3">
112
+ <UBadge color="warning" variant="subtle" icon="i-lucide-flask-conical"> Development Only </UBadge>
113
+ <h3 class="font-semibold text-neutral-900 dark:text-white">Interactive Examples</h3>
114
+ </div>
115
+ </template>
116
+
117
+ <div class="space-y-4">
118
+ <p class="text-sm text-neutral-600 dark:text-neutral-400">
119
+ Explore interactive examples for all template components and composables. Perfect for learning and as a reference for new projects.
120
+ </p>
121
+ <div class="flex items-center gap-3">
122
+ <UButton to="/docs" icon="i-lucide-sparkles" trailing-icon="i-lucide-arrow-right" color="primary" size="md"> View Examples </UButton>
123
+ <p class="text-xs text-neutral-500 dark:text-neutral-500">This link is only visible in development mode</p>
124
+ </div>
125
+ </div>
126
+ </UCard>
127
+ </div>
128
+ </div>
129
+
130
+ <!-- Footer Section -->
131
+ <div class="mx-auto max-w-7xl px-6 py-16 lg:px-8">
132
+ <div class="text-center">
133
+ <p class="text-sm text-neutral-600 dark:text-neutral-400">
134
+ Built with
135
+ <ULink to="https://nuxt.com" target="_blank" class="font-medium text-primary"> Nuxt 4 </ULink>
136
+ and
137
+ <ULink to="https://ui.nuxt.com" target="_blank" class="font-medium text-primary"> Nuxt UI </ULink>
138
+ </p>
139
+ </div>
140
+ </div>
4
141
  </div>
5
142
  </template>
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Hashes a string using SHA256
3
+ * Uses the Web Crypto API which is available in all modern browsers
4
+ *
5
+ * @param message - The string to hash
6
+ * @returns The SHA256 hash as a lowercase hex string (64 characters)
7
+ */
8
+ export async function sha256(message: string): Promise<string> {
9
+ const msgBuffer = new TextEncoder().encode(message);
10
+ const hashBuffer = await crypto.subtle.digest('SHA-256', msgBuffer);
11
+ const hashArray = Array.from(new Uint8Array(hashBuffer));
12
+ return hashArray.map((b) => b.toString(16).padStart(2, '0')).join('');
13
+ }
@@ -0,0 +1,21 @@
1
+ #!/bin/sh
2
+ set -e
3
+
4
+ # Check if node_modules exists and if package.json has changed
5
+ PACKAGE_HASH_FILE="/app/.package-hash"
6
+ CURRENT_HASH=$(md5sum /app/package.json /app/package-lock.json 2>/dev/null | md5sum | cut -d' ' -f1)
7
+
8
+ if [ ! -d "/app/node_modules" ] || [ ! -f "$PACKAGE_HASH_FILE" ] || [ "$(cat $PACKAGE_HASH_FILE 2>/dev/null)" != "$CURRENT_HASH" ]; then
9
+ echo "Installing dependencies..."
10
+ npm ci
11
+ echo "$CURRENT_HASH" > "$PACKAGE_HASH_FILE"
12
+ echo "Dependencies installed successfully."
13
+ else
14
+ echo "Dependencies are up to date."
15
+ fi
16
+
17
+ # Prepare Nuxt (generate types, etc.)
18
+ echo "Preparing Nuxt..."
19
+ npx nuxt prepare
20
+
21
+ exec "$@"
@@ -0,0 +1,4 @@
1
+ // https://nuxt.com/docs/api/configuration/nuxt-config
2
+ export default defineNuxtConfig({
3
+ devtools: { enabled: true },
4
+ });