chore(form): basic form structure

This commit is contained in:
molvqingtai 2023-11-22 01:22:31 +08:00
parent 3073f9165c
commit 8df1b08fe5
10 changed files with 915 additions and 686 deletions

View file

@ -53,6 +53,7 @@
"@radix-ui/react-popover": "^1.0.7",
"@radix-ui/react-scroll-area": "^1.0.5",
"@radix-ui/react-slot": "^1.0.2",
"@radix-ui/react-switch": "^1.0.3",
"@tailwindcss/typography": "^0.5.10",
"class-variance-authority": "^0.7.0",
"clsx": "^2.0.0",
@ -74,22 +75,22 @@
"remesh-react": "^4.1.0",
"rxjs": "^7.8.1",
"tailwind-merge": "^2.0.0",
"type-fest": "^4.7.1",
"valibot": "^0.20.1",
"type-fest": "^4.8.2",
"valibot": "^0.21.0",
"zod": "^3.22.4"
},
"devDependencies": {
"@commitlint/cli": "^18.4.2",
"@commitlint/config-conventional": "^18.4.2",
"@types/node": "^20.9.0",
"@types/react": "^18.2.37",
"@types/react-dom": "^18.2.15",
"@commitlint/cli": "^18.4.3",
"@commitlint/config-conventional": "^18.4.3",
"@types/node": "^20.9.3",
"@types/react": "^18.2.38",
"@types/react-dom": "^18.2.16",
"@vitejs/plugin-react": "^4.2.0",
"autoprefixer": "^10.4.16",
"cross-env": "^7.0.3",
"eslint": "^8.53.0",
"eslint": "^8.54.0",
"eslint-config-prettier": "^9.0.0",
"eslint-config-standard-with-typescript": "^39.1.1",
"eslint-config-standard-with-typescript": "^40.0.0",
"eslint-plugin-import": "^2.29.0",
"eslint-plugin-n": "^16.3.1",
"eslint-plugin-prettier": "^5.0.1",
@ -105,9 +106,9 @@
"rimraf": "^5.0.5",
"tailwindcss": "^3.3.5",
"tailwindcss-animate": "^1.0.7",
"typescript": "^5.2.2",
"typescript": "^5.3.2",
"webext-bridge": "^6.0.1",
"wxt": "^0.10.1"
"wxt": "^0.10.2"
},
"lint-staged": {
"*.{js,jsx,ts,tsx}": "eslint --fix"

File diff suppressed because it is too large Load diff

View file

@ -1,7 +1,12 @@
import Layout from './components/Layout'
import ProfileForm from './components/ProfileForm'
function App() {
return <Layout>11</Layout>
return (
<Layout>
<ProfileForm></ProfileForm>
</Layout>
)
}
export default App

View file

@ -0,0 +1,45 @@
import { type FC, type ChangeEvent } from 'react'
import { Globe2Icon } from 'lucide-react'
import React from 'react'
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/Avatar'
import { Label } from '@/components/ui/Label'
export interface AvatarSelectProps {
value?: string
className?: string
onload?: ((this: FileReader, ev: ProgressEvent<FileReader>) => any) | null
onerror?: ((this: FileReader, ev: ProgressEvent) => any) | null
onChange?: (src: string) => void
}
const AvatarSelect = React.forwardRef<HTMLInputElement, AvatarSelectProps>(
({ onChange, value, onerror, onload, className }, ref) => {
const handleChange = (e: ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0]
if (file) {
const reader = new FileReader()
reader.onload = (e) => {
onload?.call(reader, e)
const src = e.target?.result as string
onChange?.(src)
}
reader.onerror = (e) => onerror?.call(reader, e)
reader.readAsDataURL(file)
}
}
return (
<Label className="contents">
<Avatar className={cn('h-20 w-20 cursor-pointer border-4 border-white', className)}>
<AvatarImage src={value} alt="avatar" />
<AvatarFallback>
<Globe2Icon size="100%" className="text-gray-400" />
</AvatarFallback>
</Avatar>
<input ref={ref} hidden type="file" accept="image/*" onChange={handleChange} />
</Label>
)
}
)
AvatarSelect.displayName = 'AvatarSelect'
export default AvatarSelect

View file

