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
42 changes: 42 additions & 0 deletions app/rankings/ncaa/actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,48 @@ export async function savePlayerRankings(
return { success: true };
}

export async function deletePlayerRankings(): Promise<ActionResult> {
const supabase = await createClient();

const { data: { user }, error: authError } = await supabase.auth.getUser();
if (authError || !user) {
return { success: false, error: 'Not authenticated' };
}

const { error } = await supabase
.from('user_ncaa_rankings')
.delete()
.eq('user_id', user.id)
.eq('season_year', 2026);

if (error) {
return { success: false, error: error.message };
}

return { success: true };
}

export async function deleteExpectedGames(): Promise<ActionResult> {
const supabase = await createClient();

const { data: { user }, error: authError } = await supabase.auth.getUser();
if (authError || !user) {
return { success: false, error: 'Not authenticated' };
}

const { error } = await supabase
.from('user_ncaa_team_settings')
.delete()
.eq('user_id', user.id)
.eq('season_year', 2026);

if (error) {
return { success: false, error: error.message };
}

return { success: true };
}

export async function saveExpectedGames(
settings: { team_name: string; expected_games: number }[]
): Promise<ActionResult> {
Expand Down
24 changes: 15 additions & 9 deletions app/rankings/ncaa/player-ranking-card.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ interface PlayerRankingCardProps {
expectedGames: number;
provided: DraggableProvided;
isDragging: boolean;
isEditing: boolean;
}

export default function PlayerRankingCard({
Expand All @@ -19,6 +20,7 @@ export default function PlayerRankingCard({
expectedGames,
provided,
isDragging,
isEditing,
}: PlayerRankingCardProps) {
const borderColor = getPositionBorderColor(player.position, 'NCAAM');
const posTextColor = getPositionTextColor(player.position, 'NCAAM');
Expand All @@ -39,16 +41,20 @@ export default function PlayerRankingCard({
}}
>
{/* Drag handle */}
<div
{...provided.dragHandleProps}
className="flex-shrink-0 mr-2 sm:mr-3 cursor-grab active:cursor-grabbing"
>
<div className="flex flex-col gap-0.5">
<div className="w-4 h-0.5 bg-muted-foreground/40 rounded" />
<div className="w-4 h-0.5 bg-muted-foreground/40 rounded" />
<div className="w-4 h-0.5 bg-muted-foreground/40 rounded" />
{isEditing ? (
<div
{...provided.dragHandleProps}
className="flex-shrink-0 mr-2 sm:mr-3 cursor-grab active:cursor-grabbing"
>
<div className="flex flex-col gap-0.5">
<div className="w-4 h-0.5 bg-muted-foreground/40 rounded" />
<div className="w-4 h-0.5 bg-muted-foreground/40 rounded" />
<div className="w-4 h-0.5 bg-muted-foreground/40 rounded" />
</div>
</div>
</div>
) : (
<div {...provided.dragHandleProps} className="hidden" />
)}

{/* Rank number */}
<div className="flex-shrink-0 mr-2 sm:mr-3">
Expand Down
7 changes: 5 additions & 2 deletions app/rankings/ncaa/player-rankings-list.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,15 @@ interface PlayerRankingsListProps {
onReorder: (newOrder: PlayerWithStats[]) => void;
expectedGames: Record<string, number>;
rankMap?: Map<string, number>;
isEditing: boolean;
}

export default function PlayerRankingsList({
players,
onReorder,
expectedGames,
rankMap,
isEditing,
}: PlayerRankingsListProps) {
const handleDragEnd = (result: DropResult) => {
if (!result.destination) return;
Expand All @@ -37,22 +39,23 @@ export default function PlayerRankingsList({

return (
<DragDropContext onDragEnd={handleDragEnd}>
<Droppable droppableId="playerRankings">
<Droppable droppableId="playerRankings" isDropDisabled={!isEditing}>
{(provided) => (
<div
{...provided.droppableProps}
ref={provided.innerRef}
className="space-y-1"
>
{players.map((player, index) => (
<Draggable key={player.id} draggableId={player.id} index={index}>
<Draggable key={player.id} draggableId={player.id} index={index} isDragDisabled={!isEditing}>
{(provided, snapshot) => (
<PlayerRankingCard
player={player}
rank={rankMap?.get(player.id) ?? index + 1}
expectedGames={expectedGames[player.team_name] || 1}
provided={provided}
isDragging={snapshot.isDragging}
isEditing={isEditing}
/>
)}
</Draggable>
Expand Down
104 changes: 94 additions & 10 deletions app/rankings/ncaa/rankings-page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@ import RankingsFilters from './rankings-filters';
import PlayerRankingsList from './player-rankings-list';
import ExpectedGamesSection from './expected-games-section';
import TeamsTable from './teams-table';
import { savePlayerRankings, saveExpectedGames } from './actions';
import { computeProjectedTotal } from './utils';
import { savePlayerRankings, saveExpectedGames, deletePlayerRankings, deleteExpectedGames } from './actions';
import { computeProjectedTotal, getDefaultExpectedGames } from './utils';
import type { PlayerWithStats, NcaaTeamInfo, UserRanking, UserTeamSetting } from './utils';

type SortBy = 'projection' | 'expectedGames' | 'custom';
Expand Down Expand Up @@ -60,8 +60,14 @@ export default function RankingsPage({
}, [orderedPlayers]);

const [hasUnsavedRankings, setHasUnsavedRankings] = useState(false);
const [hasCustomRankings, setHasCustomRankings] = useState(userRankings.length > 0);
const [isEditing, setIsEditing] = useState(false);
const [isSaving, setIsSaving] = useState(false);

// Snapshot of player order before editing, for cancel
const preEditOrderRef = useRef<PlayerWithStats[]>(orderedPlayers);
const preEditSortByRef = useRef<SortBy>(sortBy);

// Debounce ref for expected games saving
const gamesTimeoutRef = useRef<NodeJS.Timeout | null>(null);

Expand Down Expand Up @@ -184,9 +190,45 @@ export default function RankingsPage({
}));
await savePlayerRankings(rankings);
setHasUnsavedRankings(false);
setHasCustomRankings(true);
setIsEditing(false);
setIsSaving(false);
}, [orderedPlayers]);

// Reset rankings to default projection order
const handleResetRankings = useCallback(async () => {
setOrderedPlayers((prev) =>
[...prev].sort((a, b) => b.projectedTotal - a.projectedTotal)
);
setSortBy('projection');
setHasUnsavedRankings(false);
setHasCustomRankings(false);
setIsEditing(false);
await deletePlayerRankings();
}, []);

// Build default expected games map from team seeds
const defaultExpectedGames = useMemo(() => {
const teamInfoMap = new Map(teamInfo.map((ti) => [ti.team_name, ti]));
const defaults: Record<string, number> = {};
for (const team of allTeams) {
defaults[team] = getDefaultExpectedGames(teamInfoMap.get(team)?.seed);
}
return defaults;
}, [teamInfo, allTeams]);

const hasCustomExpectedGames = useMemo(() => {
return allTeams.some((team) => expectedGames[team] !== defaultExpectedGames[team]);
}, [expectedGames, defaultExpectedGames, allTeams]);

// Reset expected games to seed-based defaults
const handleResetExpectedGames = useCallback(async () => {
setExpectedGames(defaultExpectedGames);
updateProjectedTotals(defaultExpectedGames);
if (gamesTimeoutRef.current) clearTimeout(gamesTimeoutRef.current);
await deleteExpectedGames();
}, [defaultExpectedGames, updateProjectedTotals]);

// Cleanup timeouts
useEffect(() => {
return () => {
Expand Down Expand Up @@ -252,17 +294,49 @@ export default function RankingsPage({
);
})}
</div>
<div className="flex items-center gap-3">
<span className="text-sm text-muted-foreground">
<div className="flex items-center gap-2">
<span className="text-sm text-muted-foreground mr-1">
{filteredPlayers.length} player{filteredPlayers.length !== 1 ? 's' : ''}
</span>
{hasUnsavedRankings && (
{(hasUnsavedRankings || hasCustomRankings) && (
<button
onClick={handleResetRankings}
className="px-3 py-1 rounded text-xs font-medium bg-muted text-muted-foreground hover:text-foreground transition-colors"
>
Reset to Default
</button>
)}
{isEditing ? (
<>
<button
onClick={() => {
setOrderedPlayers(preEditOrderRef.current);
setSortBy(preEditSortByRef.current);
setHasUnsavedRankings(false);
setIsEditing(false);
}}
className="px-3 py-1 rounded text-xs font-medium bg-muted text-muted-foreground hover:text-foreground transition-colors"
>
Cancel
</button>
<button
onClick={handleSaveRankings}
disabled={isSaving || !hasUnsavedRankings}
className="px-3 py-1 rounded text-xs font-medium bg-muted text-muted-foreground hover:text-foreground transition-colors disabled:opacity-50"
>
{isSaving ? 'Saving...' : 'Save Changes'}
</button>
</>
) : (
<button
onClick={handleSaveRankings}
disabled={isSaving}
className="px-3 py-1 rounded text-xs font-medium bg-primary text-primary-foreground hover:bg-primary/90 disabled:opacity-50 transition-colors"
onClick={() => {
preEditOrderRef.current = orderedPlayers;
preEditSortByRef.current = sortBy;
setIsEditing(true);
}}
className="px-3 py-1 rounded text-xs font-medium bg-muted text-muted-foreground hover:text-foreground transition-colors"
>
{isSaving ? 'Saving...' : 'Save Rankings'}
Edit Rankings
</button>
)}
</div>
Expand All @@ -273,15 +347,25 @@ export default function RankingsPage({
onReorder={handleReorder}
expectedGames={expectedGames}
rankMap={rankMap}
isEditing={isEditing}
/>
</TabsContent>

<TabsContent value="teams">
{hasCustomExpectedGames && (
<div className="mb-3 flex justify-end">
<button
onClick={handleResetExpectedGames}
className="px-3 py-1 rounded text-xs font-medium bg-muted text-muted-foreground hover:text-foreground transition-colors"
>
Reset to Default
</button>
</div>
)}
<TeamsTable
teams={allTeams}
teamInfo={teamInfo}
expectedGames={expectedGames}
players={orderedPlayers}
onExpectedGamesChange={handleExpectedGamesChange}
/>
</TabsContent>
Expand Down
23 changes: 4 additions & 19 deletions app/rankings/ncaa/teams-table.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,46 +9,35 @@ import {
TableRow,
} from '@/components/ui/table';
import { ScrollArea } from '@/components/ui/scroll-area';
import type { NcaaTeamInfo, PlayerWithStats } from './utils';
import type { NcaaTeamInfo } from './utils';

interface TeamsTableProps {
teams: string[];
teamInfo: NcaaTeamInfo[];
expectedGames: Record<string, number>;
players: PlayerWithStats[];
onExpectedGamesChange: (teamName: string, games: number) => void;
}

export default function TeamsTable({
teams,
teamInfo,
expectedGames,
players,
onExpectedGamesChange,
}: TeamsTableProps) {
const teamInfoMap = new Map<string, NcaaTeamInfo>();
for (const ti of teamInfo) {
teamInfoMap.set(ti.team_name, ti);
}

// Compute team-level aggregates
// Compute team-level data
const teamData = teams.map((teamName) => {
const info = teamInfoMap.get(teamName);
const teamPlayers = players.filter((p) => p.team_name === teamName);
const avgPpg =
teamPlayers.length > 0
? Math.round(
(teamPlayers.reduce((sum, p) => sum + p.averages.ppg, 0) / teamPlayers.length) * 10
) / 10
: 0;

return {
teamName,
seed: info?.seed ?? null,
region: info?.region ?? null,
expectedGames: expectedGames[teamName] || 1,
playerCount: teamPlayers.length,
avgPpg,
};
});

Expand All @@ -66,21 +55,19 @@ export default function TeamsTable({
<Table>
<TableHeader>
<TableRow>
<TableHead>Team</TableHead>
<TableHead className="w-[60px] text-center">Seed</TableHead>
<TableHead>Team</TableHead>
<TableHead className="hidden sm:table-cell">Region</TableHead>
<TableHead className="w-[80px] text-center">Games</TableHead>
<TableHead className="w-[80px] text-center">Players</TableHead>
<TableHead className="w-[80px] text-right">Avg PPG</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{teamData.map((team) => (
<TableRow key={team.teamName}>
<TableCell className="font-medium">{team.teamName}</TableCell>
<TableCell className="text-center">
{team.seed ? `#${team.seed}` : '-'}
</TableCell>
<TableCell className="font-medium">{team.teamName}</TableCell>
<TableCell className="hidden sm:table-cell">
{team.region || '-'}
</TableCell>
Expand All @@ -100,8 +87,6 @@ export default function TeamsTable({
className="bg-background border border-border rounded px-1 py-0.5 text-sm w-14 text-foreground"
/>
</TableCell>
<TableCell className="text-center">{team.playerCount}</TableCell>
<TableCell className="text-right">{team.avgPpg}</TableCell>
</TableRow>
))}
</TableBody>
Expand Down
Loading