Skip to content
Draft
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
69 changes: 62 additions & 7 deletions app/src/components/Navbar.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Menu, Search, User, X } from "lucide-react";
import { Menu, Search, ShieldCogCorner, User, X } from "lucide-react";
import { Link, useNavigate } from "@tanstack/react-router";
import { useState } from "react";

Expand All @@ -16,25 +16,37 @@ import {
import { Separator } from "@/components/ui/separator";
import { Logo } from "@/components/Logo";

// `roles`, when set, hides the item from anyone holding none of them.
const navItems: Array<{ label: string; to: string; roles?: Array<string> }> = [
const navItems: Array<{ label: string; to: string }> = [
{ label: "Browse", to: "/browse" },
{ label: "Subjects", to: "/subjects" },
{ label: "Objectives", to: "/objectives" },
{ label: "Todo", to: "/todos" },
{ label: "Review", to: "/review", roles: ["verifier", "admin"] },
];

// Profile isn't here because its link needs the signed-in username.
const profileItems = [{ label: "Settings", to: "/settings" }];

// `roles`, when set, hides the item from anyone holding none of them.
const sepcialRoleItems: Array<{
label: string;
to: string;
roles?: Array<string>;
}> = [
{
label: "Review Queue",
to: "/review",
roles: ["verifier", "lead-verifier", "admin"],
},
{ label: "Dashboard", to: "/dashboard", roles: ["lead-verifier", "admin"] },
];

export function Navbar() {
const [mobileOpen, setMobileOpen] = useState(false);
const [query, setQuery] = useState("");
const { session, roles, currentProfile } = useAuth();
const navigate = useNavigate();

const visibleNavItems = navItems.filter(
const visibleRoleItems = sepcialRoleItems.filter(
(item) => !item.roles || item.roles.some((r) => roles.includes(r))
);

Expand Down Expand Up @@ -62,7 +74,7 @@ export function Navbar() {
<div className="flex items-center gap-10">
<Logo />
<nav className="hidden items-center gap-6 md:flex">
{visibleNavItems.map((item) => (
{navItems.map((item) => (
<Link
key={item.to}
to={item.to}
Expand Down Expand Up @@ -98,6 +110,32 @@ export function Navbar() {
</Link>
</div>

{roles.length > 0 && (
<div className="hidden md:block">
<DropdownMenu modal={false}>
<DropdownMenuTrigger asChild>
<Button
variant="ghost"
size="icon"
className="h-9 w-9 rounded-md"
>
<ShieldCogCorner className="h-4 w-4" />
</Button>
</DropdownMenuTrigger>

<DropdownMenuContent align="end" className="w-48 font-mono">
{visibleRoleItems.map((item) => (
<DropdownMenuItem key={item.to} asChild>
<Link to={item.to} className="text-xs">
{item.label}
</Link>
</DropdownMenuItem>
))}
</DropdownMenuContent>
</DropdownMenu>
</div>
)}

{session ? (
/* Desktop Profile Dropdown */
<div className="hidden md:block">
Expand Down Expand Up @@ -189,7 +227,7 @@ export function Navbar() {

{/* Nav */}
<div className="flex flex-col gap-3 py-3">
{visibleNavItems.map((item) => (
{navItems.map((item) => (
<Link
key={item.to}
to={item.to}
Expand All @@ -213,6 +251,23 @@ export function Navbar() {

<Separator />

{roles.length > 0 && (
<>
{visibleRoleItems.map((item) => (
<Link
key={item.to}
to={item.to}
onClick={() => setMobileOpen(false)}
className="py-2 font-mono text-sm text-muted-foreground uppercase hover:text-foreground"
>
{item.label}
</Link>
))}
</>
)}

<Separator />

{session ? (
<>
{currentProfile && (
Expand Down
21 changes: 21 additions & 0 deletions app/src/components/icons/UserRoundArrowLeft.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
export const UserRoundArrowLeft = () => {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
className="lucide lucide-user-round-arrow-left-icon lucide-user-round-arrow-left"
>
<path d="m19 16-3 3" />
<path d="M2 21a8 8 0 0 1 12.664-6.5" />
<path d="M22 19h-6l3 3" />
<circle cx="10" cy="8" r="5" />
</svg>
);
};
113 changes: 113 additions & 0 deletions app/src/components/sidebar/DashboardSidebar.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
import { Link, useRouterState } from "@tanstack/react-router";
import { FileCheckCorner, Shield, Users } from "lucide-react";
import type { LucideProps } from "lucide-react";
import type { ForwardRefExoticComponent, RefAttributes } from "react";
import { cn } from "@/lib/utils";
import { Separator } from "@/components/ui/separator";
import { useAuth } from "@/lib/authContext";

const items: Array<{
label: string;
to: string;
icon: ForwardRefExoticComponent<
Omit<LucideProps, "ref"> & RefAttributes<SVGSVGElement>
>;
roles?: Array<string>;
}> = [
{
label: "Members",
to: "/dashboard/members",
icon: Users,
roles: ["admin"],
},
{
label: "Roles",
to: "/dashboard/roles",
icon: Shield,
roles: ["admin"],
},
{
label: "Verifier Assignments",
to: "/dashboard/assignments",
icon: FileCheckCorner,
roles: ["lead-verifier", "admin"],
},
];

export const DashboardSidebar = () => {
const pathname = useRouterState({
select: (state) => state.location.pathname,
});

const { roles } = useAuth();

const isAdmin = roles.includes("admin");
const isLeadVerifier = roles.includes("lead-verifier");

const visibleItems = items.filter(
(item) => !item.roles || item.roles.some((r) => roles.includes(r))
);

return (
<aside className="sticky top-[65px] hidden max-h-[calc(100vh-65px)] w-64 shrink-0 self-start overflow-y-auto px-6 py-6 md:block">
<div className="mb-6">
<h2 className="font-mono text-[12px] tracking-[0.08em] text-muted-foreground uppercase">
{isAdmin ? "Admin " : isLeadVerifier && "Lead Verifier "}Dashboard
</h2>
</div>

<ul>
{visibleItems.map((item) => {
const active = pathname === item.to;

return (
<li key={item.to}>
<Link
to={item.to}
className={cn(
"data-label flex items-center gap-4 px-2 py-4 hover:font-bold hover:text-brand-bright-blue",
active && "!font-bold !text-brand-bright-blue"
)}
>
<item.icon className="h-4 w-4" />
{item.label}
</Link>
<Separator />
</li>
);
})}
</ul>
</aside>
);
};

export const DashboardTabs = () => {
const pathname = useRouterState({
select: (state) => state.location.pathname,
});

return (
<nav className="-mx-4 mb-6 overflow-x-auto border-b px-4 sm:-mx-8 sm:px-8 md:hidden">
<ul className="flex w-max items-center gap-6">
{items.map((item) => {
const active = pathname === item.to;

return (
<li key={item.to}>
<Link
to={item.to}
className={cn(
"data-label flex shrink-0 items-center gap-2 border-b-2 border-transparent py-3 whitespace-nowrap",
active && "border-primary font-bold text-primary"
)}
>
<item.icon className="h-4 w-4" />
{item.label}
</Link>
</li>
);
})}
</ul>
</nav>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import {
ChoiceColumnFilter,
DateColumnFilter,
TextColumnFilter,
} from "@/components/ActivityColumnFilters";
} from "@/components/tables/ActivityColumnFilters";
import {
Table,
TableBody,
Expand Down
Loading
Loading