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.
- package/README.md +79 -0
- package/dist/helpers/constants.js +28 -0
- package/dist/helpers/git.js +32 -0
- package/dist/helpers/package-manager.js +77 -0
- package/dist/helpers/template.js +27 -0
- package/dist/helpers/utils.js +42 -0
- package/dist/index.js +96 -0
- package/eslint.config.ts +26 -0
- package/package.json +58 -0
- package/playwright.config.ts +6 -0
- package/src/helpers/git.ts +28 -0
- package/src/helpers/package-manager.ts +86 -0
- package/src/helpers/template.ts +35 -0
- package/src/helpers/utils.ts +83 -0
- package/src/index.ts +106 -0
- package/templates/base/.env.example +11 -0
- package/templates/base/README.md +58 -0
- package/templates/base/app/app.vue +20 -0
- package/templates/base/app/assets/styles.css +257 -0
- package/templates/base/app/components/dialog.vue +54 -0
- package/templates/base/app/components/navbar.vue +51 -0
- package/templates/base/app/components/toast.vue +116 -0
- package/templates/base/app/composables/use-session-monitor.ts +40 -0
- package/templates/base/app/composables/use-theme.ts +41 -0
- package/templates/base/app/composables/use-toast.ts +37 -0
- package/templates/base/app/error.vue +23 -0
- package/templates/base/app/layouts/default.vue +7 -0
- package/templates/base/app/pages/index.vue +53 -0
- package/templates/base/app/pages/sign-in.vue +39 -0
- package/templates/base/app/stores/user-store.ts +50 -0
- package/templates/base/app/utils/helpers.ts +42 -0
- package/templates/base/eslint.config.ts +74 -0
- package/templates/base/nuxt.config.ts +39 -0
- package/templates/base/package.json +39 -0
- package/templates/base/prisma/schema.prisma +30 -0
- package/templates/base/prisma.config.ts +12 -0
- package/templates/base/server/api/auth/[provider].ts +67 -0
- package/templates/base/server/api/user/index.delete.ts +8 -0
- package/templates/base/server/api/user/index.get.ts +10 -0
- package/templates/base/server/utils/auth.ts +48 -0
- package/templates/base/server/utils/db.ts +15 -0
- package/templates/base/server/utils/helpers.ts +14 -0
- package/templates/base/shared/types/auth.d.ts +31 -0
- package/templates/base/shared/types/globals.d.ts +15 -0
- package/templates/base/tsconfig.json +18 -0
- package/templates/with-i18n/app/app.vue +29 -0
- package/templates/with-i18n/app/components/navbar.vue +74 -0
- package/templates/with-i18n/app/error.vue +25 -0
- package/templates/with-i18n/app/pages/index.vue +53 -0
- package/templates/with-i18n/app/pages/sign-in.vue +39 -0
- package/templates/with-i18n/app/utils/i18n.config.ts +14 -0
- package/templates/with-i18n/app/utils/locales/en-US.json +50 -0
- package/templates/with-i18n/app/utils/locales/fr-FR.json +50 -0
- package/templates/with-i18n/nuxt.config.ts +51 -0
- package/templates/with-tests/app/pages/index.vue +51 -0
- package/templates/with-tests/nuxt.config.ts +31 -0
- package/templates/with-tests/playwright.config.ts +13 -0
- package/templates/with-tests/tests/e2e/e2e-hello.test.ts +8 -0
- package/templates/with-tests/tests/hello.test.ts +7 -0
- package/templates/with-tests/vitest.config.ts +16 -0
- package/tests/git.test.ts +54 -0
- package/tests/package-manager.test.ts +100 -0
- package/tests/template.test.ts +73 -0
- package/tests/utils.test.ts +155 -0
- package/tsconfig.json +13 -0
- 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,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>
|