feat: add account and appearance sections in forms example

This commit is contained in:
Ahmed 2023-09-11 15:24:15 +01:00
parent b12bff136e
commit abc5f5f2bc
14 changed files with 555 additions and 174 deletions

View File

@ -0,0 +1,5 @@
<script setup>
import AccountExample from "@/examples/forms/Account.vue"
</script>
<AccountExample />

View File

@ -0,0 +1,5 @@
<script setup>
import AppearanceExample from "@/examples/forms/Appearance.vue"
</script>
<AppearanceExample />

View File

@ -0,0 +1,10 @@
<script setup lang="ts">
import FormsLayout from './layouts/FormsLayout.vue'
import AccountForm from './components/AccountForm.vue'
</script>
<template>
<FormsLayout>
<AccountForm />
</FormsLayout>
</template>

View File

@ -0,0 +1,10 @@
<script setup lang="ts">
import FormsLayout from './layouts/FormsLayout.vue'
import AppearanceForm from './components/AppearanceForm.vue'
</script>
<template>
<FormsLayout>
<AppearanceForm />
</FormsLayout>
</template>

View File

@ -1,174 +1,10 @@
<script setup lang="ts">
import { ref } from 'vue'
import * as z from 'zod'
import SidebarNav from './components/SidebarNav.vue'
import { cn } from '@/lib/utils'
import { Input } from '@/lib/registry/new-york/ui/input'
import { Label } from '@/lib/registry/new-york/ui/label'
import { Separator } from '@/lib/registry/new-york/ui/separator'
import { Textarea } from '@/lib/registry/new-york/ui/textarea'
import {
Select,
SelectContent,
SelectGroup,
SelectItem,
SelectTrigger,
SelectValue,
} from '@/lib/registry/default/ui/select'
import { Button } from '@/lib/registry/default/ui/button'
const verifiedEmails = ref(['m@example.com', 'm@google.com', 'm@support.com'])
const profileForm = ref({
username: '',
email: '',
bio: 'I own a computer.',
urls: [
{ value: 'https://shadcn.com' },
{ value: 'http://twitter.com/shadcn' },
],
})
const profileFormSchema = z.object({
username: z
.string()
.min(2, {
message: 'Username must be at least 2 characters.',
})
.max(30, {
message: 'Username must not be longer than 30 characters.',
}),
email: z
.string({
required_error: 'Please select an email to display.',
})
.email(),
bio: z.string().max(160, { message: 'Bio must not be longer than 160 characters.' }).min(4, { message: 'Bio must be at least 2 characters.' }),
urls: z
.array(
z.object({
value: z.string().url({ message: 'Please enter a valid URL.' }),
}),
)
.optional(),
})
type ProfileFormValues = z.infer<typeof profileFormSchema>
const errors = ref<z.ZodFormattedError<ProfileFormValues> | null>(null)
async function handleSubmit() {
const result = profileFormSchema.safeParse(profileForm.value)
if (!result.success) {
errors.value = result.error.format()
return
}
errors.value = null
console.log('Form submitted!')
}
import FormsLayout from './layouts/FormsLayout.vue'
import ProfileForm from './components/ProfileForm.vue'
</script>
<template>
<div class="md:hidden" />
<div class="hidden space-y-6 p-10 pb-16 md:block">
<div class="space-y-0.5">
<h2 class="text-2xl font-bold tracking-tight">
Settings
</h2>
<p class="text-muted-foreground">
Manage your account settings and set e-mail preferences.
</p>
</div>
<Separator class="my-6" />
<div class="flex flex-col space-y-8 lg:flex-row lg:space-x-12 lg:space-y-0">
<aside class="-mx-4 lg:w-1/5">
<SidebarNav />
</aside>
<div class="flex-1 lg:max-w-2xl">
<div class="space-y-6">
<div>
<h3 class="text-lg font-medium">
Profile
</h3>
<p class="text-sm text-muted-foreground">
This is how others will see you on the site.
</p>
</div>
<Separator />
<form class="space-y-8" @submit.prevent="handleSubmit">
<div class="grid gap-2">
<Label for="username" :class="cn('text-sm', errors?.username && 'text-destructive')">
Username
</Label>
<Input id="username" v-model="profileForm.username" placeholder="shadcn" />
<span class="text-muted-foreground text-sm">
This is your public display name. It can be your real name or a pseudonym. You can only change this once every 30 days.
</span>
<div v-if="errors?.username" class="text-sm text-destructive">
<span v-for="error in errors.username._errors" :key="error">{{ error }}</span>
</div>
</div>
<div class="grid gap-2">
<Label for="email" :class="cn('text-sm', errors?.email && 'text-destructive')">
Email
</Label>
<Select id="email" v-model="profileForm.email">
<SelectTrigger>
<SelectValue placeholder="Select an email" />
</SelectTrigger>
<SelectContent>
<SelectGroup>
<SelectItem v-for="email in verifiedEmails" :key="email" :value="email">
{{ email }}
</SelectItem>
</SelectGroup>
</SelectContent>
</Select>
<span class="text-muted-foreground text-sm">
You can manage verified email addresses in your email settings.
</span>
<div v-if="errors?.email" class="text-sm text-destructive">
<span v-for="error in errors.email._errors" :key="error">{{ error }}</span>
</div>
</div>
<div class="grid gap-2">
<Label for="bio" :class="cn('text-sm', errors?.bio && 'text-destructive')">
Bio
</Label>
<Textarea id="bio" v-model="profileForm.bio" placeholder="Tell us about yourself." />
<span class="text-muted-foreground text-sm">
You can @mention other users and organizations to link to them.
</span>
<div v-if="errors?.bio" class="text-sm text-destructive">
<span v-for="error in errors.bio._errors" :key="error">{{ error }}</span>
</div>
</div>
<div class="grid gap-2">
<Label for="urls" :class="cn('text-sm', errors?.urls && 'text-destructive')">
URLs
</Label>
<Input v-for="(url, index) in profileForm.urls" id="urls" :key="index" v-model="url.value" />
<div v-if="errors?.urls" class="text-sm text-destructive">
<span v-for="error in errors.urls._errors" :key="error">{{ error }}</span>
</div>
<Button
type="button"
variant="outline"
size="sm"
class="text-xs w-20 mt-2"
@click="profileForm.urls?.push({ value: '' })"
>
Add URL
</Button>
</div>
<div class="flex justify-start">
<Button type="submit">
Update profile
</Button>
</div>
</form>
</div>
</div>
</div>
</div>
<FormsLayout>
<ProfileForm />
</FormsLayout>
</template>

