@plutocms/supabase 0.0.1-alpha.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 (99) hide show
  1. package/.agents/skills/nuxt/GENERATION.md +5 -0
  2. package/.agents/skills/nuxt/SKILL.md +55 -0
  3. package/.agents/skills/nuxt/references/advanced-hooks.md +289 -0
  4. package/.agents/skills/nuxt/references/advanced-layers.md +299 -0
  5. package/.agents/skills/nuxt/references/advanced-module-authoring.md +554 -0
  6. package/.agents/skills/nuxt/references/best-practices-data-fetching.md +357 -0
  7. package/.agents/skills/nuxt/references/best-practices-ssr.md +355 -0
  8. package/.agents/skills/nuxt/references/core-cli.md +263 -0
  9. package/.agents/skills/nuxt/references/core-config.md +162 -0
  10. package/.agents/skills/nuxt/references/core-data-fetching.md +236 -0
  11. package/.agents/skills/nuxt/references/core-deployment.md +224 -0
  12. package/.agents/skills/nuxt/references/core-directory-structure.md +269 -0
  13. package/.agents/skills/nuxt/references/core-modules.md +292 -0
  14. package/.agents/skills/nuxt/references/core-routing.md +226 -0
  15. package/.agents/skills/nuxt/references/features-components-autoimport.md +328 -0
  16. package/.agents/skills/nuxt/references/features-components.md +264 -0
  17. package/.agents/skills/nuxt/references/features-composables.md +276 -0
  18. package/.agents/skills/nuxt/references/features-server.md +265 -0
  19. package/.agents/skills/nuxt/references/features-state.md +194 -0
  20. package/.agents/skills/nuxt/references/rendering-modes.md +237 -0
  21. package/.agents/skills/nuxt-ui/SKILL.md +334 -0
  22. package/.agents/skills/nuxt-ui/references/components.md +377 -0
  23. package/.agents/skills/nuxt-ui/references/composables.md +127 -0
  24. package/.agents/skills/nuxt-ui/references/layouts/chat.md +195 -0
  25. package/.agents/skills/nuxt-ui/references/layouts/dashboard.md +220 -0
  26. package/.agents/skills/nuxt-ui/references/layouts/docs.md +141 -0
  27. package/.agents/skills/nuxt-ui/references/layouts/editor.md +167 -0
  28. package/.agents/skills/nuxt-ui/references/layouts/page.md +260 -0
  29. package/.agents/skills/nuxt-ui/references/theming.md +427 -0
  30. package/.agents/skills/supabase-postgres-best-practices/AGENTS.md +68 -0
  31. package/.agents/skills/supabase-postgres-best-practices/CLAUDE.md +1 -0
  32. package/.agents/skills/supabase-postgres-best-practices/README.md +116 -0
  33. package/.agents/skills/supabase-postgres-best-practices/SKILL.md +64 -0
  34. package/.agents/skills/supabase-postgres-best-practices/references/advanced-full-text-search.md +55 -0
  35. package/.agents/skills/supabase-postgres-best-practices/references/advanced-jsonb-indexing.md +49 -0
  36. package/.agents/skills/supabase-postgres-best-practices/references/conn-idle-timeout.md +46 -0
  37. package/.agents/skills/supabase-postgres-best-practices/references/conn-limits.md +44 -0
  38. package/.agents/skills/supabase-postgres-best-practices/references/conn-pooling.md +41 -0
  39. package/.agents/skills/supabase-postgres-best-practices/references/conn-prepared-statements.md +46 -0
  40. package/.agents/skills/supabase-postgres-best-practices/references/data-batch-inserts.md +54 -0
  41. package/.agents/skills/supabase-postgres-best-practices/references/data-n-plus-one.md +53 -0
  42. package/.agents/skills/supabase-postgres-best-practices/references/data-pagination.md +50 -0
  43. package/.agents/skills/supabase-postgres-best-practices/references/data-upsert.md +50 -0
  44. package/.agents/skills/supabase-postgres-best-practices/references/lock-advisory.md +56 -0
  45. package/.agents/skills/supabase-postgres-best-practices/references/lock-deadlock-prevention.md +68 -0
  46. package/.agents/skills/supabase-postgres-best-practices/references/lock-short-transactions.md +50 -0
  47. package/.agents/skills/supabase-postgres-best-practices/references/lock-skip-locked.md +54 -0
  48. package/.agents/skills/supabase-postgres-best-practices/references/monitor-explain-analyze.md +45 -0
  49. package/.agents/skills/supabase-postgres-best-practices/references/monitor-pg-stat-statements.md +55 -0
  50. package/.agents/skills/supabase-postgres-best-practices/references/monitor-vacuum-analyze.md +55 -0
  51. package/.agents/skills/supabase-postgres-best-practices/references/query-composite-indexes.md +44 -0
  52. package/.agents/skills/supabase-postgres-best-practices/references/query-covering-indexes.md +40 -0
  53. package/.agents/skills/supabase-postgres-best-practices/references/query-index-types.md +48 -0
  54. package/.agents/skills/supabase-postgres-best-practices/references/query-missing-indexes.md +43 -0
  55. package/.agents/skills/supabase-postgres-best-practices/references/query-partial-indexes.md +45 -0
  56. package/.agents/skills/supabase-postgres-best-practices/references/schema-constraints.md +80 -0
  57. package/.agents/skills/supabase-postgres-best-practices/references/schema-data-types.md +46 -0
  58. package/.agents/skills/supabase-postgres-best-practices/references/schema-foreign-key-indexes.md +59 -0
  59. package/.agents/skills/supabase-postgres-best-practices/references/schema-lowercase-identifiers.md +55 -0
  60. package/.agents/skills/supabase-postgres-best-practices/references/schema-partitioning.md +55 -0
  61. package/.agents/skills/supabase-postgres-best-practices/references/schema-primary-keys.md +61 -0
  62. package/.agents/skills/supabase-postgres-best-practices/references/security-privileges.md +54 -0
  63. package/.agents/skills/supabase-postgres-best-practices/references/security-rls-basics.md +50 -0
  64. package/.agents/skills/supabase-postgres-best-practices/references/security-rls-performance.md +57 -0
  65. package/.editorconfig +12 -0
  66. package/.env.example +2 -0
  67. package/.eslintignore +1 -0
  68. package/.prettierrc +9 -0
  69. package/.vscode/extensions.json +10 -0
  70. package/.vscode/settings.json +61 -0
  71. package/README.md +62 -0
  72. package/app/components/navbar/NavbarAdmin.vue +169 -0
  73. package/app/composables/auth.ts +81 -0
  74. package/app/layouts/setup.vue +13 -0
  75. package/app/middleware/auth.ts +27 -0
  76. package/app/middleware/setup-check.ts +25 -0
  77. package/app/pages/admin/(auth)/login.vue +84 -0
  78. package/app/pages/admin/(auth)/signup.vue +128 -0
  79. package/app/pages/admin/settings.vue +116 -0
  80. package/app/pages/admin/setup.vue +300 -0
  81. package/app/pages/admin.vue +38 -0
  82. package/bun.lock +2685 -0
  83. package/eslint.config.mjs +88 -0
  84. package/nuxt.config.ts +55 -0
  85. package/package.json +44 -0
  86. package/public/schema.sql +103 -0
  87. package/scripts/supabase-typegen.ts +36 -0
  88. package/server/api/category/list.get.ts +9 -0
  89. package/server/api/category/new.post.ts +31 -0
  90. package/server/api/settings/first_setup.get.ts +39 -0
  91. package/server/api/settings/index.get.ts +27 -0
  92. package/server/api/settings/update.post.ts +36 -0
  93. package/server/api/setup/create.post.ts +58 -0
  94. package/server/api/signup.post.ts +28 -0
  95. package/server/api/users/[id].get.ts +21 -0
  96. package/server/api/users/index.get.ts +16 -0
  97. package/shared/types/supabase.ts +406 -0
  98. package/skills-lock.json +20 -0
  99. package/tsconfig.json +18 -0
