brackt/app/components/league/CommissionersPanel.tsx
Chris Parsons 9ed0282fd0
New design (#309)
* Redesign home page with new layout and component system

- Two-column layout (My Leagues 2/3, Upcoming Events 1/3) with mobile stack
- LeagueRow: square avatar, gradient draft highlight, rank/points display, progress bar
- MyLeaguesCard, CreateLeagueCard with shared SectionCardHeader
- UpcomingEventsCard: vertical timeline with grouped multi-league events
- Shared gradient system: BracktGradients SVG defs, GradientIcon wrapper, brand.ts constants
- Button default variant updated to green→cyan gradient
- Navbar: plain nav links with gradient hover, support/admin icon buttons
- Accessibility fixes: semantic h2 headings, aria-label on LeagueAvatar and nav elements
- Storybook stories for all new components

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* Responsive league row layout and mobile polish

- League rows stack avatar+name on top, stats full-width below on mobile
- Stats spread to right side on sm+ screens with border separator on mobile
- Tighter padding on mobile (px-3/py-3), full padding on sm+
- Card headers and content use px-3 sm:px-6 to reduce mobile gutters
- Two-column home layout deferred to lg breakpoint (tablet gets stacked)
- Active leagues sorted by completion percentage descending
- Default rank 1 / 0 points for active leagues with no scoring events yet
- Fix ordinal bug for 11th/12th/13th; add aria-labels to rank change indicators
- Remove dead StatDivider className prop

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* Improve claude file.

* Add StandingsPreview card component with podium row styling

- New StandingsPreview component with gold/silver/bronze row tints for
  top 3, team avatar, and LeagueRow-style stat columns (Ranking + Points)
  with rank and 7-day point change indicators
- Fix GradientIcon in Storybook by adding BracktGradients decorator to
  preview.tsx (renamed from .ts to support JSX)
- Fix degenerate SVG gradient on horizontal strokes by switching
  BracktGradients to gradientUnits="userSpaceOnUse" with Lucide-space
  coordinates (0→24)
- Revert erroneous fill: url(#gradient) from GradientIcon; stroke-only
  fix was sufficient once gradientUnits was corrected

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* Update components on league homepage.

* Finish up league page styling.

* Work on standings page.

* Add story for RecentScoresCard

* Update Point Progression Chart.

* Sort point progression legend by ranking and add team links to standings rows

* Fix standings discrepancy on change.

* Create draft cell component.

* Update draft board page

* Draft room improvements.

* Update some draft room styling.

* Fix context menu missing.

* Move tab navigation and autodraft to header row, narrow sidebar

* Virtualize available participants list, memoize draft room props

Adds @tanstack/react-virtual to replace separate mobile/desktop lists
with a single unified virtual scroll loop. Also memoizes miniDraftGrid
and availableParticipantsSectionProps, and switches pick lookup from
Array.find to a Map for O(1) access.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* Update draft room UI.

* More draft room fixes.

* Draft room tweaks.

* Fix Rosters page.

* Queue Section fixes.

* Mobile Draft fixes.

* Fix draft board page.

* Create bracket look.

* Bracket work.

* Finish bracket page.

* Homepage initial styling

* homepage copy

* Add privacy policy. Fixes #88.

* how to play copy

* rules copy

* Fix brackets on homepage.

* Add footer to website.

* Glow on dots.

* Landing page copy.

* Fix sidebar.

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-23 13:14:55 -07:00

121 lines
4.3 KiB
TypeScript

import { format } from "date-fns";
import { Activity, ChevronRight, Crown } from "lucide-react";
import { Link } from "react-router";
import { GradientIcon } from "~/components/ui/GradientIcon";
import { formatAuditDetail } from "~/lib/audit-log-display";
import type { AuditLogEntry } from "~/models/audit-log";
// ─── Types ────────────────────────────────────────────────────────────────────
export interface CommissionerEntry {
id: string;
userId: string;
}
export interface CommissionersPanelProps {
commissioners: CommissionerEntry[];
/** Maps userId → display name */
commissionerMap: Record<string, string | null>;
currentUserId: string | null;
/** Used to determine whether a commissioner also has a team in the league. */
teams: Array<{ id: string; ownerId: string | null }>;
activityEntries: AuditLogEntry[];
/** URL for the full audit log — shown only when there are activity entries. */
auditLogUrl: string;
}
// ─── Sub-components ───────────────────────────────────────────────────────────
function CommissionerRow({
name,
isSelf,
hasTeam,
}: {
name: string;
isSelf: boolean;
hasTeam: boolean;
}) {
return (
<div className="flex items-center gap-2 min-w-0 py-1.5 border-b last:border-0">
<p className="font-medium truncate min-w-0 flex-1">
{name}
{isSelf && (
<span className="ml-2 text-xs text-primary">(You)</span>
)}
</p>
{!hasTeam && (
<p className="text-xs text-muted-foreground shrink-0">No team</p>
)}
</div>
);
}
// ─── Public API ───────────────────────────────────────────────────────────────
export function CommissionersPanel({
commissioners,
commissionerMap,
currentUserId,
teams,
activityEntries,
auditLogUrl,
}: CommissionersPanelProps) {
return (
<div className="space-y-14">
{/* Commissioners */}
<div>
<div className="flex items-center gap-2 mb-4">
<GradientIcon icon={Crown} className="h-5 w-5 shrink-0" />
<span className="text-xl font-bold leading-none tracking-tight">Commissioners</span>
</div>
<div>
{commissioners.map((commissioner) => (
<CommissionerRow
key={commissioner.id}
name={commissionerMap[commissioner.userId] || "Unknown User"}
isSelf={commissioner.userId === currentUserId}
hasTeam={teams.some((t) => t.ownerId === commissioner.userId)}
/>
))}
</div>
</div>
{/* Commish Activity */}
{activityEntries.length > 0 && (
<div>
<div className="flex items-center justify-between mb-4">
<div className="flex items-center gap-2">
<GradientIcon icon={Activity} className="h-5 w-5 shrink-0" />
<span className="text-xl font-bold leading-none tracking-tight">Commish Activity</span>
</div>
<Link
to={auditLogUrl}
className="flex items-center gap-0.5 text-xs font-semibold uppercase tracking-wide text-electric hover:underline"
>
View All
<ChevronRight className="h-3.5 w-3.5" />
</Link>
</div>
<ul className="space-y-3">
{activityEntries.map((entry) => (
<li key={entry.id} className="flex items-start gap-3 text-sm">
<span className="text-muted-foreground whitespace-nowrap shrink-0">
{format(new Date(entry.createdAt), "MMM d, HH:mm")}
</span>
<span className="min-w-0">
<span className="font-medium">
{entry.actorDisplayName ?? entry.actorClerkId}
</span>
{" — "}
<span className="text-muted-foreground">
{formatAuditDetail(entry)}
</span>
</span>
</li>
))}
</ul>
</div>
)}
</div>
);
}