Skip to content

Commit 8c712e8

Browse files
committed
feat(login): Update to implement login form with validation and toast notifications
1 parent c327a43 commit 8c712e8

File tree

2 files changed

+199
-4
lines changed

2 files changed

+199
-4
lines changed

app/features/login/index.tsx

Lines changed: 198 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,204 @@
1+
import { FormProvider, Controller, useForm } from "react-hook-form";
2+
import { zodResolver } from "@hookform/resolvers/zod";
3+
import { z } from "zod";
4+
5+
import {
6+
Card,
7+
CardContent,
8+
CardDescription,
9+
CardFooter,
10+
CardHeader,
11+
CardTitle,
12+
} from "@/components/ui/card";
13+
import { Label } from "@/components/ui/label";
14+
import { Input } from "@/components/ui/input";
115
import { Button } from "@/components/ui/button";
16+
import { Alert, AlertDescription } from "@/components/ui/alert";
17+
import { Lock, Mail, Eye, EyeOff, BookOpen } from "lucide-react";
18+
19+
import { useToast } from "@/hooks/use-toast";
20+
import { useToggle } from "@/hooks/useToggle";
21+
22+
import { useSignin } from "@/service/auth";
23+
24+
import { loginSchema, defaultValues } from "./data/schema";
25+
26+
type LoginFormValues = z.infer<typeof loginSchema>;
27+
228
export default function Login() {
29+
const { toast } = useToast();
30+
const { open: isPasswordVisibility, toggle: togglePasswordVisibility } =
31+
useToggle(false);
32+
33+
const { trigger } = useSignin();
34+
35+
const returnMethods = useForm<LoginFormValues>({
36+
resolver: zodResolver(loginSchema),
37+
values: defaultValues,
38+
});
39+
40+
const {
41+
handleSubmit,
42+
formState: { isSubmitting, errors },
43+
} = returnMethods;
44+
45+
const onSubmit = async (data: LoginFormValues) => {
46+
try {
47+
const response = await trigger({ data });
48+
const { token, expired } = response;
49+
document.cookie = `hexToken=${token};expires=${new Date(
50+
String(expired)
51+
).toUTCString()};`;
52+
53+
toast({
54+
title: "登入成功",
55+
description: "歡迎回來!",
56+
variant: "default",
57+
});
58+
} catch (error) {
59+
toast({
60+
title: "登入失敗",
61+
description:
62+
error instanceof Error ? error.message : "登入過程發生錯誤",
63+
variant: "destructive",
64+
});
65+
}
66+
};
67+
368
return (
4-
<div>
5-
<h1>Login</h1>
6-
<p>Log in to your account.</p>
7-
<Button>Click me</Button>
69+
<div className="flex items-center justify-center min-h-screen bg-amber-50">
70+
<div className="absolute inset-0 bg-[url('/api/placeholder/1920/1080')] opacity-5" />
71+
<Card className="w-full max-w-md bg-white/95 backdrop-blur-sm shadow-xl border-0">
72+
<CardHeader className="space-y-4 pb-6">
73+
<div className="flex justify-center items-center space-x-2">
74+
<div className="p-2 bg-amber-800 rounded-lg">
75+
<BookOpen className="h-8 w-8 text-amber-50" />
76+
</div>
77+
</div>
78+
<CardTitle className="text-2xl font-serif text-center text-amber-900">
79+
會員登入
80+
</CardTitle>
81+
<CardDescription className="text-center text-amber-700">
82+
歡迎回來
83+
</CardDescription>
84+
</CardHeader>
85+
86+
<CardContent>
87+
<FormProvider {...returnMethods}>
88+
<div className="space-y-6">
89+
<div className="space-y-2">
90+
<Controller
91+
name="username"
92+
control={returnMethods.control}
93+
render={({ field: { value, onChange, ref } }) => (
94+
<>
95+
<Label htmlFor="username" className="text-amber-800">
96+
電子郵件
97+
</Label>
98+
<div className="relative">
99+
<Mail className="absolute left-3 top-3 h-4 w-4 text-amber-600" />
100+
<Input
101+
id="username"
102+
type="email"
103+
placeholder="[email protected]"
104+
className="pl-10 border-amber-200 focus:border-amber-400 focus:ring-amber-400"
105+
aria-invalid={errors.username ? "true" : "false"}
106+
value={value}
107+
onChange={onChange}
108+
ref={ref}
109+
/>
110+
</div>
111+
{errors.username && (
112+
<Alert
113+
variant="destructive"
114+
className="text-red-800 bg-red-50 border-red-200"
115+
>
116+
<AlertDescription>
117+
{errors.username.message}
118+
</AlertDescription>
119+
</Alert>
120+
)}
121+
</>
122+
)}
123+
/>
124+
</div>
125+
126+
<div className="space-y-2">
127+
<Controller
128+
name="password"
129+
control={returnMethods.control}
130+
render={({ field: { value, onChange, ref } }) => (
131+
<>
132+
<Label htmlFor="password" className="text-amber-800">
133+
密碼
134+
</Label>
135+
<div className="relative">
136+
<Lock className="absolute left-3 top-3 h-4 w-4 text-amber-600" />
137+
<Input
138+
id="password"
139+
type={isPasswordVisibility ? "text" : "password"}
140+
placeholder="輸入您的密碼"
141+
className="pl-10 pr-10 border-amber-200 focus:border-amber-400 focus:ring-amber-400"
142+
aria-invalid={errors.password ? "true" : "false"}
143+
value={value}
144+
onChange={onChange}
145+
ref={ref}
146+
/>
147+
<button
148+
type="button"
149+
onClick={(e) => {
150+
e.preventDefault();
151+
togglePasswordVisibility();
152+
}}
153+
className="absolute right-3 top-3 text-amber-600 hover:text-amber-800 focus:outline-none"
154+
aria-label={
155+
isPasswordVisibility ? "隱藏密碼" : "顯示密碼"
156+
}
157+
>
158+
{isPasswordVisibility ? (
159+
<EyeOff className="h-4 w-4" />
160+
) : (
161+
<Eye className="h-4 w-4" />
162+
)}
163+
</button>
164+
</div>
165+
{errors.password && (
166+
<Alert
167+
variant="destructive"
168+
className="text-red-800 bg-red-50 border-red-200"
169+
>
170+
<AlertDescription>
171+
{errors.password.message}
172+
</AlertDescription>
173+
</Alert>
174+
)}
175+
</>
176+
)}
177+
/>
178+
</div>
179+
</div>
180+
</FormProvider>
181+
</CardContent>
182+
183+
<CardFooter className="flex flex-col gap-4 pt-6">
184+
<Button
185+
className="w-full bg-amber-700 hover:bg-amber-800 text-amber-50"
186+
onClick={handleSubmit(onSubmit)}
187+
disabled={isSubmitting}
188+
>
189+
{isSubmitting ? "登入中..." : "登入"}
190+
</Button>
191+
<div className="text-sm text-center text-amber-700 hidden">
192+
還沒有帳號?
193+
<Button
194+
variant="link"
195+
className="pl-1 text-amber-800 hover:text-amber-900"
196+
>
197+
立即註冊
198+
</Button>
199+
</div>
200+
</CardFooter>
201+
</Card>
8202
</div>
9203
);
10204
}

app/types/api.d.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ type APIResponse<T> = {
1010
data: T;
1111
status: number;
1212
message: string;
13+
[key: string]: unknown;
1314
}
1415

1516
type EndpointConfig = {

0 commit comments

Comments
 (0)