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,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,7 @@
1
+ <template>
2
+ <Navbar />
3
+
4
+ <main>
5
+ <slot />
6
+ </main>
7
+ </template>
@@ -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
+ }
@@ -0,0 +1,12 @@
1
+ import { defineConfig, env } from "prisma/config"
2
+ import "dotenv/config"
3
+
4
+ export default defineConfig({
5
+ schema: "prisma/schema.prisma",
6
+ migrations: {
7
+ path: "prisma/migrations",
8
+ },
9
+ datasource: {
10
+ url: env("DATABASE_URL"),
11
+ },
12
+ })