This commit is contained in:
2026-05-20 15:40:17 +07:00
parent 230eb9010c
commit dd3fd889a3
48 changed files with 3374 additions and 737 deletions

View File

@@ -0,0 +1,185 @@
"use client";
import { useActionState } from "react";
import Link from "next/link";
import { Icons } from "@/components/icons";
import type { FormState } from "./auth-actions";
type Action = (prev: FormState, data: FormData) => Promise<FormState>;
export function AuthForm({
mode,
action,
next = "/",
}: {
mode: "login" | "register";
action: Action;
next?: string;
}) {
const [state, formAction, pending] = useActionState<FormState, FormData>(
action,
{},
);
const isLogin = mode === "login";
const title = isLogin ? "Đăng nhập" : "Tạo tài khoản";
const subtitle = isLogin
? "Tiếp tục với địa điểm đã lưu của bạn"
: "Bắt đầu lưu địa điểm cùng nhóm nhỏ";
const cta = isLogin ? "Đăng nhập" : "Đăng ký";
const switchPrompt = isLogin ? "Chưa có tài khoản?" : "Đã có tài khoản?";
const switchBase = isLogin ? "/register" : "/login";
const switchHref =
next && next !== "/"
? `${switchBase}?next=${encodeURIComponent(next)}`
: switchBase;
const switchLabel = isLogin ? "Đăng ký" : "Đăng nhập";
return (
<form
action={formAction}
style={{
display: "flex",
flexDirection: "column",
gap: 14,
padding: 24,
background: "var(--card)",
border: "0.5px solid var(--border)",
borderRadius: "var(--radius-xl)",
boxShadow: "var(--shadow-md)",
}}
>
<input type="hidden" name="next" value={next} />
<div style={{ marginBottom: 4 }}>
<h1
className="display"
style={{
margin: 0,
fontSize: 28,
fontWeight: 700,
letterSpacing: "-0.02em",
lineHeight: 1.1,
}}
>
{title}
</h1>
<p
style={{
margin: "4px 0 0",
fontSize: 14,
color: "var(--muted-foreground)",
}}
>
{subtitle}
</p>
</div>
{!isLogin && (
<Field label="Tên hiển thị">
<Icons.User size={18} stroke={1.75} style={{ color: "var(--muted-foreground)" }} />
<input
name="name"
type="text"
required
autoComplete="name"
placeholder="Tên của bạn"
disabled={pending}
/>
</Field>
)}
<Field label="Email">
<Icons.Mail size={18} stroke={1.75} style={{ color: "var(--muted-foreground)" }} />
<input
name="email"
type="email"
required
autoComplete="email"
inputMode="email"
placeholder="ten@email.com"
disabled={pending}
/>
</Field>
<Field label="Mật khẩu">
<Icons.Lock size={18} stroke={1.75} style={{ color: "var(--muted-foreground)" }} />
<input
name="password"
type="password"
required
minLength={8}
autoComplete={isLogin ? "current-password" : "new-password"}
placeholder={isLogin ? "••••••••" : "Tối thiểu 8 ký tự"}
disabled={pending}
/>
</Field>
{state.error && (
<div
role="alert"
style={{
padding: "10px 12px",
background: "var(--danger-soft)",
color: "var(--danger)",
borderRadius: "var(--radius-md)",
fontSize: 13,
fontWeight: 500,
}}
>
{state.error}
</div>
)}
<button
type="submit"
className="btn btn--block btn--lg"
disabled={pending}
style={{ marginTop: 4 }}
>
{pending ? "Đang xử lý..." : cta}
</button>
<div
style={{
fontSize: 13,
color: "var(--muted-foreground)",
textAlign: "center",
paddingTop: 4,
}}
>
{switchPrompt}{" "}
<Link
href={switchHref}
style={{ color: "var(--primary)", fontWeight: 600, textDecoration: "none" }}
>
{switchLabel}
</Link>
</div>
</form>
);
}
function Field({
label,
children,
}: {
label: string;
children: React.ReactNode;
}) {
return (
<label style={{ display: "flex", flexDirection: "column", gap: 6 }}>
<span
style={{
fontSize: 13,
fontWeight: 600,
color: "var(--muted-foreground)",
textTransform: "uppercase",
letterSpacing: "0.04em",
}}
>
{label}
</span>
<span className="input">{children}</span>
</label>
);
}