e3895d30b4
Site admins can now temporarily view the site as a player, tournament admin, or club admin to understand the experience for each role. Changes: - Added RoleSwitcher context and provider for client-side role simulation - Added role switcher dropdown in Navigation (visible only to site admins) - Added yellow banner showing current viewing-as role with reset button - Navigation links and wordmark href now use effective role for conditional display - Added BDD feature file with 5 scenarios covering all role transitions - Added step definitions for site admin login and role switcher interactions The view-as feature is purely UI-level - server-side permissions remain unchanged. 5 scenarios, 27 steps, all passing.
193 lines
7.4 KiB
TypeScript
193 lines
7.4 KiB
TypeScript
"use client"
|
|
|
|
import Link from "next/link"
|
|
import { useSession } from "./SessionProvider"
|
|
import { authClient } from "@/lib/auth-client"
|
|
import { useEffect, useState } from "react"
|
|
import { useRoleSwitcher } from "./RoleSwitcher"
|
|
|
|
export default function Navigation() {
|
|
const { session, loading } = useSession()
|
|
const [userRole, setUserRole] = useState<string | null>(null)
|
|
const { viewAsRole, setViewAsRole, effectiveRole } = useRoleSwitcher()
|
|
|
|
useEffect(() => {
|
|
const fetchUserRole = async () => {
|
|
const userId = (session?.user as { id?: string })?.id
|
|
if (userId) {
|
|
try {
|
|
const response = await fetch(`/api/users/${userId}/role`)
|
|
if (response.ok) {
|
|
const data = await response.json()
|
|
setUserRole(data.role)
|
|
} else {
|
|
setUserRole(null)
|
|
}
|
|
} catch {
|
|
setUserRole(null)
|
|
}
|
|
} else {
|
|
setUserRole(null)
|
|
}
|
|
}
|
|
|
|
fetchUserRole()
|
|
}, [session])
|
|
|
|
const handleLogout = async () => {
|
|
setViewAsRole(null)
|
|
await authClient.signOut()
|
|
window.location.href = '/auth/login'
|
|
}
|
|
|
|
const displayRole = effectiveRole || userRole
|
|
const isSiteAdmin = userRole === "site_admin"
|
|
|
|
const wordmarkHref = session
|
|
? (displayRole === "club_admin" || displayRole === "site_admin")
|
|
? "/admin"
|
|
: "/rankings"
|
|
: "/"
|
|
|
|
const roleLabels: Record<string, string> = {
|
|
player: "Player",
|
|
tournament_admin: "Tournament Admin",
|
|
club_admin: "Club Admin",
|
|
site_admin: "Site Admin",
|
|
}
|
|
|
|
return (
|
|
<>
|
|
{viewAsRole && (
|
|
<div className="bg-yellow-50 border-b border-yellow-200 px-4 py-2">
|
|
<div className="max-w-7xl mx-auto flex items-center justify-between">
|
|
<p className="text-sm text-yellow-800">
|
|
<span className="font-medium">Viewing as {roleLabels[viewAsRole]}</span>
|
|
{" "}— you are seeing what a {roleLabels[viewAsRole]?.toLowerCase()} would see.
|
|
</p>
|
|
<button
|
|
onClick={() => setViewAsRole(null)}
|
|
className="text-sm font-medium text-yellow-800 hover:text-yellow-900 underline"
|
|
data-testid="reset-view-as"
|
|
>
|
|
Reset to Site Admin
|
|
</button>
|
|
</div>
|
|
</div>
|
|
)}
|
|
<nav className="bg-white shadow-sm">
|
|
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
|
<div className="flex justify-between h-16">
|
|
<div className="flex items-center min-w-0 overflow-hidden">
|
|
<Link
|
|
href="/"
|
|
className="text-xl font-bold text-gray-900 no-underline flex-shrink-0"
|
|
>
|
|
EuchreCamp
|
|
</Link>
|
|
<div className="hidden md:ml-6 md:flex md:space-x-8 min-w-0 overflow-hidden">
|
|
<Link
|
|
href="/rankings"
|
|
className="border-transparent text-gray-500 hover:text-gray-700 inline-flex items-center px-1 pt-1 border-b-2 text-sm font-medium"
|
|
>
|
|
Rankings
|
|
</Link>
|
|
{session && (
|
|
<>
|
|
<Link
|
|
href="/admin/tournaments"
|
|
className="border-transparent text-gray-500 hover:text-gray-700 inline-flex items-center px-1 pt-1 border-b-2 text-sm font-medium"
|
|
>
|
|
Tournaments
|
|
</Link>
|
|
{(displayRole === "club_admin" || displayRole === "site_admin") && (
|
|
<>
|
|
<Link
|
|
href="/admin"
|
|
className="border-transparent text-gray-500 hover:text-gray-700 inline-flex items-center px-1 pt-1 border-b-2 text-sm font-medium"
|
|
>
|
|
Admin
|
|
</Link>
|
|
<Link
|
|
href="/admin/matches"
|
|
className="border-transparent text-gray-500 hover:text-gray-700 inline-flex items-center px-1 pt-1 border-b-2 text-sm font-medium"
|
|
>
|
|
Matches
|
|
</Link>
|
|
<Link
|
|
href="/admin/players"
|
|
className="border-transparent text-gray-500 hover:text-gray-700 inline-flex items-center px-1 pt-1 border-b-2 text-sm font-medium"
|
|
>
|
|
Players
|
|
</Link>
|
|
<Link
|
|
href="/admin/users"
|
|
className="border-transparent text-gray-500 hover:text-gray-700 inline-flex items-center px-1 pt-1 border-b-2 text-sm font-medium"
|
|
>
|
|
Users
|
|
</Link>
|
|
<Link
|
|
href="/admin/matches/upload"
|
|
className="border-transparent text-gray-500 hover:text-gray-700 inline-flex items-center px-1 pt-1 border-b-2 text-sm font-medium"
|
|
>
|
|
Upload Matches
|
|
</Link>
|
|
</>
|
|
)}
|
|
</>
|
|
)}
|
|
</div>
|
|
</div>
|
|
<div className="flex items-center min-w-0 overflow-hidden space-x-4">
|
|
{isSiteAdmin && (
|
|
<select
|
|
value={viewAsRole || ""}
|
|
onChange={(e) => setViewAsRole(e.target.value ? e.target.value as "player" | "tournament_admin" | "club_admin" : null)}
|
|
className="text-sm border border-gray-300 rounded-md px-2 py-1 bg-white text-gray-700 focus:outline-none focus:ring-green-500 focus:border-green-500"
|
|
data-testid="role-switcher"
|
|
>
|
|
<option value="">Viewing as Site Admin</option>
|
|
<option value="player">View as Player</option>
|
|
<option value="tournament_admin">View as Tournament Admin</option>
|
|
<option value="club_admin">View as Club Admin</option>
|
|
</select>
|
|
)}
|
|
{loading ? (
|
|
<div className="text-gray-500">Loading...</div>
|
|
) : session ? (
|
|
<div className="flex items-center space-x-4">
|
|
<span className="text-gray-700 text-sm font-medium">
|
|
{(session.user as { name?: string; email?: string })?.name ||
|
|
(session.user as { name?: string; email?: string })?.email}
|
|
</span>
|
|
<button
|
|
onClick={handleLogout}
|
|
className="text-gray-500 hover:text-gray-700 text-sm font-medium"
|
|
>
|
|
Sign out
|
|
</button>
|
|
</div>
|
|
) : (
|
|
<div className="flex items-center space-x-4">
|
|
<Link
|
|
href="/auth/login"
|
|
className="text-gray-500 hover:text-gray-700 text-sm font-medium"
|
|
>
|
|
Sign in
|
|
</Link>
|
|
<Link
|
|
href="/auth/register"
|
|
className="bg-green-600 text-white px-3 py-1 rounded-md text-sm font-medium hover:bg-green-700"
|
|
>
|
|
Sign up
|
|
</Link>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</nav>
|
|
</>
|
|
)
|
|
}
|