@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.
- package/.agents/skills/nuxt/GENERATION.md +5 -0
- package/.agents/skills/nuxt/SKILL.md +55 -0
- package/.agents/skills/nuxt/references/advanced-hooks.md +289 -0
- package/.agents/skills/nuxt/references/advanced-layers.md +299 -0
- package/.agents/skills/nuxt/references/advanced-module-authoring.md +554 -0
- package/.agents/skills/nuxt/references/best-practices-data-fetching.md +357 -0
- package/.agents/skills/nuxt/references/best-practices-ssr.md +355 -0
- package/.agents/skills/nuxt/references/core-cli.md +263 -0
- package/.agents/skills/nuxt/references/core-config.md +162 -0
- package/.agents/skills/nuxt/references/core-data-fetching.md +236 -0
- package/.agents/skills/nuxt/references/core-deployment.md +224 -0
- package/.agents/skills/nuxt/references/core-directory-structure.md +269 -0
- package/.agents/skills/nuxt/references/core-modules.md +292 -0
- package/.agents/skills/nuxt/references/core-routing.md +226 -0
- package/.agents/skills/nuxt/references/features-components-autoimport.md +328 -0
- package/.agents/skills/nuxt/references/features-components.md +264 -0
- package/.agents/skills/nuxt/references/features-composables.md +276 -0
- package/.agents/skills/nuxt/references/features-server.md +265 -0
- package/.agents/skills/nuxt/references/features-state.md +194 -0
- package/.agents/skills/nuxt/references/rendering-modes.md +237 -0
- package/.agents/skills/nuxt-ui/SKILL.md +334 -0
- package/.agents/skills/nuxt-ui/references/components.md +377 -0
- package/.agents/skills/nuxt-ui/references/composables.md +127 -0
- package/.agents/skills/nuxt-ui/references/layouts/chat.md +195 -0
- package/.agents/skills/nuxt-ui/references/layouts/dashboard.md +220 -0
- package/.agents/skills/nuxt-ui/references/layouts/docs.md +141 -0
- package/.agents/skills/nuxt-ui/references/layouts/editor.md +167 -0
- package/.agents/skills/nuxt-ui/references/layouts/page.md +260 -0
- package/.agents/skills/nuxt-ui/references/theming.md +427 -0
- package/.agents/skills/supabase-postgres-best-practices/AGENTS.md +68 -0
- package/.agents/skills/supabase-postgres-best-practices/CLAUDE.md +1 -0
- package/.agents/skills/supabase-postgres-best-practices/README.md +116 -0
- package/.agents/skills/supabase-postgres-best-practices/SKILL.md +64 -0
- package/.agents/skills/supabase-postgres-best-practices/references/advanced-full-text-search.md +55 -0
- package/.agents/skills/supabase-postgres-best-practices/references/advanced-jsonb-indexing.md +49 -0
- package/.agents/skills/supabase-postgres-best-practices/references/conn-idle-timeout.md +46 -0
- package/.agents/skills/supabase-postgres-best-practices/references/conn-limits.md +44 -0
- package/.agents/skills/supabase-postgres-best-practices/references/conn-pooling.md +41 -0
- package/.agents/skills/supabase-postgres-best-practices/references/conn-prepared-statements.md +46 -0
- package/.agents/skills/supabase-postgres-best-practices/references/data-batch-inserts.md +54 -0
- package/.agents/skills/supabase-postgres-best-practices/references/data-n-plus-one.md +53 -0
- package/.agents/skills/supabase-postgres-best-practices/references/data-pagination.md +50 -0
- package/.agents/skills/supabase-postgres-best-practices/references/data-upsert.md +50 -0
- package/.agents/skills/supabase-postgres-best-practices/references/lock-advisory.md +56 -0
- package/.agents/skills/supabase-postgres-best-practices/references/lock-deadlock-prevention.md +68 -0
- package/.agents/skills/supabase-postgres-best-practices/references/lock-short-transactions.md +50 -0
- package/.agents/skills/supabase-postgres-best-practices/references/lock-skip-locked.md +54 -0
- package/.agents/skills/supabase-postgres-best-practices/references/monitor-explain-analyze.md +45 -0
- package/.agents/skills/supabase-postgres-best-practices/references/monitor-pg-stat-statements.md +55 -0
- package/.agents/skills/supabase-postgres-best-practices/references/monitor-vacuum-analyze.md +55 -0
- package/.agents/skills/supabase-postgres-best-practices/references/query-composite-indexes.md +44 -0
- package/.agents/skills/supabase-postgres-best-practices/references/query-covering-indexes.md +40 -0
- package/.agents/skills/supabase-postgres-best-practices/references/query-index-types.md +48 -0
- package/.agents/skills/supabase-postgres-best-practices/references/query-missing-indexes.md +43 -0
- package/.agents/skills/supabase-postgres-best-practices/references/query-partial-indexes.md +45 -0
- package/.agents/skills/supabase-postgres-best-practices/references/schema-constraints.md +80 -0
- package/.agents/skills/supabase-postgres-best-practices/references/schema-data-types.md +46 -0
- package/.agents/skills/supabase-postgres-best-practices/references/schema-foreign-key-indexes.md +59 -0
- package/.agents/skills/supabase-postgres-best-practices/references/schema-lowercase-identifiers.md +55 -0
- package/.agents/skills/supabase-postgres-best-practices/references/schema-partitioning.md +55 -0
- package/.agents/skills/supabase-postgres-best-practices/references/schema-primary-keys.md +61 -0
- package/.agents/skills/supabase-postgres-best-practices/references/security-privileges.md +54 -0
- package/.agents/skills/supabase-postgres-best-practices/references/security-rls-basics.md +50 -0
- package/.agents/skills/supabase-postgres-best-practices/references/security-rls-performance.md +57 -0
- package/.editorconfig +12 -0
- package/.env.example +2 -0
- package/.eslintignore +1 -0
- package/.prettierrc +9 -0
- package/.vscode/extensions.json +10 -0
- package/.vscode/settings.json +61 -0
- package/README.md +62 -0
- package/app/components/navbar/NavbarAdmin.vue +169 -0
- package/app/composables/auth.ts +81 -0
- package/app/layouts/setup.vue +13 -0
- package/app/middleware/auth.ts +27 -0
- package/app/middleware/setup-check.ts +25 -0
- package/app/pages/admin/(auth)/login.vue +84 -0
- package/app/pages/admin/(auth)/signup.vue +128 -0
- package/app/pages/admin/settings.vue +116 -0
- package/app/pages/admin/setup.vue +300 -0
- package/app/pages/admin.vue +38 -0
- package/bun.lock +2685 -0
- package/eslint.config.mjs +88 -0
- package/nuxt.config.ts +55 -0
- package/package.json +44 -0
- package/public/schema.sql +103 -0
- package/scripts/supabase-typegen.ts +36 -0
- package/server/api/category/list.get.ts +9 -0
- package/server/api/category/new.post.ts +31 -0
- package/server/api/settings/first_setup.get.ts +39 -0
- package/server/api/settings/index.get.ts +27 -0
- package/server/api/settings/update.post.ts +36 -0
- package/server/api/setup/create.post.ts +58 -0
- package/server/api/signup.post.ts +28 -0
- package/server/api/users/[id].get.ts +21 -0
- package/server/api/users/index.get.ts +16 -0
- package/shared/types/supabase.ts +406 -0
- package/skills-lock.json +20 -0
- 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>
|