From 498010e3e2429dcb244da41195d6257b0baa7d4d Mon Sep 17 00:00:00 2001 From: yohlo Date: Wed, 17 Sep 2025 09:02:20 -0500 Subject: [PATCH] various improvements --- package.json | 3 + .../1758049631_updated_player_stats.js | 37 ++ .../1758054877_updated_tournaments.js | 28 + pb_migrations/1758081731_created_reactions.js | 84 +++ src/app/routes/__root.tsx | 1 + src/app/routes/_authed/stats.tsx | 2 +- src/app/routes/_authed/tournaments/index.tsx | 4 +- src/components/page.tsx | 2 +- src/components/rich-text-editor.tsx | 40 ++ src/components/swipeable-tabs.tsx | 3 +- src/features/core/components/back-button.tsx | 6 +- src/features/core/components/header.tsx | 4 + .../core/components/settings-button.tsx | 6 +- .../players/components/player-stats-table.tsx | 509 ++++++++---------- .../players/components/profile/header.tsx | 2 +- .../players/components/profile/index.tsx | 2 +- src/features/players/types.ts | 1 + src/features/teams/components/team-list.tsx | 43 +- .../teams/components/team-profile/index.tsx | 2 +- .../components/manage-tournament.tsx | 8 +- .../tournaments/components/profile/index.tsx | 2 +- .../components/tournament-card.tsx | 108 ++-- .../components/tournament-list.tsx | 199 ++++--- .../components/upcoming-tournament/header.tsx | 19 +- src/shared/components/stats-overview.tsx | 261 +++------ 25 files changed, 733 insertions(+), 643 deletions(-) create mode 100644 pb_migrations/1758049631_updated_player_stats.js create mode 100644 pb_migrations/1758054877_updated_tournaments.js create mode 100644 pb_migrations/1758081731_created_reactions.js create mode 100644 src/components/rich-text-editor.tsx diff --git a/package.json b/package.json index ff5a532..7f60937 100644 --- a/package.json +++ b/package.json @@ -24,6 +24,9 @@ "@tanstack/react-router-devtools": "^1.130.13", "@tanstack/react-router-with-query": "^1.130.12", "@tanstack/react-start": "^1.130.15", + "@tiptap/pm": "^3.4.3", + "@tiptap/react": "^3.4.3", + "@tiptap/starter-kit": "^3.4.3", "@types/ioredis": "^4.28.10", "embla-carousel-react": "^8.6.0", "framer-motion": "^12.23.12", diff --git a/pb_migrations/1758049631_updated_player_stats.js b/pb_migrations/1758049631_updated_player_stats.js new file mode 100644 index 0000000..ce9819c --- /dev/null +++ b/pb_migrations/1758049631_updated_player_stats.js @@ -0,0 +1,37 @@ +/// +migrate((app) => { + const collection = app.findCollectionByNameOrId("pbc_1358894712") + + // update collection data + unmarshal({ + "viewQuery": "SELECT\n p.id as id,\n p.id as player_id,\n (p.first_name || ' ' || p.last_name) as player_name,\n COUNT(m.id) as matches,\n COUNT(DISTINCT m.tournament) as tournaments,\n SUM(CASE\n WHEN (m.home = t.id AND m.home_cups > m.away_cups) OR\n (m.away = t.id AND m.away_cups > m.home_cups)\n THEN 1 ELSE 0\n END) as wins,\n SUM(CASE\n WHEN (m.home = t.id AND m.home_cups < m.away_cups) OR\n (m.away = t.id AND m.away_cups < m.home_cups)\n THEN 1 ELSE 0\n END) as losses,\n SUM(CASE\n WHEN m.home = t.id THEN m.home_cups\n WHEN m.away = t.id THEN m.away_cups\n ELSE 0\n END) as total_cups_made,\n SUM(CASE\n WHEN m.home = t.id THEN m.away_cups\n WHEN m.away = t.id THEN m.home_cups\n ELSE 0\n END) as total_cups_against,\n -- Win percentage\n ROUND((CAST(SUM(CASE\n WHEN (m.home = t.id AND m.home_cups > m.away_cups) OR\n (m.away = t.id AND m.away_cups > m.home_cups)\n THEN 1 ELSE 0\n END) AS REAL) / COUNT(m.id)) * 100, 2) as win_percentage,\n -- Average cups per match\n ROUND(CAST(SUM(CASE\n WHEN m.home = t.id THEN m.home_cups\n WHEN m.away = t.id THEN m.away_cups\n ELSE 0\n END) AS REAL) / COUNT(m.id), 2) as avg_cups_per_match,\n -- Margin of Victory\n ROUND(AVG(CASE\n WHEN m.home = t.id AND m.home_cups > m.away_cups\n THEN m.home_cups - m.away_cups\n WHEN m.away = t.id AND m.away_cups > m.home_cups\n THEN m.away_cups - m.home_cups\n ELSE NULL\n END), 2) as margin_of_victory,\n -- Margin of Loss\n ROUND(AVG(CASE\n WHEN m.home = t.id AND m.home_cups < m.away_cups\n THEN m.away_cups - m.home_cups\n WHEN m.away = t.id AND m.away_cups < m.home_cups\n THEN m.home_cups - m.away_cups\n ELSE NULL\n END), 2) as margin_of_loss\n FROM players p, teams t, matches m, tournaments tour\n WHERE\n t.players LIKE '%\"' || p.id || '\"%' AND\n (m.home = t.id OR m.away = t.id) AND\n m.tournament = tour.id AND\n m.status = 'ended'\n GROUP BY p.id" + }, collection) + + // add field + collection.fields.addAt(4, new Field({ + "hidden": false, + "id": "number3837590211", + "max": null, + "min": null, + "name": "tournaments", + "onlyInt": false, + "presentable": false, + "required": false, + "system": false, + "type": "number" + })) + + return app.save(collection) +}, (app) => { + const collection = app.findCollectionByNameOrId("pbc_1358894712") + + // update collection data + unmarshal({ + "viewQuery": "SELECT\n p.id as id,\n p.id as player_id,\n (p.first_name || ' ' || p.last_name) as player_name,\n COUNT(m.id) as matches,\n SUM(CASE\n WHEN (m.home = t.id AND m.home_cups > m.away_cups) OR\n (m.away = t.id AND m.away_cups > m.home_cups)\n THEN 1 ELSE 0\n END) as wins,\n SUM(CASE\n WHEN (m.home = t.id AND m.home_cups < m.away_cups) OR\n (m.away = t.id AND m.away_cups < m.home_cups)\n THEN 1 ELSE 0\n END) as losses,\n SUM(CASE\n WHEN m.home = t.id THEN m.home_cups\n WHEN m.away = t.id THEN m.away_cups\n ELSE 0\n END) as total_cups_made,\n SUM(CASE\n WHEN m.home = t.id THEN m.away_cups\n WHEN m.away = t.id THEN m.home_cups\n ELSE 0\n END) as total_cups_against,\n -- Win percentage\n ROUND((CAST(SUM(CASE\n WHEN (m.home = t.id AND m.home_cups > m.away_cups) OR\n (m.away = t.id AND m.away_cups > m.home_cups)\n THEN 1 ELSE 0\n END) AS REAL) / COUNT(m.id)) * 100, 2) as win_percentage,\n -- Average cups per match\n ROUND(CAST(SUM(CASE\n WHEN m.home = t.id THEN m.home_cups\n WHEN m.away = t.id THEN m.away_cups\n ELSE 0\n END) AS REAL) / COUNT(m.id), 2) as avg_cups_per_match,\n -- Margin of Victory\n ROUND(AVG(CASE\n WHEN m.home = t.id AND m.home_cups > m.away_cups\n THEN m.home_cups - m.away_cups\n WHEN m.away = t.id AND m.away_cups > m.home_cups\n THEN m.away_cups - m.home_cups\n ELSE NULL\n END), 2) as margin_of_victory,\n -- Margin of Loss\n ROUND(AVG(CASE\n WHEN m.home = t.id AND m.home_cups < m.away_cups\n THEN m.away_cups - m.home_cups\n WHEN m.away = t.id AND m.away_cups < m.home_cups\n THEN m.home_cups - m.away_cups\n ELSE NULL\n END), 2) as margin_of_loss\n FROM players p, teams t, matches m, tournaments tour\n WHERE\n t.players LIKE '%\"' || p.id || '\"%' AND\n (m.home = t.id OR m.away = t.id) AND\n m.tournament = tour.id AND\n m.status = 'ended'\n GROUP BY p.id" + }, collection) + + // remove field + collection.fields.removeById("number3837590211") + + return app.save(collection) +}) diff --git a/pb_migrations/1758054877_updated_tournaments.js b/pb_migrations/1758054877_updated_tournaments.js new file mode 100644 index 0000000..61e5924 --- /dev/null +++ b/pb_migrations/1758054877_updated_tournaments.js @@ -0,0 +1,28 @@ +/// +migrate((app) => { + const collection = app.findCollectionByNameOrId("pbc_340646327") + + // add field + collection.fields.addAt(11, new Field({ + "cascadeDelete": false, + "collectionId": "pbc_3072146508", + "hidden": false, + "id": "relation1584152981", + "maxSelect": 999, + "minSelect": 0, + "name": "free_agents", + "presentable": false, + "required": false, + "system": false, + "type": "relation" + })) + + return app.save(collection) +}, (app) => { + const collection = app.findCollectionByNameOrId("pbc_340646327") + + // remove field + collection.fields.removeById("relation1584152981") + + return app.save(collection) +}) diff --git a/pb_migrations/1758081731_created_reactions.js b/pb_migrations/1758081731_created_reactions.js new file mode 100644 index 0000000..a2e9597 --- /dev/null +++ b/pb_migrations/1758081731_created_reactions.js @@ -0,0 +1,84 @@ +/// +migrate((app) => { + const collection = new Collection({ + "createRule": null, + "deleteRule": null, + "fields": [ + { + "autogeneratePattern": "[a-z0-9]{15}", + "hidden": false, + "id": "text3208210256", + "max": 15, + "min": 15, + "name": "id", + "pattern": "^[a-z0-9]+$", + "presentable": false, + "primaryKey": true, + "required": true, + "system": true, + "type": "text" + }, + { + "autogeneratePattern": "", + "hidden": false, + "id": "text3058431538", + "max": 0, + "min": 0, + "name": "emoji", + "pattern": "", + "presentable": false, + "primaryKey": false, + "required": false, + "system": false, + "type": "text" + }, + { + "cascadeDelete": false, + "collectionId": "pbc_3072146508", + "hidden": false, + "id": "relation2551806565", + "maxSelect": 1, + "minSelect": 0, + "name": "player", + "presentable": false, + "required": false, + "system": false, + "type": "relation" + }, + { + "hidden": false, + "id": "autodate2990389176", + "name": "created", + "onCreate": true, + "onUpdate": false, + "presentable": false, + "system": false, + "type": "autodate" + }, + { + "hidden": false, + "id": "autodate3332085495", + "name": "updated", + "onCreate": true, + "onUpdate": true, + "presentable": false, + "system": false, + "type": "autodate" + } + ], + "id": "pbc_1549310251", + "indexes": [], + "listRule": null, + "name": "reactions", + "system": false, + "type": "base", + "updateRule": null, + "viewRule": null + }); + + return app.save(collection); +}, (app) => { + const collection = app.findCollectionByNameOrId("pbc_1549310251"); + + return app.delete(collection); +}) diff --git a/src/app/routes/__root.tsx b/src/app/routes/__root.tsx index 0459715..1e69f6a 100644 --- a/src/app/routes/__root.tsx +++ b/src/app/routes/__root.tsx @@ -1,6 +1,7 @@ import "@mantine/core/styles.css"; import "@mantine/dates/styles.css"; import "@mantine/carousel/styles.css"; +import '@mantine/tiptap/styles.css'; import { HeadContent, Navigate, diff --git a/src/app/routes/_authed/stats.tsx b/src/app/routes/_authed/stats.tsx index cf498ee..a24cb0c 100644 --- a/src/app/routes/_authed/stats.tsx +++ b/src/app/routes/_authed/stats.tsx @@ -10,7 +10,7 @@ export const Route = createFileRoute("/_authed/stats")({ await ensureServerQueryData(queryClient, playerQueries.allStats()); }, loader: () => ({ - withPadding: true, + withPadding: false, fullWidth: true, header: { title: "Player Stats" diff --git a/src/app/routes/_authed/tournaments/index.tsx b/src/app/routes/_authed/tournaments/index.tsx index 902ab64..05ca697 100644 --- a/src/app/routes/_authed/tournaments/index.tsx +++ b/src/app/routes/_authed/tournaments/index.tsx @@ -31,7 +31,7 @@ function RouteComponent() { const { roles } = useAuth(); const sheet = useSheet(); - return + return ( { roles?.includes("Admin") ? ( @@ -49,5 +49,5 @@ function RouteComponent() { )) } - + ) } diff --git a/src/components/page.tsx b/src/components/page.tsx index b1f037c..ec7d8a6 100644 --- a/src/components/page.tsx +++ b/src/components/page.tsx @@ -22,7 +22,7 @@ const Page = ({ children, noPadding, fullWidth, ...props }: PageProps) => { {...props} > {header.collapsed && header.withBackButton && ( - + )} {header.collapsed && header.settingsLink && ( diff --git a/src/components/rich-text-editor.tsx b/src/components/rich-text-editor.tsx new file mode 100644 index 0000000..74fd872 --- /dev/null +++ b/src/components/rich-text-editor.tsx @@ -0,0 +1,40 @@ +import { useEditor } from '@tiptap/react'; +import StarterKit from '@tiptap/starter-kit'; +import { RichTextEditor as MantineRichTextEditor } from '@mantine/tiptap'; + +interface RichTextEditorProps { + value: string; + onChange: (value: string) => void; +} + +export function RichTextEditor({ + value, + onChange, +}: RichTextEditorProps) { + const editor = useEditor({ + extensions: [StarterKit], + content: value, + onUpdate: ({ editor }) => { + onChange(editor.getHTML()); + }, + }); + + return ( + + + + + + + + + + + + + + + + + ); +} diff --git a/src/components/swipeable-tabs.tsx b/src/components/swipeable-tabs.tsx index e5c705b..d64edc0 100644 --- a/src/components/swipeable-tabs.tsx +++ b/src/components/swipeable-tabs.tsx @@ -98,13 +98,11 @@ function SwipeableTabs({ updateHeight(); }, [activeTab, updateHeight]); - // Update height when content changes (after render) useEffect(() => { const timeoutId = setTimeout(updateHeight, 0); return () => clearTimeout(timeoutId); }); - // Use ResizeObserver to watch for content size changes useEffect(() => { const activeSlideRef = slideRefs.current[activeTab]; if (!activeSlideRef) return; @@ -142,6 +140,7 @@ function SwipeableTabs({ top={0} style={{ display: "flex", + paddingInline: "var(--mantine-spacing-md)", marginBottom: "var(--mantine-spacing-md)", zIndex: 100, backgroundColor: "var(--mantine-color-body)", diff --git a/src/features/core/components/back-button.tsx b/src/features/core/components/back-button.tsx index 863d543..86c1cb8 100644 --- a/src/features/core/components/back-button.tsx +++ b/src/features/core/components/back-button.tsx @@ -2,7 +2,7 @@ import { Box } from "@mantine/core" import { ArrowLeftIcon } from "@phosphor-icons/react" import { useRouter } from "@tanstack/react-router" -const BackButton = () => { +const BackButton = ({ top=20, left=20 }: { top?: number, left?: number }) => { const router = useRouter() return ( @@ -10,8 +10,8 @@ const BackButton = () => { style={{ cursor: 'pointer', zIndex: 1000 }} onClick={() => router.history.back()} pos='absolute' - left={16} - top={0} + left={left} + top={top} > diff --git a/src/features/core/components/header.tsx b/src/features/core/components/header.tsx index 2c75931..dfb0d82 100644 --- a/src/features/core/components/header.tsx +++ b/src/features/core/components/header.tsx @@ -1,12 +1,16 @@ import { Title, AppShell, Flex } from "@mantine/core"; import { HeaderConfig } from "../types/header-config"; +import useRouterConfig from "../hooks/use-router-config"; +import BackButton from "./back-button"; interface HeaderProps extends HeaderConfig {} const Header = ({ collapsed, title }: HeaderProps) => { + const { header } = useRouterConfig(); return ( + { header.withBackButton && } {title} diff --git a/src/features/core/components/settings-button.tsx b/src/features/core/components/settings-button.tsx index b4b6176..3cde337 100644 --- a/src/features/core/components/settings-button.tsx +++ b/src/features/core/components/settings-button.tsx @@ -5,6 +5,8 @@ import { memo } from "react"; interface SettingButtonProps { to: string; + top?: number; + right?: number; } const SettingsButton = ({ to }: SettingButtonProps) => { @@ -15,8 +17,8 @@ const SettingsButton = ({ to }: SettingButtonProps) => { style={{ cursor: 'pointer', zIndex: 1000 }} onClick={() => navigate({ to })} pos='absolute' - right={16} - top={0} + right={20} + top={6} > diff --git a/src/features/players/components/player-stats-table.tsx b/src/features/players/components/player-stats-table.tsx index fb211be..5ffa203 100644 --- a/src/features/players/components/player-stats-table.tsx +++ b/src/features/players/components/player-stats-table.tsx @@ -1,6 +1,5 @@ import { useState, useMemo } from "react"; import { - Table, Text, TextInput, Stack, @@ -9,10 +8,11 @@ import { ThemeIcon, Container, Title, - ScrollArea, - Paper, + Divider, + UnstyledButton, Popover, ActionIcon, + Skeleton, } from "@mantine/core"; import { MagnifyingGlassIcon, @@ -22,7 +22,8 @@ import { InfoIcon, } from "@phosphor-icons/react"; import { PlayerStats } from "../types"; -import { motion } from "framer-motion"; +import Avatar from "@/components/avatar"; +import { useNavigate } from "@tanstack/react-router"; interface PlayerStatsTableProps { playerStats: PlayerStats[]; @@ -36,7 +37,138 @@ interface SortConfig { direction: SortDirection; } +interface PlayerListItemProps { + stat: PlayerStats; + index: number; + onPlayerClick: (playerId: string) => void; +} + +const PlayerListItem = ({ stat, index, onPlayerClick }: PlayerListItemProps) => { + const calculateMMR = (stat: PlayerStats): number => { + if (stat.matches === 0) return 0; + + const winScore = stat.win_percentage; + const matchConfidence = Math.min(stat.matches / 15, 1); + const avgCupsScore = Math.min(stat.avg_cups_per_match * 10, 100); + const marginScore = stat.margin_of_victory + ? Math.min(stat.margin_of_victory * 20, 50) + : 0; + const volumeBonus = Math.min(stat.matches * 0.5, 10); + + const baseMMR = + winScore * 0.5 + + avgCupsScore * 0.25 + + marginScore * 0.15 + + volumeBonus * 0.1; + + const finalMMR = baseMMR * matchConfidence; + return Math.round(finalMMR * 10) / 10; + }; + + const mmr = calculateMMR(stat); + + return ( + <> + onPlayerClick(stat.id)} + style={{ + borderRadius: 0, + transition: "background-color 0.15s ease", + }} + styles={{ + root: { + '&:hover': { + backgroundColor: 'var(--mantine-color-gray-0)', + }, + }, + }} + > + + + + + + + {stat.player_name} + + + {stat.matches} matches + + + {stat.tournaments} tournaments + + + + + + MMR + + + {mmr.toFixed(1)} + + + + + W + + + {stat.wins} + + + + + L + + + {stat.losses} + + + + + W% + + + {stat.win_percentage.toFixed(1)}% + + + + + AVG + + + {stat.avg_cups_per_match.toFixed(1)} + + + + + CF + + + {stat.total_cups_made} + + + + + CA + + + {stat.total_cups_against} + + + + + + + + + + ); +}; + const PlayerStatsTable = ({ playerStats }: PlayerStatsTableProps) => { + const navigate = useNavigate(); const [search, setSearch] = useState(""); const [sortConfig, setSortConfig] = useState({ key: "mmr" as SortKey, @@ -47,14 +179,11 @@ const PlayerStatsTable = ({ playerStats }: PlayerStatsTableProps) => { if (stat.matches === 0) return 0; const winScore = stat.win_percentage; - const matchConfidence = Math.min(stat.matches / 15, 1); - - const avgCupsScore = Math.min(stat.avg_cups_per_match * 10, 100); + const avgCupsScore = Math.min(stat.avg_cups_per_match * 10, 100); const marginScore = stat.margin_of_victory ? Math.min(stat.margin_of_victory * 20, 50) : 0; - const volumeBonus = Math.min(stat.matches * 0.5, 10); const baseMMR = @@ -64,26 +193,9 @@ const PlayerStatsTable = ({ playerStats }: PlayerStatsTableProps) => { volumeBonus * 0.1; const finalMMR = baseMMR * matchConfidence; - return Math.round(finalMMR * 10) / 10; }; - const handleSort = (key: SortKey) => { - setSortConfig((prev) => ({ - key, - direction: prev.key === key && prev.direction === "desc" ? "asc" : "desc", - })); - }; - - const getSortIcon = (key: SortKey) => { - if (sortConfig.key !== key) return null; - return sortConfig.direction === "desc" ? ( - - ) : ( - - ); - }; - const filteredAndSortedStats = useMemo(() => { let filtered = playerStats.filter((stat) => stat.player_name.toLowerCase().includes(search.toLowerCase()) @@ -117,78 +229,29 @@ const PlayerStatsTable = ({ playerStats }: PlayerStatsTableProps) => { }); }, [playerStats, search, sortConfig]); - const formatPercentage = (value: number) => `${value.toFixed(1)}%`; - const formatDecimal = (value: number) => value.toFixed(2); + const handlePlayerClick = (playerId: string) => { + navigate({ to: `/profile/${playerId}` }); + }; - const columns = [ - { key: "player_name" as SortKey, label: "Player", width: 175 }, - { key: "mmr" as SortKey, label: "MMR", width: 90 }, - { key: "win_percentage" as SortKey, label: "Win %", width: 110 }, - { key: "matches" as SortKey, label: "Matches", width: 90 }, - { key: "wins" as SortKey, label: "Wins", width: 80 }, - { key: "losses" as SortKey, label: "Losses", width: 80 }, - { key: "total_cups_made" as SortKey, label: "Cups Made", width: 110 }, - { key: "total_cups_against" as SortKey, label: "Cups Against", width: 120 }, - { key: "avg_cups_per_match" as SortKey, label: "Avg/Match", width: 100 }, - { key: "margin_of_victory" as SortKey, label: "Win Margin", width: 110 }, - { key: "margin_of_loss" as SortKey, label: "Loss Margin", width: 110 }, - ]; + const handleSort = (key: SortKey) => { + setSortConfig((prev) => ({ + key, + direction: prev.key === key && prev.direction === "desc" ? "asc" : "desc", + })); + }; - const renderCellContent = ( - stat: PlayerStats, - column: (typeof columns)[0], - index: number - ) => { - switch (column.key) { - case "player_name": - return ( - - {stat.player_name} - - ); - case "mmr": - const mmr = calculateMMR(stat); - return ( - - - {mmr.toFixed(1)} - - - ); - case "win_percentage": - return {formatPercentage(stat.win_percentage)}; - case "wins": - return {stat.wins}; - case "losses": - return {stat.losses}; - case "total_cups_made": - return {stat.total_cups_made}; - case "matches": - return {stat.matches}; - case "avg_cups_per_match": - return {formatDecimal(stat.avg_cups_per_match)}; - case "margin_of_victory": - return ( - - {stat.margin_of_victory - ? formatDecimal(stat.margin_of_victory) - : "N/A"} - - ); - case "margin_of_loss": - return ( - - {stat.margin_of_loss ? formatDecimal(stat.margin_of_loss) : "N/A"} - - ); - default: - return {(stat as any)[column.key]}; - } + const getSortIcon = (key: SortKey) => { + if (sortConfig.key !== key) return null; + return sortConfig.direction === "desc" ? ( + + ) : ( + + ); }; if (playerStats.length === 0) { return ( - + @@ -196,9 +259,6 @@ const PlayerStatsTable = ({ playerStats }: PlayerStatsTableProps) => { No Stats Available - - Player statistics will appear here once matches have been played. - ); @@ -207,194 +267,97 @@ const PlayerStatsTable = ({ playerStats }: PlayerStatsTableProps) => { return ( - - {filteredAndSortedStats.length} of {playerStats.length} players - setSearch(e.currentTarget.value)} leftSection={} size="md" + px="md" /> - - - + + {filteredAndSortedStats.length} of {playerStats.length} players + + + Sort: + handleSort("mmr")} + style={{ display: "flex", alignItems: "center", gap: 4 }} > - - - {columns.map((column, index) => ( - handleSort(column.key)} - > - - - {column.label} - - {column.key === "mmr" && ( -
{ - e.stopPropagation(); - e.preventDefault(); - }} - onMouseDown={(e) => { - e.stopPropagation(); - }} - > - - - - - - - - - - MMR Calculation: - - - • Win Rate (50%) - - - • Average Cups/Match (25%) - - - • Average Win Margin (15%) - - - • Match Volume Bonus (10%) - - - * Confidence penalty applied for players - with <15 matches - - - ** Not an official rating - - - - -
- )} - - {getSortIcon(column.key)} - - {index === 0 && ( -
- )} - - - ))} - - - - {filteredAndSortedStats.map((stat, index) => ( - - {columns.map((column, columnIndex) => ( - -
- {renderCellContent(stat, column, index)} - {columnIndex === 0 && ( -
- )} -
- - ))} - - ))} - -
-
-
+ + MMR + + {getSortIcon("mmr")} + + + handleSort("wins")} + style={{ display: "flex", alignItems: "center", gap: 4 }} + > + + Wins + + {getSortIcon("wins")} + + + handleSort("matches")} + style={{ display: "flex", alignItems: "center", gap: 4 }} + > + + Matches + + {getSortIcon("matches")} + + + + + + + + + + + MMR Calculation: + + + • Win Rate (50%) + + + • Average Cups/Match (25%) + + + • Average Win Margin (15%) + + + • Match Volume Bonus (10%) + + + * Confidence penalty applied for players with <15 matches + + + ** Not an official rating + + + + + + + + + {filteredAndSortedStats.map((stat, index) => ( + + + {index < filteredAndSortedStats.length - 1 && } + + ))} + {filteredAndSortedStats.length === 0 && search && ( diff --git a/src/features/players/components/profile/header.tsx b/src/features/players/components/profile/header.tsx index 3c85107..ad82b04 100644 --- a/src/features/players/components/profile/header.tsx +++ b/src/features/players/components/profile/header.tsx @@ -33,7 +33,7 @@ const Header = ({ player }: HeaderProps) => { return ( <> - + {name} diff --git a/src/features/players/components/profile/index.tsx b/src/features/players/components/profile/index.tsx index 44e38e7..479022b 100644 --- a/src/features/players/components/profile/index.tsx +++ b/src/features/players/components/profile/index.tsx @@ -33,7 +33,7 @@ const Profile = ({ id }: ProfileProps) => { return ( <>
- + diff --git a/src/features/players/types.ts b/src/features/players/types.ts index 647049b..f850715 100644 --- a/src/features/players/types.ts +++ b/src/features/players/types.ts @@ -33,6 +33,7 @@ export interface PlayerStats { player_id: string; player_name: string; matches: number; + tournaments: number; wins: number; losses: number; total_cups_made: number; diff --git a/src/features/teams/components/team-list.tsx b/src/features/teams/components/team-list.tsx index 2ea15b3..28f2c30 100644 --- a/src/features/teams/components/team-list.tsx +++ b/src/features/teams/components/team-list.tsx @@ -1,4 +1,4 @@ -import { List, ListItem, Skeleton, Stack, Text } from "@mantine/core"; +import { Divider, Group, List, ListItem, Skeleton, Stack, Text } from "@mantine/core"; import Avatar from "@/components/avatar"; import { TeamInfo } from "@/features/teams/types"; import { useNavigate } from "@tanstack/react-router"; @@ -15,16 +15,16 @@ const TeamListItem = React.memo(({ team }: TeamListItemProps) => { ); return ( - <> - - {`${team.name}`} + + {`${team.name}`} + {playerNames.map((name) => ( - - {name} - - ))} + + {name} + + ))} - + ); }); @@ -59,15 +59,22 @@ const TeamList = ({ teams, loading = false }: TeamListProps) => { return ( {teams?.map((team) => ( - } - style={{ cursor: "pointer" }} - onClick={() => handleClick(team.id)} - > - - +
+ } + style={{ cursor: "pointer" }} + onClick={() => handleClick(team.id)} + styles={{ + itemWrapper: { width: "100%" }, + itemLabel: { width: "100%" } + }} + w="100%" + > + + + +
))}
); diff --git a/src/features/teams/components/team-profile/index.tsx b/src/features/teams/components/team-profile/index.tsx index 72d6153..2baaefc 100644 --- a/src/features/teams/components/team-profile/index.tsx +++ b/src/features/teams/components/team-profile/index.tsx @@ -37,7 +37,7 @@ const TeamProfile = ({ id }: ProfileProps) => { return ( <>
- + diff --git a/src/features/tournaments/components/manage-tournament.tsx b/src/features/tournaments/components/manage-tournament.tsx index f7a424e..783b0ad 100644 --- a/src/features/tournaments/components/manage-tournament.tsx +++ b/src/features/tournaments/components/manage-tournament.tsx @@ -12,6 +12,8 @@ import { import { useSheet } from "@/hooks/use-sheet"; import EditEnrolledTeams from "./edit-enrolled-teams"; import ListLink from "@/components/list-link"; +import { RichTextEditor } from "@/components/rich-text-editor"; +import React from "react"; interface ManageTournamentProps { tournamentId: string; @@ -37,6 +39,8 @@ const ManageTournament = ({ tournamentId }: ManageTournamentProps) => { close: closeEditTeams, } = useSheet(); + const [v, setV] = React.useState(""); + return ( <> @@ -86,7 +90,9 @@ const ManageTournament = ({ tournamentId }: ManageTournamentProps) => { opened={editRulesOpened} onChange={closeEditRules} > -

Test

+ + + {v} { return <>
- + ; diff --git a/src/features/tournaments/components/tournament-card.tsx b/src/features/tournaments/components/tournament-card.tsx index 529b36f..1720a8d 100644 --- a/src/features/tournaments/components/tournament-card.tsx +++ b/src/features/tournaments/components/tournament-card.tsx @@ -1,10 +1,23 @@ -import { Badge, Card, Text, Stack, Group, Box, ThemeIcon } from "@mantine/core"; +import { + Badge, + Card, + Text, + Stack, + Group, + Box, + ThemeIcon, + UnstyledButton, +} from "@mantine/core"; import { Tournament } from "@/features/tournaments/types"; import { useMemo } from "react"; -import { TrophyIcon, CalendarIcon, MapPinIcon, UsersIcon } from "@phosphor-icons/react"; +import { + TrophyIcon, + CalendarIcon, + MapPinIcon, + UsersIcon, +} from "@phosphor-icons/react"; import { useNavigate } from "@tanstack/react-router"; import Avatar from "@/components/avatar"; -import { motion } from "framer-motion"; interface TournamentCardProps { tournament: Tournament; @@ -18,9 +31,9 @@ export const TournamentCard = ({ tournament }: TournamentCardProps) => { const date = new Date(tournament.start_time); if (isNaN(date.getTime())) return null; return date.toLocaleDateString(undefined, { - month: 'short', - day: 'numeric', - year: 'numeric' + month: "short", + day: "numeric", + year: "numeric", }); }, [tournament.start_time]); @@ -31,33 +44,42 @@ export const TournamentCard = ({ tournament }: TournamentCardProps) => { const enrolledTeamsCount = tournament.teams?.length || 0; return ( - navigate({ to: `/tournaments/${tournament.id}` })} + style={{ borderRadius: "var(--mantine-radius-md)" }} + styles={{ + root: { + "&:hover": { + transform: "translateY(-2px)", + transition: "transform 0.15s ease", + }, + }, + }} > navigate({ to: `/tournaments/${tournament.id}` })} - onMouseEnter={(e) => { - e.currentTarget.style.boxShadow = "var(--mantine-shadow-md)"; - }} - onMouseLeave={(e) => { - e.currentTarget.style.boxShadow = "none"; + styles={{ + root: { + "&:hover": { + borderColor: "var(--mantine-primary-color-filled)", + boxShadow: "var(--mantine-shadow-sm)", + }, + }, }} > - - + + { : undefined } > - + + + + {tournament.name} + + {displayDate && ( + + + + + + {displayDate} + + + )} - - {tournament.name} - + + + + + + {enrolledTeamsCount} team + {enrolledTeamsCount !== 1 ? "s" : ""} + + + - + - + ); }; diff --git a/src/features/tournaments/components/tournament-list.tsx b/src/features/tournaments/components/tournament-list.tsx index 5cbd99c..91846d0 100644 --- a/src/features/tournaments/components/tournament-list.tsx +++ b/src/features/tournaments/components/tournament-list.tsx @@ -1,9 +1,9 @@ -import { Stack, Skeleton, Text, Group, Box, ThemeIcon } from "@mantine/core"; +import { List, ListItem, Divider, Skeleton, Text, Group, Box, ThemeIcon, Stack } from "@mantine/core"; import { useNavigate } from "@tanstack/react-router"; import Avatar from "@/components/avatar"; import { TournamentInfo } from "../types"; import { useCallback } from "react"; -import { motion, AnimatePresence } from "framer-motion"; +import React from "react"; import { TrophyIcon, CalendarIcon, MapPinIcon } from "@phosphor-icons/react"; interface TournamentListProps { @@ -11,6 +11,50 @@ interface TournamentListProps { loading?: boolean; } +interface TournamentListItemProps { + tournament: TournamentInfo; +} + +const TournamentListItem = React.memo(({ tournament }: TournamentListItemProps) => { + const startDate = tournament.start_time ? new Date(tournament.start_time) : null; + + return ( + + + + {tournament.name} + + + {tournament.location && ( + + + + + + {tournament.location} + + + )} + {startDate && !isNaN(startDate.getTime()) && ( + + + + + + {startDate.toLocaleDateString(undefined, { + month: 'short', + day: 'numeric', + year: 'numeric' + })} + + + )} + + + + ); +}); + const TournamentList = ({ tournaments, loading = false }: TournamentListProps) => { const navigate = useNavigate(); @@ -19,20 +63,23 @@ const TournamentList = ({ tournaments, loading = false }: TournamentListProps) = if (loading) { return ( - - {Array.from({ length: 6 }).map((_, i) => ( - - - - - - - - - - + + {Array.from({ length: 5 }).map((_, i) => ( + } + > + + + + + + + + ))} - + ); } @@ -50,96 +97,40 @@ const TournamentList = ({ tournaments, loading = false }: TournamentListProps) = } return ( - - - {tournaments.map((tournament, index) => { - const startDate = tournament.start_time ? new Date(tournament.start_time) : null; - - return ( - + {tournaments.map((tournament) => ( + <> + + + + } + style={{ cursor: "pointer" }} + onClick={() => handleClick(tournament.id)} + styles={{ + itemWrapper: { width: "100%" }, + itemLabel: { width: "100%" } + }} + w="100%" > - handleClick(tournament.id)} - onMouseEnter={(e) => { - e.currentTarget.style.backgroundColor = "var(--mantine-color-gray-0)"; - e.currentTarget.style.borderColor = "var(--mantine-primary-color-filled)"; - }} - onMouseLeave={(e) => { - e.currentTarget.style.backgroundColor = "transparent"; - e.currentTarget.style.borderColor = "var(--mantine-color-gray-3)"; - }} - > - - - - - - - - {tournament.name} - - - - {tournament.location && ( - - - - - - {tournament.location} - - - )} - - {startDate && !isNaN(startDate.getTime()) && ( - - - - - - {startDate.toLocaleDateString(undefined, { - month: 'short', - day: 'numeric', - year: 'numeric' - })} - - - )} - - - - - - ); - })} - - + + + + + ))} + ); } diff --git a/src/features/tournaments/components/upcoming-tournament/header.tsx b/src/features/tournaments/components/upcoming-tournament/header.tsx index 102a41a..6c322db 100644 --- a/src/features/tournaments/components/upcoming-tournament/header.tsx +++ b/src/features/tournaments/components/upcoming-tournament/header.tsx @@ -1,4 +1,4 @@ -import { Group, Stack, ThemeIcon, Text } from "@mantine/core"; +import { Group, Stack, ThemeIcon, Text, Flex } from "@mantine/core"; import { Tournament } from "../../types"; import Avatar from "@/components/avatar"; import { @@ -20,7 +20,7 @@ const Header = ({ tournament }: { tournament: Tournament }) => { ); return ( - + { : undefined } radius="md" - size={200} + size={300} px="xs" withBorder={false} > - + {tournament.location && ( @@ -64,16 +64,7 @@ const Header = ({ tournament }: { tournament: Tournament }) => { })} - - - - - - - {teamCount} teams enrolled - - - + ); }; diff --git a/src/shared/components/stats-overview.tsx b/src/shared/components/stats-overview.tsx index 475f3de..b18dce5 100644 --- a/src/shared/components/stats-overview.tsx +++ b/src/shared/components/stats-overview.tsx @@ -1,12 +1,11 @@ import { Box, - Grid, Text, Group, Stack, ThemeIcon, - Card, Skeleton, + Divider, } from "@mantine/core"; import { CrownIcon, @@ -27,69 +26,34 @@ interface StatsOverviewProps { isLoading?: boolean; } -const StatCard = ({ +const StatItem = ({ label, value, suffix = "", Icon, - variant = "default", - isLoading = false, }: { label: string; value: number | null; suffix?: string; Icon?: Icon; - variant?: "default" | "compact"; isLoading?: boolean; }) => { - if (variant === "compact") { - return ( - - - - - {label} - - {isLoading ? ( - - ) : ( - - {value !== null ? `${value}${suffix}` : "—"} - - )} - - {Icon && ( - - - - )} - - - ); - } - return ( - - - - - {label} - - {Icon && ( - - - - )} - - {isLoading ? ( - - ) : ( - - {value !== null ? `${value}${suffix}` : "—"} - + + + {Icon && ( + + + )} - - + + {label} + + + + {value !== null ? `${value}${suffix}` : "—"} + + ); }; @@ -120,157 +84,78 @@ const StatsOverview = ({ statsData, isLoading = false }: StatsOverviewProps) => const avgCupsPerMatch = overallStats.matches > 0 - ? overallStats.total_cups_made / overallStats.matches + ? parseFloat((overallStats.total_cups_made / overallStats.matches).toFixed(1)) : 0; const avgCupsAgainstPerMatch = overallStats.matches > 0 - ? overallStats.total_cups_against / overallStats.matches + ? parseFloat((overallStats.total_cups_against / overallStats.matches).toFixed(1)) : 0; - const avgMarginOfVictory = statsData.margin_of_victory || 0; - const avgMarginOfLoss = statsData.margin_of_loss || 0; + const avgMarginOfVictory = statsData.margin_of_victory ? parseFloat(statsData.margin_of_victory.toFixed(1)) : 0; + const avgMarginOfLoss = statsData.margin_of_loss ? parseFloat(statsData.margin_of_loss.toFixed(1)) : 0; + + const allStats = [ + { label: "Matches Played", value: overallStats.matches, Icon: BoxingGloveIcon }, + { label: "Wins", value: overallStats.wins, Icon: CrownIcon }, + { label: "Losses", value: overallStats.losses, Icon: XIcon }, + { label: "Cups Made", value: overallStats.total_cups_made, Icon: FireIcon }, + { label: "Cups Against", value: overallStats.total_cups_against, Icon: ShieldIcon }, + { label: "Avg Cups Per Game", value: avgCupsPerMatch > 0 ? avgCupsPerMatch : null, Icon: ChartLineUpIcon }, + { label: "Avg Cups Against", value: avgCupsAgainstPerMatch > 0 ? avgCupsAgainstPerMatch : null, Icon: ShieldCheckIcon }, + { label: "Avg Win Margin", value: avgMarginOfVictory > 0 ? avgMarginOfVictory : null, Icon: ArrowUpIcon }, + { label: "Avg Loss Margin", value: avgMarginOfLoss > 0 ? avgMarginOfLoss : null, Icon: ArrowDownIcon }, + ]; return ( - - - - - Match Statistics - - - - + + {allStats.map((stat, index) => ( + + - - - - - - - - - - - - - Metrics - - - - - - - - - - - - - - - - 0 - ? parseFloat(avgMarginOfVictory.toFixed(1)) - : null - } - Icon={ArrowUpIcon} - /> - - - 0 - ? parseFloat(avgMarginOfLoss.toFixed(1)) - : null - } - Icon={ArrowDownIcon} - /> - - - + {index < allStats.length - 1 && } + + ))} ); }; -const StatsSkeleton = () => ( - - - - - - - - - - - - - - - - +const StatsSkeleton = () => { + const skeletonStats = [ + { label: "Matches Played", Icon: BoxingGloveIcon }, + { label: "Wins", Icon: CrownIcon }, + { label: "Losses", Icon: XIcon }, + { label: "Cups Made", Icon: FireIcon }, + { label: "Cups Against", Icon: ShieldIcon }, + { label: "Avg Cups Per Game", Icon: ChartLineUpIcon }, + { label: "Avg Cups Against", Icon: ShieldCheckIcon }, + { label: "Avg Win Margin", Icon: ArrowUpIcon }, + { label: "Avg Loss Margin", Icon: ArrowDownIcon }, + ]; - - - - - - - - - - - - - - - - - - - - - - + return ( + + + {skeletonStats.map((stat, index) => ( + + + {index < skeletonStats.length - 1 && } + + ))} - - -) + + ); +}; export default StatsOverview;