@@ -0,0 +1,169 @@
1
+ <script setup lang="ts">
2
+ import type { DropdownMenuItem } from '@nuxt/ui'
3
+
4
+ const host = useRequestURL().host
5
+
6
+ const route = useRoute()
7
+
8
+ const { isLoggedIn, user, logout } = await useAuth()
9
+
10
+ const has_settings_modified = useState<boolean>('has_settings_modified')
11
+
12
+ const { data: settingsData, status } = await useFetch('/api/settings', {
13
+ watch: [has_settings_modified],
14
+ })
15
+
16
+ const maybeDevUrl = computed(() => {
17
+ if (import.meta.dev) {
18
+ return `http://${host}`
19
+ }
20
+
21
+ return settingsData.value?.settings.website_url || ''
22
+ })
23
+
24
+ const items = ref<DropdownMenuItem[][]>([
25
+ [
26
+ {
27
+ label: `${user.value?.first_name} ${user.value?.last_name}` || '',
28
+ avatar: {
29
+ icon: 'lucide:user',
30
+ class: 'bg-green-600',
31
+ ui: {
32
+ icon: 'text-white text-sm',
33
+ },
34
+ },
35
+ type: 'label',
36
+ },
37
+ ],
38
+
39
+ [
40
+ {
41
+ label: 'Settings',
42
+ icon: 'i-lucide-cog',
43
+ type: 'link',
44
+ to: '/admin/settings',
45
+ kbds: ['meta', ','],
46
+ onSelect() {
47
+ if (isLoggedIn.value) {
48
+ navigateTo('/admin/settings')
49
+ }
50
+ },
51
+ },
52
+ ],
53
+
54
+ [
55
+ {
56
+ label: 'GitHub',
57
+ icon: 'i-simple-icons-github',
58
+ type: 'link',
59
+ to: 'https://github.com/ojvribeiro/pluto',
60
+ target: '_blank',
61
+ external: true,
62
+ },
63
+ ],
64
+
65
+ [
66
+ {
67
+ label: 'Logout',
68
+ icon: 'i-lucide-log-out',
69
+ kbds: ['meta', 'shift', 'q'],
70
+ class: 'cursor-pointer',
71
+ onSelect() {
72
+ logout({ showToast: true })
73
+ },
74
+ },
75
+ ],
76
+ ])
77
+
78
+ defineShortcuts(extractShortcuts(items.value))
79
+ </script>
80
+
81
+ <template>
82
+ <PlutoNavbarAdmin>
83
+ <div class="flex h-full items-stretch justify-between px-4">
84
+ <div class="flex items-center gap-x-3">
85
+ <div class="h-full shrink-0 py-1">
86
+ <NuxtLink
87
+ title="Go to dashboard home"
88
+ to="/admin/home"
89
+ class="group block h-full"
90
+ >
91
+ <img
92
+ src="/img/pluto.png"
93
+ class="block h-full grayscale-100 group-hover:grayscale-0"
94
+ />
95
+ </NuxtLink>
96
+ </div>
97
+
98
+ <div class="h-full">
99
+ <ul class="flex h-full items-center text-sm">
100
+ <NavbarAdminActionButton
101
+ :show="route.path.startsWith('/admin')"
102
+ :title="`Visit ${domainFromUrl(maybeDevUrl)}`"
103
+ :icon="
104
+ status === 'pending'
105
+ ? 'lucide:loader-circle'
106
+ : 'lucide:external-link'
107
+ "
108
+ :disabled="status === 'pending'"
109
+ :to="maybeDevUrl"
110
+ target="_blank"
111
+ label="View site"
112
+ />
113
+
114
+ <ClientOnly>
115
+ <template #fallback>
116
+ <li class="h-full">
117
+ <div
118
+ class="light:text-zinc-800/50 flex h-full items-center gap-x-2 rounded-sm px-2 text-white/50"
119
+ >
120
+ <Icon name="svg-spinners:ring-resize" />
121
+ </div>
122
+ </li>
123
+ </template>
124
+
125
+ <PlutoNavbarAdminActions />
126
+ </ClientOnly>
127
+ </ul>
128
+ </div>
129
+ </div>
130
+
131
+ <div class="flex items-center gap-3">
132
+ <ColorModeButton />
133
+
134
+ <div class="h-full">
135
+ <UDropdownMenu
136
+ :items="items"
137
+ :ui="{
138
+ content: 'w-48',
139
+ }"
140
+ :modal="false"
141
+ >
142
+ <button class="group h-full py-1">
143
+ <div
144
+ class="flex h-full items-center gap-x-2 rounded-sm rounded-l-xl pr-2 pl-0.5 group-hover:bg-white/20"
145
+ >
146
+ <UAvatar
147
+ :ui="{
148
+ root: 'rounded-xl',
149
+ icon: 'text-white text-lg',
150
+ }"
151
+ :alt="user?.first_name || ''"
152
+ size="sm"
153
+ icon="lucide:user"
154
+ class="bg-green-600"
155
+ />
156
+
157
+ <span
158
+ class="light:text-zinc-800 text-sm font-bold dark:text-white"
159
+ >
160
+ {{ user?.first_name }}
161
+ </span>
162
+ </div>
163
+ </button>
164
+ </UDropdownMenu>
165
+ </div>
166
+ </div>
167
+ </div>
168
+ </PlutoNavbarAdmin>
169
+ </template>
@@ -0,0 +1,81 @@
1
+ import type { RouteLocationNormalizedGeneric } from 'vue-router'
2
+
3
+ interface PlutoSupabaseAuthOptions {
4
+ route?: RouteLocationNormalizedGeneric
5
+ }
6
+
7
+ export async function useAuth(authOptions?: PlutoSupabaseAuthOptions) {
8
+ const supabase = useSupabaseClient()
9
+ const supabaseSession = useSupabaseSession()
10
+
11
+ const supabaseUser = await supabase.auth.getUser()
12
+
13
+ const user = computed(() => supabaseUser.data.user?.user_metadata ?? null)
14
+ const isLoggedIn = computed<boolean>(() => !!supabaseSession.value)
15
+ const isSubmitting = ref<boolean>(false)
16
+
17
+ interface LoginForm {
18
+ email: string
19
+ password: string
20
+ }
21
+
22
+ async function login(form: LoginForm) {
23
+ isSubmitting.value = true
24
+
25
+ try {
26
+ const { error } = await supabase.auth.signInWithPassword({
27
+ email: form.email,
28
+ password: form.password,
29
+ })
30
+
31
+ if (error) {
32
+ console.error(`Error signing in user: ${error?.message}`)
33
+
34
+ isSubmitting.value = false
35
+
36
+ return
37
+ }
38
+
39
+ return navigateTo(
40
+ (authOptions?.route?.query.redirect as string) ?? '/admin/home'
41
+ )
42
+ } catch (error) {
43
+ if (import.meta.dev) {
44
+ console.error(error)
45
+ }
46
+
47
+ console.error(`Error during login: ${error}`)
48
+
49
+ isSubmitting.value = false
50
+ }
51
+ }
52
+
53
+ interface LogoutOptions {
54
+ redirectTo?: string
55
+ showToast?: boolean
56
+ }
57
+
58
+ async function logout(options?: LogoutOptions) {
59
+ await supabase.auth.signOut()
60
+
61
+ if (options?.showToast) {
62
+ console.warn(`User logged out`)
63
+ }
64
+
65
+ if (options?.redirectTo) {
66
+ return navigateTo(`/admin/login?redirect=${options.redirectTo}`)
67
+ }
68
+
69
+ if (authOptions?.route && authOptions.route.path.startsWith('/admin')) {
70
+ return navigateTo('/admin/login')
71
+ }
72
+ }
73
+
74
+ return {
75
+ isLoggedIn,
76
+ isSubmitting,
77
+ user,
78
+ login,
79
+ logout,
80
+ }
81
+ }
@@ -0,0 +1,13 @@
1
+ <script setup lang="ts"></script>
2
+
3
+ <template>
4
+ <div
5
+ class="dark:bg-admin-content light:bg-white font-outfit flex h-full grow flex-col"
6
+ >
7
+ <div class="flex h-full justify-center">
8
+ <div class="grow overflow-hidden">
9
+ <slot />
10
+ </div>
11
+ </div>
12
+ </div>
13
+ </template>
@@ -0,0 +1,27 @@
1
+ export default defineNuxtRouteMiddleware(async (to, from) => {
2
+ const { isLoggedIn, logout } = await useAuth({ route: to })
3
+
4
+ if (
5
+ isLoggedIn.value &&
6
+ (to.path === '/admin/login' || to.path === '/admin/signup')
7
+ ) {
8
+ return navigateTo(
9
+ to.query.redirect
10
+ ? decodeURIComponent(to.query.redirect as string)
11
+ : '/admin/home'
12
+ )
13
+ }
14
+
15
+ if (
16
+ !isLoggedIn.value &&
17
+ to.path.startsWith('/admin') &&
18
+ to.path !== '/admin/signup' &&
19
+ to.path !== '/admin/login'
20
+ ) {
21
+ if (from.query.redirect) {
22
+ logout({ redirectTo: from.query.redirect as string, showToast: true })
23
+ } else {
24
+ logout()
25
+ }
26
+ }
27
+ })
@@ -0,0 +1,25 @@
1
+ export default defineNuxtRouteMiddleware(async (to, from) => {
2
+ if (to.path !== '/admin/setup' && to.path.startsWith('/admin')) {
3
+ try {
4
+ const data = await $fetch('/api/settings/first_setup')
5
+
6
+ if (
7
+ data.is_first_setup === true &&
8
+ from.path.startsWith('/admin') &&
9
+ to.path !== '/admin/setup'
10
+ ) {
11
+ return navigateTo('/admin/setup')
12
+ }
13
+
14
+ if (
15
+ data.is_first_setup === false &&
16
+ from.path.startsWith('/admin') &&
17
+ to.path === '/admin/setup'
18
+ ) {
19
+ return navigateTo('/')
20
+ }
21
+ } catch(error) {
22
+ return navigateTo('/admin/setup')
23
+ }
24
+ }
25
+ })
@@ -0,0 +1,84 @@
1
+ <script setup lang="ts">
2
+ interface Form {
3
+ email: string
4
+ password: string
5
+ }
6
+
7
+ definePageMeta({
8
+ layout: 'auth',
9
+ })
10
+
11
+ useHead({
12
+ title: 'Login',
13
+ })
14
+
15
+ const { isSubmitting, login } = await useAuth()
16
+
17
+ const form = ref<Form>({
18
+ email: '',
19
+ password: '',
20
+ })
21
+
22
+ function submitForm() {
23
+ login(form.value)
24
+ }
25
+ </script>
26
+
27
+ <template>
28
+ <div class="grid h-full place-items-center">
29
+ <UCard class="w-100">
30
+ <form @submit.prevent="submitForm">
31
+ <div class="flex flex-col gap-y-6">
32
+ <h1 class="text-3xl font-bold">Login</h1>
33
+
34
+ <UFormField label="Email">
35
+ <UInput
36
+ v-model="form.email"
37
+ placeholder="victor@example.com"
38
+ class="w-full"
39
+ autocomplete="email"
40
+ />
41
+ </UFormField>
42
+
43
+ <UFormField label="Password">
44
+ <UInput
45
+ v-model="form.password"
46
+ placeholder="••••"
47
+ class="w-full"
48
+ type="password"
49
+ />
50
+ </UFormField>
51
+
52
+ <div class="flex items-center justify-between gap-x-4">
53
+ <UFormField>
54
+ <UButton
55
+ :loading="isSubmitting"
56
+ type="submit"
57
+ icon="lucide:log-in"
58
+ >
59
+ Login
60
+ </UButton>
61
+ </UFormField>
62
+
63
+ <UFormField>
64
+ <UButton variant="link" to="/admin/signup" class="px-0">
65
+ Create Account
66
+ </UButton>
67
+ </UFormField>
68
+ </div>
69
+
70
+ <div>
71
+ <UButton
72
+ variant="link"
73
+ icon="lucide:arrow-left"
74
+ to="/"
75
+ class="px-0"
76
+ >
77
+ Back to Home
78
+ </UButton>
79
+ </div>
80
+ </div>
81
+ </form>
82
+ </UCard>
83
+ </div>
84
+ </template>
@@ -0,0 +1,128 @@
1
+ <script setup lang="ts">
2
+ definePageMeta({
3
+ layout: 'auth',
4
+ })
5
+
6
+ useHead({
7
+ title: 'Sign Up',
8
+ })
9
+
10
+ const form = ref({
11
+ email: '',
12
+ first_name: '',
13
+ last_name: '',
14
+ password: '',
15
+ confirm_password: '',
16
+ })
17
+
18
+ const isSubmitting = ref<boolean>(false)
19
+
20
+ const passwordMatch = computed(() => {
21
+ return form.value.password === form.value.confirm_password
22
+ })
23
+
24
+ async function submitForm() {
25
+ if (!passwordMatch.value) {
26
+ return
27
+ }
28
+
29
+ isSubmitting.value = true
30
+ try {
31
+ await $fetch('/api/signup', {
32
+ method: 'POST',
33
+ body: {
34
+ email: form.value.email,
35
+ first_name: form.value.first_name,
36
+ last_name: form.value.last_name,
37
+ password: form.value.password,
38
+ },
39
+ })
40
+
41
+ navigateTo('/admin/login?rel=new')
42
+ } catch (error) {
43
+ console.error('Error signing up:', error)
44
+ } finally {
45
+ isSubmitting.value = false
46
+ }
47
+ }
48
+ </script>
49
+
50
+ <template>
51
+ <div class="grid h-full place-items-center">
52
+ <UCard class="w-100">
53
+ <form @submit.prevent="submitForm">
54
+ <div class="flex flex-col gap-y-6">
55
+ <h1 class="text-3xl font-bold">Create Account</h1>
56
+
57
+ <UFormField label="Email">
58
+ <UInput
59
+ v-model="form.email"
60
+ type="email"
61
+ placeholder="victor@example.com"
62
+ class="w-full"
63
+ />
64
+ </UFormField>
65
+
66
+ <div class="flex gap-x-4">
67
+ <UFormField label="First Name">
68
+ <UInput
69
+ v-model="form.first_name"
70
+ placeholder="e.g: John"
71
+ class="w-full"
72
+ />
73
+ </UFormField>
74
+
75
+ <UFormField label="Last Name">
76
+ <UInput
77
+ v-model="form.last_name"
78
+ placeholder="e.g: Doe"
79
+ class="w-full"
80
+ />
81
+ </UFormField>
82
+ </div>
83
+
84
+ <UFormField :error="!passwordMatch" label="Password">
85
+ <UInput
86
+ v-model="form.password"
87
+ placeholder="••••"
88
+ class="w-full"
89
+ type="password"
90
+ />
91
+ </UFormField>
92
+
93
+ <UFormField :error="!passwordMatch" label="Confirm Password">
94
+ <UInput
95
+ v-model="form.confirm_password"
96
+ placeholder="••••"
97
+ class="w-full"
98
+ type="password"
99
+ />
100
+ </UFormField>
101
+
102
+ <div class="flex items-center justify-between gap-x-4">
103
+ <UFormField>
104
+ <UButton
105
+ to="/admin/login"
106
+ icon="lucide:arrow-left"
107
+ variant="link"
108
+ class="px-0"
109
+ >
110
+ Back to Login
111
+ </UButton>
112
+ </UFormField>
113
+
114
+ <UFormField>
115
+ <UButton
116
+ :disabled="isSubmitting || !passwordMatch"
117
+ :loading="isSubmitting"
118
+ type="submit"
119
+ >
120
+ Create account
121
+ </UButton>
122
+ </UFormField>
123
+ </div>
124
+ </div>
125
+ </form>
126
+ </UCard>
127
+ </div>
128
+ </template>
@@ -0,0 +1,116 @@
1
+ <script setup lang="ts">
2
+ import { object, string } from 'yup'
3
+
4
+ type Form = Partial<
5
+ Record<
6
+ Database['public']['Tables']['settings']['Insert']['setting_name'],
7
+ string
8
+ >
9
+ >
10
+
11
+ useHead({
12
+ title: 'Settings',
13
+ })
14
+
15
+ const toast = useToast()
16
+
17
+ const has_settings_modified = useState('has_settings_modified', () => {
18
+ return Date.now()
19
+ })
20
+
21
+ const { data } = await useFetch('/api/settings')
22
+
23
+ const schema = object({
24
+ website_title: string().required().label('Website Title'),
25
+ website_description: string().required().label('Website Description'),
26
+ website_url: string().url().required().label('Website URL'),
27
+ })
28
+
29
+ const form = ref<Form>({
30
+ website_title: data.value?.settings.website_title || '',
31
+ website_description: data.value?.settings.website_description || '',
32
+ website_url: data.value?.settings.website_url || '',
33
+ })
34
+
35
+ const isSubmitting = ref<boolean>(false)
36
+
37
+ async function submitForm() {
38
+ const payload = form.value
39
+
40
+ isSubmitting.value = true
41
+
42
+ try {
43
+ await $fetch('/api/settings/update', {
44
+ method: 'POST',
45
+ body: payload,
46
+ })
47
+
48
+ toast.add({
49
+ title: 'Settings updated successfully',
50
+ color: 'success',
51
+ })
52
+
53
+ has_settings_modified.value = Date.now()
54
+ } catch (error) {
55
+ if (import.meta.dev) {
56
+ console.error('Error updating settings:', error)
57
+ }
58
+
59
+ toast.add({
60
+ title: 'Failed to update settings.',
61
+ color: 'error',
62
+ })
63
+ } finally {
64
+ isSubmitting.value = false
65
+ }
66
+ }
67
+ </script>
68
+
69
+ <template>
70
+ <AdminView>
71
+ <h1 class="text-4xl font-bold">Settings</h1>
72
+
73
+ <UCard>
74
+ <UForm :schema="schema" :state="form" @submit="submitForm">
75
+ <div class="flex flex-col gap-y-6">
76
+ <UFormField label="Website Title" name="website_title" class="w-1/2">
77
+ <UInput
78
+ v-model="form.website_title"
79
+ placeholder="e.g. My Website"
80
+ />
81
+ </UFormField>
82
+
83
+ <UFormField
84
+ label="Website Description"
85
+ name="website_description"
86
+ class="w-1/2"
87
+ >
88
+ <UInput
89
+ v-model="form.website_description"
90
+ placeholder="e.g. My Website Description"
91
+ />
92
+ </UFormField>
93
+
94
+ <UFormField label="Website URL" name="website_url" class="w-1/2">
95
+ <UInput
96
+ v-model="form.website_url"
97
+ type="url"
98
+ placeholder="e.g. https://example.com"
99
+ />
100
+ </UFormField>
101
+
102
+ <div>
103
+ <UButton
104
+ :loading="isSubmitting"
105
+ :disabled="isSubmitting"
106
+ type="submit"
107
+ icon="lucide:save"
108
+ >
109
+ Save
110
+ </UButton>
111
+ </div>
112
+ </div>
113
+ </UForm>
114
+ </UCard>
115
+ </AdminView>
116
+ </template>