feat(public_page): allow unauthenticated browsing with auth-gated interactivity

- Add get_optional_user dependency; dashboard returns guest-safe payload
- AuthGuard only redirects /following and /notifications for guests
- Sidebar hides auth-required nav items and shows Sign In/Register for guests
- Dashboard shows trending bills as "Most Popular" for unauthenticated visitors
- FollowButton opens AuthModal instead of acting when not signed in
- Members page pins followed members at the top for quick unfollowing
- useFollows skips API call and invalidates dashboard on follow/unfollow

Authored-By: Jack Levy
This commit is contained in:
Jack Levy
2026-03-01 15:54:54 -05:00
parent 73881b2404
commit ddd74a02d5
9 changed files with 314 additions and 128 deletions

View File

@@ -20,13 +20,13 @@ import { ThemeToggle } from "./ThemeToggle";
import { useAuthStore } from "@/stores/authStore";
const NAV = [
{ href: "/", label: "Dashboard", icon: LayoutDashboard, adminOnly: false },
{ href: "/bills", label: "Bills", icon: FileText, adminOnly: false },
{ href: "/members", label: "Members", icon: Users, adminOnly: false },
{ href: "/topics", label: "Topics", icon: Tags, adminOnly: false },
{ href: "/following", label: "Following", icon: Heart, adminOnly: false },
{ href: "/notifications", label: "Notifications", icon: Bell, adminOnly: false },
{ href: "/settings", label: "Admin", icon: Settings, adminOnly: true },
{ href: "/", label: "Dashboard", icon: LayoutDashboard, adminOnly: false, requiresAuth: false },
{ href: "/bills", label: "Bills", icon: FileText, adminOnly: false, requiresAuth: false },
{ href: "/members", label: "Members", icon: Users, adminOnly: false, requiresAuth: false },
{ href: "/topics", label: "Topics", icon: Tags, adminOnly: false, requiresAuth: false },
{ href: "/following", label: "Following", icon: Heart, adminOnly: false, requiresAuth: true },
{ href: "/notifications", label: "Notifications", icon: Bell, adminOnly: false, requiresAuth: true },
{ href: "/settings", label: "Admin", icon: Settings, adminOnly: true, requiresAuth: false },
];
export function Sidebar({ onClose }: { onClose?: () => void }) {
@@ -34,6 +34,7 @@ export function Sidebar({ onClose }: { onClose?: () => void }) {
const router = useRouter();
const qc = useQueryClient();
const user = useAuthStore((s) => s.user);
const token = useAuthStore((s) => s.token);
const logout = useAuthStore((s) => s.logout);
function handleLogout() {
@@ -55,7 +56,11 @@ export function Sidebar({ onClose }: { onClose?: () => void }) {
</div>
<nav className="flex-1 p-3 space-y-1">
{NAV.filter(({ adminOnly }) => !adminOnly || user?.is_admin).map(({ href, label, icon: Icon }) => {
{NAV.filter(({ adminOnly, requiresAuth }) => {
if (adminOnly && !user?.is_admin) return false;
if (requiresAuth && !token) return false;
return true;
}).map(({ href, label, icon: Icon }) => {
const active = href === "/" ? pathname === "/" : pathname.startsWith(href);
return (
<Link
@@ -77,18 +82,31 @@ export function Sidebar({ onClose }: { onClose?: () => void }) {
</nav>
<div className="p-3 border-t border-border space-y-2">
{user && (
<div className="flex items-center justify-between">
<span className="text-xs text-muted-foreground truncate max-w-[120px]" title={user.email}>
{user.email}
</span>
<button
onClick={handleLogout}
className="p-1 rounded-md text-muted-foreground hover:text-foreground hover:bg-accent"
title="Sign out"
>
<LogOut className="w-3.5 h-3.5" />
</button>
{token ? (
<>
{user && (
<div className="flex items-center justify-between">
<span className="text-xs text-muted-foreground truncate max-w-[120px]" title={user.email}>
{user.email}
</span>
<button
onClick={handleLogout}
className="p-1 rounded-md text-muted-foreground hover:text-foreground hover:bg-accent"
title="Sign out"
>
<LogOut className="w-3.5 h-3.5" />
</button>
</div>
)}
</>
) : (
<div className="flex flex-col gap-2">
<Link href="/register" onClick={onClose} className="w-full px-3 py-1.5 text-sm font-medium text-center rounded-md bg-primary text-primary-foreground hover:bg-primary/90 transition-colors">
Register
</Link>
<Link href="/login" onClick={onClose} className="w-full px-3 py-1.5 text-sm font-medium text-center rounded-md border border-border text-foreground hover:bg-accent transition-colors">
Sign in
</Link>
</div>
)}
<div className="flex items-center justify-between">