chore(form): basic form structure
This commit is contained in:
parent
3073f9165c
commit
8df1b08fe5
10 changed files with 915 additions and 686 deletions
23
package.json
23
package.json
|
@ -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"
|
||||
|
|
1377
pnpm-lock.yaml
1377
pnpm-lock.yaml
File diff suppressed because it is too large
Load diff
|
@ -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
|
||||
|
|
45
src/app/options/components/AvatarSelect.tsx
Normal file
45
src/app/options/components/AvatarSelect.tsx
Normal 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
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
|
88
src/app/options/components/ProfileForm.tsx
Normal file
88
src/app/options/components/ProfileForm.tsx
Normal 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
|
|
@ -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 */
|
||||
|
|
22
src/components/ui/Input.tsx
Normal file
22
src/components/ui/Input.tsx
Normal 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 }
|
27
src/components/ui/Switch.tsx
Normal file
27
src/components/ui/Switch.tsx
Normal 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 }
|
|
@ -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,
|
||||
|
|
Loading…
Reference in a new issue