Skip to content

Commit 07e3e15

Browse files
committed
Add reusable Input and Select components to eliminate form element redundancy
1 parent 85420b0 commit 07e3e15

File tree

4 files changed

+208
-73
lines changed

4 files changed

+208
-73
lines changed

client/src/components/ui/Forms/login.tsx

Lines changed: 8 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import Link from "next/link";
22

33
import { Button } from "../button";
44
import { GoogleIcon } from "../google-icon";
5+
import { Input } from "../input";
56
import { Separator } from "../separator";
67

78
const LoginForm = () => {
@@ -24,30 +25,25 @@ const LoginForm = () => {
2425
</div>
2526

2627
<form className="flex w-full flex-col">
27-
<div className="flex flex-col gap-2.5 pb-5">
28-
<label htmlFor="email" className="font-medium text-primary">
29-
Email
30-
</label>
31-
<input
28+
<div className="pb-5">
29+
<Input
3230
id="email"
3331
type="email"
3432
name="email"
33+
label="Email"
3534
placeholder="Enter Email"
36-
className="bg-input h-12 rounded-md border border-border px-4 py-3 text-base placeholder-border"
35+
className="text-base"
3736
required
3837
/>
3938
</div>
4039

41-
<div className="flex flex-1 flex-col gap-2.5 pb-5">
42-
<label htmlFor="password" className="font-medium text-primary">
43-
Password
44-
</label>
45-
<input
40+
<div className="pb-5">
41+
<Input
4642
id="password"
4743
type="password"
4844
name="password"
45+
label="Password"
4946
placeholder="Enter Password"
50-
className="bg-input h-12 rounded-md border border-border px-4 py-3 placeholder-border"
5147
required
5248
/>
5349
</div>

client/src/components/ui/Forms/signup.tsx

Lines changed: 41 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import Link from "next/link";
22

33
import { Button } from "../button";
44
import { GoogleIcon } from "../google-icon";
5+
import { Input } from "../input";
56
import { Separator } from "../separator";
67

78
const SignUpForm = () => {
@@ -26,78 +27,57 @@ const SignUpForm = () => {
2627

2728
<form className="flex w-full flex-col">
2829
<div className="flex flex-col gap-4 pb-10 md:flex-row md:gap-x-8 md:pb-12">
29-
<div className="flex flex-1 flex-col gap-2.5">
30-
<label htmlFor="firstname" className="font-medium text-primary">
31-
First Name
32-
</label>
33-
<input
34-
id="firstname"
35-
type="text"
36-
name="firstname"
37-
placeholder="Enter First Name"
38-
className="bg-input h-12 rounded-md border border-border px-4 py-3 placeholder-border"
39-
required
40-
/>
41-
</div>
42-
<div className="flex flex-1 flex-col gap-2.5">
43-
<label htmlFor="lastname" className="font-medium text-primary">
44-
Last Name
45-
</label>
46-
<input
47-
id="lastname"
48-
type="text"
49-
name="lastname"
50-
placeholder="Enter Last Name"
51-
className="bg-input h-12 rounded-md border border-border px-4 py-3 placeholder-border"
52-
required
53-
/>
54-
</div>
30+
<Input
31+
id="firstname"
32+
type="text"
33+
name="firstname"
34+
label="First Name"
35+
placeholder="Enter First Name"
36+
containerClassName="flex-1"
37+
required
38+
/>
39+
<Input
40+
id="lastname"
41+
type="text"
42+
name="lastname"
43+
label="Last Name"
44+
placeholder="Enter Last Name"
45+
containerClassName="flex-1"
46+
required
47+
/>
5548
</div>
5649

57-
<div className="flex flex-col gap-2.5 pb-4 sm:pb-8">
58-
<label htmlFor="email" className="font-medium text-primary">
59-
Email
60-
</label>
61-
<input
50+
<div className="pb-4 sm:pb-8">
51+
<Input
6252
id="email"
6353
type="email"
6454
name="email"
55+
label="Email"
6556
placeholder="Enter Email"
66-
className="bg-input h-12 rounded-md border border-border px-4 py-3 text-base placeholder-border"
57+
className="text-base"
6758
required
6859
/>
6960
</div>
7061

7162
<div className="flex flex-col gap-4 pb-10 md:flex-row md:gap-x-8 md:pb-12">
72-
<div className="flex flex-1 flex-col gap-2.5">
73-
<label htmlFor="password" className="font-medium text-primary">
74-
Password
75-
</label>
76-
<input
77-
id="password"
78-
type="password"
79-
name="password"
80-
placeholder="Enter Password"
81-
className="bg-input h-12 rounded-md border border-border px-4 py-3 placeholder-border"
82-
required
83-
/>
84-
</div>
85-
<div className="flex flex-1 flex-col gap-2.5">
86-
<label
87-
htmlFor="confirmPassword"
88-
className="font-medium text-primary"
89-
>
90-
Confirm Password
91-
</label>
92-
<input
93-
id="confirmPassword"
94-
type="password"
95-
name="confirmPassword"
96-
placeholder="Confirm Password"
97-
className="bg-input h-12 rounded-md border border-border px-4 py-3 placeholder-border"
98-
required
99-
/>
100-
</div>
63+
<Input
64+
id="password"
65+
type="password"
66+
name="password"
67+
label="Password"
68+
placeholder="Enter Password"
69+
containerClassName="flex-1"
70+
required
71+
/>
72+
<Input
73+
id="confirmPassword"
74+
type="password"
75+
name="confirmPassword"
76+
label="Confirm Password"
77+
placeholder="Confirm Password"
78+
containerClassName="flex-1"
79+
required
80+
/>
10181
</div>
10282

10383
<Button

client/src/components/ui/input.tsx

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
import * as React from "react";
2+
3+
import { cn } from "@/lib/utils";
4+
5+
export interface InputProps
6+
extends React.InputHTMLAttributes<HTMLInputElement> {
7+
label?: string;
8+
containerClassName?: string;
9+
optional?: boolean;
10+
leftIcon?: React.ReactNode;
11+
rightIcon?: React.ReactNode;
12+
error?: string;
13+
}
14+
15+
const Input = React.forwardRef<HTMLInputElement, InputProps>(
16+
(
17+
{
18+
className,
19+
label,
20+
containerClassName,
21+
optional = false,
22+
leftIcon,
23+
rightIcon,
24+
error,
25+
id,
26+
...props
27+
},
28+
ref,
29+
) => {
30+
return (
31+
<div className={cn("flex flex-col gap-2.5", containerClassName)}>
32+
{label && (
33+
<label htmlFor={id} className="font-medium text-primary">
34+
{label}
35+
{optional && (
36+
<span className="ml-1 text-sm text-subtle">(optional)</span>
37+
)}
38+
</label>
39+
)}
40+
<div className="relative">
41+
{leftIcon && (
42+
<div className="absolute left-3 top-1/2 -translate-y-1/2 transform text-subtle">
43+
{leftIcon}
44+
</div>
45+
)}
46+
<input
47+
id={id}
48+
className={cn(
49+
"bg-input h-12 w-full rounded-md border border-border px-4 py-3 placeholder-border focus:border-primary focus:outline-none focus:ring-2 focus:ring-primary",
50+
leftIcon && "pl-10",
51+
rightIcon && "pr-10",
52+
error && "border-red-500 focus:border-red-500 focus:ring-red-500",
53+
className,
54+
)}
55+
ref={ref}
56+
{...props}
57+
/>
58+
{rightIcon && (
59+
<div className="absolute right-3 top-1/2 -translate-y-1/2 transform text-subtle">
60+
{rightIcon}
61+
</div>
62+
)}
63+
</div>
64+
{error && <p className="text-sm text-red-500">{error}</p>}
65+
</div>
66+
);
67+
},
68+
);
69+
70+
Input.displayName = "Input";
71+
72+
export { Input };

client/src/components/ui/select.tsx

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
import * as React from "react";
2+
3+
import { cn } from "@/lib/utils";
4+
5+
export interface SelectProps
6+
extends React.SelectHTMLAttributes<HTMLSelectElement> {
7+
label?: string;
8+
containerClassName?: string;
9+
optional?: boolean;
10+
options: Array<{ value: string; label: string }>;
11+
placeholder?: string;
12+
error?: string;
13+
}
14+
15+
const Select = React.forwardRef<HTMLSelectElement, SelectProps>(
16+
(
17+
{
18+
className,
19+
label,
20+
containerClassName,
21+
optional = false,
22+
options,
23+
placeholder,
24+
error,
25+
id,
26+
...props
27+
},
28+
ref,
29+
) => {
30+
return (
31+
<div className={cn("flex flex-col gap-2.5", containerClassName)}>
32+
{label && (
33+
<label htmlFor={id} className="font-medium text-primary">
34+
{label}
35+
{optional && (
36+
<span className="ml-1 text-sm text-subtle">(optional)</span>
37+
)}
38+
</label>
39+
)}
40+
<div className="relative">
41+
<select
42+
id={id}
43+
className={cn(
44+
"bg-input h-12 appearance-none rounded-md border border-border px-4 py-3 pr-10 text-primary focus:border-primary focus:outline-none focus:ring-2 focus:ring-primary",
45+
error && "border-red-500 focus:border-red-500 focus:ring-red-500",
46+
className,
47+
)}
48+
ref={ref}
49+
{...props}
50+
>
51+
{placeholder && (
52+
<option value="" disabled>
53+
{placeholder}
54+
</option>
55+
)}
56+
{options.map((option) => (
57+
<option key={option.value} value={option.value}>
58+
{option.label}
59+
</option>
60+
))}
61+
</select>
62+
{/* Custom dropdown arrow */}
63+
<div className="pointer-events-none absolute right-3 top-1/2 -translate-y-1/2 transform">
64+
<svg
65+
className="h-5 w-5 text-primary"
66+
fill="none"
67+
stroke="currentColor"
68+
viewBox="0 0 24 24"
69+
>
70+
<path
71+
strokeLinecap="round"
72+
strokeLinejoin="round"
73+
strokeWidth={2}
74+
d="M19 9l-7 7-7-7"
75+
/>
76+
</svg>
77+
</div>
78+
</div>
79+
{error && <p className="text-sm text-red-500">{error}</p>}
80+
</div>
81+
);
82+
},
83+
);
84+
85+
Select.displayName = "Select";
86+
87+
export { Select };

0 commit comments

Comments
 (0)