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,116 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<Teleport to="body">
|
|
3
|
+
<div class="toast-container" aria-live="polite" aria-atomic="true">
|
|
4
|
+
<TransitionGroup name="toast">
|
|
5
|
+
<div
|
|
6
|
+
v-for="toast in toasts" :key="toast.id"
|
|
7
|
+
class="toast" :class="[`toast-${toast.type}`]"
|
|
8
|
+
role="alert"
|
|
9
|
+
>
|
|
10
|
+
<div class="toast-content">
|
|
11
|
+
<icon :name="toast.type === 'success' ? 'mdi:check-circle-outline' : 'mdi:close-circle-outline'" size="20" />
|
|
12
|
+
<span class="toast-message">{{ toast.message }}</span>
|
|
13
|
+
</div>
|
|
14
|
+
<button class="toast-close" aria-label="Close notification" @click="dismiss(toast.id)">
|
|
15
|
+
<icon name="mdi:close" size="15" />
|
|
16
|
+
</button>
|
|
17
|
+
</div>
|
|
18
|
+
</TransitionGroup>
|
|
19
|
+
</div>
|
|
20
|
+
</Teleport>
|
|
21
|
+
</template>
|
|
22
|
+
|
|
23
|
+
<script setup lang="ts">
|
|
24
|
+
const { toasts, dismiss } = useToast()
|
|
25
|
+
</script>
|
|
26
|
+
|
|
27
|
+
<style scoped>
|
|
28
|
+
.toast-container {
|
|
29
|
+
position: fixed;
|
|
30
|
+
bottom: 1rem;
|
|
31
|
+
right: 1rem;
|
|
32
|
+
z-index: 50;
|
|
33
|
+
display: flex;
|
|
34
|
+
flex-direction: column;
|
|
35
|
+
gap: 0.5rem;
|
|
36
|
+
max-width: 24rem;
|
|
37
|
+
pointer-events: none;
|
|
38
|
+
}
|
|
39
|
+
@media (max-width: 640px) {
|
|
40
|
+
.toast-container {
|
|
41
|
+
left: 1rem;
|
|
42
|
+
right: 1rem;
|
|
43
|
+
max-width: none;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
.toast {
|
|
48
|
+
display: flex;
|
|
49
|
+
align-items: center;
|
|
50
|
+
justify-content: space-between;
|
|
51
|
+
gap: 0.75rem;
|
|
52
|
+
padding: 0.875rem 1rem;
|
|
53
|
+
border-radius: 0.5rem;
|
|
54
|
+
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
|
55
|
+
pointer-events: auto;
|
|
56
|
+
backdrop-filter: blur(8px);
|
|
57
|
+
border: 1px solid rgba(255, 255, 255, 0.1);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
.toast-content {
|
|
61
|
+
display: flex;
|
|
62
|
+
align-items: center;
|
|
63
|
+
gap: 0.75rem;
|
|
64
|
+
flex: 1;
|
|
65
|
+
min-width: 0;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
.toast-message {
|
|
69
|
+
font-size: 0.875rem;
|
|
70
|
+
line-height: 1.25rem;
|
|
71
|
+
word-break: break-word;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
.toast-close {
|
|
75
|
+
display: flex;
|
|
76
|
+
align-items: center;
|
|
77
|
+
justify-content: center;
|
|
78
|
+
padding: 0.25rem;
|
|
79
|
+
border: none;
|
|
80
|
+
background: transparent;
|
|
81
|
+
cursor: pointer;
|
|
82
|
+
border-radius: 0.25rem;
|
|
83
|
+
flex-shrink: 0;
|
|
84
|
+
transition: background-color 0.2s;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
.toast-close:hover {
|
|
88
|
+
background-color: rgba(0, 0, 0, 0.1);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
.toast-success {
|
|
92
|
+
background-color: color-mix(in oklab, var(--color-success) 20%, transparent);
|
|
93
|
+
color: var(--success);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
.toast-error {
|
|
97
|
+
background-color: color-mix(in oklab, var(--color-danger) 20%, transparent);
|
|
98
|
+
color: var(--danger);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
.toast-enter-active,
|
|
102
|
+
.toast-leave-active {
|
|
103
|
+
transition: all 0.3s ease;
|
|
104
|
+
}
|
|
105
|
+
.toast-enter-from {
|
|
106
|
+
opacity: 0;
|
|
107
|
+
transform: translateX(100%);
|
|
108
|
+
}
|
|
109
|
+
.toast-leave-to {
|
|
110
|
+
opacity: 0;
|
|
111
|
+
transform: translateX(100%);
|
|
112
|
+
}
|
|
113
|
+
.toast-move {
|
|
114
|
+
transition: transform 0.3s ease;
|
|
115
|
+
}
|
|
116
|
+
</style>
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
export function useSessionMonitor() {
|
|
2
|
+
const { loggedIn } = useUserSession()
|
|
3
|
+
const sessionCheckInterval = ref<NodeJS.Timeout | null>(null)
|
|
4
|
+
|
|
5
|
+
const clearSessionCheck = () => {
|
|
6
|
+
if (sessionCheckInterval.value) {
|
|
7
|
+
clearInterval(sessionCheckInterval.value)
|
|
8
|
+
sessionCheckInterval.value = null
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
watch(loggedIn, (isLoggedIn) => {
|
|
13
|
+
clearSessionCheck()
|
|
14
|
+
|
|
15
|
+
if (isLoggedIn && import.meta.client) {
|
|
16
|
+
sessionCheckInterval.value = setInterval(async () => {
|
|
17
|
+
try {
|
|
18
|
+
await $fetch("/api/auth/validate", { method: "POST", credentials: "include" })
|
|
19
|
+
}
|
|
20
|
+
catch (err: any) {
|
|
21
|
+
if (err.statusCode === 401 || err.status === 401) {
|
|
22
|
+
await signOut()
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
}, 5 * 60 * 1000) // 5 minutes
|
|
26
|
+
}
|
|
27
|
+
}, { immediate: true })
|
|
28
|
+
|
|
29
|
+
const handleStorageChange = async (e: StorageEvent) => {
|
|
30
|
+
if (e.key === "nuxt-session" && !e.newValue && loggedIn.value) {
|
|
31
|
+
await navigateTo("/")
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
onMounted(() => globalThis.addEventListener("storage", handleStorageChange))
|
|
36
|
+
onBeforeUnmount(() => {
|
|
37
|
+
clearSessionCheck()
|
|
38
|
+
globalThis.removeEventListener("storage", handleStorageChange)
|
|
39
|
+
})
|
|
40
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
export function useTheme() {
|
|
2
|
+
const colorMode = useState<"light" | "dark">("theme", () => "light")
|
|
3
|
+
const storageKey = "nuxt-color-mode"
|
|
4
|
+
|
|
5
|
+
const updateHtmlClass = () => {
|
|
6
|
+
const html = globalThis.document.documentElement
|
|
7
|
+
html.classList.remove("light", "dark")
|
|
8
|
+
html.classList.add(colorMode.value)
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const syncThemeFromLocalStorage = () => {
|
|
12
|
+
const saved = globalThis.localStorage.getItem(storageKey)
|
|
13
|
+
if (saved === "dark" || saved === "light") {
|
|
14
|
+
colorMode.value = saved
|
|
15
|
+
}
|
|
16
|
+
else {
|
|
17
|
+
const prefersDark = globalThis.matchMedia("(prefers-color-scheme: dark)").matches
|
|
18
|
+
colorMode.value = prefersDark ? "dark" : "light"
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
updateHtmlClass()
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const toggleTheme = () => {
|
|
25
|
+
colorMode.value = colorMode.value === "dark" ? "light" : "dark"
|
|
26
|
+
globalThis.localStorage.setItem(storageKey, colorMode.value)
|
|
27
|
+
updateHtmlClass()
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
onMounted(() => {
|
|
31
|
+
syncThemeFromLocalStorage()
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
const themeIcon = computed(() => colorMode.value === "light" ? "mdi:weather-night" : "mdi:weather-sunny")
|
|
35
|
+
|
|
36
|
+
return {
|
|
37
|
+
colorMode,
|
|
38
|
+
toggleTheme,
|
|
39
|
+
themeIcon,
|
|
40
|
+
}
|
|
41
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
const toasts = ref<Toast[]>([])
|
|
2
|
+
let toastIdCounter = 0
|
|
3
|
+
|
|
4
|
+
export function useToast() {
|
|
5
|
+
function show(message: string, type: "success" | "error", duration = 5000) {
|
|
6
|
+
const id = `toast-${++toastIdCounter}`
|
|
7
|
+
const toast: Toast = { id, message, type, duration }
|
|
8
|
+
|
|
9
|
+
toasts.value.push(toast)
|
|
10
|
+
if (duration > 0) {
|
|
11
|
+
setTimeout(() => dismiss(id), duration)
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
return id
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function success(message: string, duration = 5000) {
|
|
18
|
+
return show(message, "success", duration)
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function error(message: string, duration = 7000) {
|
|
22
|
+
return show(message, "error", duration)
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function dismiss(id: string) {
|
|
26
|
+
const index = toasts.value.findIndex(t => t.id === id)
|
|
27
|
+
if (index !== -1) {
|
|
28
|
+
toasts.value.splice(index, 1)
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function clear() {
|
|
33
|
+
toasts.value = []
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
return { toasts: readonly(toasts), show, success, error, dismiss, clear }
|
|
37
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div class="flex h-screen flex-col items-center justify-center gap-4">
|
|
3
|
+
<h1>
|
|
4
|
+
{{ error.status }}
|
|
5
|
+
</h1>
|
|
6
|
+
|
|
7
|
+
<p class="text-center text-muted-foreground">
|
|
8
|
+
{{ error.statusText || "Unknown error" }}
|
|
9
|
+
</p>
|
|
10
|
+
|
|
11
|
+
<nuxt-link to="/">
|
|
12
|
+
Go back home
|
|
13
|
+
</nuxt-link>
|
|
14
|
+
</div>
|
|
15
|
+
</template>
|
|
16
|
+
|
|
17
|
+
<script setup lang="ts">
|
|
18
|
+
import type { NuxtError } from "#app"
|
|
19
|
+
|
|
20
|
+
defineProps<{
|
|
21
|
+
error: NuxtError
|
|
22
|
+
}>()
|
|
23
|
+
</script>
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div class="flex flex-col items-center justify-center gap-8">
|
|
3
|
+
<header class="flex w-full flex-col items-center gap-4 p-4">
|
|
4
|
+
<div class="flex flex-col items-center gap-2 text-center">
|
|
5
|
+
<icon name="simple-icons:nuxt" size="50" class="text-primary" />
|
|
6
|
+
<h1>
|
|
7
|
+
Nuxt Bake
|
|
8
|
+
</h1>
|
|
9
|
+
<p>
|
|
10
|
+
If you're seeing this, it means you've successfully set up your Nuxt.js project via <code class="text-primary">Nuxt Bake</code>.
|
|
11
|
+
</p>
|
|
12
|
+
</div>
|
|
13
|
+
</header>
|
|
14
|
+
|
|
15
|
+
<hr class="w-4/5 border-primary!">
|
|
16
|
+
|
|
17
|
+
<div class="flex flex-col items-center gap-2">
|
|
18
|
+
<h2 class="text-lg font-semibold">
|
|
19
|
+
Features
|
|
20
|
+
</h2>
|
|
21
|
+
|
|
22
|
+
<ul class="grid grid-cols-2 gap-4 md:grid-cols-5">
|
|
23
|
+
<li v-for="(feature, index) in FEATURES" :key="index" class="flex flex-row items-center gap-2 text-xs font-semibold md:text-sm">
|
|
24
|
+
<icon :name="feature.icon" :size="20" class="text-primary" />
|
|
25
|
+
<span>{{ feature.label }}</span>
|
|
26
|
+
</li>
|
|
27
|
+
</ul>
|
|
28
|
+
</div>
|
|
29
|
+
</div>
|
|
30
|
+
</template>
|
|
31
|
+
|
|
32
|
+
<script setup lang="ts">
|
|
33
|
+
const { public: { baseURL } } = useRuntimeConfig()
|
|
34
|
+
|
|
35
|
+
const FEATURES = [
|
|
36
|
+
{ icon: "simple-icons:nuxtdotjs", label: "Nuxt 4" },
|
|
37
|
+
{ icon: "simple-icons:typescript", label: "TypeScript" },
|
|
38
|
+
{ icon: "simple-icons:tailwindcss", label: "Tailwind CSS 4" },
|
|
39
|
+
{ icon: "simple-icons:iconify", label: "Nuxt Icons" },
|
|
40
|
+
{ icon: "ph:text-aa", label: "Google Fonts" },
|
|
41
|
+
{ icon: "icon-park-outline:pineapple", label: "Pinia" },
|
|
42
|
+
{ icon: "ph:lock-key-open", label: "OAuth" },
|
|
43
|
+
{ icon: "simple-icons:prisma", label: "Prisma" },
|
|
44
|
+
{ icon: "simple-icons:eslint", label: "ESLint" },
|
|
45
|
+
{ icon: "ph:magnifying-glass", label: "SEO" },
|
|
46
|
+
]
|
|
47
|
+
|
|
48
|
+
useHead({
|
|
49
|
+
title: "Home",
|
|
50
|
+
link: [{ rel: "canonical", href: `${baseURL}` }],
|
|
51
|
+
meta: [{ name: "description", content: "Home page." }],
|
|
52
|
+
})
|
|
53
|
+
</script>
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div class="flex flex-col items-center justify-center gap-8">
|
|
3
|
+
<header class="my-4 flex flex-col items-center justify-center gap-2">
|
|
4
|
+
<h2>
|
|
5
|
+
Sign In
|
|
6
|
+
</h2>
|
|
7
|
+
</header>
|
|
8
|
+
|
|
9
|
+
<div class="my-4 flex flex-col items-center gap-4">
|
|
10
|
+
<p class="text-lg font-semibold text-muted-foreground">
|
|
11
|
+
Choose a provider to continue.
|
|
12
|
+
</p>
|
|
13
|
+
<div class="flex flex-row items-center gap-4">
|
|
14
|
+
<button
|
|
15
|
+
v-for="provider in OAUTH_PROVIDERS" :key="provider.name"
|
|
16
|
+
class="btn" @click="navigateTo(`/api/auth/${provider.name}`, { external: true })"
|
|
17
|
+
>
|
|
18
|
+
<icon :name="provider.icon" size="25" />
|
|
19
|
+
<span>{{ provider.label }}</span>
|
|
20
|
+
</button>
|
|
21
|
+
</div>
|
|
22
|
+
</div>
|
|
23
|
+
</div>
|
|
24
|
+
</template>
|
|
25
|
+
|
|
26
|
+
<script setup lang="ts">
|
|
27
|
+
const { public: { baseURL } } = useRuntimeConfig()
|
|
28
|
+
|
|
29
|
+
const OAUTH_PROVIDERS = [
|
|
30
|
+
{ name: "github", label: "Sign In With GitHub", icon: "simple-icons:github" },
|
|
31
|
+
{ name: "google", label: "Sign In With Google", icon: "simple-icons:google" },
|
|
32
|
+
]
|
|
33
|
+
|
|
34
|
+
useHead({
|
|
35
|
+
title: "Sign In",
|
|
36
|
+
link: [{ rel: "canonical", href: `${baseURL}/sign-in` }],
|
|
37
|
+
meta: [{ name: "description", content: "Sign In page" }],
|
|
38
|
+
})
|
|
39
|
+
</script>
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
export const useUserStore = defineStore("user", () => {
|
|
2
|
+
const toast = useToast()
|
|
3
|
+
const user = ref<Record<string, any> | null>(null)
|
|
4
|
+
const loading = ref(false)
|
|
5
|
+
|
|
6
|
+
async function getUser() {
|
|
7
|
+
loading.value = true
|
|
8
|
+
|
|
9
|
+
try {
|
|
10
|
+
const res = await $fetch<{ userData: User }>("/api/user", { method: "GET", credentials: "include" })
|
|
11
|
+
user.value = res.userData
|
|
12
|
+
return res
|
|
13
|
+
}
|
|
14
|
+
catch (err: any) {
|
|
15
|
+
const message = getErrorMessage(err, "Failed to get user")
|
|
16
|
+
toast.error(message)
|
|
17
|
+
console.error("getUser error:", err)
|
|
18
|
+
throw err
|
|
19
|
+
}
|
|
20
|
+
finally {
|
|
21
|
+
loading.value = false
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
async function deleteUser() {
|
|
26
|
+
loading.value = true
|
|
27
|
+
|
|
28
|
+
try {
|
|
29
|
+
await $fetch("/api/user", { method: "DELETE", credentials: "include" })
|
|
30
|
+
user.value = null
|
|
31
|
+
toast.success("User deleted successfully")
|
|
32
|
+
}
|
|
33
|
+
catch (err: any) {
|
|
34
|
+
const message = getErrorMessage(err, "Failed to delete user")
|
|
35
|
+
toast.error(message)
|
|
36
|
+
console.error("deleteUser error:", err)
|
|
37
|
+
throw err
|
|
38
|
+
}
|
|
39
|
+
finally {
|
|
40
|
+
loading.value = false
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return {
|
|
45
|
+
user,
|
|
46
|
+
loading,
|
|
47
|
+
getUser,
|
|
48
|
+
deleteUser,
|
|
49
|
+
}
|
|
50
|
+
})
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Returns a formatted date string or a placeholder if the date is null/undefined.
|
|
3
|
+
*/
|
|
4
|
+
export function formatDate(date?: string | Date | null): string {
|
|
5
|
+
if (!date) {
|
|
6
|
+
return "-"
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
const dt = typeof date === "string" ? new Date(date) : date
|
|
10
|
+
const formatted = dt.toLocaleDateString("en-US", {
|
|
11
|
+
year: "2-digit",
|
|
12
|
+
month: "short",
|
|
13
|
+
day: "numeric",
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
return formatted.charAt(0).toLowerCase() + formatted.slice(1)
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Extracts the error message from various error formats (Nuxt/H3/Zod).
|
|
21
|
+
*/
|
|
22
|
+
export function getErrorMessage(err: any, fallback: string): string {
|
|
23
|
+
return err?.data?.statusMessage || err?.data?.message || err?.statusMessage || err?.message || fallback
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Signs in the user by redirecting to the provider's authentication endpoint.
|
|
28
|
+
*/
|
|
29
|
+
export function signIn(provider: string) {
|
|
30
|
+
navigateTo(`/api/auth/${provider}`, { external: true })
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
Signs out the current user by calling the logout endpoint and clearing the session.
|
|
35
|
+
*/
|
|
36
|
+
export async function signOut() {
|
|
37
|
+
const { clear } = useUserSession()
|
|
38
|
+
|
|
39
|
+
await $fetch("/api/auth/logout", { method: "POST", credentials: "include" })
|
|
40
|
+
await clear()
|
|
41
|
+
await navigateTo("/")
|
|
42
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import antfu from "@antfu/eslint-config"
|
|
2
|
+
import eslintPluginBetterTailwindcss from "eslint-plugin-better-tailwindcss"
|
|
3
|
+
|
|
4
|
+
export default antfu({
|
|
5
|
+
vue: true,
|
|
6
|
+
typescript: true,
|
|
7
|
+
jsonc: true,
|
|
8
|
+
formatters: {
|
|
9
|
+
css: true,
|
|
10
|
+
html: true,
|
|
11
|
+
markdown: true,
|
|
12
|
+
},
|
|
13
|
+
stylistic: {
|
|
14
|
+
indent: 2,
|
|
15
|
+
quotes: "double",
|
|
16
|
+
semi: false,
|
|
17
|
+
},
|
|
18
|
+
plugins: {
|
|
19
|
+
"better-tailwindcss": eslintPluginBetterTailwindcss,
|
|
20
|
+
},
|
|
21
|
+
settings: {
|
|
22
|
+
"better-tailwindcss": {
|
|
23
|
+
entryPoint: "app/assets/styles.css",
|
|
24
|
+
detectComponentClasses: true,
|
|
25
|
+
},
|
|
26
|
+
},
|
|
27
|
+
rules: {
|
|
28
|
+
"no-new": "off",
|
|
29
|
+
"no-undef": "off",
|
|
30
|
+
"no-alert": "off",
|
|
31
|
+
"no-console": "off",
|
|
32
|
+
"node/prefer-global/process": "off",
|
|
33
|
+
"curly": ["error", "all"],
|
|
34
|
+
"object-curly-newline": ["error", {
|
|
35
|
+
ObjectExpression: { multiline: false, consistent: true },
|
|
36
|
+
ObjectPattern: { multiline: false, consistent: true },
|
|
37
|
+
ImportDeclaration: { multiline: false, consistent: true },
|
|
38
|
+
ExportDeclaration: { multiline: false, consistent: true },
|
|
39
|
+
}],
|
|
40
|
+
"vue/object-curly-newline": ["error", {
|
|
41
|
+
ObjectExpression: { multiline: false, consistent: true },
|
|
42
|
+
ObjectPattern: { multiline: false, consistent: true },
|
|
43
|
+
ImportDeclaration: { multiline: false, consistent: true },
|
|
44
|
+
ExportDeclaration: { multiline: false, consistent: true },
|
|
45
|
+
}],
|
|
46
|
+
"vue/block-order": ["error", {
|
|
47
|
+
order: ["template", "script", "style"],
|
|
48
|
+
}],
|
|
49
|
+
"vue/define-macros-order": ["error", {
|
|
50
|
+
order: ["defineProps", "defineEmits"],
|
|
51
|
+
}],
|
|
52
|
+
"vue/singleline-html-element-content-newline": ["error", {
|
|
53
|
+
ignoreWhenEmpty: true,
|
|
54
|
+
ignoreWhenNoAttributes: true,
|
|
55
|
+
}],
|
|
56
|
+
"vue/multiline-html-element-content-newline": ["error", {
|
|
57
|
+
ignoreWhenEmpty: true,
|
|
58
|
+
allowEmptyLines: false,
|
|
59
|
+
}],
|
|
60
|
+
"vue/html-closing-bracket-newline": ["error", {
|
|
61
|
+
singleline: "never",
|
|
62
|
+
multiline: "always",
|
|
63
|
+
selfClosingTag: { singleline: "never", multiline: "always" },
|
|
64
|
+
}],
|
|
65
|
+
"vue/max-attributes-per-line": ["warn", {
|
|
66
|
+
singleline: { max: 4 },
|
|
67
|
+
multiline: { max: 2 },
|
|
68
|
+
}],
|
|
69
|
+
...eslintPluginBetterTailwindcss.configs["recommended-warn"].rules,
|
|
70
|
+
"better-tailwindcss/no-unregistered-classes": "off",
|
|
71
|
+
"better-tailwindcss/no-unknown-classes": "off",
|
|
72
|
+
"better-tailwindcss/enforce-consistent-line-wrapping": "off",
|
|
73
|
+
},
|
|
74
|
+
})
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import tailwindcss from "@tailwindcss/vite"
|
|
2
|
+
|
|
3
|
+
export default defineNuxtConfig({
|
|
4
|
+
modules: [
|
|
5
|
+
"@nuxt/fonts",
|
|
6
|
+
"@nuxt/icon",
|
|
7
|
+
"@nuxtjs/color-mode",
|
|
8
|
+
"@nuxtjs/seo",
|
|
9
|
+
"@pinia/nuxt",
|
|
10
|
+
"nuxt-auth-utils",
|
|
11
|
+
],
|
|
12
|
+
runtimeConfig: {
|
|
13
|
+
public: {
|
|
14
|
+
baseUrl: process.env.NUXT_PUBLIC_BASE_URL,
|
|
15
|
+
},
|
|
16
|
+
},
|
|
17
|
+
nitro: {
|
|
18
|
+
externals: {
|
|
19
|
+
inline: ["unhead"],
|
|
20
|
+
},
|
|
21
|
+
},
|
|
22
|
+
vite: {
|
|
23
|
+
plugins: [tailwindcss() as any],
|
|
24
|
+
},
|
|
25
|
+
css: ["~/assets/styles.css"],
|
|
26
|
+
site: {
|
|
27
|
+
url: process.env.NUXT_PUBLIC_BASE_URL,
|
|
28
|
+
},
|
|
29
|
+
colorMode: {
|
|
30
|
+
classSuffix: "",
|
|
31
|
+
preference: "system",
|
|
32
|
+
fallback: "light",
|
|
33
|
+
storageKey: "nuxt-color-mode",
|
|
34
|
+
},
|
|
35
|
+
icon: {
|
|
36
|
+
mode: "svg",
|
|
37
|
+
clientBundle: { scan: true },
|
|
38
|
+
},
|
|
39
|
+
})
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "my-nuxt-app",
|
|
3
|
+
"type": "module",
|
|
4
|
+
"private": true,
|
|
5
|
+
"scripts": {
|
|
6
|
+
"dev": "nuxt dev",
|
|
7
|
+
"build": "nuxt build",
|
|
8
|
+
"postinstall": "prisma generate && nuxt prepare",
|
|
9
|
+
"typecheck": "nuxt typecheck",
|
|
10
|
+
"lint": "eslint .",
|
|
11
|
+
"lint:fix": "eslint . --fix",
|
|
12
|
+
"db:generate": "prisma generate",
|
|
13
|
+
"db:push": "prisma db push",
|
|
14
|
+
"db:migrate": "prisma migrate dev"
|
|
15
|
+
},
|
|
16
|
+
"dependencies": {
|
|
17
|
+
"@nuxt/fonts": "0.14.0",
|
|
18
|
+
"@nuxt/icon": "2.2.1",
|
|
19
|
+
"@nuxtjs/color-mode": "4.0.0",
|
|
20
|
+
"@nuxtjs/seo": "3.4.0",
|
|
21
|
+
"@pinia/nuxt": "0.11.3",
|
|
22
|
+
"@prisma/adapter-pg": "7.4.0",
|
|
23
|
+
"@prisma/client": "7.4.0",
|
|
24
|
+
"@tailwindcss/vite": "4.2.1",
|
|
25
|
+
"dotenv": "17.3.1",
|
|
26
|
+
"nuxt": "4.3.1",
|
|
27
|
+
"nuxt-auth-utils": "0.5.29",
|
|
28
|
+
"pg": "8.18.0",
|
|
29
|
+
"tailwindcss": "4.2.1"
|
|
30
|
+
},
|
|
31
|
+
"devDependencies": {
|
|
32
|
+
"@antfu/eslint-config": "7.5.0",
|
|
33
|
+
"eslint": "10.0.0",
|
|
34
|
+
"eslint-plugin-better-tailwindcss": "4.3.0",
|
|
35
|
+
"eslint-plugin-format": "2.0.0",
|
|
36
|
+
"prisma": "7.4.0",
|
|
37
|
+
"typescript": "5.9.3"
|
|
38
|
+
}
|
|
39
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
generator client {
|
|
2
|
+
provider = "prisma-client"
|
|
3
|
+
output = "../.data/prisma"
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
datasource db {
|
|
7
|
+
provider = "postgresql"
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
model User {
|
|
11
|
+
id String @id @default(cuid())
|
|
12
|
+
email String @unique
|
|
13
|
+
name String
|
|
14
|
+
image String?
|
|
15
|
+
createdAt DateTime @default(now())
|
|
16
|
+
updatedAt DateTime @updatedAt
|
|
17
|
+
accounts Account[]
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
model Account {
|
|
21
|
+
id String @id @default(cuid())
|
|
22
|
+
userId String
|
|
23
|
+
provider String
|
|
24
|
+
providerAccountId String
|
|
25
|
+
createdAt DateTime @default(now())
|
|
26
|
+
updatedAt DateTime @updatedAt
|
|
27
|
+
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
|
28
|
+
|
|
29
|
+
@@unique([provider, providerAccountId])
|
|
30
|
+
}
|