Skip to content
Merged
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
50 changes: 40 additions & 10 deletions apps/web/src/components/bias-indicator.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@ function validateThresholds(raw: unknown): number[] {
) {
return DEFAULT_THRESHOLDS;
}
// Ensure ascending order for the first 4 values
const sorted = (raw.slice(0, 4) as number[]).sort((a, b) => a - b);
return sorted;
}
Expand Down Expand Up @@ -48,29 +47,60 @@ const BiasIndicator = ({
return "Right";
};

// Determine text color
const getTextColor = (biasValue: number) => {
if (biasValue < thresholds[0]) return "text-bias-left";
if (biasValue < thresholds[1]) return "text-bias-left-muted";
if (biasValue <= thresholds[2]) return "text-bias-center";
if (biasValue <= thresholds[3]) return "text-bias-right-muted";
return "text-bias-right";
};

const sizeClasses = {
sm: "h-1.5 w-16",
md: "h-2 w-24",
lg: "h-3 w-32",
sm: "h-1.5 w-20",
md: "h-2 w-28",
lg: "h-2.5 w-36",
};

const dotSizeClasses = {
sm: "h-2.5 w-2.5",
md: "h-3 w-3",
lg: "h-4 w-4",
sm: "size-3",
md: "size-4",
lg: "size-5",
};

const textSizeClasses = {
sm: "text-xs",
md: "text-xs",
lg: "text-sm",
};

return (
<div className="flex items-center gap-2">
<div
className={`relative rounded-full bg-bias-track ${sizeClasses[size]}`}
role="meter"
aria-valuenow={bias}
aria-valuemin={-5}
aria-valuemax={5}
aria-label={`Bias: ${getLabel(bias)}`}
>
{/* Gradient track for visual interest */}
<div className="absolute inset-0 rounded-full overflow-hidden opacity-40">
<div className="absolute inset-0 bg-linear-to-r from-bias-left via-bias-center to-bias-right" />
</div>

{/* Indicator dot */}
<div
className={`absolute top-1/2 -translate-y-1/2 rounded-full border-2 border-background ${getColor(bias)} ${dotSizeClasses[size]}`}
style={{ left: `${position}%`, transform: "translate(-50%, -50%)" }}
className={`absolute top-1/2 rounded-full border-2 border-card shadow-sm ${getColor(bias)} ${dotSizeClasses[size]}`}
style={{
left: `${Math.max(0, Math.min(100, position))}%`,
transform: "translate(-50%, -50%)",
}}
/>
</div>
<span className="text-xs font-medium text-muted-foreground">
<span
className={`font-medium ${textSizeClasses[size]} ${getTextColor(bias)}`}
>
{getLabel(bias)}
</span>
</div>
Expand Down
20 changes: 18 additions & 2 deletions apps/web/src/components/bookmark-button.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -84,19 +84,35 @@ export default function BookmarkButton({
);

const bookmarked = isBookmarked === true;
const iconSize = size === "sm" ? "size-4" : "size-5";
const buttonSize = size === "sm" ? "size-8" : "size-9";

return (
<Button
type="button"
variant="ghost"
size="icon"
className={cn("shrink-0", bookmarked && "text-primary", className)}
className={cn(
"shrink-0 rounded-full transition-all",
buttonSize,
bookmarked
? "text-primary bg-primary/10 hover:bg-primary/20"
: "text-muted-foreground hover:text-primary hover:bg-primary/10",
className
)}
disabled={toggle.isPending}
onClick={handleClick}
aria-label={bookmarked ? "Remove bookmark" : "Bookmark this event"}
aria-pressed={bookmarked}
>
<Bookmark className={cn("size-4", bookmarked && "fill-current")} />
<Bookmark
className={cn(
iconSize,
"transition-transform",
bookmarked && "fill-current scale-110",
toggle.isPending && "animate-pulse"
)}
/>
</Button>
);
}
2 changes: 1 addition & 1 deletion apps/web/src/components/feed/event-card.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ const EventCard = ({
type="button"
variant="outline"
size="sm"
className="h-7 rounded-full border-border/80 bg-background/70 px-3 text-xs"
className="h-7 rounded-full border-border/80 bg-background/70 px-3 text-xs pointer-events-none"
>
{topic}
</Button>
Expand Down
131 changes: 84 additions & 47 deletions apps/web/src/components/header.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { useState } from "react";
import { Link } from "@tanstack/react-router";
import { Menu } from "lucide-react";
import { Link, useRouterState } from "@tanstack/react-router";
import { Menu, Newspaper, Bookmark, LayoutDashboard, Home } from "lucide-react";
import { Button } from "@/components/ui/button";
import {
Sheet,
Expand All @@ -10,65 +10,102 @@ import {
} from "@/components/ui/sheet";

const links = [
{ to: "/", label: "Home" },
{ to: "/feed", label: "Feed" },
{ to: "/bookmarks", label: "Bookmarks" },
{ to: "/dashboard", label: "Dashboard" },
{ to: "/", label: "Home", icon: Home },
{ to: "/feed", label: "Feed", icon: Newspaper },
{ to: "/bookmarks", label: "Bookmarks", icon: Bookmark },
{ to: "/dashboard", label: "Dashboard", icon: LayoutDashboard },
] as const;

export default function Header() {
const [open, setOpen] = useState(false);
const routerState = useRouterState();
const currentPath = routerState.location.pathname;

return (
<header>
<div className="flex items-center justify-between px-6 py-4">
<Link to="/" className="text-xl font-bold">
Biviant
</Link>
<header className="sticky top-0 z-50 bg-background/80 backdrop-blur-lg border-b border-border">
<div className="container mx-auto max-w-6xl">
<div className="flex items-center justify-between px-4 h-16">
{/* Logo */}
<Link to="/" className="flex items-center gap-2 group">
<div className="relative flex items-center justify-center size-9 rounded-lg bg-primary text-primary-foreground font-bold text-lg transition-transform group-hover:scale-105">
B
<div className="absolute inset-0 rounded-lg bg-primary/20 blur-md -z-10" />
</div>
<span className="text-xl font-bold tracking-tight">Biviant</span>
</Link>

{/* Desktop nav */}
<nav aria-label="Primary" className="hidden md:flex gap-6">
{links.map(({ to, label }) => (
<Link
key={to}
to={to}
className="text-sm font-medium hover:text-primary transition-colors"
>
{label}
</Link>
))}
</nav>

{/* Mobile burger */}
<Sheet open={open} onOpenChange={setOpen}>
<SheetTrigger asChild>
<Button
variant="ghost"
size="icon"
className="md:hidden"
aria-label="Open menu"
>
<Menu className="size-5" />
</Button>
</SheetTrigger>
<SheetContent side="right" className="w-64">
<SheetTitle className="text-lg font-bold mb-6">Menu</SheetTitle>
<nav aria-label="Mobile" className="flex flex-col gap-4">
{links.map(({ to, label }) => (
{/* Desktop nav */}
<nav aria-label="Primary" className="hidden md:flex items-center gap-1">
{links.map(({ to, label, icon: Icon }) => {
const isActive = to === "/" ? currentPath === "/" : currentPath.startsWith(to);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🌐 Web query:

TanStack Router useMatchRoute vs useRouterState pathname for active link styling

💡 Result:

For active link styling in TanStack Router, prefer the built-in Link component's activeProps and activeOptions over manual hooks. useMatchRoute is the recommended hook for custom logic or non-Link elements, while useRouterState({ select: (s) => s.location.pathname }) provides raw pathname access but lacks route-aware matching. useMatchRoute matches the current (or pending) location against a route pattern, returning params or false. It's ideal for complex matching like fuzzy parent/child routes or specific params. Example with useMatchRoute for custom nav: const matchRoute = useMatchRoute; const isActive = !!matchRoute({ to: '/dashboard', fuzzy: true }); useRouterState pathname is simpler but requires manual string comparison (e.g., startsWith), missing type safety, params, search/hash options, and fuzzy matching. Link handles matching internally (prefix by default, configurable via activeOptions={exact: true, etc.}), applying activeProps or data-status='active'. <Link to='/posts' activeProps={{ className: 'active' }} activeOptions={{ exact: true }}> Posts Use hooks only for custom components needing granular control (e.g., different icon/text styles). Link suffices for most cases, being optimized and typesafe.

Citations:


🏁 Script executed:

# First, locate and examine the header.tsx file
find apps/web -name "header.tsx" -type f

Repository: flvvius/news

Length of output: 91


🏁 Script executed:

# Also search for route definitions to understand what routes exist
fd -t f -e ts -e tsx | xargs grep -l "router.createRootRoute\|createRoute\|'/[a-z]" | head -20

Repository: flvvius/news

Length of output: 110


🏁 Script executed:

# Look for TanStack Router configuration
find . -name "*route*" -o -name "*router*" | grep -E "\.(ts|tsx|config)$" | head -20

Repository: flvvius/news

Length of output: 114


🏁 Script executed:

# Examine header.tsx to see the code at lines 40 and 84
wc -l apps/web/src/components/header.tsx

Repository: flvvius/news

Length of output: 95


🏁 Script executed:

# Read the relevant sections of header.tsx
sed -n '1,100p' apps/web/src/components/header.tsx | cat -n

Repository: flvvius/news

Length of output: 4988


🏁 Script executed:

# Check the route structure
head -100 apps/web/src/router.tsx | cat -n

Repository: flvvius/news

Length of output: 2069


🏁 Script executed:

# Check the route definitions in routeTree.gen.ts
cat apps/web/src/routeTree.gen.ts

Repository: flvvius/news

Length of output: 5737


🏁 Script executed:

# Also check if there are any other route files that might define additional routes
find apps/web/src -name "*route*" -type f | head -20

Repository: flvvius/news

Length of output: 110


🏁 Script executed:

# Check if there are more routes or if event/$slug has nested routes
find apps/web/src/routes -type f | sort

Repository: flvvius/news

Length of output: 320


🏁 Script executed:

# Verify the current routes to understand if there could be conflicts
ls -la apps/web/src/routes/

Repository: flvvius/news

Length of output: 664


startsWith(to) can over-match future sibling routes.

currentPath.startsWith("/feed") would also activate for a hypothetical /feedback. Today's routes don't conflict, but the safer pattern is currentPath === to || currentPath.startsWith(${to}/), which only matches the exact route or its descendants. The same expression is duplicated for desktop and mobile nav, so extracting it into a helper avoids drift.

Alternatively, use TanStack Router's Link component with activeOptions={{ exact: true }} or useMatchRoute() for custom active-state logic, both of which handle route matching idiomatically.

Proposed fix
-          const isActive = to === "/" ? currentPath === "/" : currentPath.startsWith(to);
+          const isActive =
+            to === "/"
+              ? currentPath === "/"
+              : currentPath === to || currentPath.startsWith(`${to}/`);

Or extract as a helper:

+const isLinkActive = (currentPath: string, to: string) =>
+  to === "/"
+    ? currentPath === "/"
+    : currentPath === to || currentPath.startsWith(`${to}/`);

Also applies to: 84-84

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/web/src/components/header.tsx` at line 40, The current active-check uses
currentPath.startsWith(to) which can false-positive match siblings; replace that
logic with a small helper (e.g., isNavActive or isActivePath) that returns
currentPath === to || currentPath.startsWith(`${to}/`) (and keep the special
root case when to === "/"), and use that helper for both desktop and mobile navs
to avoid duplication and drift; alternatively, swap to TanStack Router's Link
with activeOptions or useMatchRoute if preferred.

return (
<Link
key={to}
to={to}
className="text-base font-medium hover:text-primary transition-colors"
onClick={() => setOpen(false)}
className={`
flex items-center gap-2 px-4 py-2 rounded-lg text-sm font-medium transition-all
${isActive
? "bg-primary/10 text-primary"
: "text-muted-foreground hover:text-foreground hover:bg-muted/50"
}
`}
>
<Icon className="size-4" />
{label}
</Link>
))}
</nav>
</SheetContent>
</Sheet>
);
})}
</nav>

{/* Mobile burger */}
<Sheet open={open} onOpenChange={setOpen}>
<SheetTrigger asChild>
<Button
variant="ghost"
size="icon"
className="md:hidden"
aria-label="Open menu"
>
<Menu className="size-5" />
</Button>
</SheetTrigger>
<SheetContent side="right" className="w-72 p-0">
<div className="flex flex-col h-full">
<div className="p-6 border-b border-border">
<SheetTitle className="flex items-center gap-2">
<div className="flex items-center justify-center size-8 rounded-lg bg-primary text-primary-foreground font-bold">
B
</div>
<span className="font-bold">Biviant</span>
</SheetTitle>
</div>
<nav aria-label="Mobile" className="flex flex-col gap-1 p-4">
{links.map(({ to, label, icon: Icon }) => {
const isActive = to === "/" ? currentPath === "/" : currentPath.startsWith(to);
return (
<Link
key={to}
to={to}
className={`
flex items-center gap-3 px-4 py-3 rounded-lg text-base font-medium transition-all
${isActive
? "bg-primary/10 text-primary"
: "text-muted-foreground hover:text-foreground hover:bg-muted/50"
}
`}
onClick={() => setOpen(false)}
>
<Icon className="size-5" />
{label}
</Link>
);
})}
</nav>
</div>
</SheetContent>
</Sheet>
</div>
</div>
<hr />
</header>
);
}
Loading