218 lines
5.7 KiB
TypeScript
218 lines
5.7 KiB
TypeScript
"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="Username (tuỳ chọn)">
|
|
<Icons.User size={18} stroke={1.75} style={{ color: "var(--muted-foreground)" }} />
|
|
<input
|
|
name="username"
|
|
type="text"
|
|
autoComplete="username"
|
|
pattern="[a-z0-9](?:[a-z0-9._]{1,28})[a-z0-9]"
|
|
minLength={3}
|
|
maxLength={30}
|
|
placeholder="vd: minh.nguyen — dùng để đăng nhập"
|
|
disabled={pending}
|
|
style={{ textTransform: "lowercase" }}
|
|
/>
|
|
</Field>
|
|
</>
|
|
)}
|
|
|
|
{isLogin ? (
|
|
<Field label="Email hoặc username">
|
|
<Icons.User size={18} stroke={1.75} style={{ color: "var(--muted-foreground)" }} />
|
|
<input
|
|
name="identifier"
|
|
type="text"
|
|
required
|
|
autoComplete="username"
|
|
autoCapitalize="off"
|
|
spellCheck={false}
|
|
placeholder="ten@email.com hoặc minh.nguyen"
|
|
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>
|
|
);
|
|
}
|