npm create vite@latest .
https://ui.shadcn.com/docs/installation/vite
install step by step
npm install react-router-dom
npm install react-icons
npm install react-hook-form zod @hookform/resolvers
import { z } from "zod";
import { zodResolver } from "@hookform/resolvers/zod";
import { useForm } from "react-hook-form";
// 1 define scheme
const LoginSchema = z.object({
email: z.string().email("Invalid email address"),
password: z.string().min(1, "At least one letter"),
});
// 2. ts type
type LoginSchemaType = z.infer<typeof LoginSchema>;
const LoginPage = () => {
// 3. useForm with ts ty0pe and resolver
const {
register,
handleSubmit,
formState: { errors },
} = useForm<LoginSchemaType>({ resolver: zodResolver(LoginSchema) });
// 4. event onSubmit
const onSubmit = (data: LoginSchemaType) => {
console.log("Form Data:", data);
};
// 5. UI input
// 5.1 form `onSubmit={handleSubmit(onSubmit)}`
// 5.2 register field `{...register("email")}`
// 5.3 error `{errors.email ? errors.email.message : " "}`
return (
<form onSubmit={handleSubmit(onSubmit)}>
<label htmlFor="email">Email:</label>
<Input id="email" {...register("email")} />
<p style={{ color: "red", minHeight: "1.5em" }}>
{errors.email ? errors.email.message : " "}
</p>
<Button type="submit"> Submit </Button>
</form>
);
};
/* gap 生效, 1,2,3 之间间隔是 2 */
<form>
<div className="flex flex-col gap-2">
<...1...>
<...2...>
<...3...>
</div>
</form>
/* gap 失效 1,2,3 之间间隔是 0 */
<div className="flex flex-col gap-2">
<form>
<...1...>
<...2...>
<...3...>
</form>
</div>
useEffect runs on the initial render, even if dependencies are added. To prevent this, leverages conditional checks directly in useEffect.
prevent UI from jumping by reserving space for the error message
min-height
<p className=" text-red-500 min-h-6"></p>
<p style={{ color: "red", minHeight: "1.5em" }}></p>
the same effect of below code
<DashSubPage name={m.name} age={m.age} /> <br />
<DashSubPage {...m} />
const DashboardPage = () => {
const m = {
name: "a",
age: 11,
};
return (
<div>
dashboard_page <br />
<DashSubPage name={m.name} age={m.age} /> <br />
<DashSubPage {...m} />
</div>
);
};
interface DashSubPageProp {
name: String;
age: Number;
}
const DashSubPage = ({ name, age }: DashSubPageProp) => {
return (
<div>
<span>name: {name}</span> <span>age: {age.toString()}</span>
</div>
);
};