View File

@ -0,0 +1,158 @@
<script setup lang="ts">
import { ref } from 'vue'
import * as z from 'zod'
import { format } from 'date-fns'
import { cn } from '@/lib/utils'
import RadixIconsCalendar from '~icons/radix-icons/calendar'
import { Input } from '@/lib/registry/new-york/ui/input'
import { Label } from '@/lib/registry/new-york/ui/label'
import { Separator } from '@/lib/registry/new-york/ui/separator'
import {
Select,
SelectContent,
SelectGroup,
SelectItem,
SelectTrigger,
SelectValue,
} from '@/lib/registry/new-york/ui/select'
import { Button } from '@/lib/registry/new-york/ui/button'
import {
Popover,
PopoverContent,
PopoverTrigger,
} from '@/lib/registry/default/ui/popover'
import { Calendar } from '@/lib/registry/default/ui/calendar'
const accountForm = ref({
name: '',
dob: null,
language: '',
})
const languages = [
{ label: 'English', value: 'en' },
{ label: 'French', value: 'fr' },
{ label: 'German', value: 'de' },
{ label: 'Spanish', value: 'es' },
{ label: 'Portuguese', value: 'pt' },
{ label: 'Russian', value: 'ru' },
{ label: 'Japanese', value: 'ja' },
{ label: 'Korean', value: 'ko' },
{ label: 'Chinese', value: 'zh' },
] as const
const accountFormSchema = z.object({
name: z
.string()
.min(2, {
message: 'Name must be at least 2 characters.',
})
.max(30, {
message: 'Name must not be longer than 30 characters.',
}),
dob: z.date({
required_error: 'A date of birth is required.',
}),
language: z.string().nonempty({
message: 'Please select a language.',
}),
})
type AccountFormValues = z.infer<typeof accountFormSchema>
const errors = ref<z.ZodFormattedError<AccountFormValues> | null>(null)
async function handleSubmit() {
const result = accountFormSchema.safeParse(accountForm.value)
if (!result.success) {
errors.value = result.error.format()
console.log(errors.value)
return
}
console.log('Form submitted!', accountForm.value)
}
</script>
<template>
<div>
<h3 class="text-lg font-medium">
Account
</h3>
<p class="text-sm text-muted-foreground">
Update your account settings. Set your preferred language and timezone.
</p>
</div>
<Separator />
<form class="space-y-8" @submit.prevent="handleSubmit">
<div class="grid gap-2">
<Label for="name" :class="cn('text-sm', errors?.name && 'text-destructive')">
Name
</Label>
<Input id="name" v-model="accountForm.name" placeholder="Your name" />
<span class="text-muted-foreground text-sm">
This is the name that will be displayed on your profile and in emails.
</span>
<div v-if="errors?.name" class="text-sm text-destructive">
<span v-for="error in errors.name._errors" :key="error">{{ error }}</span>
</div>
</div>
<div class="grid gap-2">
<Label for="dob" :class="cn('text-sm', errors?.dob && 'text-destructive')">
Date of Birth
</Label>
<Popover>
<PopoverTrigger as-child>
<Button
variant="outline"
:class="cn(
'w-[280px] pl-3 text-left font-normal',
!accountForm.dob && 'text-muted-foreground',
)"
>
<span>{{ accountForm.dob ? format(accountForm.dob, "PPP") : "Pick a date" }}</span>
<RadixIconsCalendar class="ml-auto h-4 w-4 opacity-50" />
</Button>
</PopoverTrigger>
<PopoverContent class="p-0">
<Calendar v-model="accountForm.dob" />
</PopoverContent>
</Popover>
<span class="text-muted-foreground text-sm">
Your date of birth is used to calculate your age.
</span>
<div v-if="errors?.dob" class="text-sm text-destructive">
<span v-for="error in errors.dob._errors" :key="error">{{ error }}</span>
</div>
</div>
<div class="grid gap-2">
<Label for="language" :class="cn('text-sm', errors?.language && 'text-destructive')">
Language
</Label>
<Select id="language" v-model="accountForm.language">
<SelectTrigger class="w-[200px]">
<SelectValue placeholder="Select a language" />
</SelectTrigger>
<SelectContent>
<SelectGroup>
<SelectItem v-for="language in languages" :key="language.value" :value="language.value">
{{ language.label }}
</SelectItem>
</SelectGroup>
</SelectContent>
</Select>
<span class="text-muted-foreground text-sm">
This is the language that will be used in the dashboard.
</span>
<div v-if="errors?.language" class="text-sm text-destructive">
<span v-for="error in errors.language._errors" :key="error">{{ error }}</span>
</div>
</div>
<div class="flex justify-start">
<Button type="submit">
Update account
</Button>
</div>
</form>
</template>

