feat: spotify like table

This commit is contained in:
TrèsAbhi committed 2025-10-14 12:11:31 -05:00
1 parent 37adc04cda
commit e1a6e4e38d
12 files changed
+159 -114

No files matched your search

+4 -1
View File
@@ -862,7 +862,10 @@
"playlist": {
"clear": "Clear",
"table": {
"tank": "Tank"
"index": "#",
"never": "Never",
"tank": "Tank",
"last_played": "Last played"
},
"no_login_warning": "You are not logged in. BlitzKit will not keep track of your played tanks.",
"generate": "Generate playlist",
@@ -0,0 +1,7 @@
import { Flex } from "@radix-ui/themes";
export function CurrentlyPlaying() {
return <Flex>
</Flex>;
}
@@ -1,16 +1,23 @@
import { Button, Flex, Table } from "@radix-ui/themes";
import { Box, Button, Flex } from "@radix-ui/themes";
import { useLocale } from "../hooks/useLocale";
import { Playlist } from "../stores/playlist";
import { PlaylistTankEntry } from "./PlaylistTankEntry";
import { CurrentlyPlaying } from "./CurrentlyPlaying";
import { PlaylistTable } from "./PlaylistTable";
export function PlaylistOrder() {
const { strings } = useLocale();
const list = Playlist.use((state) => state.list);
if (!list) return null;
return (
<Flex direction="column" gap="4" align="center" flexGrow="1" flexBasis="0">
<Flex
direction="column"
gap="4"
align="center"
flexGrow="1"
flexBasis="0"
maxHeight="100%"
>
<CurrentlyPlaying />
<Button
onClick={() => {
Playlist.mutate((draft) => {
@@ -21,28 +28,9 @@ export function PlaylistOrder() {
{strings.website.tools.playlist.clear}
</Button>
<Table.Root variant="surface">
<Table.Header>
<Table.Row>
<Table.ColumnHeaderCell />
<Table.ColumnHeaderCell>
{strings.website.tools.playlist.table.tank}
</Table.ColumnHeaderCell>
<Table.ColumnHeaderCell />
</Table.Row>
</Table.Header>
<Table.Body>
{list.map(({ tank, checked }, index) => (
<PlaylistTankEntry
key={tank.id}
tank={tank}
checked={checked}
index={index}
/>
))}
</Table.Body>
</Table.Root>
<Box flexGrow="1" width="100%" position="relative">
<PlaylistTable />
</Box>
</Flex>
);
}
@@ -0,0 +1,55 @@
import { Table, Text } from "@radix-ui/themes";
import { useLocale } from "../hooks/useLocale";
import { Playlist } from "../stores/playlist";
import { PlaylistTankEntry } from "./PlaylistTankEntry";
import { StickyColumnHeaderCell } from "./StickyColumnHeaderCell";
import { StickyTableRoot } from "./StickyTableRoot";
export function PlaylistTable() {
const { strings } = useLocale();
const list = Playlist.use((state) => state.list);
if (!list) return null;
return (
<StickyTableRoot
style={{
position: "absolute",
top: 0,
left: 0,
height: "100%",
width: "100%",
borderRadius: 0,
}}
variant="surface"
>
<Table.Header>
<Table.Row>
<StickyColumnHeaderCell width="0">
<Text color="gray">
{strings.website.tools.playlist.table.index}
</Text>
</StickyColumnHeaderCell>
<StickyColumnHeaderCell width="1">
{strings.website.tools.playlist.table.tank}
</StickyColumnHeaderCell>
<StickyColumnHeaderCell width="1">
{strings.website.tools.playlist.table.last_played}
</StickyColumnHeaderCell>
<StickyColumnHeaderCell width="0" />
<StickyColumnHeaderCell width="0" />
</Table.Row>
</Table.Header>
<Table.Body>
{list.map((props, index) => (
<PlaylistTankEntry key={props.tank.id} index={index} {...props} />
))}
</Table.Body>
</StickyTableRoot>
);
}
@@ -1,18 +1,31 @@
import type { TankDefinition } from "@blitzkit/core";
import { DragHandleDots2Icon, MinusCircledIcon } from "@radix-ui/react-icons";
import { Checkbox, Flex, IconButton, Table, Text } from "@radix-ui/themes";
import { Playlist } from "../stores/playlist";
import { MinusCircledIcon } from "@radix-ui/react-icons";
import { Checkbox, IconButton, Table } from "@radix-ui/themes";
import { useLocale } from "../hooks/useLocale";
import { Playlist, type PlaylistEntry } from "../stores/playlist";
import { TankRowHeaderCell } from "./TankRowHeaderCell";
interface Props {
tank: TankDefinition;
checked: boolean;
interface Props extends PlaylistEntry {
index: number;
}
export function PlaylistTankEntry({ tank, checked, index }: Props) {
export function PlaylistTankEntry({ tank, checked, last, index }: Props) {
const { locale, strings } = useLocale();
const rtf = new Intl.RelativeTimeFormat(locale, { numeric: "auto" });
const nowDays = Date.now() / (1000 * 60 * 60 * 24);
const lastDays = last ? last / (60 * 60 * 24) : undefined;
return (
<Table.Row>
<Table.Cell>{index + 1}</Table.Cell>
<TankRowHeaderCell tank={tank} />
<Table.Cell>
{lastDays
? rtf.format(Math.floor(lastDays - nowDays), "day")
: strings.website.tools.playlist.table.never}
</Table.Cell>
<Table.Cell
onClick={() => {
Playlist.mutate((draft) => {
@@ -23,41 +36,18 @@ export function PlaylistTankEntry({ tank, checked, index }: Props) {
<Checkbox variant="classic" checked={checked} />
</Table.Cell>
<TankRowHeaderCell tank={tank} />
<Table.Cell>
<Flex
align="center"
height="calc(100% + 2 * var(--space-3) - 2px)"
gap="4"
pl="2"
my="-3"
mr="-3"
<IconButton
variant="ghost"
size="1"
onClick={() => {
Playlist.mutate((draft) => {
draft.list!.splice(index, 1);
});
}}
>
<IconButton
variant="ghost"
size="1"
onClick={() => {
Playlist.mutate((draft) => {
draft.list!.splice(index, 1);
});
}}
>
<MinusCircledIcon />
</IconButton>
<Flex
width="5rem"
justify="center"
align="center"
height="100%"
style={{ cursor: "grab", backgroundColor: "var(--gray-1)" }}
>
<Text color="gray" trim="end">
<DragHandleDots2Icon />
</Text>
</Flex>
</Flex>
<MinusCircledIcon />
</IconButton>
</Table.Cell>
</Table.Row>
);
@@ -1,15 +1,14 @@
import { Table } from '@radix-ui/themes';
import type { RowHeaderCellProps } from '@radix-ui/themes/src/components/table.js';
import { Table } from "@radix-ui/themes";
import type { RowHeaderCellProps } from "@radix-ui/themes/src/components/table.js";
export function StickyRowHeaderCell({ style, ...props }: RowHeaderCellProps) {
return (
<Table.RowHeaderCell
style={{
display: 'flex',
position: 'sticky',
display: "flex",
position: "sticky",
left: 0,
backgroundColor: 'var(--color-panel)',
boxShadow: 'var(--shadow-6)',
backgroundColor: "var(--color-panel)",
...style,
}}
{...props}
@@ -1,9 +1,9 @@
import { type TankDefinition, tankIcon, TankType } from '@blitzkit/core';
import { useLocale } from '../hooks/useLocale';
import { classIcons } from './ClassIcon';
import { ExperimentIcon } from './ExperimentIcon';
import { LinkI18n } from './LinkI18n';
import { StickyRowHeaderCell } from './StickyRowHeaderCell';
import { type TankDefinition, tankIcon, TankType } from "@blitzkit/core";
import { useLocale } from "../hooks/useLocale";
import { classIcons } from "./ClassIcon";
import { ExperimentIcon } from "./ExperimentIcon";
import { LinkI18n } from "./LinkI18n";
import { StickyRowHeaderCell } from "./StickyRowHeaderCell";
interface TankRowHeaderCellProps {
tank: TankDefinition;
@@ -14,20 +14,17 @@ export function TankRowHeaderCell({ tank }: TankRowHeaderCellProps) {
const Icon = classIcons[tank.class];
return (
<StickyRowHeaderCell
width={{ initial: '144px', sm: '240px' }}
style={{ overflow: 'hidden' }}
>
<StickyRowHeaderCell style={{ overflow: "hidden" }}>
<LinkI18n locale={locale} href={`/tanks/${tank.slug}`} tabIndex={-1}>
<img
alt={unwrap(tank.name)}
draggable={false}
src={tankIcon(tank.id)}
style={{
margin: 'calc(-1 * var(--table-cell-padding)) 0',
height: 'calc(100% + 2 * var(--table-cell-padding))',
aspectRatio: '4 / 3',
objectFit: 'cover',
margin: "calc(-1 * var(--table-cell-padding)) 0",
height: "calc(100% + 2 * var(--table-cell-padding))",
aspectRatio: "4 / 3",
objectFit: "cover",
}}
/>
</LinkI18n>
@@ -36,34 +33,34 @@ export function TankRowHeaderCell({ tank }: TankRowHeaderCellProps) {
locale={locale}
color={
tank.type === TankType.COLLECTOR
? 'blue'
? "blue"
: tank.type === TankType.PREMIUM
? 'amber'
: 'gray'
? "amber"
: "gray"
}
highContrast={tank.type === TankType.RESEARCHABLE}
underline="hover"
wrap="nowrap"
href={`/tanks/${tank.slug}`}
style={{
paddingLeft: 'var(--space-2)',
display: 'flex',
alignItems: 'center',
gap: 'var(--space-1)',
whiteSpace: 'nowrap',
overflow: 'hidden',
textOverflow: 'ellipsis',
paddingLeft: "var(--space-2)",
display: "flex",
alignItems: "center",
gap: "var(--space-1)",
whiteSpace: "nowrap",
overflow: "hidden",
textOverflow: "ellipsis",
}}
size={{ initial: '1', sm: '2' }}
size={{ initial: "1", sm: "2" }}
>
<Icon
width="1em"
height="1em"
style={{ minWidth: '1em', minHeight: '1em' }}
style={{ minWidth: "1em", minHeight: "1em" }}
/>
{tank.testing && (
<ExperimentIcon style={{ width: '1em', height: '1em' }} />
<ExperimentIcon style={{ width: "1em", height: "1em" }} />
)}
{unwrap(tank.name)}
@@ -35,6 +35,8 @@ export async function generatePlaylist() {
tank,
battles: stats[tank.id]?.all.battles,
wins: stats[tank.id]?.all.wins,
last: stats[tank.id]?.last_battle_time,
checked: false,
}));
});
}
@@ -8,9 +8,11 @@ import BaseLayout from "./BaseLayout.astro";
interface Props {
tool: Tool;
locale: string;
noNavbar?: boolean;
noFooter?: boolean;
}
const { tool, locale } = Astro.props;
const { tool, locale, noNavbar, noFooter } = Astro.props;
const strings = getStrings(locale);
const toolStrings =
strings.website.tools[
@@ -20,7 +22,7 @@ const toolStrings =
<ToolOpenGraph {tool} />
<BaseLayout {locale}>
<BaseLayout {locale} {noNavbar} {noFooter}>
<Box
style={{
backgroundImage: `url(/assets/images/tools/${tool.id}.webp)`,
@@ -29,7 +29,7 @@ export function Page({
return (
<LocaleProvider locale={locale}>
<PageWrapper color="tomato" direction="row">
<PageWrapper color="tomato" direction="row" maxWidth="unset" p="0">
{!list && <PlaylistGenerator />}
{list && <PlaylistOrder />}
</PageWrapper>
@@ -8,7 +8,7 @@ export { getStaticPaths } from "../index.astro";
const { locale } = Astro.params;
---
<ToolLayout {locale} tool={tools.playlist}>
<ToolLayout {locale} tool={tools.playlist} noFooter>
<Page {locale} client:only="react">
<Page {locale} skeleton slot="fallback" />
</Page>
+9 -7
View File
@@ -1,14 +1,16 @@
import type { TankDefinition } from "@blitzkit/core";
import { Varuna } from "varuna";
export interface PlaylistEntry {
tank: TankDefinition;
battles?: number;
wins?: number;
checked: boolean;
last?: number;
}
export interface Playlist {
list?: {
index: number;
tank: TankDefinition;
battles?: number;
wins?: number;
checked: boolean;
}[];
list?: PlaylistEntry[];
}
export const Playlist = new Varuna<Playlist>({}, "playlist");