@ -7,9 +7,7 @@ export interface AppLayoutProps {
const Layout: FC<AppLayoutProps> = ({ children }) => {
return (
<main className="grid min-h-screen min-w-screen items-center justify-center bg-gray-50 bg-[url(@/assets/images/texture.png)]">
<div className="relative h-[90vh] w-[90vw] max-w-screen-2xl overflow-hidden rounded-xl bg-slate-50 shadow-lg">
{children}
</div>
<div className="relative rounded-xl bg-slate-50 shadow-lg">{children}</div>
</main>
)
}

View file

@ -0,0 +1,88 @@
import { object, string, type Output, minLength, maxLength, toTrimmed, boolean } from 'valibot'
import { useForm } from 'react-hook-form'
import { valibotResolver } from '@hookform/resolvers/valibot'
import AvatarSelect from './AvatarSelect'
import { Button } from '@/components/ui/Button'
import { Form, FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage } from '@/components/ui/Form'
import { Input } from '@/components/ui/Input'
import { Switch } from '@/components/ui/Switch'
const formSchema = object({
username: string([
toTrimmed(),
minLength(1, 'Please enter your username.'),
maxLength(8, 'Your username must have 8 characters or more.')
]),
avatar: string(),
darkMode: boolean()
})
const ProfileForm = () => {
const form = useForm({
resolver: valibotResolver(formSchema),
defaultValues: {
username: '',
avatar: '',
darkMode: window.matchMedia('(prefers-color-scheme: dark)').matches
}
})
const handleSubmit = (data: Output<typeof formSchema>) => {
console.log(data)
}
return (
<Form {...form}>
<form onSubmit={form.handleSubmit(handleSubmit)} className="relative w-96 space-y-8 p-10">
<FormField
control={form.control}
name="avatar"
render={({ field }) => (
<FormItem>
<FormControl>
<AvatarSelect
className="absolute left-1/2 top-0 -translate-x-1/2 -translate-y-1/2 shadow-lg"
{...field}
></AvatarSelect>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="username"
render={({ field }) => (
<FormItem>
<FormLabel>Username</FormLabel>
<FormControl>
<Input placeholder="Please enter your username" {...field} />
</FormControl>
<FormDescription>This is your public display name.</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="darkMode"
render={({ field }) => (
<FormItem>
<FormLabel>DarkMode</FormLabel>
<FormControl>
<Switch checked={field.value} onCheckedChange={field.onChange}></Switch>
</FormControl>
<FormDescription>This is your public display name.</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<Button type="submit">Submit</Button>
</form>
</Form>
)
}
ProfileForm.displayName = 'ProfileForm'
export default ProfileForm

View file

@ -3,7 +3,8 @@
@tailwind utilities;
@layer base {
:host {
:host,
:root {
--background: 0 0% 100%;
--foreground: 222.2 84% 4.9%;
@ -72,7 +73,8 @@
* {
@apply border-border;
}
:host {
:host,
:root {
@apply bg-background text-foreground;
/* Disabled inherit */

View file

@ -0,0 +1,22 @@
import * as React from 'react'
import { cn } from '@/utils/index'
export interface InputProps extends React.InputHTMLAttributes<HTMLInputElement> {}
const Input = React.forwardRef<HTMLInputElement, InputProps>(({ className, type, ...props }, ref) => {
return (
<input
type={type}
className={cn(
'flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-sm shadow-sm transition-colors file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50',
className
)}
ref={ref}
{...props}
/>
)
})
Input.displayName = 'Input'
export { Input }

View file

@ -0,0 +1,27 @@
import * as React from 'react'
import * as SwitchPrimitives from '@radix-ui/react-switch'
import { cn } from '@/utils/index'
const Switch = React.forwardRef<
React.ElementRef<typeof SwitchPrimitives.Root>,
React.ComponentPropsWithoutRef<typeof SwitchPrimitives.Root>
>(({ className, ...props }, ref) => (
<SwitchPrimitives.Root
className={cn(
'peer inline-flex h-5 w-9 shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent shadow-sm transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=unchecked]:bg-input',
className
)}
{...props}
ref={ref}
>
<SwitchPrimitives.Thumb
className={cn(
'pointer-events-none block h-4 w-4 rounded-full bg-background shadow-lg ring-0 transition-transform data-[state=checked]:translate-x-4 data-[state=unchecked]:translate-x-0'
)}
/>
</SwitchPrimitives.Root>
))
Switch.displayName = SwitchPrimitives.Root.displayName
export { Switch }

View file

@ -4,7 +4,7 @@ import animate from 'tailwindcss-animate'
export default {
darkMode: ['class'],
content: ['./src/**/*.{ts,tsx,css}', './index.html'],
content: ['./src/**/*.{ts,tsx,css,html}'],
theme: {
container: {
center: true,