Stop the slugifier from deleting the letter D-stroke

This commit is contained in:
2026-08-19 13:07:13 +07:00
parent 24c55d1627
commit 4490a1abf0
14 changed files with 739 additions and 198 deletions
+1 -1
View File
@@ -27,7 +27,7 @@ export function AccountMenu() {
if (!user) {
return (
<Link
href="/admin/login"
href="/login"
className="flex items-center gap-1.5 rounded-full border border-border-subtle bg-surface px-3 py-1.5 text-xs font-semibold text-txt-secondary hover:bg-surface-hover"
>
<LogIn className="h-3.5 w-3.5" />
+135
View File
@@ -0,0 +1,135 @@
"use client";
import { useState } from "react";
import Link from "next/link";
import { useRouter } from "next/navigation";
import { login, AuthError } from "@duoc-thu/api-client";
interface LoginFormProps {
heading: string;
/** `/admin/login` sets this. It changes only where a successful login
* lands and what a non-admin is told afterwards -- never whether the
* login itself is allowed to happen, which is auth-service's call. */
requireAdmin?: boolean;
}
/**
* Shared by `/login` (any doctor) and `/admin/login` (administrators).
*
* The two pages used to be one page, and it only served admins: `AccountMenu`
* pointed its "Đăng nhập" button at `/admin/login`, so a doctor signing in
* from the chat UI with a `user` account got a red "Tài khoản này không có
* quyền quản trị" while the header beside it switched to their name. Both were
* accurate and together they were nonsense -- the login had succeeded and the
* session cookie was already set; only the admin redirect had not happened.
*
* So a non-admin result is reported here as what it is: signed in, without
* administrative rights. Not an error, and not silently swallowed either.
*/
export function LoginForm({ heading, requireAdmin = false }: LoginFormProps) {
const router = useRouter();
const [username, setUsername] = useState("");
const [password, setPassword] = useState("");
const [error, setError] = useState<string | null>(null);
const [signedInAs, setSignedInAs] = useState<string | null>(null);
const [pending, setPending] = useState(false);
async function onSubmit(event: React.FormEvent) {
event.preventDefault();
setError(null);
setSignedInAs(null);
setPending(true);
try {
const user = await login({ username, password });
if (requireAdmin && user.role !== "admin") {
// Signed in, just not an administrator. Say so plainly and offer the
// way onward rather than leaving them on a dead form.
setSignedInAs(user.username);
return;
}
router.push(user.role === "admin" && requireAdmin ? "/admin" : "/");
router.refresh();
} catch (err) {
setError(
err instanceof AuthError && err.status === 401
? "Sai tên đăng nhập hoặc mật khẩu."
: "Không thể đăng nhập lúc này. Vui lòng thử lại."
);
} finally {
setPending(false);
}
}
if (signedInAs) {
return (
<div className="flex flex-1 items-center justify-center p-6">
<div className="w-full max-w-sm space-y-3 rounded-2xl border border-border-subtle bg-surface p-6 shadow-sm">
<h1 className="text-lg font-bold text-txt-primary">
Đã đăng nhập với tài khoản {signedInAs}
</h1>
<p className="text-xs text-txt-secondary">
Tài khoản này không quyền quản trị, nhưng bạn đã đăng nhập
thể tra cứu bình thường.
</p>
<Link
href="/"
className="block w-full rounded-lg bg-accent-primary py-2 text-center text-sm font-semibold text-txt-inverse"
>
Vào trang tra cứu
</Link>
</div>
</div>
);
}
return (
<div className="flex flex-1 items-center justify-center p-6">
<form
onSubmit={onSubmit}
className="w-full max-w-sm space-y-4 rounded-2xl border border-border-subtle bg-surface p-6 shadow-sm"
>
<h1 className="text-lg font-bold text-txt-primary">{heading}</h1>
<div className="space-y-1">
<label className="text-xs font-semibold text-txt-secondary" htmlFor="username">
Tên đăng nhập
</label>
<input
id="username"
className="w-full rounded-lg border border-border-subtle bg-app px-3 py-2 text-sm text-txt-primary"
value={username}
onChange={(e) => setUsername(e.target.value)}
autoComplete="username"
required
/>
</div>
<div className="space-y-1">
<label className="text-xs font-semibold text-txt-secondary" htmlFor="password">
Mật khẩu
</label>
<input
id="password"
type="password"
className="w-full rounded-lg border border-border-subtle bg-app px-3 py-2 text-sm text-txt-primary"
value={password}
onChange={(e) => setPassword(e.target.value)}
autoComplete="current-password"
required
/>
</div>
{error && <p className="text-xs font-medium text-status-danger">{error}</p>}
<button
type="submit"
disabled={pending}
className="w-full rounded-lg bg-accent-primary py-2 text-sm font-semibold text-txt-inverse disabled:opacity-60"
>
{pending ? "Đang đăng nhập..." : "Đăng nhập"}
</button>
{!requireAdmin && (
<p className="text-center text-[0.7rem] text-txt-muted">
Không bắt buộc bạn vẫn tra cứu đưc không cần đăng nhập.
</p>
)}
</form>
</div>
);
}