View File

@ -0,0 +1,169 @@
<script setup lang="ts">
import { ref } from 'vue'
import * as z from 'zod'
import { cn } from '@/lib/utils'
import { Label } from '@/lib/registry/new-york/ui/label'
import { Separator } from '@/lib/registry/new-york/ui/separator'
import { RadioGroup, RadioGroupItem } from '@/lib/registry/default/ui/radio-group'
import {
Select,
SelectContent,
SelectGroup,
SelectItem,
SelectTrigger,
SelectValue,
} from '@/lib/registry/new-york/ui/select'
import { Button } from '@/lib/registry/new-york/ui/button'
const appearenceForm = ref({
theme: '',
font: '',
})
const appearanceFormSchema = z.object({
theme: z.enum(['light', 'dark'], {
required_error: 'Please select a theme.',
}),
font: z.enum(['inter', 'manrope', 'system'], {
invalid_type_error: 'Select a font',
required_error: 'Please select a font.',
}),
})
type AppearanceFormValues = z.infer<typeof appearanceFormSchema>
const errors = ref<z.ZodFormattedError<AppearanceFormValues> | null>(null)
async function handleSubmit() {
const result = appearanceFormSchema.safeParse(appearenceForm.value)
if (!result.success) {
errors.value = result.error.format()
console.log(errors.value)
return
}
console.log('Form submitted!', appearenceForm.value)
}
</script>
<template>
<div>
<h3 class="text-lg font-medium">
Appearence
</h3>
<p class="text-sm text-muted-foreground">
Customize the appearance of the app. Automatically switch between day and night themes.
</p>
</div>
<Separator />
<form class="space-y-8" @submit.prevent="handleSubmit">
<div class="grid gap-2">
<Label for="font" :class="cn('text-sm', errors?.font && 'text-destructive')">
Font
</Label>
<Select id="font" v-model="appearenceForm.font">
<SelectTrigger class="w-[200px]">
<SelectValue placeholder="Select a font" />
</SelectTrigger>
<SelectContent>
<SelectGroup>
<SelectItem value="inter">
Inter
</SelectItem>
<SelectItem value="manrope">
Manrope
</SelectItem>
<SelectItem value="system">
System
</SelectItem>
</SelectGroup>
</SelectContent>
</Select>
<span class="text-muted-foreground text-xs">
Set the font you want to use in the dashboard.
</span>
<div v-if="errors?.font" class="text-sm text-destructive">
<span v-for="error in errors.font._errors" :key="error">{{ error }}</span>
</div>
</div>
<div class="grid gap-2">
<Label for="theme" :class="cn('text-sm', errors?.theme && 'text-destructive')">
Theme
</Label>
<span class="text-muted-foreground text-xs">
Select the theme for the dashboard.
</span>
<RadioGroup
v-model="appearenceForm.theme"
default-value="light"
class="grid max-w-md grid-cols-2 gap-8 pt-2"
>
<div class="grid gap-2">
<Label class="[&:has([data-state=checked])>div]:border-primary">
<div>
<RadioGroupItem value="light" class="sr-only" />
</div>
<div class="items-center rounded-md border-2 border-muted p-1 hover:border-accent">
<div class="space-y-2 rounded-sm bg-[#ecedef] p-2">
<div class="space-y-2 rounded-md bg-white p-2 shadow-sm">
<div class="h-2 w-[80px] rounded-lg bg-[#ecedef]" />
<div class="h-2 w-[100px] rounded-lg bg-[#ecedef]" />
</div>
<div class="flex items-center space-x-2 rounded-md bg-white p-2 shadow-sm">
<div class="h-4 w-4 rounded-full bg-[#ecedef]" />
<div class="h-2 w-[100px] rounded-lg bg-[#ecedef]" />
</div>
<div class="flex items-center space-x-2 rounded-md bg-white p-2 shadow-sm">
<div class="h-4 w-4 rounded-full bg-[#ecedef]" />
<div class="h-2 w-[100px] rounded-lg bg-[#ecedef]" />
</div>
</div>
</div>
<span class="block w-full p-2 text-center font-normal">
Light
</span>
</Label>
</div>
<div>
<Label class="[&:has([data-state=checked])>div]:border-primary">
<div>
<RadioGroupItem value="dark" class="sr-only" />
</div>
<div class="items-center rounded-md border-2 border-muted bg-popover p-1 hover:bg-accent hover:text-accent-foreground">
<div class="space-y-2 rounded-sm bg-slate-950 p-2">
<div class="space-y-2 rounded-md bg-slate-800 p-2 shadow-sm">
<div class="h-2 w-[80px] rounded-lg bg-slate-400" />
<div class="h-2 w-[100px] rounded-lg bg-slate-400" />
</div>
<div class="flex items-center space-x-2 rounded-md bg-slate-800 p-2 shadow-sm">
<div class="h-4 w-4 rounded-full bg-slate-400" />
<div class="h-2 w-[100px] rounded-lg bg-slate-400" />
</div>
<div class="flex items-center space-x-2 rounded-md bg-slate-800 p-2 shadow-sm">
<div class="h-4 w-4 rounded-full bg-slate-400" />
<div class="h-2 w-[100px] rounded-lg bg-slate-400" />
</div>
</div>
</div>
<span class="block w-full p-2 text-center font-normal">
Dark
</span>
</Label>
</div>
<div class="col-span-2">
<span v-if="errors?.theme" class="text-sm text-destructive">
<span v-for="error in errors.theme._errors" :key="error">{{ error }}</span>
</span>
</div>
</RadioGroup>
</div>
<div class="flex justify-start">
<Button type="submit">
Update preferences
</Button>
</div>
</form>
</template>

