nuxt-bake 1.0.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.
Files changed (66) hide show
  1. package/README.md +79 -0
  2. package/dist/helpers/constants.js +28 -0
  3. package/dist/helpers/git.js +32 -0
  4. package/dist/helpers/package-manager.js +77 -0
  5. package/dist/helpers/template.js +27 -0
  6. package/dist/helpers/utils.js +42 -0
  7. package/dist/index.js +96 -0
  8. package/eslint.config.ts +26 -0
  9. package/package.json +58 -0
  10. package/playwright.config.ts +6 -0
  11. package/src/helpers/git.ts +28 -0
  12. package/src/helpers/package-manager.ts +86 -0
  13. package/src/helpers/template.ts +35 -0
  14. package/src/helpers/utils.ts +83 -0
  15. package/src/index.ts +106 -0
  16. package/templates/base/.env.example +11 -0
  17. package/templates/base/README.md +58 -0
  18. package/templates/base/app/app.vue +20 -0
  19. package/templates/base/app/assets/styles.css +257 -0
  20. package/templates/base/app/components/dialog.vue +54 -0
  21. package/templates/base/app/components/navbar.vue +51 -0
  22. package/templates/base/app/components/toast.vue +116 -0
  23. package/templates/base/app/composables/use-session-monitor.ts +40 -0
  24. package/templates/base/app/composables/use-theme.ts +41 -0
  25. package/templates/base/app/composables/use-toast.ts +37 -0
  26. package/templates/base/app/error.vue +23 -0
  27. package/templates/base/app/layouts/default.vue +7 -0
  28. package/templates/base/app/pages/index.vue +53 -0
  29. package/templates/base/app/pages/sign-in.vue +39 -0
  30. package/templates/base/app/stores/user-store.ts +50 -0
  31. package/templates/base/app/utils/helpers.ts +42 -0
  32. package/templates/base/eslint.config.ts +74 -0
  33. package/templates/base/nuxt.config.ts +39 -0
  34. package/templates/base/package.json +39 -0
  35. package/templates/base/prisma/schema.prisma +30 -0
  36. package/templates/base/prisma.config.ts +12 -0
  37. package/templates/base/server/api/auth/[provider].ts +67 -0
  38. package/templates/base/server/api/user/index.delete.ts +8 -0
  39. package/templates/base/server/api/user/index.get.ts +10 -0
  40. package/templates/base/server/utils/auth.ts +48 -0
  41. package/templates/base/server/utils/db.ts +15 -0
  42. package/templates/base/server/utils/helpers.ts +14 -0
  43. package/templates/base/shared/types/auth.d.ts +31 -0
  44. package/templates/base/shared/types/globals.d.ts +15 -0
  45. package/templates/base/tsconfig.json +18 -0
  46. package/templates/with-i18n/app/app.vue +29 -0
  47. package/templates/with-i18n/app/components/navbar.vue +74 -0
  48. package/templates/with-i18n/app/error.vue +25 -0
  49. package/templates/with-i18n/app/pages/index.vue +53 -0
  50. package/templates/with-i18n/app/pages/sign-in.vue +39 -0
  51. package/templates/with-i18n/app/utils/i18n.config.ts +14 -0
  52. package/templates/with-i18n/app/utils/locales/en-US.json +50 -0
  53. package/templates/with-i18n/app/utils/locales/fr-FR.json +50 -0
  54. package/templates/with-i18n/nuxt.config.ts +51 -0
  55. package/templates/with-tests/app/pages/index.vue +51 -0
  56. package/templates/with-tests/nuxt.config.ts +31 -0
  57. package/templates/with-tests/playwright.config.ts +13 -0
  58. package/templates/with-tests/tests/e2e/e2e-hello.test.ts +8 -0
  59. package/templates/with-tests/tests/hello.test.ts +7 -0
  60. package/templates/with-tests/vitest.config.ts +16 -0
  61. package/tests/git.test.ts +54 -0
  62. package/tests/package-manager.test.ts +100 -0
  63. package/tests/template.test.ts +73 -0
  64. package/tests/utils.test.ts +155 -0
  65. package/tsconfig.json +13 -0
  66. package/vitest.config.ts +15 -0
