feat: rebuild CSP practice workflow, UX and automation
这个提交包含在:
@@ -0,0 +1,68 @@
|
||||
"use client";
|
||||
|
||||
import Link from "next/link";
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
import { clearToken, readToken } from "@/lib/auth";
|
||||
|
||||
const links = [
|
||||
["首页", "/"],
|
||||
["登录", "/auth"],
|
||||
["题库", "/problems"],
|
||||
["提交", "/submissions"],
|
||||
["错题本", "/wrong-book"],
|
||||
["比赛", "/contests"],
|
||||
["知识库", "/kb"],
|
||||
["导入任务", "/imports"],
|
||||
["在线运行", "/run"],
|
||||
["我的", "/me"],
|
||||
["排行榜", "/leaderboard"],
|
||||
["API文档", "/api-docs"],
|
||||
] as const;
|
||||
|
||||
export function AppNav() {
|
||||
const [hasToken, setHasToken] = useState<boolean>(() => Boolean(readToken()));
|
||||
|
||||
useEffect(() => {
|
||||
const refresh = () => setHasToken(Boolean(readToken()));
|
||||
window.addEventListener("storage", refresh);
|
||||
window.addEventListener("focus", refresh);
|
||||
return () => {
|
||||
window.removeEventListener("storage", refresh);
|
||||
window.removeEventListener("focus", refresh);
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<header className="border-b bg-white">
|
||||
<div className="mx-auto flex max-w-6xl flex-wrap items-center gap-2 px-4 py-3">
|
||||
{links.map(([label, href]) => (
|
||||
<Link
|
||||
key={href}
|
||||
href={href}
|
||||
className="rounded-md border px-3 py-1 text-sm hover:bg-zinc-100"
|
||||
>
|
||||
{label}
|
||||
</Link>
|
||||
))}
|
||||
|
||||
<div className="ml-auto flex items-center gap-2 text-sm">
|
||||
<span className={hasToken ? "text-emerald-700" : "text-zinc-500"}>
|
||||
{hasToken ? "已登录" : "未登录"}
|
||||
</span>
|
||||
{hasToken && (
|
||||
<button
|
||||
onClick={() => {
|
||||
clearToken();
|
||||
setHasToken(false);
|
||||
}}
|
||||
className="rounded-md border px-3 py-1 hover:bg-zinc-100"
|
||||
>
|
||||
退出
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
);
|
||||
}
|
||||
在新工单中引用
屏蔽一个用户