Files
dusuniot/src/routes/my-account.tsx
T

202 lines
7.3 KiB
TypeScript

/**
* # 路由 /my-account 账户页(Tailwind + shadcn/ui 实现)
* 未登录:登录 / 注册两个 Tab 表单;已登录:资料卡 + 订单列表 + 退出登录。
*/
import { createFileRoute, Link, useRouter } from "@tanstack/react-router";
import { useState } from "react";
import { getCurrentUser, loginFn, registerFn, logoutFn } from "~/server/auth";
import { useT } from "~/i18n";
import { getMyOrders } from "~/server/commerce";
import { getLayoutData, Layout } from "~/components/website/Layout";
import { Badge } from "~/components/ui/badge";
import { Button } from "~/components/ui/button";
import { Input } from "~/components/ui/input";
import { Label } from "~/components/ui/label";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "~/components/ui/tabs";
export const Route = createFileRoute("/my-account")({
loader: async () => {
const user = await getCurrentUser();
const [layout, orders] = await Promise.all([getLayoutData(), user ? getMyOrders() : Promise.resolve([])]);
return { layout, user, orders };
},
head: () => ({ meta: [{ title: "My Account - DusunIoT" }] }),
component: MyAccountPage,
});
/** # 函数 组件 账户页面(外壳:公共布局) */
function MyAccountPage() {
const { layout } = Route.useLoaderData();
return (
<Layout layout={layout}>
<AccountInner />
</Layout>
);
}
/** # 函数 组件 账户内容区 */
function AccountInner() {
const t = useT();
const { user, orders } = Route.useLoaderData();
const router = useRouter();
/** # 函数 退出登录 */
async function handleLogout() {
await logoutFn();
router.invalidate();
}
if (!user) {
return (
<div className="site-container">
<h1 className="mt-5 mb-3.5 text-[26px]">My Account</h1>
<div className="max-w-[520px] rounded-lg border border-line bg-soft p-5">
<Tabs defaultValue="login">
<TabsList>
<TabsTrigger value="login">{t("acct_login")}</TabsTrigger>
<TabsTrigger value="register">{t("acct_register")}</TabsTrigger>
</TabsList>
<TabsContent value="login">
<LoginForm onDone={() => router.invalidate()} />
</TabsContent>
<TabsContent value="register">
<RegisterForm onDone={() => router.invalidate()} />
</TabsContent>
</Tabs>
</div>
</div>
);
}
return (
<div className="site-container">
<h1 className="mt-5 mb-3.5 text-[26px]">My Account</h1>
<div className="mb-5 rounded-lg border border-line bg-soft p-5">
<h3 className="mt-0">Profile</h3>
<p className="m-0">
<strong>{user.displayName || user.email}</strong> ({user.email}) — role: {user.role}
<button className="ml-3.5 cursor-pointer border-0 bg-none text-[13px] text-muted-foreground underline hover:text-[#b91c1c]" onClick={() => void handleLogout()} type="button">
Log out
</button>
</p>
</div>
<h2>{t("acct_recent_orders")}</h2>
{orders.length === 0 ? (
<div className="py-[60px] text-center text-muted-foreground">
<p>{t("acct_no_orders")}</p>
<Button asChild>
<Link to="/shop/">{t("checkout_browse")}</Link>
</Button>
</div>
) : (
<table className="w-full border-collapse bg-white text-sm">
<thead>
<tr>
{(["acct_th_order", "acct_th_date", "acct_th_items", "acct_th_total", "acct_th_status", "acct_th_payment"] as const).map((k) => (
<th key={k} className="border-b border-line bg-soft px-3 py-2.5 text-left font-semibold">
{t(k)}
</th>
))}
</tr>
</thead>
<tbody>
{orders.map((o) => (
<tr key={o.id}>
<td className="border-b border-line px-3 py-2.5 text-left">
<strong>{o.orderNumber}</strong>
</td>
<td className="border-b border-line px-3 py-2.5 text-left">{o.createdAt ? new Date(o.createdAt).toLocaleDateString("en-US") : "-"}</td>
<td className="border-b border-line px-3 py-2.5 text-left">{Number(o.itemCount)}</td>
<td className="border-b border-line px-3 py-2.5 text-left">${Number(o.total).toFixed(2)}</td>
<td className="border-b border-line px-3 py-2.5 text-left">
<Badge variant={o.status === "completed" ? "success" : "default"}>{o.status}</Badge>
</td>
<td className="border-b border-line px-3 py-2.5 text-left">
<Badge variant={o.paymentStatus === "paid" ? "success" : "warning"}>{o.paymentStatus}</Badge>
</td>
</tr>
))}
</tbody>
</table>
)}
</div>
);
}
/** # 组件 登录表单 */
function LoginForm({ onDone }: { onDone: () => void }) {
const t = useT();
const [authError, setAuthError] = useState("");
/** # 函数 登录提交 */
async function handleLogin(e: React.FormEvent<HTMLFormElement>) {
e.preventDefault();
setAuthError("");
const fd = new FormData(e.currentTarget);
const res = await loginFn({ data: { email: String(fd.get("email") ?? ""), password: String(fd.get("password") ?? "") } });
if (!res.ok) return setAuthError(res.error);
onDone();
}
return (
<form className="grid grid-cols-1 gap-3.5" onSubmit={handleLogin}>
{authError ? <p className="m-0 rounded-md border border-[#f3c1c1] bg-[#fdeaea] px-4 py-3 text-[#b91c1c]">{authError}</p> : null}
<Label className="col-span-full">
Email
<Input name="email" type="email" required autoComplete="email" />
</Label>
<Label className="col-span-full">
Password
<Input name="password" type="password" required autoComplete="current-password" />
</Label>
<div className="col-span-full">
<Button type="submit">{t("acct_login")}</Button>
</div>
</form>
);
}
/** # 组件 注册表单 */
function RegisterForm({ onDone }: { onDone: () => void }) {
const [authError, setAuthError] = useState("");
/** # 函数 注册提交 */
async function handleRegister(e: React.FormEvent<HTMLFormElement>) {
e.preventDefault();
setAuthError("");
const fd = new FormData(e.currentTarget);
const res = await registerFn({
data: {
email: String(fd.get("email") ?? ""),
password: String(fd.get("password") ?? ""),
displayName: String(fd.get("displayName") ?? "") || undefined,
},
});
if (!res.ok) return setAuthError(res.error);
onDone();
}
return (
<form className="grid grid-cols-1 gap-3.5" onSubmit={handleRegister}>
{authError ? <p className="m-0 rounded-md border border-[#f3c1c1] bg-[#fdeaea] px-4 py-3 text-[#b91c1c]">{authError}</p> : null}
<Label>
Display name (optional)
<Input name="displayName" type="text" />
</Label>
<Label>
Email
<Input name="email" type="email" required autoComplete="email" />
</Label>
<Label>
Password (min 8 chars)
<Input name="password" type="password" required minLength={8} autoComplete="new-password" />
</Label>
<div>
<Button type="submit" variant="outline">
Register
</Button>
</div>
</form>
);
}