first commit

This commit is contained in:
root
2025-11-30 19:49:01 +08:00
commit 8dcb2a5898
46 changed files with 14428 additions and 0 deletions

View 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>

View 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>

View 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>

View 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>