@@ -0,0 +1,83 @@
1
+ import path from "node:path"
2
+ import fs from "fs-extra"
3
+ import inquirer from "inquirer"
4
+
5
+ export type Preset = "standard" | "with-i18n" | "with-tests"
6
+
7
+ export const REPO_URL = "https://github.com/matimortari/nuxt-bake.git"
8
+
9
+ export const PRESET_EXTRA_SCRIPTS: Record<Preset, Record<string, string>> = {
10
+ "standard": {},
11
+ "with-i18n": {},
12
+ "with-tests": {
13
+ "test": "vitest",
14
+ "test:e2e": "playwright test",
15
+ "coverage": "vitest --coverage",
16
+ },
17
+ }
18
+
19
+ export const PRESET_EXTRA_PACKAGES: Record<Preset, { dependencies?: Record<string, string>, devDependencies?: Record<string, string>, scripts?: Record<string, string> }> = {
20
+ "standard": {},
21
+ "with-i18n": {
22
+ dependencies: {
23
+ "@nuxtjs/i18n": "10.2.3",
24
+ },
25
+ },
26
+ "with-tests": {
27
+ devDependencies: {
28
+ "@nuxt/test-utils": "3.23.0",
29
+ "@vitest/coverage-v8": "4.0.18",
30
+ "@vue/test-utils": "3.20.0",
31
+ "happy-dom": "20.3.4",
32
+ "@playwright/test": "1.58.2",
33
+ "vitest": "4.0.18",
34
+ },
35
+ },
36
+ }
37
+
38
+ export function getProjectNameFromArgs() {
39
+ const args = process.argv.slice(2)
40
+ const nIndex = args.findIndex(a => a === "-n" || a === "--name")
41
+ if (nIndex !== -1 && args.length > nIndex + 1) {
42
+ return args[nIndex + 1]
43
+ }
44
+
45
+ return null
46
+ }
47
+
48
+ export async function promptForProjectName() {
49
+ let projectName = getProjectNameFromArgs()
50
+ if (!projectName) {
51
+ const { projectName: answerName } = await inquirer.prompt({
52
+ type: "input",
53
+ name: "projectName",
54
+ message: "Enter your new project folder name:",
55
+ default: "my-nuxt-app",
56
+ validate: input => (input ? true : "Project folder name cannot be empty"),
57
+ })
58
+
59
+ projectName = answerName
60
+ }
61
+
62
+ return projectName
63
+ }
64
+
65
+ export async function validateTargetDirectory(projectName: string) {
66
+ const targetDir = path.resolve(process.cwd(), projectName)
67
+ const exists = await fs.pathExists(targetDir)
68
+ if (exists) {
69
+ const { overwrite } = await inquirer.prompt({
70
+ type: "confirm",
71
+ name: "overwrite",
72
+ message: `Directory "${projectName}" already exists. Overwrite?`,
73
+ default: false,
74
+ })
75
+ if (!overwrite) {
76
+ return null
77
+ }
78
+
79
+ await fs.remove(targetDir)
80
+ }
81
+
82
+ return targetDir
83
+ }
package/src/index.ts ADDED
@@ -0,0 +1,106 @@
1
+ #!/usr/bin/env node
2
+ import type { Preset } from "./helpers/utils"
3
+ import fs from "fs-extra"
4
+ import inquirer from "inquirer"
5
+ import ora from "ora"
6
+ import { cloneRepoToTemp, promptAndInitGit } from "./helpers/git"
7
+ import { installDependencies, promptForPackageManager, updatePackageJson } from "./helpers/package-manager"
8
+ import { copyPresetFiles, copyRootTemplate } from "./helpers/template"
9
+ import { PRESET_EXTRA_PACKAGES, PRESET_EXTRA_SCRIPTS, promptForProjectName, REPO_URL, validateTargetDirectory } from "./helpers/utils"
10
+
11
+ async function run() {
12
+ let tmpDir
13
+
14
+ try {
15
+ const projectName = await promptForProjectName()
16
+ if (!projectName) {
17
+ console.log("\nProject name is required. Please provide a valid name.\n")
18
+ process.exit(1)
19
+ }
20
+
21
+ const targetDir = await validateTargetDirectory(projectName)
22
+ if (!targetDir) {
23
+ console.log(`\nFolder "${projectName}" already exists. Please choose another name or remove it.\n`)
24
+ process.exit(1)
25
+ }
26
+
27
+ const spinnerClone = ora("Creating project root...").start()
28
+ tmpDir = cloneRepoToTemp(REPO_URL)
29
+ if (!tmpDir) {
30
+ spinnerClone.fail("Failed to clone repository.")
31
+ process.exit(1)
32
+ }
33
+
34
+ const rootTemplateDir = await copyRootTemplate(tmpDir, targetDir)
35
+ spinnerClone.succeed()
36
+
37
+ const { preset } = await inquirer.prompt<{ preset: Preset }>({
38
+ type: "list",
39
+ name: "preset",
40
+ message: "Select a preset:",
41
+ choices: [
42
+ { name: "Standard", value: "" },
43
+ { name: "With i18n", value: "with-i18n" },
44
+ { name: "With Tests", value: "with-tests" },
45
+ ],
46
+ })
47
+
48
+ await copyPresetFiles(tmpDir, preset, targetDir)
49
+ await updatePackageJson(rootTemplateDir, targetDir, preset, {
50
+ dependencies: PRESET_EXTRA_PACKAGES[preset]?.dependencies || {},
51
+ devDependencies: PRESET_EXTRA_PACKAGES[preset]?.devDependencies || {},
52
+ scripts: PRESET_EXTRA_SCRIPTS[preset] || {},
53
+ })
54
+
55
+ const { installDeps } = await inquirer.prompt({
56
+ type: "confirm",
57
+ name: "installDeps",
58
+ message: "Install dependencies now?",
59
+ default: true,
60
+ })
61
+ if (installDeps) {
62
+ const pkgManager = await promptForPackageManager()
63
+ const spinnerInstall = ora("Installing dependencies...").start()
64
+ await installDependencies(targetDir, pkgManager)
65
+ spinnerInstall.succeed("Dependencies installed!")
66
+ }
67
+
68
+ const { initGit } = await inquirer.prompt({
69
+ type: "confirm",
70
+ name: "initGit",
71
+ message: "Initialize a Git repository?",
72
+ default: true,
73
+ })
74
+ if (initGit) {
75
+ const spinnerGit = ora("Initializing Git repo...").start()
76
+ promptAndInitGit(targetDir)
77
+ spinnerGit.succeed(`Git repository initialized for ${projectName}`)
78
+ }
79
+
80
+ console.log(`
81
+ Project setup complete!
82
+
83
+ Next steps:
84
+ 1. Navigate to your project:
85
+ cd ${projectName}
86
+
87
+ 2. Migrate or push database schemas:
88
+ npm run db:migrate
89
+ npm run db:push
90
+
91
+ 3. Start the development server:
92
+ npm run dev
93
+ `)
94
+ }
95
+ catch (err: any) {
96
+ console.error("Error:", err)
97
+ process.exit(1)
98
+ }
99
+ finally {
100
+ if (tmpDir && await fs.pathExists(tmpDir)) {
101
+ await fs.remove(tmpDir)
102
+ }
103
+ }
104
+ }
105
+
106
+ run()
@@ -0,0 +1,11 @@
1
+ NUXT_PUBLIC_BASE_URL=""
2
+
3
+ DATABASE_URL=""
4
+
5
+ NUXT_SESSION_PASSWORD=""
6
+
7
+ NUXT_OAUTH_GITHUB_CLIENT_ID=""
8
+ NUXT_OAUTH_GITHUB_CLIENT_SECRET=""
9
+
10
+ NUXT_OAUTH_GOOGLE_CLIENT_ID=""
11
+ NUXT_OAUTH_GOOGLE_CLIENT_SECRET=""
@@ -0,0 +1,58 @@
1
+ # Nuxt Bake
2
+
3
+ This project was scaffolded using [**Nuxt Bake**](https://www.npmjs.com/package/nuxt-bake) – a modern Nuxt.js starter with best practices built-in.
4
+
5
+ ## Getting Started
6
+
7
+ ### 1. Install Dependencies
8
+
9
+ ```bash
10
+ npm install
11
+ ```
12
+
13
+ ### 2. Configure Environment Variables
14
+
15
+ Copy the `.env.example` file to `.env` and fill in your environment-specific values:
16
+
17
+ ```bash
18
+ cp .env.example .env
19
+ ```
20
+
21
+ Refer to the `.env.example` file for details on required variables.
22
+
23
+ ### 3. Set Up the Database
24
+
25
+ Run Prisma migrations to set up your database schema:
26
+
27
+ ```bash
28
+ npm run db:migrate
29
+ ```
30
+
31
+ Or use push for prototyping (no migration files):
32
+
33
+ ```bash
34
+ npm run db:push
35
+ ```
36
+
37
+ ### 4. Start Development Server
38
+
39
+ ```bash
40
+ npm run dev
41
+ ```
42
+
43
+ Your app will be available at `http://localhost:3000`.
44
+
45
+ ## Available Scripts
46
+
47
+ - `dev` – Start the development server
48
+ - `build` – Build for production
49
+ - `typecheck` – Run TypeScript type checking
50
+ - `lint` – Lint the codebase
51
+ - `lint:fix` – Lint and auto-fix issues
52
+ - `db:generate` – Generate Prisma Client
53
+ - `db:push` – Push schema changes to the database
54
+ - `db:migrate` – Create and apply migrations
55
+
56
+ ## Learn More
57
+
58
+ Visit the [Nuxt Bake documentation](https://www.npmjs.com/package/nuxt-bake) for more information about this starter template.
@@ -0,0 +1,20 @@
1
+ <template>
2
+ <NuxtLayout>
3
+ <NuxtPage />
4
+ </NuxtLayout>
5
+
6
+ <Toast />
7
+ </template>
8
+
9
+ <script setup lang="ts">
10
+ useSeoMeta({
11
+ ogImage: "/og-image.png",
12
+ twitterImage: "/og-image.png",
13
+ })
14
+
15
+ useHead({
16
+ htmlAttrs: { lang: "en" },
17
+ link: [{ rel: "icon", href: "/favicon.svg" }],
18
+ meta: [{ name: "viewport", content: "width=device-width, initial-scale=1" }],
19
+ })
20
+ </script>
@@ -0,0 +1,257 @@
1
+ @import "tailwindcss";
2
+
3
+ /*
4
+ Base styles
5
+ */
6
+ :root {
7
+ background-color: var(--background);
8
+ color: var(--foreground);
9
+ font-family: var(--font-default);
10
+ overflow-x: hidden;
11
+ scroll-behavior: smooth;
12
+ margin: 0;
13
+ padding: 0;
14
+ }
15
+
16
+ /*
17
+ Base color tokens
18
+ */
19
+ :root {
20
+ --background: #f1f1f1;
21
+ --foreground: #0f0f0f;
22
+ --card: #e7e4e4;
23
+ --input: #e7e4e4;
24
+ --muted: #e1e1e1;
25
+ --muted-foreground: #707070;
26
+ --surface-foreground: #e5e7eb;
27
+ --primary: #56cb82;
28
+ --secondary: #2a3643;
29
+ --danger: #ed3c50;
30
+ --success: #42b883;
31
+ }
32
+
33
+ html.dark {
34
+ --background: #0f0f0f;
35
+ --foreground: #f1f1f1;
36
+ --card: #1a1a1a;
37
+ --input: #1a1a1a;
38
+ --muted: #242424;
39
+ --muted-foreground: #9b9b9b;
40
+ --surface-foreground: #e5e7eb;
41
+ --primary: #56cb82;
42
+ --secondary: #2a3643;
43
+ --danger: #ed3c50;
44
+ --success: #42b883;
45
+ }
46
+
47
+ @theme {
48
+ --color-background: var(--background);
49
+ --color-foreground: var(--foreground);
50
+ --color-card: var(--card);
51
+ --color-input: var(--input);
52
+ --color-muted: var(--muted);
53
+ --color-muted-foreground: var(--muted-foreground);
54
+ --color-surface-foreground: var(--surface-foreground);
55
+ --color-primary: var(--primary);
56
+ --color-secondary: var(--secondary);
57
+ --color-danger: var(--danger);
58
+ --color-success: var(--success);
59
+
60
+ --font-default: "Inter", sans-serif;
61
+ --font-mono: "JetBrains Mono", monospace;
62
+ }
63
+
64
+ /*
65
+ Global resets and accessibility
66
+ */
67
+ @layer base {
68
+ button:not(:disabled),
69
+ [role="button"]:not(:disabled) {
70
+ cursor: pointer;
71
+ }
72
+
73
+ *,
74
+ ::after,
75
+ ::before,
76
+ ::backdrop,
77
+ ::file-selector-button {
78
+ border-color: var(--muted);
79
+ }
80
+ }
81
+
82
+ /*
83
+ Typography
84
+ */
85
+ h1 {
86
+ font-size: 2.25rem;
87
+ line-height: 2.25rem;
88
+ font-weight: 800;
89
+ }
90
+ h2 {
91
+ font-size: 2rem;
92
+ line-height: 2rem;
93
+ font-weight: 800;
94
+ }
95
+ h3 {
96
+ font-size: 1.875rem;
97
+ line-height: 1.875rem;
98
+ font-weight: 700;
99
+ }
100
+ h4 {
101
+ font-size: 1.75rem;
102
+ line-height: 1.75rem;
103
+ font-weight: 700;
104
+ }
105
+ h5 {
106
+ font-size: 1.5rem;
107
+ line-height: 1.5rem;
108
+ font-weight: 700;
109
+ }
110
+ h6 {
111
+ font-size: 1.25rem;
112
+ line-height: 1.25rem;
113
+ font-weight: 700;
114
+ }
115
+
116
+ /*
117
+ UI elements
118
+ */
119
+ .card {
120
+ background-color: var(--card);
121
+ border: 1px solid var(--muted);
122
+ border-radius: 0.5rem;
123
+ padding: 0.5rem;
124
+ }
125
+
126
+ .overlay {
127
+ background-color: var(--card);
128
+ border: 1px solid var(--muted);
129
+ border-radius: 0.5rem;
130
+ padding: 0.5rem;
131
+ z-index: 50;
132
+ box-shadow: 4px 4px 8px 2px rgba(0, 0, 0, 0.4);
133
+ }
134
+
135
+ /*
136
+ Form elements
137
+ */
138
+ input,
139
+ textarea,
140
+ select {
141
+ appearance: none;
142
+ background-color: var(--card);
143
+ color: var(--muted-foreground);
144
+ border: 1px solid var(--muted);
145
+ accent-color: var(--muted);
146
+ border-radius: 0.5rem;
147
+ padding: 0.5rem;
148
+ font-size: 0.875rem;
149
+ }
150
+
151
+ /*
152
+ Buttons
153
+ */
154
+ .btn,
155
+ .btn-primary,
156
+ .btn-secondary,
157
+ .btn-danger,
158
+ .btn-success,
159
+ .btn-ghost {
160
+ display: flex;
161
+ flex-direction: row;
162
+ align-items: center;
163
+ justify-content: center;
164
+ padding: 0.5rem;
165
+ gap: 0.5rem;
166
+ font-size: 0.875rem;
167
+ font-weight: 600;
168
+ border-radius: 0.5rem;
169
+ white-space: nowrap;
170
+ user-select: none;
171
+ transition: all 0.2s ease-in-out;
172
+ }
173
+
174
+ .btn:hover,
175
+ .btn-primary:hover,
176
+ .btn-secondary:hover,
177
+ .btn-danger:hover,
178
+ .btn-success:hover,
179
+ .btn-ghost:hover {
180
+ transform: scale(1.02);
181
+ filter: brightness(0.9);
182
+ }
183
+
184
+ .btn:active,
185
+ .btn-primary:active,
186
+ .btn-secondary:active,
187
+ .btn-danger:active,
188
+ .btn-success:active,
189
+ .btn-ghost:active {
190
+ transform: scale(1);
191
+ filter: brightness(0.6);
192
+ }
193
+
194
+ .btn:disabled,
195
+ .btn-primary:disabled,
196
+ .btn-secondary:disabled,
197
+ .btn-danger:disabled,
198
+ .btn-success:disabled,
199
+ .btn-ghost:disabled {
200
+ filter: brightness(0.8);
201
+ cursor: not-allowed;
202
+ }
203
+
204
+ .btn {
205
+ --btn-bg: var(--card);
206
+ --btn-fg: var(--foreground);
207
+ }
208
+ .btn-primary {
209
+ --btn-bg: var(--primary);
210
+ --btn-fg: var(--surface-foreground);
211
+ }
212
+ .btn-secondary {
213
+ --btn-bg: var(--secondary);
214
+ --btn-fg: var(--surface-foreground);
215
+ }
216
+ .btn-danger {
217
+ --btn-bg: var(--danger);
218
+ --btn-fg: var(--surface-foreground);
219
+ }
220
+ .btn-success {
221
+ --btn-bg: var(--success);
222
+ --btn-fg: var(--surface-foreground);
223
+ }
224
+ .btn-ghost {
225
+ --btn-bg: transparent;
226
+ --btn-fg: var(--foreground);
227
+ border-color: transparent;
228
+ }
229
+ .btn-ghost:hover {
230
+ background-color: color-mix(in srgb, var(--muted) 60%, transparent);
231
+ }
232
+
233
+ .btn,
234
+ .btn-primary,
235
+ .btn-secondary,
236
+ .btn-danger,
237
+ .btn-success,
238
+ .btn-ghost {
239
+ background-color: var(--btn-bg);
240
+ color: var(--btn-fg);
241
+ }
242
+
243
+ /*
244
+ Background color utilities
245
+ */
246
+ .bg-primary {
247
+ background-color: var(--primary);
248
+ color: var(--surface-foreground);
249
+ }
250
+ .bg-secondary {
251
+ background-color: var(--secondary);
252
+ color: var(--surface-foreground);
253
+ }
254
+ .bg-muted {
255
+ background-color: var(--muted);
256
+ color: var(--muted-foreground);
257
+ }
@@ -0,0 +1,54 @@
1
+ <template>
2
+ <teleport to="body">
3
+ <transition name="fade">
4
+ <div v-if="isOpen" class="fixed inset-0 z-50 flex items-center justify-center bg-black/80" @mousedown.self="emit('update:isOpen', false)">
5
+ <div class="overlay">
6
+ <header class="flex flex-row items-center justify-between gap-4">
7
+ <h3>
8
+ {{ title }}
9
+ </h3>
10
+
11
+ <button aria-label="Close Dialog" class="btn-ghost" @mousedown="emit('update:isOpen', false)">
12
+ <icon name="mdi:close" size="20" />
13
+ </button>
14
+ </header>
15
+
16
+ <section>
17
+ <slot />
18
+ </section>
19
+ </div>
20
+ </div>
21
+ </transition>
22
+ </teleport>
23
+ </template>
24
+
25
+ <script setup lang="ts">
26
+ const props = withDefaults(defineProps<{
27
+ isOpen: boolean
28
+ title?: string
29
+ }>(), {
30
+ title: "Dialog Title",
31
+ })
32
+
33
+ const emit = defineEmits<{ "update:isOpen": [value: boolean] }>()
34
+
35
+ function onEscape(e: KeyboardEvent) {
36
+ if (e.key === "Escape" && props.isOpen) {
37
+ emit("update:isOpen", false)
38
+ }
39
+ }
40
+
41
+ onMounted(() => document.addEventListener("keydown", onEscape))
42
+ onBeforeUnmount(() => document.removeEventListener("keydown", onEscape))
43
+ </script>
44
+
45
+ <style scoped>
46
+ .fade-enter-active,
47
+ .fade-leave-active {
48
+ transition: opacity 0.4s ease;
49
+ }
50
+ .fade-enter-from,
51
+ .fade-leave-to {
52
+ opacity: 0;
53
+ }
54
+ </style>
@@ -0,0 +1,51 @@
1
+ <template>
2
+ <nav class="flex w-full flex-row items-center justify-between gap-2 p-4">
3
+ <div class="flex flex-row items-center gap-2">
4
+ <nuxt-link to="/">
5
+ <icon name="simple-icons:nuxt" size="35" class="text-primary" />
6
+ </nuxt-link>
7
+
8
+ <div v-if="userStore.user" class="flex flex-row items-center gap-2">
9
+ <p class="text-sm">
10
+ Hi, <span class="font-semibold text-primary">{{ userStore.user?.name }}</span>
11
+ </p>
12
+ <button class="btn" @click="signOut">
13
+ Logout
14
+ </button>
15
+ </div>
16
+
17
+ <div v-else class="flex flex-row items-center gap-2">
18
+ <p class="text-sm">
19
+ Unauthenticated
20
+ </p>
21
+ <nuxt-link to="/sign-in" class="btn">
22
+ Sign In
23
+ </nuxt-link>
24
+ </div>
25
+ </div>
26
+
27
+ <div class="flex flex-row items-center gap-2">
28
+ <nuxt-link to="https://github.com/matimortari/nuxt-bake" class="btn">
29
+ <icon name="simple-icons:github" size="20" />
30
+ </nuxt-link>
31
+
32
+ <button class="btn" @click="toggleTheme">
33
+ <icon :name="themeIcon" size="20" />
34
+ </button>
35
+ </div>
36
+ </nav>
37
+ </template>
38
+
39
+ <script setup lang="ts">
40
+ const { toggleTheme, themeIcon } = useTheme()
41
+ const userStore = useUserStore()
42
+
43
+ onMounted(async () => {
44
+ try {
45
+ await userStore.getUser()
46
+ }
47
+ catch (err: any) {
48
+ console.error("Failed to fetch user:", err)
49
+ }
50
+ })
51
+ </script>