diff --git a/docs/changelogs/1.29.0.md b/docs/changelogs/1.29.0.md new file mode 100644 index 000000000..009d3aa74 --- /dev/null +++ b/docs/changelogs/1.29.0.md @@ -0,0 +1,12 @@ +# BlitzKit 1.29.0 + +## Features + +- Added the Playlist tool + - Don't know what to play today? Let BlitzKit do the thinking! + - Navigate to [the tool on BlitzKit](https://blitzkit.app/playlist) + - Make sure you login with your Wargaming account to unlock all features like live tracking and filtering by owned tanks + - Use filters to pick tiers, nations, etc. then hit Generate playlist + - BlitzKit will generate a checklist for you to go through as you play + - Don't like the order of tanks? Hit Shuffle, Sort by Last played and Least played, or straight up remove tanks from the list by using the (-) button + - If you're logged in, BlitzKit will automatically check off tanks as you play them diff --git a/package.json b/package.json index bb8c2e34c..475045f76 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "blitzkit", - "version": "1.28.4", + "version": "1.29.0", "license": "BlitzKit License", "private": true, "type": "module", diff --git a/packages/core/src/math/fisherYates.ts b/packages/core/src/math/fisherYates.ts new file mode 100644 index 000000000..74198bd7d --- /dev/null +++ b/packages/core/src/math/fisherYates.ts @@ -0,0 +1,8 @@ +export function fisherYates(array: Type) { + for (let i = array.length - 1; i > 0; i--) { + const j = Math.floor(Math.random() * (i + 1)); + [array[i], array[j]] = [array[j], array[i]]; + } + + return array; +} diff --git a/packages/core/src/math/index.ts b/packages/core/src/math/index.ts index bb4a1859b..b8a2089a0 100644 --- a/packages/core/src/math/index.ts +++ b/packages/core/src/math/index.ts @@ -1,7 +1,8 @@ -export * from './axis'; -export * from './formatCompact'; -export * from './isNumber'; -export * from './NaNFallback'; -export * from './normalizeAngleRad'; -export * from './normalToEuler'; -export * from './numberToRomanNumeral'; +export * from "./axis"; +export * from "./fisherYates"; +export * from "./formatCompact"; +export * from "./isNumber"; +export * from "./NaNFallback"; +export * from "./normalizeAngleRad"; +export * from "./normalToEuler"; +export * from "./numberToRomanNumeral"; diff --git a/packages/i18n/strings/en.json b/packages/i18n/strings/en.json index 1cd725156..a57a6a6ed 100644 --- a/packages/i18n/strings/en.json +++ b/packages/i18n/strings/en.json @@ -859,6 +859,43 @@ }, "tools": { + "playlist": { + "reset": "Reset", + "shuffle": "Shuffle", + "remove": "Remove played", + "tracking": "BlitzKit is tracking your battles", + + "sort": { + "button": "Sort by", + "last_played": "Last played", + "least_played": "Least played" + }, + + "table": { + "index": "#", + "never": "Never", + "tank": "Tank", + "last_played": "Last played", + "battles": "Career battles", + "unrecorded": "--", + "wins": "Wins" + }, + + "large_warning": { + "title": "That's a lot of tanks!", + "description": "Are you sure you want to create a playlist of {count} tanks? That's {time} hours of gameplay at 50% winrate!", + "cancel": "Cancel", + "proceed": "Proceed" + }, + + "generate": "Generate playlist", + "name": "Playlist", + "description": "Tank playlist generator", + "seo_description": "Don't know what to play in World of Tanks Blitz? Use BlitzKit Playlist to get suggestions!", + "button": "Generate", + "pool_size": "{count} possible tanks" + }, + "mixer": { "name": "Mixer", "button": "Mix and match", diff --git a/packages/i18n/strings/es.json b/packages/i18n/strings/es.json index 44c35cdc6..3662b4289 100644 --- a/packages/i18n/strings/es.json +++ b/packages/i18n/strings/es.json @@ -762,6 +762,38 @@ } }, "tools": { + "playlist": { + "reset": "Restablecer", + "shuffle": "Shuffle", + "remove": "Remove played", + "tracking": "BlitzKit is tracking your battles", + "sort": { + "button": "Ordenar por", + "last_played": "Last played", + "least_played": "Least played" + }, + "table": { + "index": "#", + "never": "Never", + "tank": "Tanque", + "last_played": "Last played", + "battles": "Career battles", + "unrecorded": "--", + "wins": "Victorias" + }, + "large_warning": { + "title": "That's a lot of tanks!", + "description": "Are you sure you want to create a playlist of {count} tanks? That's {time} hours of gameplay at 50% winrate!", + "cancel": "Cancelar", + "proceed": "Proceed" + }, + "generate": "Generate playlist", + "name": "Playlist", + "description": "Tank playlist generator", + "seo_description": "Don't know what to play in World of Tanks Blitz? Use BlitzKit Playlist to get suggestions!", + "button": "Generate", + "pool_size": "{count} possible tanks" + }, "mixer": { "name": "Mixer", "button": "Mix and match", diff --git a/packages/i18n/strings/fr.json b/packages/i18n/strings/fr.json index 0994940ac..49d823e1e 100644 --- a/packages/i18n/strings/fr.json +++ b/packages/i18n/strings/fr.json @@ -762,6 +762,38 @@ } }, "tools": { + "playlist": { + "reset": "Reset", + "shuffle": "Shuffle", + "remove": "Remove played", + "tracking": "BlitzKit is tracking your battles", + "sort": { + "button": "Sort by", + "last_played": "Last played", + "least_played": "Least played" + }, + "table": { + "index": "#", + "never": "Never", + "tank": "Tank", + "last_played": "Last played", + "battles": "Career battles", + "unrecorded": "--", + "wins": "Victoires" + }, + "large_warning": { + "title": "That's a lot of tanks!", + "description": "Are you sure you want to create a playlist of {count} tanks? That's {time} hours of gameplay at 50% winrate!", + "cancel": "Cancel", + "proceed": "Proceed" + }, + "generate": "Generate playlist", + "name": "Playlist", + "description": "Tank playlist generator", + "seo_description": "Don't know what to play in World of Tanks Blitz? Use BlitzKit Playlist to get suggestions!", + "button": "Generate", + "pool_size": "{count} possible tanks" + }, "mixer": { "name": "Mixer", "button": "Mix and match", diff --git a/packages/i18n/strings/ja.json b/packages/i18n/strings/ja.json index 4eba64418..f1734055d 100644 --- a/packages/i18n/strings/ja.json +++ b/packages/i18n/strings/ja.json @@ -762,6 +762,38 @@ } }, "tools": { + "playlist": { + "reset": "リセット", + "shuffle": "Shuffle", + "remove": "Remove played", + "tracking": "BlitzKit is tracking your battles", + "sort": { + "button": "並べ替え項目", + "last_played": "Last played", + "least_played": "Least played" + }, + "table": { + "index": "#", + "never": "Never", + "tank": "車輌名", + "last_played": "Last played", + "battles": "Career battles", + "unrecorded": "--", + "wins": "勝利" + }, + "large_warning": { + "title": "That's a lot of tanks!", + "description": "Are you sure you want to create a playlist of {count} tanks? That's {time} hours of gameplay at 50% winrate!", + "cancel": "Cancel", + "proceed": "Proceed" + }, + "generate": "Generate playlist", + "name": "Playlist", + "description": "Tank playlist generator", + "seo_description": "Don't know what to play in World of Tanks Blitz? Use BlitzKit Playlist to get suggestions!", + "button": "Generate", + "pool_size": "{count} possible tanks" + }, "mixer": { "name": "Mixer", "button": "Mix and match", diff --git a/packages/i18n/strings/pl.json b/packages/i18n/strings/pl.json index dc2c2df51..112df784b 100644 --- a/packages/i18n/strings/pl.json +++ b/packages/i18n/strings/pl.json @@ -762,6 +762,38 @@ } }, "tools": { + "playlist": { + "reset": "Reset", + "shuffle": "Shuffle", + "remove": "Remove played", + "tracking": "BlitzKit is tracking your battles", + "sort": { + "button": "Sort by", + "last_played": "Last played", + "least_played": "Least played" + }, + "table": { + "index": "#", + "never": "Never", + "tank": "Tank", + "last_played": "Last played", + "battles": "Career battles", + "unrecorded": "--", + "wins": "Zwycięstwa" + }, + "large_warning": { + "title": "That's a lot of tanks!", + "description": "Are you sure you want to create a playlist of {count} tanks? That's {time} hours of gameplay at 50% winrate!", + "cancel": "Cancel", + "proceed": "Proceed" + }, + "generate": "Generate playlist", + "name": "Playlist", + "description": "Tank playlist generator", + "seo_description": "Don't know what to play in World of Tanks Blitz? Use BlitzKit Playlist to get suggestions!", + "button": "Generate", + "pool_size": "{count} possible tanks" + }, "mixer": { "name": "Mixer", "button": "Mix and match", diff --git a/packages/i18n/strings/pt.json b/packages/i18n/strings/pt.json index adff3e1b3..037c93c60 100644 --- a/packages/i18n/strings/pt.json +++ b/packages/i18n/strings/pt.json @@ -762,6 +762,38 @@ } }, "tools": { + "playlist": { + "reset": "Restaurar", + "shuffle": "Shuffle", + "remove": "Remove played", + "tracking": "BlitzKit is tracking your battles", + "sort": { + "button": "Ordenar por", + "last_played": "Last played", + "least_played": "Least played" + }, + "table": { + "index": "#", + "never": "Never", + "tank": "Tanque", + "last_played": "Last played", + "battles": "Career battles", + "unrecorded": "--", + "wins": "Vitórias" + }, + "large_warning": { + "title": "That's a lot of tanks!", + "description": "Are you sure you want to create a playlist of {count} tanks? That's {time} hours of gameplay at 50% winrate!", + "cancel": "Cancelar", + "proceed": "Proceed" + }, + "generate": "Generate playlist", + "name": "Playlist", + "description": "Tank playlist generator", + "seo_description": "Don't know what to play in World of Tanks Blitz? Use BlitzKit Playlist to get suggestions!", + "button": "Generate", + "pool_size": "{count} possible tanks" + }, "mixer": { "name": "Mixer", "button": "Mix and match", diff --git a/packages/i18n/strings/ru.json b/packages/i18n/strings/ru.json index e0941f1e4..7d892eb12 100644 --- a/packages/i18n/strings/ru.json +++ b/packages/i18n/strings/ru.json @@ -762,6 +762,38 @@ } }, "tools": { + "playlist": { + "reset": "Сброс", + "shuffle": "Shuffle", + "remove": "Remove played", + "tracking": "BlitzKit is tracking your battles", + "sort": { + "button": "Сортировка", + "last_played": "Last played", + "least_played": "Least played" + }, + "table": { + "index": "#", + "never": "Never", + "tank": "Танк", + "last_played": "Last played", + "battles": "Career battles", + "unrecorded": "--", + "wins": "Победы" + }, + "large_warning": { + "title": "That's a lot of tanks!", + "description": "Are you sure you want to create a playlist of {count} tanks? That's {time} hours of gameplay at 50% winrate!", + "cancel": "Отмена", + "proceed": "Proceed" + }, + "generate": "Generate playlist", + "name": "Playlist", + "description": "Tank playlist generator", + "seo_description": "Don't know what to play in World of Tanks Blitz? Use BlitzKit Playlist to get suggestions!", + "button": "Generate", + "pool_size": "{count} possible tanks" + }, "mixer": { "name": "Смешать", "button": "Смешать и собрать", diff --git a/packages/i18n/strings/uk.json b/packages/i18n/strings/uk.json index b7f3c025b..de11ec099 100644 --- a/packages/i18n/strings/uk.json +++ b/packages/i18n/strings/uk.json @@ -762,6 +762,38 @@ } }, "tools": { + "playlist": { + "reset": "Reset", + "shuffle": "Shuffle", + "remove": "Remove played", + "tracking": "BlitzKit is tracking your battles", + "sort": { + "button": "Сортувати за", + "last_played": "Last played", + "least_played": "Least played" + }, + "table": { + "index": "#", + "never": "Never", + "tank": "Танк", + "last_played": "Last played", + "battles": "Career battles", + "unrecorded": "--", + "wins": "Перемоги" + }, + "large_warning": { + "title": "That's a lot of tanks!", + "description": "Are you sure you want to create a playlist of {count} tanks? That's {time} hours of gameplay at 50% winrate!", + "cancel": "Cancel", + "proceed": "Proceed" + }, + "generate": "Generate playlist", + "name": "Playlist", + "description": "Tank playlist generator", + "seo_description": "Don't know what to play in World of Tanks Blitz? Use BlitzKit Playlist to get suggestions!", + "button": "Generate", + "pool_size": "{count} possible tanks" + }, "mixer": { "name": "Mixer", "button": "Mix and match", diff --git a/packages/i18n/strings/zh.json b/packages/i18n/strings/zh.json index 6519f17be..3d9c0b04b 100644 --- a/packages/i18n/strings/zh.json +++ b/packages/i18n/strings/zh.json @@ -284,7 +284,7 @@ "name": "阶级", "description": "阶级", "choices": { - "tier": "Tier {tier_1} - {tier_2}" + "tier": "坦克等级 {tier_1} - {tier_2}" } } }, @@ -296,7 +296,7 @@ }, "filters": { "none": "无筛选器", - "tier": "Tier {tier}" + "tier": "坦克等级 {tier}" }, "no_data": { "battles_in_period": "在这段时间里没有参与任何战斗", @@ -347,7 +347,7 @@ }, "errors": { "non_tech_tree": "# {tank} is not on the tech tree\n\nYou cannot research a premium/collector tank.", - "tier_1": "# {tank} is a tier I\n\nTier I tanks are always unlocked for free for all players.", + "tier_1": "# {tank} 是一级坦克\n\n一级坦克一直是对所有玩家免费解锁的。", "already_researched": "# {tank} is already researched\n\nTry providing a `starting-tank` when using the command to view the totals for a line you have already researched.", "start_end_equal": "# {tank} was provided twice\n\nMake sure `starting-tank` and `target-tank` are different.", "tanks_not_on_line": "# {tank_1} and {tank_2} are not on the same line\n\nMake sure `starting-tank` and `target-tank` are on the same tech-tree line.", @@ -762,6 +762,38 @@ } }, "tools": { + "playlist": { + "reset": "重置", + "shuffle": "Shuffle", + "remove": "Remove played", + "tracking": "BlitzKit is tracking your battles", + "sort": { + "button": "排序依据", + "last_played": "Last played", + "least_played": "Least played" + }, + "table": { + "index": "#", + "never": "Never", + "tank": "战车", + "last_played": "Last played", + "battles": "Career battles", + "unrecorded": "--", + "wins": "胜利" + }, + "large_warning": { + "title": "That's a lot of tanks!", + "description": "Are you sure you want to create a playlist of {count} tanks? That's {time} hours of gameplay at 50% winrate!", + "cancel": "取消", + "proceed": "Proceed" + }, + "generate": "Generate playlist", + "name": "Playlist", + "description": "Tank playlist generator", + "seo_description": "Don't know what to play in World of Tanks Blitz? Use BlitzKit Playlist to get suggestions!", + "button": "Generate", + "pool_size": "{count} possible tanks" + }, "mixer": { "name": "Mixer", "button": "Mix and match", diff --git a/packages/website/public/assets/images/tools/playlist.webp b/packages/website/public/assets/images/tools/playlist.webp new file mode 100644 index 000000000..3504be291 Binary files /dev/null and b/packages/website/public/assets/images/tools/playlist.webp differ diff --git a/packages/website/src/components/CurrentlyPlaying.tsx b/packages/website/src/components/CurrentlyPlaying.tsx new file mode 100644 index 000000000..08427e3f1 --- /dev/null +++ b/packages/website/src/components/CurrentlyPlaying.tsx @@ -0,0 +1,7 @@ +import { Flex } from "@radix-ui/themes"; + +export function CurrentlyPlaying() { + return + + ; +} diff --git a/packages/website/src/components/LocaleRedirector.astro b/packages/website/src/components/LocaleRedirector.astro index b234ba4f3..68f61e45b 100644 --- a/packages/website/src/components/LocaleRedirector.astro +++ b/packages/website/src/components/LocaleRedirector.astro @@ -4,11 +4,11 @@ const isAPI = window.location.pathname.startsWith("/api"); const isI18nPath = locales.supported.some(({ locale }) => - window.location.pathname.startsWith(`/${locale}`) + window.location.pathname.startsWith(`/${locale}/`) ); const bestLocale = getBestLocale(); const currentPath = locales.supported.find(({ locale }) => - window.location.pathname.startsWith(`/${locale}`) + window.location.pathname.startsWith(`/${locale}/`) ); if ( diff --git a/packages/website/src/components/PlaylistActions.tsx b/packages/website/src/components/PlaylistActions.tsx new file mode 100644 index 000000000..faeb14fa3 --- /dev/null +++ b/packages/website/src/components/PlaylistActions.tsx @@ -0,0 +1,106 @@ +import { fisherYates } from "@blitzkit/core"; +import { CaretDownIcon } from "@radix-ui/react-icons"; +import { Button, DropdownMenu, Flex, Spinner } from "@radix-ui/themes"; +import { useLocale } from "../hooks/useLocale"; +import { App } from "../stores/app"; +import { Playlist } from "../stores/playlist"; + +export function PlaylistActions() { + const { strings } = useLocale(); + const wargaming = App.use((state) => state.logins.wargaming); + + return ( + <> + + + + + + {wargaming && ( + <> + + + + + + + + + { + Playlist.mutate((draft) => { + draft.list = draft.list?.sort( + (a, b) => (a.then?.last ?? 0) - (b.then?.last ?? 0) + ); + }); + }} + > + {strings.website.tools.playlist.sort.last_played} + + { + Playlist.mutate((draft) => { + draft.list = draft.list?.sort( + (a, b) => + (a.then?.battles ?? 0) - (b.then?.battles ?? 0) + ); + }); + }} + > + {strings.website.tools.playlist.sort.least_played} + + + + + )} + + + {wargaming && ( + + + {strings.website.tools.playlist.tracking} + + )} + + ); +} diff --git a/packages/website/src/components/PlaylistGenerator.tsx b/packages/website/src/components/PlaylistGenerator.tsx new file mode 100644 index 000000000..85b9c5d0a --- /dev/null +++ b/packages/website/src/components/PlaylistGenerator.tsx @@ -0,0 +1,174 @@ +import { fisherYates, tankIcon } from "@blitzkit/core"; +import { literals } from "@blitzkit/i18n"; +import { ChevronRightIcon } from "@radix-ui/react-icons"; +import { + AlertDialog, + Box, + Button, + Flex, + Grid, + Skeleton, + Text, +} from "@radix-ui/themes"; +import { useMemo, useState } from "react"; +import usePromise from "react-promise-suspense"; +import { awaitableTankDefinitions } from "../core/awaitables/tankDefinitions"; +import { filterTanks } from "../core/blitzkit/filterTanks"; +import { generatePlaylist } from "../core/blitzkit/generatePlaylist"; +import { useLocale } from "../hooks/useLocale"; +import { App } from "../stores/app"; +import { TankFilters } from "../stores/tankFilters"; +import type { MaybeSkeletonComponentProps } from "../types/maybeSkeletonComponentProps"; +import { LinkI18n } from "./LinkI18n"; +import { FilterControl } from "./TankSearch/components/FilterControl"; + +const tankDefinitions = await awaitableTankDefinitions; + +const POOL_HEIGHT = "16rem"; + +export function PlaylistGenerator({ skeleton }: MaybeSkeletonComponentProps) { + const [largeWarningOpen, setLargeWarningOpen] = useState(false); + const { strings, locale } = useLocale(); + const wargaming = App.use((state) => state.logins.wargaming); + const filters = TankFilters.use(); + const randomTankList = useMemo( + () => fisherYates(Object.values(tankDefinitions.tanks)), + [filters] + ); + const tanks = usePromise( + () => filterTanks(filters, randomTankList, wargaming?.id), + // react-promise-suspense has awful type annotations + [filters] as any + ); + const tanksTrimmed = useMemo(() => tanks.slice(0, 65), [filters]); + + return ( + <> + + + + {strings.website.tools.playlist.large_warning.title} + + + {literals( + strings.website.tools.playlist.large_warning.description, + { + count: tanks.length, + time: Math.round(tanks.length * ((2 * 4) / 60)), + } + )} + + + + + + + + + + + + + + + + + + + + + + + + {tanksTrimmed.map((tank) => + skeleton ? ( + + + + ) : ( + + ) + )} + + + + + + {literals(strings.website.tools.playlist.pool_size, { + count: tanks.length, + })} + + + + + + ); +} diff --git a/packages/website/src/components/PlaylistOrder.tsx b/packages/website/src/components/PlaylistOrder.tsx new file mode 100644 index 000000000..f9fa71ab9 --- /dev/null +++ b/packages/website/src/components/PlaylistOrder.tsx @@ -0,0 +1,26 @@ +import { Box, Flex } from "@radix-ui/themes"; +import { CurrentlyPlaying } from "./CurrentlyPlaying"; +import { PlaylistActions } from "./PlaylistActions"; +import { PlaylistTable } from "./PlaylistTable"; +import { PlaylistTracking } from "./PlaylistTracking"; + +export function PlaylistOrder() { + return ( + + + + + + + + + + ); +} diff --git a/packages/website/src/components/PlaylistTable.tsx b/packages/website/src/components/PlaylistTable.tsx new file mode 100644 index 000000000..bbf6abdc0 --- /dev/null +++ b/packages/website/src/components/PlaylistTable.tsx @@ -0,0 +1,64 @@ +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 ( + + + + + + {strings.website.tools.playlist.table.index} + + + + + + + + + {strings.website.tools.playlist.table.tank} + + + + {strings.website.tools.playlist.table.wins} + + + + {strings.website.tools.playlist.table.battles} + + + + {strings.website.tools.playlist.table.last_played} + + + + + + {list.map((props, index) => ( + + ))} + + + ); +} diff --git a/packages/website/src/components/PlaylistTankEntry.tsx b/packages/website/src/components/PlaylistTankEntry.tsx new file mode 100644 index 000000000..5a783d5f9 --- /dev/null +++ b/packages/website/src/components/PlaylistTankEntry.tsx @@ -0,0 +1,107 @@ +import { + MinusCircledIcon, + TriangleDownIcon, + TriangleUpIcon, +} from "@radix-ui/react-icons"; +import { Checkbox, Flex, IconButton, Table, Text } from "@radix-ui/themes"; +import { memo } from "react"; +import { awaitableTankDefinitions } from "../core/awaitables/tankDefinitions"; +import { useLocale } from "../hooks/useLocale"; +import { Playlist, type PlaylistEntry } from "../stores/playlist"; +import { TankRowHeaderCell } from "./TankRowHeaderCell"; + +interface Props extends PlaylistEntry { + index: number; +} + +const tankDefinitions = await awaitableTankDefinitions; + +export const PlaylistTankEntry = memo( + ({ id, checked, then, now, index }) => { + const { locale, strings } = useLocale(); + const rtf = new Intl.RelativeTimeFormat(locale, { numeric: "auto" }); + const dateNow = Date.now(); + const tank = tankDefinitions.tanks[id]; + + return ( + + {index + 1} + + { + Playlist.mutate((draft) => { + draft.list![index].checked = !checked; + }); + }} + > + then.battles) || + checked + } + /> + + + + { + Playlist.mutate((draft) => { + draft.list!.splice(index, 1); + }); + }} + > + + + + + + + + {now && then && now.battles !== then.battles ? ( + + + + + {now.wins - then.wins} + + + + + + + {now.battles - then.battles - now.wins + then.wins} + + + + ) : ( + strings.website.tools.playlist.table.unrecorded + )} + + + + {now?.battles ?? strings.website.tools.playlist.table.unrecorded} + + + + {now && now.last !== 0 + ? rtf.format( + Math.ceil((now.last - dateNow / 1000) / (60 * 60 * 24)), + "day" + ) + : strings.website.tools.playlist.table.unrecorded} + + + ); + }, + (a, b) => + a.checked === b.checked && + a.then?.battles === b.then?.battles && + a.index === b.index +); diff --git a/packages/website/src/components/PlaylistTracking.tsx b/packages/website/src/components/PlaylistTracking.tsx new file mode 100644 index 000000000..dd8e3b34b --- /dev/null +++ b/packages/website/src/components/PlaylistTracking.tsx @@ -0,0 +1,14 @@ +import { useEffect } from "react"; +import { updatePlaylist } from "../core/blitzkit/updatePlaylist"; + +export function PlaylistTracking() { + useEffect(() => { + const interval = setInterval(updatePlaylist, 5000); + + return () => { + clearInterval(interval); + }; + }, []); + + return null; +} diff --git a/packages/website/src/components/StickyRowHeaderCell.tsx b/packages/website/src/components/StickyRowHeaderCell.tsx index 17c14fd38..e3aaa61bf 100644 --- a/packages/website/src/components/StickyRowHeaderCell.tsx +++ b/packages/website/src/components/StickyRowHeaderCell.tsx @@ -1,15 +1,13 @@ -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 ( - - {unwrap(tank.name)} - - + - + + - {tank.testing && ( - - )} + - {unwrap(tank.name)} + {tank.testing && ( + + )} + + {unwrap(tank.name)} + ); diff --git a/packages/website/src/components/TankSearch/components/FilterControl.tsx b/packages/website/src/components/TankSearch/components/FilterControl.tsx index 175e96003..5261b43e4 100644 --- a/packages/website/src/components/TankSearch/components/FilterControl.tsx +++ b/packages/website/src/components/TankSearch/components/FilterControl.tsx @@ -19,6 +19,7 @@ import { Popover, Text, Tooltip, + type ButtonProps, type FlexProps, } from "@radix-ui/themes"; import { isEqual, times } from "lodash-es"; @@ -37,10 +38,6 @@ import { ResearchedIcon } from "../../ResearchedIcon"; import { ScienceIcon } from "../../ScienceIcon"; import { ScienceOffIcon } from "../../ScienceOffIcon"; -interface FilterControlProps { - compact?: boolean; -} - const gameDefinitions = await awaitableGameDefinitions; const shellTypeIcons: Record = { @@ -50,13 +47,21 @@ const shellTypeIcons: Record = { [ShellType.HEAT]: "hc", }; +const size: ButtonProps["size"] = { initial: "1", xs: "2" }; + function ShellFilter({ index, premium }: { index: number; premium?: boolean }) { const shells = TankFilters.use((state) => state.shells); return ( - + {shells[index] === null && ( @@ -83,6 +88,7 @@ function ShellFilter({ index, premium }: { index: number; premium?: boolean }) { return ( = { disabled: true, button: "bronze", }, - guess: { - id: "guess", - button: "cyan", + playlist: { + id: "playlist", + button: "tomato", }, mixer: { id: "mixer", button: "gray", }, + guess: { + id: "guess", + button: "cyan", + }, gallery: { id: "gallery", button: "gold", diff --git a/packages/website/src/core/blitzkit/generatePlaylist.ts b/packages/website/src/core/blitzkit/generatePlaylist.ts new file mode 100644 index 000000000..ccd8a13c7 --- /dev/null +++ b/packages/website/src/core/blitzkit/generatePlaylist.ts @@ -0,0 +1,44 @@ +import { + fisherYates, + getTankStats, + idToRegion, + type IndividualTankStats, +} from "@blitzkit/core"; +import { App } from "../../stores/app"; +import { Playlist } from "../../stores/playlist"; +import { TankFilters } from "../../stores/tankFilters"; +import { awaitableTankDefinitions } from "../awaitables/tankDefinitions"; +import { filterTanks } from "./filterTanks"; + +const tankDefinitions = await awaitableTankDefinitions; +const tanks = Object.values(tankDefinitions.tanks); + +export async function generatePlaylist() { + const id = App.state.logins.wargaming?.id; + const filtered = await filterTanks(TankFilters.state, tanks, id); + const statsList = id ? await getTankStats(idToRegion(id), id) : undefined; + const stats: Record = {}; + + if (statsList) { + for (const tankStat of statsList) { + stats[tankStat.tank_id] = tankStat; + } + } + + fisherYates(filtered); + + Playlist.mutate((draft) => { + draft.list = filtered.map((tank) => { + const tankStats = stats[tank.id]; + const then = tankStats + ? { + battles: tankStats.all.battles, + wins: tankStats.all.wins, + last: tankStats.last_battle_time, + } + : undefined; + + return { id: tank.id, then, now: then }; + }); + }); +} diff --git a/packages/website/src/core/blitzkit/pwa.ts b/packages/website/src/core/blitzkit/pwa.ts deleted file mode 100644 index e69de29bb..000000000 diff --git a/packages/website/src/core/blitzkit/updatePlaylist.ts b/packages/website/src/core/blitzkit/updatePlaylist.ts new file mode 100644 index 000000000..d49435766 --- /dev/null +++ b/packages/website/src/core/blitzkit/updatePlaylist.ts @@ -0,0 +1,32 @@ +import { + getTankStats, + idToRegion, + type IndividualTankStats, +} from "@blitzkit/core"; +import { App } from "../../stores/app"; +import { Playlist } from "../../stores/playlist"; + +export async function updatePlaylist() { + const id = App.state.logins.wargaming?.id; + const statsList = id ? await getTankStats(idToRegion(id), id) : undefined; + const stats: Record = {}; + + if (statsList) { + for (const tankStat of statsList) { + stats[tankStat.tank_id] = tankStat; + } + } + + Playlist.mutate((draft) => { + draft.list?.map((item) => { + const tankStats = stats[item.id]; + item.now = tankStats + ? { + battles: tankStats.all.battles, + wins: tankStats.all.wins, + last: tankStats.last_battle_time, + } + : undefined; + }); + }); +} diff --git a/packages/website/src/layouts/BaseLayout.astro b/packages/website/src/layouts/BaseLayout.astro index 006807c4f..dc1070859 100644 --- a/packages/website/src/layouts/BaseLayout.astro +++ b/packages/website/src/layouts/BaseLayout.astro @@ -16,11 +16,11 @@ export interface Props { noFooter?: boolean; noNavbar?: boolean; integrated?: boolean; + locale: string; } const servicing = import.meta.env.SERVICING; -const { noFooter, noNavbar, integrated } = Astro.props; -const locale = Astro.params.locale; +const { noFooter, noNavbar, integrated, locale } = Astro.props; --- diff --git a/packages/website/src/layouts/ToolLayout.astro b/packages/website/src/layouts/ToolLayout.astro index 11f4190ab..be9997afe 100644 --- a/packages/website/src/layouts/ToolLayout.astro +++ b/packages/website/src/layouts/ToolLayout.astro @@ -7,10 +7,12 @@ import BaseLayout from "./BaseLayout.astro"; interface Props { tool: Tool; + locale: string; + noNavbar?: boolean; + noFooter?: boolean; } -const { tool } = Astro.props; -const { locale } = Astro.params; +const { tool, locale, noNavbar, noFooter } = Astro.props; const strings = getStrings(locale); const toolStrings = strings.website.tools[ @@ -20,7 +22,7 @@ const toolStrings = - + - + diff --git a/packages/website/src/pages/[...locale]/playlist/_index.tsx b/packages/website/src/pages/[...locale]/playlist/_index.tsx new file mode 100644 index 000000000..727262d7b --- /dev/null +++ b/packages/website/src/pages/[...locale]/playlist/_index.tsx @@ -0,0 +1,32 @@ +import { PageWrapper } from "../../../components/PageWrapper"; +import { PlaylistGenerator } from "../../../components/PlaylistGenerator"; +import { PlaylistOrder } from "../../../components/PlaylistOrder"; +import { LocaleProvider } from "../../../hooks/useLocale"; +import { App } from "../../../stores/app"; +import { Playlist } from "../../../stores/playlist"; +import { TankFilters } from "../../../stores/tankFilters"; +import type { MaybeSkeletonComponentProps } from "../../../types/maybeSkeletonComponentProps"; + +interface Props { + locale: string; +} + +export function Page({ + locale, + skeleton, +}: Props & MaybeSkeletonComponentProps) { + const generated = Playlist.use((state) => state.list !== undefined); + + if (App.state.logins.wargaming && TankFilters.state.ownership !== "owned") { + TankFilters.state.ownership = "owned"; + } + + return ( + + + {!generated && } + {generated && } + + + ); +} diff --git a/packages/website/src/pages/[...locale]/playlist/index.astro b/packages/website/src/pages/[...locale]/playlist/index.astro new file mode 100644 index 000000000..47582ba99 --- /dev/null +++ b/packages/website/src/pages/[...locale]/playlist/index.astro @@ -0,0 +1,18 @@ +--- +import ToolOpenGraph from "../../../components/ToolOpenGraph.astro"; +import { tools } from "../../../constants/tools"; +import BaseLayout from "../../../layouts/BaseLayout.astro"; +import { Page } from "./_index.tsx"; + +export { getStaticPaths } from "../index.astro"; + +const { locale } = Astro.params; +--- + + + + + + + + diff --git a/packages/website/src/stores/playlist.ts b/packages/website/src/stores/playlist.ts new file mode 100644 index 000000000..f2f2ba132 --- /dev/null +++ b/packages/website/src/stores/playlist.ts @@ -0,0 +1,14 @@ +import { Varuna } from "varuna"; + +export interface PlaylistEntry { + id: number; + then?: { battles: number; wins: number; last: number }; + now?: { battles: number; wins: number; last: number }; + checked?: boolean; +} + +export interface Playlist { + list?: PlaylistEntry[]; +} + +export const Playlist = new Varuna({}, "playlist"); diff --git a/packages/website/tsconfig.json b/packages/website/tsconfig.json index d0f9bc7b1..db4d5dcac 100644 --- a/packages/website/tsconfig.json +++ b/packages/website/tsconfig.json @@ -2,7 +2,7 @@ "extends": ["../../tsconfig.json", "astro/tsconfigs/base"], "compilerOptions": { "jsx": "react-jsx", - "jsxImportSource": "react" - }, - "include": [".astro/types.d.ts"] + "jsxImportSource": "react", + "types": ["vite/client"] + } }