Skip to content

Commit fddea96

Browse files
committed
Enhanced Game versions selection popup
1 parent 810697b commit fddea96

1 file changed

Lines changed: 168 additions & 13 deletions

File tree

src/components/VersionSelectDialog.tsx

Lines changed: 168 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,17 @@ import {
66
DialogDescription,
77
DialogTitle,
88
} from "@/components/tailwind/dialog";
9+
import {
10+
Table,
11+
TableBody,
12+
TableCell,
13+
TableHead,
14+
TableHeader,
15+
TableRow,
16+
} from "@tw/table";
17+
import { CheckIcon } from "@heroicons/react/24/solid";
918
import { Button } from "@tw/button";
19+
import { useMemo, useState } from "react";
1020

1121
interface VersionSelectDialogProps {
1222
open: boolean;
@@ -16,34 +26,179 @@ interface VersionSelectDialogProps {
1626
onClose: () => void;
1727
}
1828

29+
type SortKey = "version" | "indexed_at" | "type" | "early_access" | "size";
30+
1931
export function VersionSelectDialog({
2032
open,
2133
gameTitle,
2234
versions,
2335
onSelect,
2436
onClose,
2537
}: VersionSelectDialogProps) {
38+
const [sortBy, setSortBy] = useState<SortKey>("indexed_at");
39+
const [sortDirection, setSortDirection] = useState<"asc" | "desc">("desc");
40+
41+
const toSizeNumber = (size: string) => {
42+
const parsed = Number(size);
43+
return Number.isFinite(parsed) ? parsed : 0;
44+
};
45+
46+
const formatSize = (rawSize: string) => {
47+
const bytes = toSizeNumber(rawSize);
48+
if (bytes <= 0) return "0 B";
49+
if (bytes < 1024) return `${bytes} B`;
50+
const units = ["KB", "MB", "GB", "TB", "PB"];
51+
let value = bytes / 1024;
52+
let unitIndex = 0;
53+
while (value >= 1024 && unitIndex < units.length - 1) {
54+
value /= 1024;
55+
unitIndex++;
56+
}
57+
const precision = value < 10 ? 2 : value < 100 ? 1 : 0;
58+
return `${value.toFixed(precision)} ${units[unitIndex]}`;
59+
};
60+
61+
const formatGameType = (type: string) =>
62+
type
63+
.replace(/_/g, " ")
64+
.toLowerCase()
65+
.replace(/\b\w/g, (c) => c.toUpperCase());
66+
67+
const formatIndexedAt = (value: Date) => {
68+
const date = value instanceof Date ? value : new Date(value);
69+
if (Number.isNaN(date.getTime())) return "-";
70+
return date.toLocaleString();
71+
};
72+
73+
const sortedVersions = useMemo(() => {
74+
const multiplier = sortDirection === "asc" ? 1 : -1;
75+
return [...versions].sort((left, right) => {
76+
let compareValue = 0;
77+
78+
if (sortBy === "version") {
79+
compareValue = (left.version || "").localeCompare(right.version || "", undefined, {
80+
sensitivity: "base",
81+
numeric: true,
82+
});
83+
} else if (sortBy === "indexed_at") {
84+
compareValue =
85+
new Date(left.indexed_at).getTime() - new Date(right.indexed_at).getTime();
86+
} else if (sortBy === "type") {
87+
compareValue = left.type.localeCompare(right.type, undefined, {
88+
sensitivity: "base",
89+
});
90+
} else if (sortBy === "early_access") {
91+
compareValue = Number(left.early_access) - Number(right.early_access);
92+
} else if (sortBy === "size") {
93+
compareValue = toSizeNumber(left.size) - toSizeNumber(right.size);
94+
}
95+
96+
return compareValue * multiplier;
97+
});
98+
}, [versions, sortBy, sortDirection]);
99+
100+
const handleSort = (nextSortBy: SortKey) => {
101+
if (sortBy === nextSortBy) {
102+
setSortDirection((prev) => (prev === "asc" ? "desc" : "asc"));
103+
return;
104+
}
105+
setSortBy(nextSortBy);
106+
setSortDirection("asc");
107+
};
108+
109+
const sortIndicator = (key: SortKey) => {
110+
if (sortBy !== key) return "↕";
111+
return sortDirection === "asc" ? "↑" : "↓";
112+
};
113+
26114
if (!open) return null;
27115

28116
return (
29-
<Dialog open onClose={onClose} size="lg">
117+
<Dialog open onClose={onClose} size="5xl">
30118
<DialogTitle>Select Version</DialogTitle>
31119
<DialogDescription>
32120
Choose which version of {gameTitle} you want to download.
33121
</DialogDescription>
34122
<DialogBody className="pt-3">
35-
<div className="flex flex-col gap-2">
36-
{versions.map((version) => (
37-
<button
38-
key={version.id}
39-
type="button"
40-
onClick={() => onSelect(version)}
41-
className="w-full rounded-md border border-zinc-300/60 dark:border-zinc-700 px-3 py-2 text-left text-sm hover:bg-zinc-100 dark:hover:bg-zinc-800"
42-
>
43-
({version.id}) {version.version || "Unknown Version"}
44-
</button>
45-
))}
46-
</div>
123+
<Table className="[--gutter:--spacing(3)] max-h-[55vh] overflow-y-auto [&::-webkit-scrollbar]:h-2 [&::-webkit-scrollbar]:w-2 [&::-webkit-scrollbar-track]:bg-transparent [&::-webkit-scrollbar-thumb]:bg-zinc-300 [&::-webkit-scrollbar-thumb]:rounded-full dark:[&::-webkit-scrollbar-thumb]:bg-zinc-600 [scrollbar-color:#a1a1aa_transparent] dark:[scrollbar-color:#52525b_transparent]">
124+
<TableHead>
125+
<TableRow>
126+
<TableHeader className="sticky top-0 z-10 bg-white dark:bg-zinc-900">
127+
<button
128+
type="button"
129+
onClick={() => handleSort("version")}
130+
className="inline-flex items-center gap-1"
131+
>
132+
Version {sortIndicator("version")}
133+
</button>
134+
</TableHeader>
135+
<TableHeader className="sticky top-0 z-10 bg-white dark:bg-zinc-900">
136+
<button
137+
type="button"
138+
onClick={() => handleSort("indexed_at")}
139+
className="inline-flex items-center gap-1"
140+
>
141+
Date Added {sortIndicator("indexed_at")}
142+
</button>
143+
</TableHeader>
144+
<TableHeader className="sticky top-0 z-10 bg-white dark:bg-zinc-900">
145+
<button
146+
type="button"
147+
onClick={() => handleSort("type")}
148+
className="inline-flex items-center gap-1"
149+
>
150+
Game Type {sortIndicator("type")}
151+
</button>
152+
</TableHeader>
153+
<TableHeader className="sticky top-0 z-10 bg-white dark:bg-zinc-900 text-center">
154+
<button
155+
type="button"
156+
onClick={() => handleSort("early_access")}
157+
className="inline-flex items-center gap-1"
158+
>
159+
Early Access {sortIndicator("early_access")}
160+
</button>
161+
</TableHeader>
162+
<TableHeader className="sticky top-0 z-10 bg-white dark:bg-zinc-900">
163+
<button
164+
type="button"
165+
onClick={() => handleSort("size")}
166+
className="inline-flex items-center gap-1"
167+
>
168+
Size {sortIndicator("size")}
169+
</button>
170+
</TableHeader>
171+
</TableRow>
172+
</TableHead>
173+
<TableBody>
174+
{sortedVersions.map((version) => (
175+
<TableRow
176+
key={version.id}
177+
className="cursor-pointer hover:bg-zinc-100/80 dark:hover:bg-zinc-800/70"
178+
onClick={() => onSelect(version)}
179+
>
180+
<TableCell>
181+
<span
182+
className="block max-w-[25rem] overflow-hidden text-ellipsis whitespace-nowrap"
183+
title={`(${version.id}) ${version.version || "Unknown Version"}`}
184+
>
185+
{version.version || "Unknown Version"}
186+
</span>
187+
</TableCell>
188+
<TableCell>{formatIndexedAt(version.indexed_at)}</TableCell>
189+
<TableCell>{formatGameType(version.type)}</TableCell>
190+
<TableCell className="text-center">
191+
{version.early_access ? (
192+
<span className="inline-flex items-center justify-center" title="Early Access">
193+
<CheckIcon className="h-4 w-4" />
194+
</span>
195+
) : null}
196+
</TableCell>
197+
<TableCell>{formatSize(version.size)}</TableCell>
198+
</TableRow>
199+
))}
200+
</TableBody>
201+
</Table>
47202
</DialogBody>
48203
<DialogActions>
49204
<Button plain onClick={onClose}>

0 commit comments

Comments
 (0)