Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
111 changes: 97 additions & 14 deletions frontend/app/dashboard/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,12 @@ import Link from "next/link";
import { useQuery, useConvexAuth } from "convex/react";
import { useUser, useClerk } from "@clerk/nextjs";
import { api } from "@/convex/_generated/api";
import type { UserResource } from "@clerk/types";
import {
DatasetCard,
type DatasetCardData,
} from "@/components/dataset/DatasetCard";
import { ThemeToggle } from "@/components/ThemeToggle";
import { useTheme } from "@/components/ThemeToggle";
import { QuotaBadge } from "@/components/QuotaBadge";
import { EVENTS, track } from "@/lib/analytics";

Expand Down Expand Up @@ -85,19 +86,7 @@ export default function DashboardPage() {
<div className="flex items-center gap-4">
<QuotaBadge />
<div className="w-px h-4 bg-border" />
<ThemeToggle />
<div className="w-px h-4 bg-border" />
{/* PII: mask the email in session replays */}
<span data-ph-mask-text="true" className="text-xs text-muted">
{user?.primaryEmailAddress?.emailAddress}
</span>
<div className="w-px h-4 bg-border" />
<button
onClick={() => signOut()}
className="text-xs text-muted hover:text-foreground transition-colors"
>
Sign out
</button>
<ProfileMenu user={user} onSignOut={() => signOut()} />
</div>
</header>

Expand Down Expand Up @@ -257,3 +246,97 @@ function Section({
</div>
);
}

function ProfileMenu({
user,
onSignOut,
}: {
user: UserResource | null | undefined;
onSignOut: () => void;
}) {
const [open, setOpen] = useState(false);
const menuRef = useRef<HTMLDivElement>(null);

useEffect(() => {
if (!open) return;
function handleClick(e: MouseEvent) {
if (menuRef.current && !menuRef.current.contains(e.target as Node))
setOpen(false);
}
document.addEventListener("mousedown", handleClick);
return () => document.removeEventListener("mousedown", handleClick);
}, [open]);

const { theme, toggle: toggleTheme } = useTheme();
const name = user?.fullName || user?.firstName || "User";
const email = user?.primaryEmailAddress?.emailAddress;
const imageUrl = user?.imageUrl;

return (
<div ref={menuRef} className="relative">
<button
type="button"
onClick={() => setOpen((o) => !o)}
aria-haspopup="menu"
aria-expanded={open}
aria-controls="profile-menu"
className="flex items-center gap-1.5 rounded-full py-0.5 pl-0.5 pr-2 hover:bg-foreground/[0.05] transition-colors"
>
{imageUrl ? (
<img
src={imageUrl}
alt=""
className="h-6 w-6 rounded-full object-cover"
/>
) : (
<div className="h-6 w-6 rounded-full bg-foreground/10 flex items-center justify-center text-[11px] font-medium text-foreground">
{name[0]?.toUpperCase()}
</div>
)}
<span className="text-xs font-medium text-foreground">{name}</span>
</button>

{open && (
<div id="profile-menu" role="menu" className="absolute right-0 top-full mt-1.5 w-52 rounded-xl border border-border bg-surface shadow-xl ring-1 ring-black/[0.04] z-50 overflow-hidden">
<div className="px-3 py-2 border-b border-border">
Comment thread
coderabbitai[bot] marked this conversation as resolved.
<p className="text-xs font-medium text-foreground truncate">{name}</p>
{email && (
<p data-ph-mask-text="true" className="text-[11px] text-muted truncate mt-0.5">
{email}
</p>
)}
</div>
<div className="p-1">
<button
onClick={toggleTheme}
className="w-full flex items-center justify-between px-2 py-1.5 rounded-lg text-xs text-foreground hover:bg-foreground/[0.05] transition-colors"
>
<span>Dark mode</span>
<span
className={`relative inline-flex h-4 w-7 shrink-0 items-center rounded-full transition-colors ${theme === "dark" ? "bg-foreground" : "bg-foreground/20"}`}
>
<span
className={`inline-block h-3 w-3 rounded-full bg-surface transition-transform ${theme === "dark" ? "translate-x-3.5" : "translate-x-0.5"}`}
/>
</span>
</button>
<button
onClick={() => {
setOpen(false);
onSignOut();
}}
className="w-full flex items-center gap-2 px-2 py-1.5 rounded-lg text-xs text-red-500 hover:bg-red-500/[0.08] transition-colors"
>
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.75" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
<path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4" />
<polyline points="16 17 21 12 16 7" />
<line x1="21" y1="12" x2="9" y2="12" />
</svg>
Sign out
</button>
</div>
</div>
)}
</div>
);
}
Loading