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

@@ -2,11 +2,37 @@
import { useState } from "react";
import Link from "next/link";
import { Search } from "lucide-react";
import { useMembers } from "@/lib/hooks/useMembers";
import { Search, Heart } from "lucide-react";
import { useMembers, useMember } from "@/lib/hooks/useMembers";
import { useFollows } from "@/lib/hooks/useFollows";
import { useAuthStore } from "@/stores/authStore";
import { FollowButton } from "@/components/shared/FollowButton";
import { cn, partyBadgeColor } from "@/lib/utils";
function FollowedMemberRow({ bioguideId }: { bioguideId: string }) {
const { data: member } = useMember(bioguideId);
if (!member) return null;
return (
<div className="bg-card border border-border rounded-lg p-4 flex items-start justify-between gap-3">
<div className="flex-1 min-w-0">
<Link href={`/members/${member.bioguide_id}`} className="font-medium text-sm hover:text-primary transition-colors">
{member.name}
</Link>
<div className="flex items-center gap-1.5 mt-1">
{member.party && (
<span className={cn("px-1.5 py-0.5 rounded text-xs font-medium", partyBadgeColor(member.party))}>
{member.party}
</span>
)}
{member.state && <span className="text-xs text-muted-foreground">{member.state}</span>}
{member.chamber && <span className="text-xs text-muted-foreground">{member.chamber}</span>}
</div>
</div>
<FollowButton type="member" value={member.bioguide_id} />
</div>
);
}
export default function MembersPage() {
const [q, setQ] = useState("");
const [chamber, setChamber] = useState("");
@@ -18,6 +44,10 @@ export default function MembersPage() {
page, per_page: 50,
});
const token = useAuthStore((s) => s.token);
const { data: follows } = useFollows();
const followedMemberIds = follows?.filter((f) => f.follow_type === "member").map((f) => f.follow_value) ?? [];
return (
<div className="space-y-6">
<div>
@@ -51,6 +81,21 @@ export default function MembersPage() {
</select>
</div>
{token && followedMemberIds.length > 0 && (
<div className="space-y-3">
<h2 className="font-semibold text-sm flex items-center gap-2">
<Heart className="w-4 h-4 text-red-500 fill-red-500" />
Following ({followedMemberIds.length})
</h2>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3">
{followedMemberIds.map((id) => (
<FollowedMemberRow key={id} bioguideId={id} />
))}
</div>
<div className="border-t border-border pt-2" />
</div>
)}
{isLoading ? (
<div className="text-center py-20 text-muted-foreground">Loading members...</div>
) : (