View File

@ -0,0 +1,152 @@
<script setup lang="ts">
import { ref } from 'vue'
import * as z from 'zod'
import { cn } from '@/lib/utils'
import { Input } from '@/lib/registry/new-york/ui/input'
import { Label } from '@/lib/registry/new-york/ui/label'
import { Separator } from '@/lib/registry/new-york/ui/separator'
import { Textarea } from '@/lib/registry/new-york/ui/textarea'
import {
Select,
SelectContent,
SelectGroup,
SelectItem,
SelectTrigger,
SelectValue,
} from '@/lib/registry/new-york/ui/select'
import { Button } from '@/lib/registry/new-york/ui/button'
const verifiedEmails = ref(['m@example.com', 'm@google.com', 'm@support.com'])
const profileForm = ref({
username: '',
email: '',
bio: 'I own a computer.',
urls: [
{ value: 'https://shadcn.com' },
{ value: 'http://twitter.com/shadcn' },
],
})
const profileFormSchema = z.object({
username: z
.string()
.min(2, {
message: 'Username must be at least 2 characters.',
})
.max(30, {
message: 'Username must not be longer than 30 characters.',
}),
email: z
.string({
required_error: 'Please select an email to display.',
})
.email(),
bio: z.string().max(160, { message: 'Bio must not be longer than 160 characters.' }).min(4, { message: 'Bio must be at least 2 characters.' }),
urls: z
.array(
z.object({
value: z.string().url({ message: 'Please enter a valid URL.' }),
}),
)
.optional(),
})
type ProfileFormValues = z.infer<typeof profileFormSchema>
const errors = ref<z.ZodFormattedError<ProfileFormValues> | null>(null)
async function handleSubmit() {
const result = profileFormSchema.safeParse(profileForm.value)
if (!result.success) {
errors.value = result.error.format()
return
}
errors.value = null
console.log('Form submitted!')
}
</script>
<template>
<div>
<h3 class="text-lg font-medium">
Profile
</h3>
<p class="text-sm text-muted-foreground">
This is how others will see you on the site.
</p>
</div>
<Separator />
<form class="space-y-8" @submit.prevent="handleSubmit">
<div class="grid gap-2">
<Label for="username" :class="cn('text-sm', errors?.username && 'text-destructive')">
Username
</Label>
<Input id="username" v-model="profileForm.username" placeholder="shadcn" />
<span class="text-muted-foreground text-sm">
This is your public display name. It can be your real name or a pseudonym. You can only change this once every 30 days.
</span>
<div v-if="errors?.username" class="text-sm text-destructive">
<span v-for="error in errors.username._errors" :key="error">{{ error }}</span>
</div>
</div>
<div class="grid gap-2">
<Label for="email" :class="cn('text-sm', errors?.email && 'text-destructive')">
Email
</Label>
<Select id="email" v-model="profileForm.email">
<SelectTrigger>
<SelectValue placeholder="Select an email" />
</SelectTrigger>
<SelectContent>
<SelectGroup>
<SelectItem v-for="email in verifiedEmails" :key="email" :value="email">
{{ email }}
</SelectItem>
</SelectGroup>
</SelectContent>
</Select>
<span class="text-muted-foreground text-sm">
You can manage verified email addresses in your email settings.
</span>
<div v-if="errors?.email" class="text-sm text-destructive">
<span v-for="error in errors.email._errors" :key="error">{{ error }}</span>
</div>
</div>
<div class="grid gap-2">
<Label for="bio" :class="cn('text-sm', errors?.bio && 'text-destructive')">
Bio
</Label>
<Textarea id="bio" v-model="profileForm.bio" placeholder="Tell us about yourself." />
<span class="text-muted-foreground text-sm">
You can @mention other users and organizations to link to them.
</span>
<div v-if="errors?.bio" class="text-sm text-destructive">
<span v-for="error in errors.bio._errors" :key="error">{{ error }}</span>
</div>
</div>
<div class="grid gap-2">
<Label for="urls" :class="cn('text-sm', errors?.urls && 'text-destructive')">
URLs
</Label>
<Input v-for="(url, index) in profileForm.urls" id="urls" :key="index" v-model="url.value" />
<div v-if="errors?.urls" class="text-sm text-destructive">
<span v-for="error in errors.urls._errors" :key="error">{{ error }}</span>
</div>
<Button
type="button"
variant="outline"
size="sm"
class="text-xs w-20 mt-2"
@click="profileForm.urls?.push({ value: '' })"
>
Add URL
</Button>
</div>
<div class="flex justify-start">
<Button type="submit">
Update profile
</Button>
</div>
</form>
</template>

