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-popover": "^1.0.7",
|
||||||
"@radix-ui/react-scroll-area": "^1.0.5",
|
"@radix-ui/react-scroll-area": "^1.0.5",
|
||||||
"@radix-ui/react-slot": "^1.0.2",
|
"@radix-ui/react-slot": "^1.0.2",
|
||||||
|
"@radix-ui/react-switch": "^1.0.3",
|
||||||
"@tailwindcss/typography": "^0.5.10",
|
"@tailwindcss/typography": "^0.5.10",
|
||||||
"class-variance-authority": "^0.7.0",
|
"class-variance-authority": "^0.7.0",
|
||||||
"clsx": "^2.0.0",
|
"clsx": "^2.0.0",
|
||||||
|
@ -74,22 +75,22 @@
|
||||||
"remesh-react": "^4.1.0",
|
"remesh-react": "^4.1.0",
|
||||||
"rxjs": "^7.8.1",
|
"rxjs": "^7.8.1",
|
||||||
"tailwind-merge": "^2.0.0",
|
"tailwind-merge": "^2.0.0",
|
||||||
"type-fest": "^4.7.1",
|
"type-fest": "^4.8.2",
|
||||||
"valibot": "^0.20.1",
|
"valibot": "^0.21.0",
|
||||||
"zod": "^3.22.4"
|
"zod": "^3.22.4"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@commitlint/cli": "^18.4.2",
|
"@commitlint/cli": "^18.4.3",
|
||||||
"@commitlint/config-conventional": "^18.4.2",
|
"@commitlint/config-conventional": "^18.4.3",
|
||||||
"@types/node": "^20.9.0",
|
"@types/node": "^20.9.3",
|
||||||
"@types/react": "^18.2.37",
|
"@types/react": "^18.2.38",
|
||||||
"@types/react-dom": "^18.2.15",
|
"@types/react-dom": "^18.2.16",
|
||||||
"@vitejs/plugin-react": "^4.2.0",
|
"@vitejs/plugin-react": "^4.2.0",
|
||||||
"autoprefixer": "^10.4.16",
|
"autoprefixer": "^10.4.16",
|
||||||
"cross-env": "^7.0.3",
|
"cross-env": "^7.0.3",
|
||||||
"eslint": "^8.53.0",
|
"eslint": "^8.54.0",
|
||||||
"eslint-config-prettier": "^9.0.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-import": "^2.29.0",
|
||||||
"eslint-plugin-n": "^16.3.1",
|
"eslint-plugin-n": "^16.3.1",
|
||||||
"eslint-plugin-prettier": "^5.0.1",
|
"eslint-plugin-prettier": "^5.0.1",
|
||||||
|
@ -105,9 +106,9 @@
|
||||||
"rimraf": "^5.0.5",
|
"rimraf": "^5.0.5",
|
||||||
"tailwindcss": "^3.3.5",
|
"tailwindcss": "^3.3.5",
|
||||||
"tailwindcss-animate": "^1.0.7",
|
"tailwindcss-animate": "^1.0.7",
|
||||||
"typescript": "^5.2.2",
|
"typescript": "^5.3.2",
|
||||||
"webext-bridge": "^6.0.1",
|
"webext-bridge": "^6.0.1",
|
||||||
"wxt": "^0.10.1"
|
"wxt": "^0.10.2"
|
||||||
},
|
},
|
||||||
"lint-staged": {
|
"lint-staged": {
|
||||||
"*.{js,jsx,ts,tsx}": "eslint --fix"
|
"*.{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 Layout from './components/Layout'
|
||||||
|
import ProfileForm from './components/ProfileForm'
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
return <Layout>11</Layout>
|
return (
|
||||||
|
<Layout>
|
||||||
|
<ProfileForm></ProfileForm>
|
||||||
|
</Layout>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export default App
|
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 }) => {
|
const Layout: FC<AppLayoutProps> = ({ children }) => {
|
||||||
return (
|
return (
|
||||||
<main className="grid min-h-screen min-w-screen items-center justify-center bg-gray-50 bg-[url(@/assets/images/texture.png)]">
|
<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">
|
<div className="relative rounded-xl bg-slate-50 shadow-lg">{children}</div>
|
||||||
{children}
|
|
||||||
</div>
|
|
||||||
</main>
|
</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;
|
@tailwind utilities;
|
||||||
|
|
||||||
@layer base {
|
@layer base {
|
||||||
:host {
|
:host,
|
||||||
|
:root {
|
||||||
--background: 0 0% 100%;
|
--background: 0 0% 100%;
|
||||||
--foreground: 222.2 84% 4.9%;
|
--foreground: 222.2 84% 4.9%;
|
||||||
|
|
||||||
|
@ -72,7 +73,8 @@
|
||||||
* {
|
* {
|
||||||
@apply border-border;
|
@apply border-border;
|
||||||
}
|
}
|
||||||
:host {
|
:host,
|
||||||
|
:root {
|
||||||
@apply bg-background text-foreground;
|
@apply bg-background text-foreground;
|
||||||
|
|
||||||
/* Disabled inherit */
|
/* 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 {
|
export default {
|
||||||
darkMode: ['class'],
|
darkMode: ['class'],
|
||||||
content: ['./src/**/*.{ts,tsx,css}', './index.html'],
|
content: ['./src/**/*.{ts,tsx,css,html}'],
|
||||||
theme: {
|
theme: {
|
||||||
container: {
|
container: {
|
||||||
center: true,
|
center: true,
|
||||||
|
|
Loading…
Reference in a new issue