Spaces:
Running
Running
| import { useEffect, useRef, useState } from "react"; | |
| import { useNavigate } from "react-router-dom"; | |
| import { Github, LogOut, ChevronDown, FolderGit2 } from "lucide-react"; | |
| import { navigateToLogin, navigateToLoginPopup, isEmbedded } from "../shared/api/client"; | |
| import { useLogout, useMe } from "../features/auth/useAuth"; | |
| /** | |
| * 顶栏登录态:未登录 = Sign in with GitHub;已登录 = 头像 + 下拉(Logout)。 | |
| * 浅色体系统一样式;appearance prop 仅为调用方兼容保留。 | |
| */ | |
| export function AuthButton({ appearance = "light" }: { appearance?: "light" | "dark" }) { | |
| void appearance; | |
| const meQ = useMe(); | |
| const logoutM = useLogout(); | |
| const nav = useNavigate(); | |
| const [open, setOpen] = useState(false); | |
| const rootRef = useRef<HTMLDivElement>(null); | |
| useEffect(() => { | |
| if (!open) return; | |
| const onDocDown = (e: MouseEvent) => { | |
| if (rootRef.current && !rootRef.current.contains(e.target as Node)) setOpen(false); | |
| }; | |
| const onKey = (e: KeyboardEvent) => { | |
| if (e.key === "Escape") setOpen(false); | |
| }; | |
| document.addEventListener("mousedown", onDocDown); | |
| document.addEventListener("keydown", onKey); | |
| return () => { | |
| document.removeEventListener("mousedown", onDocDown); | |
| document.removeEventListener("keydown", onKey); | |
| }; | |
| }, [open]); | |
| // 加载中/后端未接入 auth:按未登录渲染(不阻塞页面) | |
| const user = meQ.data?.authenticated ? meQ.data.user : null; | |
| if (!user) { | |
| return ( | |
| <button | |
| type="button" | |
| onClick={() => { | |
| if (isEmbedded()) { | |
| navigateToLoginPopup(); | |
| window.dispatchEvent(new Event("ov-oauth-popup-opened")); | |
| } else { | |
| navigateToLogin(); | |
| } | |
| }} | |
| className="inline-flex h-9 items-center gap-2 rounded-full bg-ink px-4 text-[13px] font-medium text-surface transition hover:bg-white focus-ring" | |
| > | |
| <Github size={14} /> | |
| Sign in | |
| </button> | |
| ); | |
| } | |
| return ( | |
| <div ref={rootRef} className="relative"> | |
| <button | |
| type="button" | |
| onClick={() => setOpen((v) => !v)} | |
| aria-expanded={open} | |
| aria-haspopup="menu" | |
| aria-label={`Signed in as ${user.login}`} | |
| className="flex h-9 items-center gap-1.5 rounded-full border border-line bg-surface-raised p-1 pr-2 transition-colors hover:border-line-strong hover:bg-surface-sunken focus-ring" | |
| > | |
| {user.avatar_url ? ( | |
| <img | |
| src={user.avatar_url} | |
| alt="" | |
| className="h-7 w-7 rounded-full object-cover" | |
| referrerPolicy="no-referrer" | |
| /> | |
| ) : ( | |
| <span className="flex h-7 w-7 items-center justify-center rounded-full bg-accent-100 text-xs font-semibold text-accent-700"> | |
| {user.login.slice(0, 1).toUpperCase()} | |
| </span> | |
| )} | |
| <ChevronDown | |
| size={13} | |
| className={`text-ink-tertiary transition-transform ${open ? "rotate-180" : ""}`} | |
| /> | |
| </button> | |
| {open && ( | |
| <div | |
| role="menu" | |
| className="absolute right-0 top-full z-40 mt-2 w-44 overflow-hidden rounded-xl border border-line bg-surface-raised shadow-[0_16px_48px_-12px_rgba(0,0,0,0.7)]" | |
| > | |
| <div className="border-b border-line px-3.5 py-2.5 text-[13px] font-medium text-ink"> | |
| {user.login} | |
| </div> | |
| <a | |
| role="menuitem" | |
| href="/my" | |
| onClick={(e) => { | |
| e.preventDefault(); | |
| setOpen(false); | |
| nav("/my"); | |
| }} | |
| className="flex w-full items-center gap-2 px-3.5 py-2.5 text-left text-[13px] text-ink-secondary transition-colors hover:bg-surface-sunken hover:text-ink" | |
| > | |
| <FolderGit2 size={14} /> | |
| My submissions | |
| </a> | |
| <button | |
| type="button" | |
| role="menuitem" | |
| onClick={() => { | |
| setOpen(false); | |
| logoutM.mutate(); | |
| }} | |
| disabled={logoutM.isPending} | |
| className="flex w-full items-center gap-2 px-3.5 py-2.5 text-left text-[13px] text-ink-secondary transition-colors hover:bg-surface-sunken hover:text-ink" | |
| > | |
| <LogOut size={14} /> | |
| {logoutM.isPending ? "Signing out…" : "Sign out"} | |
| </button> | |
| </div> | |
| )} | |
| </div> | |
| ); | |
| } | |