brackt/app/components/league/CommissionersPanel.tsx
Chris Parsons 5268e07365
Migrate auth from Clerk to BetterAuth (#354)
* Fix BetterAuth field mapping and add owner-prefixed team names

- Fix auth.server.ts: use camelCase Drizzle field names for BetterAuth
  adapter (was snake_case, causing user inserts to fail); add
  generateId: "uuid" so PostgreSQL UUID columns accept generated IDs
- Add prependOwnerToTeamName / stripOwnerFromTeamName utilities
- Invite join, league creation, and settings assign/remove all now
  manage the owner-prefix on team names atomically

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

* Complete BetterAuth migration: auth flows, rename, and cleanup

- Add /forgot-password and /reset-password pages (full password reset flow)
- Add 'Forgot password?' link on login page
- Fix register.tsx: add explicit window.location fallback after signUp
- Rename commissionerAuditLog.actorClerkId → actorUserId (migration 0084)
  and update all references in model, routes, components, and tests
- Rewrite docs/agents/auth.md to document BetterAuth (removes all Clerk refs)
- Update test fixtures and schema comments to remove Clerk ID references;
  use UUID-format IDs throughout test data
- Update docs/agents/domain-models.md ownerId description

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

* Fix BetterAuth ID generation, clean up review issues

- Set generateId: false so BetterAuth omits id from inserts; add
  $defaultFn(crypto.randomUUID) to accounts/sessions/verifications
  so Drizzle fills it in (fixes null id constraint violation on signup)
- Move renameTeam to static import; rename before removeTeamOwner so
  a failed rename leaves owner intact rather than leaving a stale prefix
- Clarify stripOwnerFromTeamName toTitleCase assumption in comment
- Annotate reset-password token fallback to explain loader guarantee

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

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-29 10:03:50 -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.actorUserId}
</span>
{" — "}
<span className="text-muted-foreground">
{formatAuditDetail(entry)}
</span>
</span>
</li>
))}
</ul>
</div>
)}
</div>
);
}