feat(frontend): Update nuxt-ui, add login, signup, logout
This commit is contained in:
@@ -23,3 +23,11 @@ Couldn't read your auth config. Error: Could not locate the bindings file. Tried
|
||||
https://github.com/WiseLibs/better-sqlite3/issues/1320
|
||||
|
||||
https://github.com/WiseLibs/better-sqlite3/issues/146
|
||||
|
||||
### This version of Node.js requires NODE_MODULE_VERSION 131.
|
||||
|
||||
```
|
||||
rm -fr node_modules; pnpm store prune
|
||||
```
|
||||
|
||||
https://github.com/elizaOS/eliza/pull/665
|
||||
|
||||
@@ -1,2 +1,16 @@
|
||||
@import 'tailwindcss';
|
||||
@import 'tailwindcss' theme(static);
|
||||
@import '@nuxt/ui-pro';
|
||||
|
||||
@theme static {
|
||||
--color-green-50: #effdf5;
|
||||
--color-green-100: #d9fbe8;
|
||||
--color-green-200: #b3f5d1;
|
||||
--color-green-300: #75edae;
|
||||
--color-green-400: #00dc82;
|
||||
--color-green-500: #00c16a;
|
||||
--color-green-600: #00a155;
|
||||
--color-green-700: #007f45;
|
||||
--color-green-800: #016538;
|
||||
--color-green-900: #0a5331;
|
||||
--color-green-950: #052e16;
|
||||
}
|
||||
|
||||
185
legalconsenthub/components/UserMenu.vue
Normal file
185
legalconsenthub/components/UserMenu.vue
Normal file
@@ -0,0 +1,185 @@
|
||||
<template>
|
||||
<UDropdownMenu
|
||||
:items="items"
|
||||
:content="{ align: 'center', collisionPadding: 12 }"
|
||||
:ui="{ content: collapsed ? 'w-48' : 'w-(--reka-dropdown-menu-trigger-width)' }"
|
||||
>
|
||||
<UButton
|
||||
v-bind="{
|
||||
...user,
|
||||
label: collapsed ? undefined : user?.name,
|
||||
trailingIcon: collapsed ? undefined : 'i-lucide-chevrons-up-down'
|
||||
}"
|
||||
color="neutral"
|
||||
variant="ghost"
|
||||
block
|
||||
:square="collapsed"
|
||||
class="data-[state=open]:bg-(--ui-bg-elevated)"
|
||||
:ui="{
|
||||
trailingIcon: 'text-(--ui-text-dimmed)'
|
||||
}"
|
||||
/>
|
||||
|
||||
<template #chip-leading="{ item }">
|
||||
<span
|
||||
:style="{ '--chip': `var(--color-${(item as any).chip}-400)` }"
|
||||
class="ms-0.5 size-2 rounded-full bg-(--chip)"
|
||||
/>
|
||||
</template>
|
||||
</UDropdownMenu>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { DropdownMenuItem } from '@nuxt/ui'
|
||||
|
||||
defineProps<{
|
||||
collapsed?: boolean
|
||||
}>()
|
||||
|
||||
const colorMode = useColorMode()
|
||||
const appConfig = useAppConfig()
|
||||
|
||||
const colors = [
|
||||
'red',
|
||||
'orange',
|
||||
'amber',
|
||||
'yellow',
|
||||
'lime',
|
||||
'green',
|
||||
'emerald',
|
||||
'teal',
|
||||
'cyan',
|
||||
'sky',
|
||||
'blue',
|
||||
'indigo',
|
||||
'violet',
|
||||
'purple',
|
||||
'fuchsia',
|
||||
'pink',
|
||||
'rose'
|
||||
]
|
||||
const neutrals = ['slate', 'gray', 'zinc', 'neutral', 'stone']
|
||||
|
||||
const { data: session } = await useSession(useFetch)
|
||||
|
||||
const user = ref({
|
||||
name: session?.value?.user?.name,
|
||||
avatar: {
|
||||
src: '/_nuxt/public/favicon.ico',
|
||||
alt: session?.value?.user?.name
|
||||
}
|
||||
})
|
||||
|
||||
const items = computed<DropdownMenuItem[][]>(() => [
|
||||
[
|
||||
{
|
||||
type: 'label',
|
||||
label: user.value.name,
|
||||
avatar: user.value.avatar
|
||||
}
|
||||
],
|
||||
[
|
||||
{
|
||||
label: 'Profile',
|
||||
icon: 'i-lucide-user'
|
||||
},
|
||||
{
|
||||
label: 'Settings',
|
||||
icon: 'i-lucide-settings',
|
||||
to: '/settings'
|
||||
}
|
||||
],
|
||||
[
|
||||
{
|
||||
label: 'Theme',
|
||||
icon: 'i-lucide-palette',
|
||||
children: [
|
||||
{
|
||||
label: 'Primary',
|
||||
slot: 'chip',
|
||||
chip: appConfig.ui.colors.primary,
|
||||
content: {
|
||||
align: 'center',
|
||||
collisionPadding: 16
|
||||
},
|
||||
children: colors.map((color) => ({
|
||||
label: color,
|
||||
chip: color,
|
||||
slot: 'chip',
|
||||
checked: appConfig.ui.colors.primary === color,
|
||||
type: 'checkbox',
|
||||
onSelect: (e) => {
|
||||
e.preventDefault()
|
||||
|
||||
appConfig.ui.colors.primary = color
|
||||
}
|
||||
}))
|
||||
},
|
||||
{
|
||||
label: 'Neutral',
|
||||
slot: 'chip',
|
||||
chip: appConfig.ui.colors.neutral,
|
||||
content: {
|
||||
align: 'end',
|
||||
collisionPadding: 16
|
||||
},
|
||||
children: neutrals.map((color) => ({
|
||||
label: color,
|
||||
chip: color,
|
||||
slot: 'chip',
|
||||
type: 'checkbox',
|
||||
checked: appConfig.ui.colors.neutral === color,
|
||||
onSelect: (e) => {
|
||||
e.preventDefault()
|
||||
|
||||
appConfig.ui.colors.neutral = color
|
||||
}
|
||||
}))
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
label: 'Appearance',
|
||||
icon: 'i-lucide-sun-moon',
|
||||
children: [
|
||||
{
|
||||
label: 'Light',
|
||||
icon: 'i-lucide-sun',
|
||||
type: 'checkbox',
|
||||
checked: colorMode.value === 'light',
|
||||
onSelect(e: Event) {
|
||||
e.preventDefault()
|
||||
|
||||
colorMode.preference = 'light'
|
||||
}
|
||||
},
|
||||
{
|
||||
label: 'Dark',
|
||||
icon: 'i-lucide-moon',
|
||||
type: 'checkbox',
|
||||
checked: colorMode.value === 'dark',
|
||||
onUpdateChecked(checked: boolean) {
|
||||
if (checked) {
|
||||
colorMode.preference = 'dark'
|
||||
}
|
||||
},
|
||||
onSelect(e: Event) {
|
||||
e.preventDefault()
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
[
|
||||
{
|
||||
label: 'Log out',
|
||||
icon: 'i-lucide-log-out',
|
||||
async onSelect(e: Event) {
|
||||
e.preventDefault()
|
||||
signOut()
|
||||
await navigateTo('/login')
|
||||
}
|
||||
}
|
||||
]
|
||||
])
|
||||
</script>
|
||||
16
legalconsenthub/layouts/auth.vue
Normal file
16
legalconsenthub/layouts/auth.vue
Normal file
@@ -0,0 +1,16 @@
|
||||
<template>
|
||||
<div class="h-screen flex items-center justify-center overlay">
|
||||
<UButton
|
||||
icon="i-lucide-chevron-left"
|
||||
to="/"
|
||||
size="xl"
|
||||
color="neutral"
|
||||
variant="subtle"
|
||||
class="absolute left-8 top-8 rounded-full"
|
||||
/>
|
||||
|
||||
<UPageCard variant="subtle" class="max-w-sm w-full">
|
||||
<slot />
|
||||
</UPageCard>
|
||||
</div>
|
||||
</template>
|
||||
@@ -3,6 +3,7 @@
|
||||
<UDashboardSearch />
|
||||
|
||||
<UDashboardSidebar
|
||||
v-model:open="open"
|
||||
collapsible
|
||||
resizable
|
||||
class="bg-(--ui-bg-elevated)/25"
|
||||
@@ -21,15 +22,17 @@
|
||||
|
||||
<UNavigationMenu :collapsed="collapsed" :items="links[1]" orientation="vertical" class="mt-auto" />
|
||||
</template>
|
||||
|
||||
<template #footer="{ collapsed }">
|
||||
<UserMenu :collapsed="collapsed" />
|
||||
</template>
|
||||
</UDashboardSidebar>
|
||||
|
||||
<slot />
|
||||
|
||||
<!-- <HelpSlideover /> -->
|
||||
<!-- <NotificationsSlideover /> -->
|
||||
</UDashboardGroup>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
const links = [[], []]
|
||||
const open = ref(false)
|
||||
</script>
|
||||
|
||||
7
legalconsenthub/middleware/auth.ts
Normal file
7
legalconsenthub/middleware/auth.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
export default defineNuxtRouteMiddleware(async (_to, _from) => {
|
||||
const { data: session } = await useSession(useFetch)
|
||||
|
||||
if (!session.value) {
|
||||
return navigateTo('/login')
|
||||
}
|
||||
})
|
||||
@@ -1,5 +1,6 @@
|
||||
export default defineNuxtConfig({
|
||||
modules: ['@nuxt/ui-pro', '@nuxt/eslint'],
|
||||
sourcemap: true,
|
||||
modules: ['@nuxt/ui-pro', '@nuxt/eslint', '@pinia/nuxt'],
|
||||
css: ['~/assets/css/main.css'],
|
||||
runtimeConfig: {
|
||||
public: {
|
||||
|
||||
@@ -7,29 +7,31 @@
|
||||
"dev": "nuxt dev",
|
||||
"generate": "nuxt generate",
|
||||
"preview": "nuxt preview",
|
||||
"postinstall": "nuxt prepare",
|
||||
"postinstall": "nuxt prepare && pnpm run fix:bettersqlite",
|
||||
"format": "prettier . --write",
|
||||
"type-check": "nuxi typecheck",
|
||||
"lint": "eslint .",
|
||||
"lint:fix": "eslint . --fix",
|
||||
"api:generate": "openapi-generator-cli generate -i ../legalconsenthub-backend/api/legalconsenthub.yml -g typescript-fetch -o .api-client",
|
||||
"fix:bettersqlite": "cd node_modules/better-sqlite3 && npx node-gyp rebuild && cd ../.."
|
||||
"fix:bettersqlite": "cd node_modules/better-sqlite3 && pnpm dlx node-gyp rebuild && cd ../.."
|
||||
},
|
||||
"dependencies": {
|
||||
"@nuxt/ui-pro": "3.0.0-alpha.13",
|
||||
"better-auth": "^1.1.16",
|
||||
"better-sqlite3": "^11.8.1",
|
||||
"nuxt": "^3.15.4",
|
||||
"@nuxt/ui-pro": "3.0.1",
|
||||
"@pinia/nuxt": "0.10.1",
|
||||
"better-auth": "1.1.16",
|
||||
"better-sqlite3": "11.8.1",
|
||||
"nuxt": "3.16.1",
|
||||
"pinia": "3.0.1",
|
||||
"vue": "latest",
|
||||
"vue-router": "latest"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@nuxt/eslint": "^1.1.0",
|
||||
"@openapitools/openapi-generator-cli": "^2.16.3",
|
||||
"@types/better-sqlite3": "^7.6.12",
|
||||
"eslint": "^9.20.1",
|
||||
"@nuxt/eslint": "1.1.0",
|
||||
"@openapitools/openapi-generator-cli": "2.16.3",
|
||||
"@types/better-sqlite3": "7.6.12",
|
||||
"eslint": "9.20.1",
|
||||
"prettier": "3.5.1",
|
||||
"typescript": "^5.7.3",
|
||||
"vue-tsc": "^2.2.2"
|
||||
"typescript": "5.7.3",
|
||||
"vue-tsc": "2.2.2"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -39,6 +39,10 @@ import { ComplianceStatus, type PagedApplicationFormDto } from '~/.api-client'
|
||||
import { useApplicationFormValidator } from '~/composables/useApplicationFormValidator'
|
||||
import type { FormElementId } from '~/types/FormElement'
|
||||
|
||||
definePageMeta({
|
||||
middleware: ['auth']
|
||||
})
|
||||
|
||||
const { getAllApplicationFormTemplates } = useApplicationFormTemplate()
|
||||
const { createApplicationForm } = useApplicationForm()
|
||||
const { validateFormElements, getHighestComplianceStatus } = useApplicationFormValidator()
|
||||
|
||||
@@ -52,6 +52,11 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { ApplicationFormDto, PagedApplicationFormDto } from '~/.api-client'
|
||||
|
||||
definePageMeta({
|
||||
middleware: ['auth']
|
||||
})
|
||||
|
||||
const { getAllApplicationForms, deleteApplicationFormById } = useApplicationForm()
|
||||
const route = useRoute()
|
||||
|
||||
|
||||
@@ -1,45 +1,113 @@
|
||||
<template>
|
||||
<UDashboardPanel id="home">
|
||||
<template #header>
|
||||
<UDashboardNavbar title="Home" :ui="{ right: 'gap-3' }">
|
||||
<template #leading>
|
||||
<UDashboardSidebarCollapse />
|
||||
</template>
|
||||
|
||||
<template #right>
|
||||
<UDropdownMenu :items="items">
|
||||
<UButton icon="i-lucide-plus" size="md" class="rounded-full" />
|
||||
</UDropdownMenu>
|
||||
</template>
|
||||
</UDashboardNavbar>
|
||||
|
||||
<UDashboardToolbar>
|
||||
<template #left> toolbar left </template>
|
||||
</UDashboardToolbar>
|
||||
<UAuthForm
|
||||
:fields="fields"
|
||||
:schema="schema"
|
||||
:providers="providers"
|
||||
title="Welcome back"
|
||||
icon="i-lucide-lock"
|
||||
@submit="onSubmit"
|
||||
>
|
||||
<template #description>
|
||||
Don't have an account? <ULink to="/signup" class="text-primary-500 font-medium">Sign up</ULink>.
|
||||
</template>
|
||||
|
||||
<template #body>
|
||||
<div>
|
||||
<pre>{{ session }}</pre>
|
||||
<h1>{{ session.data ? 'Du bist eingeloggt' : 'Nicht eingeloggt' }}</h1>
|
||||
<button v-if="session?.data" @click="authClient.signOut()">Sign out</button>
|
||||
</div>
|
||||
<Register />
|
||||
<Login />
|
||||
<template #password-hint>
|
||||
<ULink to="/" class="text-primary-500 font-medium">Forgot password?</ULink>
|
||||
</template>
|
||||
</UDashboardPanel>
|
||||
|
||||
<template #footer>
|
||||
By signing in, you agree to our <ULink to="/" class="text-primary-500 font-medium">Terms of Service</ULink>.
|
||||
</template>
|
||||
</UAuthForm>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
const session = authClient.useSession()
|
||||
import * as z from 'zod'
|
||||
import type { FormSubmitEvent } from '@nuxt/ui'
|
||||
|
||||
const items = [
|
||||
[
|
||||
{
|
||||
label: 'Neuer Mitbestimmungsantrag',
|
||||
icon: 'i-lucide-send',
|
||||
to: '/create'
|
||||
}
|
||||
]
|
||||
definePageMeta({ layout: 'auth' })
|
||||
|
||||
useSeoMeta({ title: 'Login' })
|
||||
|
||||
const toast = useToast()
|
||||
|
||||
const fields = [
|
||||
{
|
||||
name: 'email',
|
||||
type: 'text' as const,
|
||||
label: 'Email',
|
||||
placeholder: 'Enter your email',
|
||||
required: true
|
||||
},
|
||||
{
|
||||
name: 'password',
|
||||
label: 'Password',
|
||||
type: 'password' as const,
|
||||
placeholder: 'Enter your password'
|
||||
},
|
||||
{
|
||||
name: 'remember',
|
||||
label: 'Remember me',
|
||||
type: 'checkbox' as const
|
||||
}
|
||||
]
|
||||
|
||||
const providers = [
|
||||
{
|
||||
label: 'Google',
|
||||
icon: 'i-simple-icons-google',
|
||||
onClick: () => {
|
||||
toast.add({ title: 'Google', description: 'Login with Google' })
|
||||
}
|
||||
},
|
||||
{
|
||||
label: 'GitHub',
|
||||
icon: 'i-simple-icons-github',
|
||||
onClick: () => {
|
||||
toast.add({ title: 'GitHub', description: 'Login with GitHub' })
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
const schema = z.object({
|
||||
email: z.string().email('Invalid email'),
|
||||
password: z.string().min(8, 'Must be at least 8 characters')
|
||||
})
|
||||
|
||||
type Schema = z.output<typeof schema>
|
||||
|
||||
function onSubmit(payload: FormSubmitEvent<Schema>) {
|
||||
if (!payload.data.email || !payload.data.password) {
|
||||
alert('Bitte alle Felder ausfüllen')
|
||||
return
|
||||
}
|
||||
authClient.signIn.email(
|
||||
{
|
||||
email: payload.data.email,
|
||||
password: payload.data.password
|
||||
},
|
||||
{
|
||||
onRequest: () => {
|
||||
// TODO: Show loading spinner
|
||||
console.log('Sending login request')
|
||||
},
|
||||
onResponse: () => {
|
||||
// TODO: Hide loading spinner
|
||||
console.log('Receiving login response')
|
||||
},
|
||||
onSuccess: async (ctx) => {
|
||||
console.log('Successfully logged in!', ctx)
|
||||
await navigateTo('/')
|
||||
},
|
||||
onError: (ctx) => {
|
||||
console.log(ctx.error.message)
|
||||
useToast().add({
|
||||
title: 'Fehler bei der Anmeldung',
|
||||
description: ctx.error.message,
|
||||
color: 'error'
|
||||
})
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
</script>
|
||||
|
||||
107
legalconsenthub/pages/signup.vue
Normal file
107
legalconsenthub/pages/signup.vue
Normal file
@@ -0,0 +1,107 @@
|
||||
<template>
|
||||
<UAuthForm
|
||||
:fields="fields"
|
||||
:schema="schema"
|
||||
:providers="providers"
|
||||
title="Create an account"
|
||||
:submit="{ label: 'Create account' }"
|
||||
@submit="onSubmit"
|
||||
>
|
||||
<template #description>
|
||||
Already have an account? <ULink to="/login" class="text-primary-500 font-medium">Login</ULink>.
|
||||
</template>
|
||||
|
||||
<template #footer>
|
||||
By signing up, you agree to our <ULink to="/" class="text-primary-500 font-medium">Terms of Service</ULink>.
|
||||
</template>
|
||||
</UAuthForm>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import * as z from 'zod'
|
||||
import type { FormSubmitEvent } from '@nuxt/ui'
|
||||
|
||||
definePageMeta({ layout: 'auth' })
|
||||
|
||||
useSeoMeta({ title: 'Sign up' })
|
||||
|
||||
const toast = useToast()
|
||||
|
||||
const fields = [
|
||||
{
|
||||
name: 'name',
|
||||
type: 'text' as const,
|
||||
label: 'Name',
|
||||
placeholder: 'Enter your name'
|
||||
},
|
||||
{
|
||||
name: 'email',
|
||||
type: 'text' as const,
|
||||
label: 'Email',
|
||||
placeholder: 'Enter your email'
|
||||
},
|
||||
{
|
||||
name: 'password',
|
||||
label: 'Password',
|
||||
type: 'password' as const,
|
||||
placeholder: 'Enter your password'
|
||||
}
|
||||
]
|
||||
|
||||
const providers = [
|
||||
{
|
||||
label: 'Google',
|
||||
icon: 'i-simple-icons-google',
|
||||
onClick: () => {
|
||||
toast.add({ title: 'Google', description: 'Login with Google' })
|
||||
}
|
||||
},
|
||||
{
|
||||
label: 'GitHub',
|
||||
icon: 'i-simple-icons-github',
|
||||
onClick: () => {
|
||||
toast.add({ title: 'GitHub', description: 'Login with GitHub' })
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
const schema = z.object({
|
||||
name: z.string().min(1, 'Name is required'),
|
||||
email: z.string().email('Invalid email'),
|
||||
password: z.string().min(8, 'Must be at least 8 characters')
|
||||
})
|
||||
|
||||
type Schema = z.output<typeof schema>
|
||||
|
||||
function onSubmit(payload: FormSubmitEvent<Schema>) {
|
||||
signUp.email(
|
||||
{
|
||||
email: payload.data.email,
|
||||
password: payload.data.password,
|
||||
name: payload.data.name
|
||||
},
|
||||
{
|
||||
onRequest: () => {
|
||||
// TODO: Show loading spinner
|
||||
console.log('Sending register request')
|
||||
},
|
||||
onResponse: () => {
|
||||
// TODO: Hide loading spinner
|
||||
console.log('Receiving register response')
|
||||
},
|
||||
onSuccess: async () => {
|
||||
console.log('Successfully registered!')
|
||||
await navigateTo('/')
|
||||
},
|
||||
onError: (ctx) => {
|
||||
console.log(ctx.error.message)
|
||||
useToast().add({
|
||||
title: 'Fehler bei der Registrierung',
|
||||
description: ctx.error.message,
|
||||
color: 'error'
|
||||
})
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
</script>
|
||||
4288
legalconsenthub/pnpm-lock.yaml
generated
4288
legalconsenthub/pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@@ -3,3 +3,5 @@ import { createAuthClient } from 'better-auth/vue'
|
||||
export const authClient = createAuthClient({
|
||||
baseURL: 'http://localhost:3000'
|
||||
})
|
||||
|
||||
export const { signIn, signOut, signUp, useSession, forgetPassword, resetPassword } = authClient
|
||||
|
||||
Reference in New Issue
Block a user