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:
@@ -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>
|
||||
) : (
|
||||
|
||||
@@ -1,14 +1,17 @@
|
||||
"use client";
|
||||
|
||||
import { TrendingUp, BookOpen, RefreshCw } from "lucide-react";
|
||||
import { TrendingUp, BookOpen, Flame, RefreshCw } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { useDashboard } from "@/lib/hooks/useDashboard";
|
||||
import { BillCard } from "@/components/shared/BillCard";
|
||||
import { adminAPI } from "@/lib/api";
|
||||
import { useState } from "react";
|
||||
import { useAuthStore } from "@/stores/authStore";
|
||||
|
||||
export default function DashboardPage() {
|
||||
const { data, isLoading, refetch } = useDashboard();
|
||||
const [polling, setPolling] = useState(false);
|
||||
const token = useAuthStore((s) => s.token);
|
||||
|
||||
const triggerPoll = async () => {
|
||||
setPolling(true);
|
||||
@@ -43,15 +46,38 @@ export default function DashboardPage() {
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4 md:gap-8">
|
||||
<div className="md:col-span-2 space-y-4">
|
||||
<h2 className="font-semibold flex items-center gap-2">
|
||||
<BookOpen className="w-4 h-4" />
|
||||
Your Feed
|
||||
{data?.follows && (
|
||||
{token ? <BookOpen className="w-4 h-4" /> : <Flame className="w-4 h-4" />}
|
||||
{token ? "Your Feed" : "Most Popular"}
|
||||
{token && data?.follows && (
|
||||
<span className="text-xs text-muted-foreground font-normal">
|
||||
({data.follows.bills} bills · {data.follows.members} members · {data.follows.topics} topics)
|
||||
</span>
|
||||
)}
|
||||
</h2>
|
||||
{!data?.feed?.length ? (
|
||||
{!token ? (
|
||||
<div className="space-y-3">
|
||||
<div className="rounded-lg border border-dashed px-4 py-3 flex items-center justify-between gap-4">
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Sign in to personalise this feed with bills and members you follow.
|
||||
</p>
|
||||
<div className="flex gap-2 shrink-0">
|
||||
<Link href="/register" className="px-3 py-1.5 text-xs font-medium rounded-md bg-primary text-primary-foreground hover:bg-primary/90 transition-colors">
|
||||
Register
|
||||
</Link>
|
||||
<Link href="/login" className="px-3 py-1.5 text-xs font-medium rounded-md border border-border text-foreground hover:bg-accent transition-colors">
|
||||
Sign in
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
{data?.trending?.length ? (
|
||||
<div className="space-y-3">
|
||||
{data.trending.map((bill) => (
|
||||
<BillCard key={bill.bill_id} bill={bill} />
|
||||
))}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
) : !data?.feed?.length ? (
|
||||
<div className="bg-card border border-border rounded-lg p-8 text-center text-muted-foreground">
|
||||
<p className="text-sm">Your feed is empty.</p>
|
||||
<p className="text-xs mt-1">Follow bills, members, or topics to see activity here.</p>
|
||||
|
||||
Reference in New Issue
Block a user