first commit
This commit is contained in:
158
app/pages/settings/index.vue
Normal file
158
app/pages/settings/index.vue
Normal file
@@ -0,0 +1,158 @@
|
||||
<script setup lang="ts">
|
||||
import * as z from 'zod'
|
||||
import type { FormSubmitEvent } from '@nuxt/ui'
|
||||
|
||||
const fileRef = ref<HTMLInputElement>()
|
||||
|
||||
const profileSchema = z.object({
|
||||
name: z.string().min(2, 'Too short'),
|
||||
email: z.string().email('Invalid email'),
|
||||
username: z.string().min(2, 'Too short'),
|
||||
avatar: z.string().optional(),
|
||||
bio: z.string().optional()
|
||||
})
|
||||
|
||||
type ProfileSchema = z.output<typeof profileSchema>
|
||||
|
||||
const profile = reactive<Partial<ProfileSchema>>({
|
||||
name: 'Benjamin Canac',
|
||||
email: 'ben@nuxtlabs.com',
|
||||
username: 'benjamincanac',
|
||||
avatar: undefined,
|
||||
bio: undefined
|
||||
})
|
||||
const toast = useToast()
|
||||
async function onSubmit(event: FormSubmitEvent<ProfileSchema>) {
|
||||
toast.add({
|
||||
title: 'Success',
|
||||
description: 'Your settings have been updated.',
|
||||
icon: 'i-lucide-check',
|
||||
color: 'success'
|
||||
})
|
||||
console.log(event.data)
|
||||
}
|
||||
|
||||
function onFileChange(e: Event) {
|
||||
const input = e.target as HTMLInputElement
|
||||
|
||||
if (!input.files?.length) {
|
||||
return
|
||||
}
|
||||
|
||||
profile.avatar = URL.createObjectURL(input.files[0]!)
|
||||
}
|
||||
|
||||
function onFileClick() {
|
||||
fileRef.value?.click()
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<UForm
|
||||
id="settings"
|
||||
:schema="profileSchema"
|
||||
:state="profile"
|
||||
@submit="onSubmit"
|
||||
>
|
||||
<UPageCard
|
||||
title="Profile"
|
||||
description="These informations will be displayed publicly."
|
||||
variant="naked"
|
||||
orientation="horizontal"
|
||||
class="mb-4"
|
||||
>
|
||||
<UButton
|
||||
form="settings"
|
||||
label="Save changes"
|
||||
color="neutral"
|
||||
type="submit"
|
||||
class="w-fit lg:ms-auto"
|
||||
/>
|
||||
</UPageCard>
|
||||
|
||||
<UPageCard variant="subtle">
|
||||
<UFormField
|
||||
name="name"
|
||||
label="Name"
|
||||
description="Will appear on receipts, invoices, and other communication."
|
||||
required
|
||||
class="flex max-sm:flex-col justify-between items-start gap-4"
|
||||
>
|
||||
<UInput
|
||||
v-model="profile.name"
|
||||
autocomplete="off"
|
||||
/>
|
||||
</UFormField>
|
||||
<USeparator />
|
||||
<UFormField
|
||||
name="email"
|
||||
label="Email"
|
||||
description="Used to sign in, for email receipts and product updates."
|
||||
required
|
||||
class="flex max-sm:flex-col justify-between items-start gap-4"
|
||||
>
|
||||
<UInput
|
||||
v-model="profile.email"
|
||||
type="email"
|
||||
autocomplete="off"
|
||||
/>
|
||||
</UFormField>
|
||||
<USeparator />
|
||||
<UFormField
|
||||
name="username"
|
||||
label="Username"
|
||||
description="Your unique username for logging in and your profile URL."
|
||||
required
|
||||
class="flex max-sm:flex-col justify-between items-start gap-4"
|
||||
>
|
||||
<UInput
|
||||
v-model="profile.username"
|
||||
type="username"
|
||||
autocomplete="off"
|
||||
/>
|
||||
</UFormField>
|
||||
<USeparator />
|
||||
<UFormField
|
||||
name="avatar"
|
||||
label="Avatar"
|
||||
description="JPG, GIF or PNG. 1MB Max."
|
||||
class="flex max-sm:flex-col justify-between sm:items-center gap-4"
|
||||
>
|
||||
<div class="flex flex-wrap items-center gap-3">
|
||||
<UAvatar
|
||||
:src="profile.avatar"
|
||||
:alt="profile.name"
|
||||
size="lg"
|
||||
/>
|
||||
<UButton
|
||||
label="Choose"
|
||||
color="neutral"
|
||||
@click="onFileClick"
|
||||
/>
|
||||
<input
|
||||
ref="fileRef"
|
||||
type="file"
|
||||
class="hidden"
|
||||
accept=".jpg, .jpeg, .png, .gif"
|
||||
@change="onFileChange"
|
||||
>
|
||||
</div>
|
||||
</UFormField>
|
||||
<USeparator />
|
||||
<UFormField
|
||||
name="bio"
|
||||
label="Bio"
|
||||
description="Brief description for your profile. URLs are hyperlinked."
|
||||
class="flex max-sm:flex-col justify-between items-start gap-4"
|
||||
:ui="{ container: 'w-full' }"
|
||||
>
|
||||
<UTextarea
|
||||
v-model="profile.bio"
|
||||
:rows="5"
|
||||
autoresize
|
||||
class="w-full"
|
||||
/>
|
||||
</UFormField>
|
||||
</UPageCard>
|
||||
</UForm>
|
||||
</template>
|
||||
45
app/pages/settings/members.vue
Normal file
45
app/pages/settings/members.vue
Normal file
@@ -0,0 +1,45 @@
|
||||
<script setup lang="ts">
|
||||
import type { Member } from '~/types'
|
||||
|
||||
const { data: members } = await useFetch<Member[]>('/api/members', { default: () => [] })
|
||||
|
||||
const q = ref('')
|
||||
|
||||
const filteredMembers = computed(() => {
|
||||
return members.value.filter((member) => {
|
||||
return member.name.search(new RegExp(q.value, 'i')) !== -1 || member.username.search(new RegExp(q.value, 'i')) !== -1
|
||||
})
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<UPageCard
|
||||
title="Members"
|
||||
description="Invite new members by email address."
|
||||
variant="naked"
|
||||
orientation="horizontal"
|
||||
class="mb-4"
|
||||
>
|
||||
<UButton
|
||||
label="Invite people"
|
||||
color="neutral"
|
||||
class="w-fit lg:ms-auto"
|
||||
/>
|
||||
</UPageCard>
|
||||
|
||||
<UPageCard variant="subtle" :ui="{ container: 'p-0 sm:p-0 gap-y-0', wrapper: 'items-stretch', header: 'p-4 mb-0 border-b border-default' }">
|
||||
<template #header>
|
||||
<UInput
|
||||
v-model="q"
|
||||
icon="i-lucide-search"
|
||||
placeholder="Search members"
|
||||
autofocus
|
||||
class="w-full"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<SettingsMembersList :members="filteredMembers" />
|
||||
</UPageCard>
|
||||
</div>
|
||||
</template>
|
||||
71
app/pages/settings/notifications.vue
Normal file
71
app/pages/settings/notifications.vue
Normal file
@@ -0,0 +1,71 @@
|
||||
<script setup lang="ts">
|
||||
const state = reactive<{ [key: string]: boolean }>({
|
||||
email: true,
|
||||
desktop: false,
|
||||
product_updates: true,
|
||||
weekly_digest: false,
|
||||
important_updates: true
|
||||
})
|
||||
|
||||
const sections = [{
|
||||
title: 'Notification channels',
|
||||
description: 'Where can we notify you?',
|
||||
fields: [{
|
||||
name: 'email',
|
||||
label: 'Email',
|
||||
description: 'Receive a daily email digest.'
|
||||
}, {
|
||||
name: 'desktop',
|
||||
label: 'Desktop',
|
||||
description: 'Receive desktop notifications.'
|
||||
}]
|
||||
}, {
|
||||
title: 'Account updates',
|
||||
description: 'Receive updates about Nuxt UI.',
|
||||
fields: [{
|
||||
name: 'weekly_digest',
|
||||
label: 'Weekly digest',
|
||||
description: 'Receive a weekly digest of news.'
|
||||
}, {
|
||||
name: 'product_updates',
|
||||
label: 'Product updates',
|
||||
description: 'Receive a monthly email with all new features and updates.'
|
||||
}, {
|
||||
name: 'important_updates',
|
||||
label: 'Important updates',
|
||||
description: 'Receive emails about important updates like security fixes, maintenance, etc.'
|
||||
}]
|
||||
}]
|
||||
|
||||
async function onChange() {
|
||||
// Do something with data
|
||||
console.log(state)
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div v-for="(section, index) in sections" :key="index">
|
||||
<UPageCard
|
||||
:title="section.title"
|
||||
:description="section.description"
|
||||
variant="naked"
|
||||
class="mb-4"
|
||||
/>
|
||||
|
||||
<UPageCard variant="subtle" :ui="{ container: 'divide-y divide-default' }">
|
||||
<UFormField
|
||||
v-for="field in section.fields"
|
||||
:key="field.name"
|
||||
:name="field.name"
|
||||
:label="field.label"
|
||||
:description="field.description"
|
||||
class="flex items-center justify-between not-last:pb-4 gap-2"
|
||||
>
|
||||
<USwitch
|
||||
v-model="state[field.name]"
|
||||
@update:model-value="onChange"
|
||||
/>
|
||||
</UFormField>
|
||||
</UPageCard>
|
||||
</div>
|
||||
</template>
|
||||
69
app/pages/settings/security.vue
Normal file
69
app/pages/settings/security.vue
Normal file
@@ -0,0 +1,69 @@
|
||||
<script setup lang="ts">
|
||||
import * as z from 'zod'
|
||||
import type { FormError } from '@nuxt/ui'
|
||||
|
||||
const passwordSchema = z.object({
|
||||
current: z.string().min(8, 'Must be at least 8 characters'),
|
||||
new: z.string().min(8, 'Must be at least 8 characters')
|
||||
})
|
||||
|
||||
type PasswordSchema = z.output<typeof passwordSchema>
|
||||
|
||||
const password = reactive<Partial<PasswordSchema>>({
|
||||
current: undefined,
|
||||
new: undefined
|
||||
})
|
||||
|
||||
const validate = (state: Partial<PasswordSchema>): FormError[] => {
|
||||
const errors: FormError[] = []
|
||||
if (state.current && state.new && state.current === state.new) {
|
||||
errors.push({ name: 'new', message: 'Passwords must be different' })
|
||||
}
|
||||
return errors
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<UPageCard
|
||||
title="Password"
|
||||
description="Confirm your current password before setting a new one."
|
||||
variant="subtle"
|
||||
>
|
||||
<UForm
|
||||
:schema="passwordSchema"
|
||||
:state="password"
|
||||
:validate="validate"
|
||||
class="flex flex-col gap-4 max-w-xs"
|
||||
>
|
||||
<UFormField name="current">
|
||||
<UInput
|
||||
v-model="password.current"
|
||||
type="password"
|
||||
placeholder="Current password"
|
||||
class="w-full"
|
||||
/>
|
||||
</UFormField>
|
||||
|
||||
<UFormField name="new">
|
||||
<UInput
|
||||
v-model="password.new"
|
||||
type="password"
|
||||
placeholder="New password"
|
||||
class="w-full"
|
||||
/>
|
||||
</UFormField>
|
||||
|
||||
<UButton label="Update" class="w-fit" type="submit" />
|
||||
</UForm>
|
||||
</UPageCard>
|
||||
|
||||
<UPageCard
|
||||
title="Account"
|
||||
description="No longer want to use our service? You can delete your account here. This action is not reversible. All information related to this account will be deleted permanently."
|
||||
class="bg-gradient-to-tl from-error/10 from-5% to-default"
|
||||
>
|
||||
<template #footer>
|
||||
<UButton label="Delete account" color="error" />
|
||||
</template>
|
||||
</UPageCard>
|
||||
</template>
|
||||
Reference in New Issue
Block a user