-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(Overview): Replace outdated view
- Loading branch information
1 parent
66c2351
commit 2d02c06
Showing
15 changed files
with
169 additions
and
310 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,219 +1,88 @@ | ||
import { useCallback, useEffect, useRef, useState } from "react" | ||
import { useState } from "react" | ||
|
||
import { RefreshCw, Star } from "lucide-react" | ||
|
||
import { Icon } from "~/components/Icon" | ||
import { LoadingData } from "~/components/LoadingData" | ||
import { NoData } from "~/components/NoData" | ||
import { Button } from "~/components/ui/button" | ||
import { Table } from "~/components/ui/table" | ||
import { GameStats, UserStats, useGames } from "~/data/gamesExternal" | ||
import { usePlayers } from "~/data/players" | ||
import { textColor } from "~/utils/colors" | ||
import { noOverflow } from "~/utils/styles" | ||
import { cn } from "~/utils/utils" | ||
|
||
const averageStat = ( | ||
games: GameStats[], | ||
user: keyof Omit<GameStats, "name" | "date" | "color">, | ||
stat: keyof UserStats | ||
) => { | ||
const validGames = games.filter(game => typeof game[user][stat] === "number") | ||
const sum = validGames.reduce((acc, game) => { | ||
const userValue = game[user][stat] ?? 0 | ||
return acc + userValue | ||
}, 0) | ||
return sum / validGames.length | ||
} | ||
|
||
const averageStats = (games: GameStats[]) => ({ | ||
online: { | ||
playtime: averageStat(games, "online", "playtime"), | ||
rating: averageStat(games, "online", "rating"), | ||
}, | ||
player1: { | ||
playtime: averageStat(games, "player1", "playtime"), | ||
rating: averageStat(games, "player1", "rating"), | ||
}, | ||
player2: { | ||
playtime: averageStat(games, "player2", "playtime"), | ||
rating: averageStat(games, "player2", "rating"), | ||
}, | ||
}) | ||
|
||
const formatNumber = (number?: number) => | ||
number?.toFixed(1).replace(/\.0$/, "") ?? "-" | ||
import { Game, useGames } from "~/data/games" | ||
import { today } from "~/utils/date" | ||
|
||
const Hours = ({ hours }: { hours?: number }) => ( | ||
<span className="inline-flex items-center gap-2 whitespace-nowrap"> | ||
{formatNumber(hours)} <span className="text-muted-foreground">h</span> | ||
</span> | ||
) | ||
import { GameModal } from "./GameModal" | ||
import { GamesTable } from "./GamesTable" | ||
|
||
const Rating = ({ rating }: { rating?: number }) => ( | ||
<span className={cn("inline-flex items-center gap-2 whitespace-nowrap")}> | ||
{formatNumber(rating)} | ||
<Icon icon={Star} size="sm" className={textColor({ color: "yellow" })} /> | ||
</span> | ||
) | ||
|
||
const GamesTable = ({ games }: { games: GameStats[] }) => { | ||
const { players } = usePlayers() | ||
const average = averageStats(games) | ||
const AddGame = ({ label }: { label: string }) => { | ||
const { addGame } = useGames() | ||
const [adding, setAdding] = useState(false) | ||
|
||
return ( | ||
<Table.Root> | ||
<Table.Header> | ||
<Table.Row className="border-none"> | ||
<Table.Head>Name</Table.Head> | ||
<Table.Head>Date</Table.Head> | ||
<Table.Head colSpan={2} className="text-center"> | ||
Online | ||
</Table.Head> | ||
{players.map(player => ( | ||
<Table.Head key={player.id} colSpan={2} className="text-center"> | ||
{player.name} | ||
</Table.Head> | ||
))} | ||
</Table.Row> | ||
|
||
{/* | ||
<Table.Row> | ||
<Table.Head>Name</Table.Head> | ||
<Table.Head>Date</Table.Head> | ||
<Table.Head className="text-center"> | ||
<Icon icon={Clock} color="current" size="sm" /> | ||
</Table.Head> | ||
<Table.Head className="text-center"> | ||
<Icon icon={Star} color="current" size="sm" /> | ||
</Table.Head> | ||
<Table.Head className="text-center"> | ||
<Icon icon={Clock} color="current" size="sm" /> | ||
</Table.Head> | ||
<Table.Head className="text-center"> | ||
<Icon icon={Star} color="current" size="sm" /> | ||
</Table.Head> | ||
<Table.Head className="text-center"> | ||
<Icon icon={Clock} color="current" size="sm" /> | ||
</Table.Head> | ||
<Table.Head className="text-center"> | ||
<Icon icon={Star} color="current" size="sm" /> | ||
</Table.Head> | ||
</Table.Row> | ||
*/} | ||
</Table.Header> | ||
|
||
<Table.Body> | ||
{games.map(game => ( | ||
<Table.Row key={game.name}> | ||
<Table.Cell>{game.name}</Table.Cell> | ||
<Table.Cell className={noOverflow}>{game.date}</Table.Cell> | ||
<Table.Cell className="text-end"> | ||
<Hours hours={game.online.playtime} /> | ||
</Table.Cell> | ||
<Table.Cell className="text-end"> | ||
<Rating rating={game.online.rating} /> | ||
</Table.Cell> | ||
<Table.Cell className="text-end"> | ||
<Hours hours={game.player1.playtime} /> | ||
</Table.Cell> | ||
<Table.Cell className="text-end"> | ||
<Rating rating={game.player1.rating} /> | ||
</Table.Cell> | ||
<Table.Cell className="text-end"> | ||
<Hours hours={game.player2.playtime} /> | ||
</Table.Cell> | ||
<Table.Cell className="text-end"> | ||
<Rating rating={game.player2.rating} /> | ||
</Table.Cell> | ||
</Table.Row> | ||
))} | ||
</Table.Body> | ||
|
||
<Table.Footer> | ||
<Table.Row> | ||
<Table.Cell colSpan={2}>Average</Table.Cell> | ||
<Table.Cell align="right"> | ||
<Hours hours={average.online.playtime} /> | ||
</Table.Cell> | ||
<Table.Cell align="right"> | ||
<Rating rating={average.online.rating} /> | ||
</Table.Cell> | ||
<Table.Cell align="right"> | ||
<Hours hours={average.player1.playtime} /> | ||
</Table.Cell> | ||
<Table.Cell align="right"> | ||
<Rating rating={average.player1.rating} /> | ||
</Table.Cell> | ||
<Table.Cell align="right"> | ||
<Hours hours={average.player2.playtime} /> | ||
</Table.Cell> | ||
<Table.Cell align="right"> | ||
<Rating rating={average.player2.rating} /> | ||
</Table.Cell> | ||
</Table.Row> | ||
</Table.Footer> | ||
</Table.Root> | ||
<> | ||
{adding && ( | ||
<GameModal | ||
initialValue={{ date: today() }} | ||
onCancel={() => setAdding(false)} | ||
title="Add game" | ||
description={`Fill in th information about your game and click on "Save" to add it.`} | ||
onConfirm={({ date, name, player }) => { | ||
setAdding(false) | ||
if (name && date) | ||
addGame({ date, name, playerId: player?.id ?? "" }) | ||
}} | ||
/> | ||
)} | ||
<Button variant="ghost" onClick={() => setAdding(true)}> | ||
{label} | ||
</Button> | ||
</> | ||
) | ||
} | ||
|
||
const useDelayValueChange = <T,>(value: T, delay = 500) => { | ||
const [blockedValue, setBlockedValue] = useState(value) | ||
const isBlocked = useRef(false) | ||
const latestValue = useRef(value) | ||
const timeout = useRef<NodeJS.Timeout>() | ||
|
||
const blockChanges = useCallback(() => { | ||
isBlocked.current = true | ||
if (timeout.current) { | ||
clearTimeout(timeout.current) | ||
} | ||
timeout.current = setTimeout(() => { | ||
setBlockedValue(latestValue.current) | ||
isBlocked.current = false | ||
}, delay) | ||
}, [delay]) | ||
|
||
useEffect(() => { | ||
if (value == null) { | ||
setBlockedValue(value) | ||
blockChanges() | ||
return | ||
} | ||
|
||
if (isBlocked.current) { | ||
latestValue.current = value | ||
return | ||
} | ||
|
||
setBlockedValue(value) | ||
}, [value, blockChanges]) | ||
|
||
return blockedValue | ||
} | ||
|
||
export const Overview = () => { | ||
const { games, refreshGames } = useGames() | ||
const { games, removeGame, editGame } = useGames() | ||
const [editing, setEditing] = useState<Game | undefined>() | ||
|
||
const debouncedGames = useDelayValueChange(games, 2000) | ||
|
||
if (!debouncedGames) | ||
if (games.length === 0) | ||
return ( | ||
<div className="flex size-full items-center justify-center"> | ||
<LoadingData label="Loading data..." /> | ||
<div className="flex size-full flex-col items-center justify-center gap-4"> | ||
<NoData | ||
label={ | ||
<> | ||
You didn't roll any games yet. | ||
<br /> | ||
Visit the Game Picker to change that! | ||
</> | ||
} | ||
/> | ||
<AddGame label="Or add one manually" /> | ||
</div> | ||
) | ||
|
||
return ( | ||
<div className="-mr-2 -mt-2 flex h-full flex-col gap-2"> | ||
<div className="flex flex-col overflow-auto [&>*]:h-full [&>*]:flex-1"> | ||
<GamesTable games={debouncedGames} /> | ||
</div> | ||
<div> | ||
<Button variant="flat" onClick={refreshGames}> | ||
<Icon icon={RefreshCw} size="sm" /> | ||
Reload | ||
</Button> | ||
<> | ||
{editing && ( | ||
<GameModal | ||
initialValue={editing} | ||
onCancel={() => setEditing(undefined)} | ||
title="Edit game" | ||
description={`Edit "${editing.name}" to your liking and click on "Save" to confirm.`} | ||
onConfirm={({ id, name, player, ...rest }) => { | ||
setEditing(undefined) | ||
if (id) | ||
editGame(id, { | ||
name: name?.slice(0, 50), | ||
playerId: player?.id, | ||
...rest, | ||
}) | ||
}} | ||
/> | ||
)} | ||
|
||
<div className="-m-2 flex h-[calc(100%+theme(height.4))] flex-col gap-2"> | ||
<div className="flex flex-col overflow-auto [&>*]:h-full [&>*]:flex-1"> | ||
<GamesTable | ||
data={games} | ||
onEdit={setEditing} | ||
onDelete={({ id }) => removeGame(id)} | ||
/> | ||
</div> | ||
</div> | ||
</div> | ||
</> | ||
) | ||
} |
File renamed without changes.
Oops, something went wrong.