View File

@ -0,0 +1,28 @@
<script setup lang="ts">
import SidebarNav from '../components/SidebarNav.vue'
</script>
<template>
<div class="md:hidden" />
<div class="hidden space-y-6 p-10 pb-16 md:block">
<div class="space-y-0.5">
<h2 class="text-2xl font-bold tracking-tight">
Settings
</h2>
<p class="text-muted-foreground">
Manage your account settings and set e-mail preferences.
</p>
</div>
<Separator class="my-6" />
<div class="flex flex-col space-y-8 lg:flex-row lg:space-x-12 lg:space-y-0">
<aside class="-mx-4 lg:w-1/5">
<SidebarNav />
</aside>
<div class="flex-1 lg:max-w-2xl">
<div class="space-y-6">
<slot />
</div>
</div>
</div>
</div>
</template>

View File

@ -1,12 +1,16 @@
<script setup lang="ts">
import { RadioGroupRoot, type RadioGroupRootProps } from 'radix-vue'
import { cn } from '@/lib/utils'
import { RadioGroupRoot, type RadioGroupRootEmits, type RadioGroupRootProps } from 'radix-vue'
import { cn, useEmitAsProps } from '@/lib/utils'
const props = defineProps<RadioGroupRootProps & { class?: string }>()
const emits = defineEmits<RadioGroupRootEmits>()
const emitsAsProps = useEmitAsProps(emits)
</script>
<template>
<RadioGroupRoot :class="cn('grid gap-2', props.class)" v-bind="props">
<RadioGroupRoot :class="cn('grid gap-2', props.class)" v-bind="{ ...props, ...emitsAsProps }">
<slot />
</RadioGroupRoot>
</template>

View File

@ -33,7 +33,7 @@ const emitsAsProps = useEmitAsProps(emits)
>
<SelectViewport
:class="
cn('p-1',
cn('p-0',
position === 'popper'
&& 'h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)]')"
>

View File

@ -17,7 +17,7 @@ const props = withDefaults(
v-bind="props"
:class="[
cn(
'flex h-10 w-full items-center justify-between rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50',
'flex h-9 w-full items-center justify-between rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50',
props.class,
),
props.invalid

View File

@ -5,6 +5,10 @@ export default {
darkMode: 'class',
content: ['./index.html', './src/**/*.{vue,js,ts,jsx,tsx,md}', './.vitepress/**/*.{vue,js,ts,jsx,tsx}'],
theme: {
fontFamily: {
sans: ['Inter', 'sans-serif'],
mono: ['JetBrains Mono', 'monospace'],
},
container: {
center: true,
padding: '2rem',