mirror of
https://github.com/blitzkit/blitzkit.git
synced 2026-10-21 02:37:05 +02:00
feat: spotify like table
This commit is contained in:
1 parent
37adc04cda
commit
e1a6e4e38d
12 files changed
+159
-114
No files matched your search
@@ -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>
|
||||
|
||||
@@ -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");
|
||||
Reference in new issue
Block a user