mirror of
https://github.com/blitzkit/blitzkit.git
synced 2026-10-24 03:59:50 +02:00
Merge branch 'main' into fix-modules
This commit is contained in:
commit
501a618d5f
36 files changed
+1051
-82
No files matched your search
@@ -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
|
||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "blitzkit",
|
"name": "blitzkit",
|
||||||
"version": "1.28.4",
|
"version": "1.29.0",
|
||||||
"license": "BlitzKit License",
|
"license": "BlitzKit License",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
|
|||||||
@@ -0,0 +1,8 @@
|
|||||||
|
export function fisherYates<Type extends unknown[]>(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;
|
||||||
|
}
|
||||||
@@ -1,7 +1,8 @@
|
|||||||
export * from './axis';
|
export * from "./axis";
|
||||||
export * from './formatCompact';
|
export * from "./fisherYates";
|
||||||
export * from './isNumber';
|
export * from "./formatCompact";
|
||||||
export * from './NaNFallback';
|
export * from "./isNumber";
|
||||||
export * from './normalizeAngleRad';
|
export * from "./NaNFallback";
|
||||||
export * from './normalToEuler';
|
export * from "./normalizeAngleRad";
|
||||||
export * from './numberToRomanNumeral';
|
export * from "./normalToEuler";
|
||||||
|
export * from "./numberToRomanNumeral";
|
||||||
@@ -859,6 +859,43 @@
|
|||||||
},
|
},
|
||||||
|
|
||||||
"tools": {
|
"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": {
|
"mixer": {
|
||||||
"name": "Mixer",
|
"name": "Mixer",
|
||||||
"button": "Mix and match",
|
"button": "Mix and match",
|
||||||
|
|||||||
@@ -762,6 +762,38 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"tools": {
|
"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": {
|
"mixer": {
|
||||||
"name": "Mixer",
|
"name": "Mixer",
|
||||||
"button": "Mix and match",
|
"button": "Mix and match",
|
||||||
|
|||||||
@@ -762,6 +762,38 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"tools": {
|
"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": {
|
"mixer": {
|
||||||
"name": "Mixer",
|
"name": "Mixer",
|
||||||
"button": "Mix and match",
|
"button": "Mix and match",
|
||||||
|
|||||||
@@ -762,6 +762,38 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"tools": {
|
"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": {
|
"mixer": {
|
||||||
"name": "Mixer",
|
"name": "Mixer",
|
||||||
"button": "Mix and match",
|
"button": "Mix and match",
|
||||||
|
|||||||
@@ -762,6 +762,38 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"tools": {
|
"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": {
|
"mixer": {
|
||||||
"name": "Mixer",
|
"name": "Mixer",
|
||||||
"button": "Mix and match",
|
"button": "Mix and match",
|
||||||
|
|||||||
@@ -762,6 +762,38 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"tools": {
|
"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": {
|
"mixer": {
|
||||||
"name": "Mixer",
|
"name": "Mixer",
|
||||||
"button": "Mix and match",
|
"button": "Mix and match",
|
||||||
|
|||||||
@@ -762,6 +762,38 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"tools": {
|
"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": {
|
"mixer": {
|
||||||
"name": "Смешать",
|
"name": "Смешать",
|
||||||
"button": "Смешать и собрать",
|
"button": "Смешать и собрать",
|
||||||
|
|||||||
@@ -762,6 +762,38 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"tools": {
|
"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": {
|
"mixer": {
|
||||||
"name": "Mixer",
|
"name": "Mixer",
|
||||||
"button": "Mix and match",
|
"button": "Mix and match",
|
||||||
|
|||||||
@@ -284,7 +284,7 @@
|
|||||||
"name": "阶级",
|
"name": "阶级",
|
||||||
"description": "阶级",
|
"description": "阶级",
|
||||||
"choices": {
|
"choices": {
|
||||||
"tier": "Tier {tier_1} - {tier_2}"
|
"tier": "坦克等级 {tier_1} - {tier_2}"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -296,7 +296,7 @@
|
|||||||
},
|
},
|
||||||
"filters": {
|
"filters": {
|
||||||
"none": "无筛选器",
|
"none": "无筛选器",
|
||||||
"tier": "Tier {tier}"
|
"tier": "坦克等级 {tier}"
|
||||||
},
|
},
|
||||||
"no_data": {
|
"no_data": {
|
||||||
"battles_in_period": "在这段时间里没有参与任何战斗",
|
"battles_in_period": "在这段时间里没有参与任何战斗",
|
||||||
@@ -347,7 +347,7 @@
|
|||||||
},
|
},
|
||||||
"errors": {
|
"errors": {
|
||||||
"non_tech_tree": "# {tank} is not on the tech tree\n\nYou cannot research a premium/collector tank.",
|
"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.",
|
"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.",
|
"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.",
|
"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": {
|
"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": {
|
"mixer": {
|
||||||
"name": "Mixer",
|
"name": "Mixer",
|
||||||
"button": "Mix and match",
|
"button": "Mix and match",
|
||||||
|
|||||||
Binary file not shown.
|
After Width: | Height: | Size: 208 KiB |
@@ -0,0 +1,7 @@
|
|||||||
|
import { Flex } from "@radix-ui/themes";
|
||||||
|
|
||||||
|
export function CurrentlyPlaying() {
|
||||||
|
return <Flex>
|
||||||
|
|
||||||
|
</Flex>;
|
||||||
|
}
|
||||||
@@ -4,11 +4,11 @@
|
|||||||
|
|
||||||
const isAPI = window.location.pathname.startsWith("/api");
|
const isAPI = window.location.pathname.startsWith("/api");
|
||||||
const isI18nPath = locales.supported.some(({ locale }) =>
|
const isI18nPath = locales.supported.some(({ locale }) =>
|
||||||
window.location.pathname.startsWith(`/${locale}`)
|
window.location.pathname.startsWith(`/${locale}/`)
|
||||||
);
|
);
|
||||||
const bestLocale = getBestLocale();
|
const bestLocale = getBestLocale();
|
||||||
const currentPath = locales.supported.find(({ locale }) =>
|
const currentPath = locales.supported.find(({ locale }) =>
|
||||||
window.location.pathname.startsWith(`/${locale}`)
|
window.location.pathname.startsWith(`/${locale}/`)
|
||||||
);
|
);
|
||||||
|
|
||||||
if (
|
if (
|
||||||
|
|||||||
@@ -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 (
|
||||||
|
<>
|
||||||
|
<Flex gap="2">
|
||||||
|
<Button
|
||||||
|
size={{ initial: "1", xs: "2" }}
|
||||||
|
variant="outline"
|
||||||
|
color="red"
|
||||||
|
onClick={() => {
|
||||||
|
Playlist.mutate((draft) => {
|
||||||
|
draft.list = undefined;
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{strings.website.tools.playlist.reset}
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
size={{ initial: "1", xs: "2" }}
|
||||||
|
variant={wargaming ? "outline" : undefined}
|
||||||
|
onClick={() => {
|
||||||
|
Playlist.mutate((draft) => {
|
||||||
|
if (draft.list) fisherYates(draft.list);
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{strings.website.tools.playlist.shuffle}
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
{wargaming && (
|
||||||
|
<>
|
||||||
|
<Button
|
||||||
|
size={{ initial: "1", xs: "2" }}
|
||||||
|
variant="outline"
|
||||||
|
onClick={() => {
|
||||||
|
Playlist.mutate((draft) => {
|
||||||
|
draft.list = draft.list?.filter(
|
||||||
|
(entry) =>
|
||||||
|
(!entry.now ||
|
||||||
|
!entry.then ||
|
||||||
|
entry.now.battles === entry.then.battles) &&
|
||||||
|
!entry.checked
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{strings.website.tools.playlist.remove}
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<DropdownMenu.Root>
|
||||||
|
<DropdownMenu.Trigger>
|
||||||
|
<Button size={{ initial: "1", xs: "2" }}>
|
||||||
|
{strings.website.tools.playlist.sort.button}
|
||||||
|
<CaretDownIcon />
|
||||||
|
</Button>
|
||||||
|
</DropdownMenu.Trigger>
|
||||||
|
|
||||||
|
<DropdownMenu.Content>
|
||||||
|
<DropdownMenu.Item
|
||||||
|
onClick={() => {
|
||||||
|
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}
|
||||||
|
</DropdownMenu.Item>
|
||||||
|
<DropdownMenu.Item
|
||||||
|
onClick={() => {
|
||||||
|
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}
|
||||||
|
</DropdownMenu.Item>
|
||||||
|
</DropdownMenu.Content>
|
||||||
|
</DropdownMenu.Root>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Flex>
|
||||||
|
|
||||||
|
{wargaming && (
|
||||||
|
<Flex gap="2" align="center">
|
||||||
|
<Spinner />
|
||||||
|
{strings.website.tools.playlist.tracking}
|
||||||
|
</Flex>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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 (
|
||||||
|
<>
|
||||||
|
<AlertDialog.Root
|
||||||
|
open={largeWarningOpen}
|
||||||
|
onOpenChange={setLargeWarningOpen}
|
||||||
|
>
|
||||||
|
<AlertDialog.Content>
|
||||||
|
<AlertDialog.Title>
|
||||||
|
{strings.website.tools.playlist.large_warning.title}
|
||||||
|
</AlertDialog.Title>
|
||||||
|
<AlertDialog.Description>
|
||||||
|
{literals(
|
||||||
|
strings.website.tools.playlist.large_warning.description,
|
||||||
|
{
|
||||||
|
count: tanks.length,
|
||||||
|
time: Math.round(tanks.length * ((2 * 4) / 60)),
|
||||||
|
}
|
||||||
|
)}
|
||||||
|
</AlertDialog.Description>
|
||||||
|
|
||||||
|
<Flex justify="end" gap="2" mt="4">
|
||||||
|
<AlertDialog.Cancel>
|
||||||
|
<Button variant="outline">
|
||||||
|
{strings.website.tools.playlist.large_warning.cancel}
|
||||||
|
</Button>
|
||||||
|
</AlertDialog.Cancel>
|
||||||
|
<AlertDialog.Action onClick={generatePlaylist}>
|
||||||
|
<Button color="red">
|
||||||
|
{strings.website.tools.playlist.large_warning.proceed}
|
||||||
|
</Button>
|
||||||
|
</AlertDialog.Action>
|
||||||
|
</Flex>
|
||||||
|
</AlertDialog.Content>
|
||||||
|
</AlertDialog.Root>
|
||||||
|
|
||||||
|
<Flex justify="center" width="100%" align="center" px="4">
|
||||||
|
<Flex
|
||||||
|
flexGrow="1"
|
||||||
|
gap="9"
|
||||||
|
align="center"
|
||||||
|
justify="center"
|
||||||
|
maxWidth="80rem"
|
||||||
|
direction={{ initial: "column-reverse", md: "row" }}
|
||||||
|
>
|
||||||
|
<Flex direction="column" gap="4" align="center">
|
||||||
|
<FilterControl />
|
||||||
|
|
||||||
|
<Button
|
||||||
|
onClick={() => {
|
||||||
|
if (tanks.length >= 32) {
|
||||||
|
setLargeWarningOpen(true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
generatePlaylist();
|
||||||
|
}}
|
||||||
|
size="3"
|
||||||
|
>
|
||||||
|
{strings.website.tools.playlist.generate} <ChevronRightIcon />
|
||||||
|
</Button>
|
||||||
|
</Flex>
|
||||||
|
|
||||||
|
<Flex
|
||||||
|
direction="column"
|
||||||
|
flexGrow="1"
|
||||||
|
flexBasis="0"
|
||||||
|
align="center"
|
||||||
|
gap="4"
|
||||||
|
width="100%"
|
||||||
|
>
|
||||||
|
<Grid
|
||||||
|
columns="repeat(auto-fill, minmax(4rem, 1fr))"
|
||||||
|
flow="dense"
|
||||||
|
overflow="hidden"
|
||||||
|
position="relative"
|
||||||
|
maxHeight={POOL_HEIGHT}
|
||||||
|
width="100%"
|
||||||
|
>
|
||||||
|
{tanksTrimmed.map((tank) =>
|
||||||
|
skeleton ? (
|
||||||
|
<Box
|
||||||
|
key={tank.id}
|
||||||
|
width="100%"
|
||||||
|
height="100%"
|
||||||
|
style={{ aspectRatio: "4 / 3" }}
|
||||||
|
p="1"
|
||||||
|
>
|
||||||
|
<Skeleton width="100%" height="100%" />
|
||||||
|
</Box>
|
||||||
|
) : (
|
||||||
|
<LinkI18n
|
||||||
|
key={tank.id}
|
||||||
|
target="_blank"
|
||||||
|
locale={locale}
|
||||||
|
href={`/tanks/${tank.slug}`}
|
||||||
|
style={{
|
||||||
|
aspectRatio: "4 / 3",
|
||||||
|
backgroundSize: "cover",
|
||||||
|
backgroundPosition: "center",
|
||||||
|
backgroundImage: `url(${tankIcon(tank.id)})`,
|
||||||
|
backgroundRepeat: "no-repeat",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
)}
|
||||||
|
|
||||||
|
<Box
|
||||||
|
position="absolute"
|
||||||
|
top="0"
|
||||||
|
left="0"
|
||||||
|
width="100%"
|
||||||
|
height="100%"
|
||||||
|
style={{
|
||||||
|
pointerEvents: "none",
|
||||||
|
background: `linear-gradient(transparent, var(--gray-1) ${POOL_HEIGHT})`,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
<Text color="gray">
|
||||||
|
{literals(strings.website.tools.playlist.pool_size, {
|
||||||
|
count: tanks.length,
|
||||||
|
})}
|
||||||
|
</Text>
|
||||||
|
</Flex>
|
||||||
|
</Flex>
|
||||||
|
</Flex>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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 (
|
||||||
|
<Flex
|
||||||
|
direction="column"
|
||||||
|
gap="4"
|
||||||
|
align="center"
|
||||||
|
flexGrow="1"
|
||||||
|
flexBasis="0"
|
||||||
|
maxHeight="100%"
|
||||||
|
>
|
||||||
|
<CurrentlyPlaying />
|
||||||
|
<PlaylistActions />
|
||||||
|
<PlaylistTracking />
|
||||||
|
|
||||||
|
<Box flexGrow="1" width="100%" position="relative">
|
||||||
|
<PlaylistTable />
|
||||||
|
</Box>
|
||||||
|
</Flex>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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 (
|
||||||
|
<StickyTableRoot
|
||||||
|
style={{
|
||||||
|
position: "absolute",
|
||||||
|
top: 0,
|
||||||
|
left: 0,
|
||||||
|
height: "100%",
|
||||||
|
width: "100%",
|
||||||
|
borderRadius: 0,
|
||||||
|
whiteSpace: "nowrap",
|
||||||
|
}}
|
||||||
|
variant="surface"
|
||||||
|
>
|
||||||
|
<Table.Header>
|
||||||
|
<Table.Row>
|
||||||
|
<StickyColumnHeaderCell width="0">
|
||||||
|
<Text color="gray">
|
||||||
|
{strings.website.tools.playlist.table.index}
|
||||||
|
</Text>
|
||||||
|
</StickyColumnHeaderCell>
|
||||||
|
|
||||||
|
<StickyColumnHeaderCell width="0" />
|
||||||
|
|
||||||
|
<StickyColumnHeaderCell width="0" />
|
||||||
|
|
||||||
|
<StickyColumnHeaderCell width="1">
|
||||||
|
{strings.website.tools.playlist.table.tank}
|
||||||
|
</StickyColumnHeaderCell>
|
||||||
|
|
||||||
|
<StickyColumnHeaderCell width="1">
|
||||||
|
{strings.website.tools.playlist.table.wins}
|
||||||
|
</StickyColumnHeaderCell>
|
||||||
|
|
||||||
|
<StickyColumnHeaderCell width="1">
|
||||||
|
{strings.website.tools.playlist.table.battles}
|
||||||
|
</StickyColumnHeaderCell>
|
||||||
|
|
||||||
|
<StickyColumnHeaderCell width="1">
|
||||||
|
{strings.website.tools.playlist.table.last_played}
|
||||||
|
</StickyColumnHeaderCell>
|
||||||
|
</Table.Row>
|
||||||
|
</Table.Header>
|
||||||
|
|
||||||
|
<Table.Body>
|
||||||
|
{list.map((props, index) => (
|
||||||
|
<PlaylistTankEntry key={props.id} index={index} {...props} />
|
||||||
|
))}
|
||||||
|
</Table.Body>
|
||||||
|
</StickyTableRoot>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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<Props>(
|
||||||
|
({ 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 (
|
||||||
|
<Table.Row>
|
||||||
|
<Table.Cell>{index + 1}</Table.Cell>
|
||||||
|
|
||||||
|
<Table.Cell
|
||||||
|
onClick={() => {
|
||||||
|
Playlist.mutate((draft) => {
|
||||||
|
draft.list![index].checked = !checked;
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Checkbox
|
||||||
|
size="3"
|
||||||
|
variant="classic"
|
||||||
|
checked={
|
||||||
|
(checked === undefined &&
|
||||||
|
now &&
|
||||||
|
then &&
|
||||||
|
now.battles > then.battles) ||
|
||||||
|
checked
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</Table.Cell>
|
||||||
|
|
||||||
|
<Table.Cell>
|
||||||
|
<IconButton
|
||||||
|
variant="ghost"
|
||||||
|
size="1"
|
||||||
|
onClick={() => {
|
||||||
|
Playlist.mutate((draft) => {
|
||||||
|
draft.list!.splice(index, 1);
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<MinusCircledIcon />
|
||||||
|
</IconButton>
|
||||||
|
</Table.Cell>
|
||||||
|
|
||||||
|
<TankRowHeaderCell tank={tank} />
|
||||||
|
|
||||||
|
<Table.Cell>
|
||||||
|
{now && then && now.battles !== then.battles ? (
|
||||||
|
<Flex gap="2">
|
||||||
|
<Text color="green">
|
||||||
|
<Flex align="center">
|
||||||
|
<TriangleUpIcon />
|
||||||
|
{now.wins - then.wins}
|
||||||
|
</Flex>
|
||||||
|
</Text>
|
||||||
|
|
||||||
|
<Text color="tomato">
|
||||||
|
<Flex align="center">
|
||||||
|
<TriangleDownIcon />
|
||||||
|
{now.battles - then.battles - now.wins + then.wins}
|
||||||
|
</Flex>
|
||||||
|
</Text>
|
||||||
|
</Flex>
|
||||||
|
) : (
|
||||||
|
strings.website.tools.playlist.table.unrecorded
|
||||||
|
)}
|
||||||
|
</Table.Cell>
|
||||||
|
|
||||||
|
<Table.Cell>
|
||||||
|
{now?.battles ?? strings.website.tools.playlist.table.unrecorded}
|
||||||
|
</Table.Cell>
|
||||||
|
|
||||||
|
<Table.Cell>
|
||||||
|
{now && now.last !== 0
|
||||||
|
? rtf.format(
|
||||||
|
Math.ceil((now.last - dateNow / 1000) / (60 * 60 * 24)),
|
||||||
|
"day"
|
||||||
|
)
|
||||||
|
: strings.website.tools.playlist.table.unrecorded}
|
||||||
|
</Table.Cell>
|
||||||
|
</Table.Row>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
(a, b) =>
|
||||||
|
a.checked === b.checked &&
|
||||||
|
a.then?.battles === b.then?.battles &&
|
||||||
|
a.index === b.index
|
||||||
|
);
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -1,15 +1,13 @@
|
|||||||
import { Table } from '@radix-ui/themes';
|
import { Table } from "@radix-ui/themes";
|
||||||
import type { RowHeaderCellProps } from '@radix-ui/themes/src/components/table.js';
|
import type { RowHeaderCellProps } from "@radix-ui/themes/src/components/table.js";
|
||||||
|
|
||||||
export function StickyRowHeaderCell({ style, ...props }: RowHeaderCellProps) {
|
export function StickyRowHeaderCell({ style, ...props }: RowHeaderCellProps) {
|
||||||
return (
|
return (
|
||||||
<Table.RowHeaderCell
|
<Table.RowHeaderCell
|
||||||
style={{
|
style={{
|
||||||
display: 'flex',
|
position: "sticky",
|
||||||
position: 'sticky',
|
|
||||||
left: 0,
|
left: 0,
|
||||||
backgroundColor: 'var(--color-panel)',
|
backgroundColor: "var(--color-panel)",
|
||||||
boxShadow: 'var(--shadow-6)',
|
|
||||||
...style,
|
...style,
|
||||||
}}
|
}}
|
||||||
{...props}
|
{...props}
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
import { type TankDefinition, tankIcon, TankType } from '@blitzkit/core';
|
import { type TankDefinition, tankIcon, TankType } from "@blitzkit/core";
|
||||||
import { useLocale } from '../hooks/useLocale';
|
import { Box, Flex } from "@radix-ui/themes";
|
||||||
import { classIcons } from './ClassIcon';
|
import { useLocale } from "../hooks/useLocale";
|
||||||
import { ExperimentIcon } from './ExperimentIcon';
|
import { classIcons } from "./ClassIcon";
|
||||||
import { LinkI18n } from './LinkI18n';
|
import { ExperimentIcon } from "./ExperimentIcon";
|
||||||
import { StickyRowHeaderCell } from './StickyRowHeaderCell';
|
import { LinkI18n } from "./LinkI18n";
|
||||||
|
import { StickyRowHeaderCell } from "./StickyRowHeaderCell";
|
||||||
|
|
||||||
interface TankRowHeaderCellProps {
|
interface TankRowHeaderCellProps {
|
||||||
tank: TankDefinition;
|
tank: TankDefinition;
|
||||||
@@ -14,59 +15,48 @@ export function TankRowHeaderCell({ tank }: TankRowHeaderCellProps) {
|
|||||||
const Icon = classIcons[tank.class];
|
const Icon = classIcons[tank.class];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<StickyRowHeaderCell
|
<StickyRowHeaderCell style={{ overflow: "hidden" }}>
|
||||||
width={{ initial: '144px', sm: '240px' }}
|
|
||||||
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',
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</LinkI18n>
|
|
||||||
|
|
||||||
<LinkI18n
|
<LinkI18n
|
||||||
locale={locale}
|
locale={locale}
|
||||||
color={
|
color={
|
||||||
tank.type === TankType.COLLECTOR
|
tank.type === TankType.COLLECTOR
|
||||||
? 'blue'
|
? "blue"
|
||||||
: tank.type === TankType.PREMIUM
|
: tank.type === TankType.PREMIUM
|
||||||
? 'amber'
|
? "amber"
|
||||||
: 'gray'
|
: "gray"
|
||||||
}
|
}
|
||||||
highContrast={tank.type === TankType.RESEARCHABLE}
|
highContrast={tank.type === TankType.RESEARCHABLE}
|
||||||
underline="hover"
|
underline="hover"
|
||||||
wrap="nowrap"
|
wrap="nowrap"
|
||||||
href={`/tanks/${tank.slug}`}
|
href={`/tanks/${tank.slug}`}
|
||||||
style={{
|
style={{ height: "100%" }}
|
||||||
paddingLeft: 'var(--space-2)',
|
size={{ initial: "1", sm: "2" }}
|
||||||
display: 'flex',
|
|
||||||
alignItems: 'center',
|
|
||||||
gap: 'var(--space-1)',
|
|
||||||
whiteSpace: 'nowrap',
|
|
||||||
overflow: 'hidden',
|
|
||||||
textOverflow: 'ellipsis',
|
|
||||||
}}
|
|
||||||
size={{ initial: '1', sm: '2' }}
|
|
||||||
>
|
>
|
||||||
<Icon
|
<Flex height="100%" align="center" gap="1">
|
||||||
width="1em"
|
<Box
|
||||||
height="1em"
|
draggable={false}
|
||||||
style={{ minWidth: '1em', minHeight: '1em' }}
|
height="calc(100% + 2 * var(--table-cell-padding))"
|
||||||
/>
|
style={{
|
||||||
|
aspectRatio: "4 / 3",
|
||||||
|
backgroundImage: `url(${tankIcon(tank.id)})`,
|
||||||
|
backgroundSize: "cover",
|
||||||
|
backgroundPosition: "center",
|
||||||
|
backgroundRepeat: "no-repeat",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
{tank.testing && (
|
<Icon
|
||||||
<ExperimentIcon style={{ width: '1em', height: '1em' }} />
|
width="1em"
|
||||||
)}
|
height="1em"
|
||||||
|
style={{ minWidth: "1em", minHeight: "1em" }}
|
||||||
|
/>
|
||||||
|
|
||||||
{unwrap(tank.name)}
|
{tank.testing && (
|
||||||
|
<ExperimentIcon style={{ width: "1em", height: "1em" }} />
|
||||||
|
)}
|
||||||
|
|
||||||
|
{unwrap(tank.name)}
|
||||||
|
</Flex>
|
||||||
</LinkI18n>
|
</LinkI18n>
|
||||||
</StickyRowHeaderCell>
|
</StickyRowHeaderCell>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ import {
|
|||||||
Popover,
|
Popover,
|
||||||
Text,
|
Text,
|
||||||
Tooltip,
|
Tooltip,
|
||||||
|
type ButtonProps,
|
||||||
type FlexProps,
|
type FlexProps,
|
||||||
} from "@radix-ui/themes";
|
} from "@radix-ui/themes";
|
||||||
import { isEqual, times } from "lodash-es";
|
import { isEqual, times } from "lodash-es";
|
||||||
@@ -37,10 +38,6 @@ import { ResearchedIcon } from "../../ResearchedIcon";
|
|||||||
import { ScienceIcon } from "../../ScienceIcon";
|
import { ScienceIcon } from "../../ScienceIcon";
|
||||||
import { ScienceOffIcon } from "../../ScienceOffIcon";
|
import { ScienceOffIcon } from "../../ScienceOffIcon";
|
||||||
|
|
||||||
interface FilterControlProps {
|
|
||||||
compact?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
const gameDefinitions = await awaitableGameDefinitions;
|
const gameDefinitions = await awaitableGameDefinitions;
|
||||||
|
|
||||||
const shellTypeIcons: Record<ShellType, string> = {
|
const shellTypeIcons: Record<ShellType, string> = {
|
||||||
@@ -50,13 +47,21 @@ const shellTypeIcons: Record<ShellType, string> = {
|
|||||||
[ShellType.HEAT]: "hc",
|
[ShellType.HEAT]: "hc",
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const size: ButtonProps["size"] = { initial: "1", xs: "2" };
|
||||||
|
|
||||||
function ShellFilter({ index, premium }: { index: number; premium?: boolean }) {
|
function ShellFilter({ index, premium }: { index: number; premium?: boolean }) {
|
||||||
const shells = TankFilters.use((state) => state.shells);
|
const shells = TankFilters.use((state) => state.shells);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Popover.Root>
|
<Popover.Root>
|
||||||
<Popover.Trigger>
|
<Popover.Trigger>
|
||||||
<IconButton variant="soft" radius="none" color="gray" highContrast>
|
<IconButton
|
||||||
|
size={size}
|
||||||
|
variant="soft"
|
||||||
|
radius="none"
|
||||||
|
color="gray"
|
||||||
|
highContrast
|
||||||
|
>
|
||||||
{shells[index] === null && (
|
{shells[index] === null && (
|
||||||
<Text color="gray" style={{ display: "contents" }}>
|
<Text color="gray" style={{ display: "contents" }}>
|
||||||
<MissingShellIcon width="1em" height="1em" />
|
<MissingShellIcon width="1em" height="1em" />
|
||||||
@@ -83,6 +88,7 @@ function ShellFilter({ index, premium }: { index: number; premium?: boolean }) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<IconButton
|
<IconButton
|
||||||
|
size={size}
|
||||||
key={shellType}
|
key={shellType}
|
||||||
value={`${shellType}`}
|
value={`${shellType}`}
|
||||||
radius="none"
|
radius="none"
|
||||||
@@ -127,6 +133,7 @@ function OwnershipFilter(props: FlexProps) {
|
|||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
<IconButton
|
<IconButton
|
||||||
|
size={size}
|
||||||
disabled={!wargaming}
|
disabled={!wargaming}
|
||||||
variant={tankFilters.ownership === "all" ? "solid" : "soft"}
|
variant={tankFilters.ownership === "all" ? "solid" : "soft"}
|
||||||
radius="none"
|
radius="none"
|
||||||
@@ -142,6 +149,7 @@ function OwnershipFilter(props: FlexProps) {
|
|||||||
</IconButton>
|
</IconButton>
|
||||||
|
|
||||||
<IconButton
|
<IconButton
|
||||||
|
size={size}
|
||||||
disabled={!wargaming}
|
disabled={!wargaming}
|
||||||
variant={tankFilters.ownership === "owned" ? "solid" : "soft"}
|
variant={tankFilters.ownership === "owned" ? "solid" : "soft"}
|
||||||
radius="none"
|
radius="none"
|
||||||
@@ -157,6 +165,7 @@ function OwnershipFilter(props: FlexProps) {
|
|||||||
</IconButton>
|
</IconButton>
|
||||||
|
|
||||||
<IconButton
|
<IconButton
|
||||||
|
size={size}
|
||||||
disabled={!wargaming}
|
disabled={!wargaming}
|
||||||
variant={tankFilters.ownership === "unowned" ? "solid" : "soft"}
|
variant={tankFilters.ownership === "unowned" ? "solid" : "soft"}
|
||||||
radius="none"
|
radius="none"
|
||||||
@@ -195,6 +204,7 @@ export function FilterControl() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<IconButton
|
<IconButton
|
||||||
|
size={size}
|
||||||
key={tier}
|
key={tier}
|
||||||
variant={selected ? "solid" : "soft"}
|
variant={selected ? "solid" : "soft"}
|
||||||
radius="none"
|
radius="none"
|
||||||
@@ -233,6 +243,7 @@ export function FilterControl() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<IconButton
|
<IconButton
|
||||||
|
size={size}
|
||||||
key={tier}
|
key={tier}
|
||||||
variant={selected ? "solid" : "soft"}
|
variant={selected ? "solid" : "soft"}
|
||||||
radius="none"
|
radius="none"
|
||||||
@@ -270,6 +281,7 @@ export function FilterControl() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<IconButton
|
<IconButton
|
||||||
|
size={size}
|
||||||
key={nation}
|
key={nation}
|
||||||
variant={selected ? "solid" : "soft"}
|
variant={selected ? "solid" : "soft"}
|
||||||
color={selected ? undefined : "gray"}
|
color={selected ? undefined : "gray"}
|
||||||
@@ -312,6 +324,7 @@ export function FilterControl() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<IconButton
|
<IconButton
|
||||||
|
size={size}
|
||||||
key={nation}
|
key={nation}
|
||||||
style={{ flex: 1 }}
|
style={{ flex: 1 }}
|
||||||
variant={selected ? "solid" : "soft"}
|
variant={selected ? "solid" : "soft"}
|
||||||
@@ -351,6 +364,7 @@ export function FilterControl() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<IconButton
|
<IconButton
|
||||||
|
size={size}
|
||||||
key={tankClass}
|
key={tankClass}
|
||||||
variant={selected ? "solid" : "soft"}
|
variant={selected ? "solid" : "soft"}
|
||||||
radius="none"
|
radius="none"
|
||||||
@@ -380,6 +394,7 @@ export function FilterControl() {
|
|||||||
direction="column"
|
direction="column"
|
||||||
>
|
>
|
||||||
<IconButton
|
<IconButton
|
||||||
|
size={size}
|
||||||
variant={tankFilters.gunType.includes("regular") ? "solid" : "soft"}
|
variant={tankFilters.gunType.includes("regular") ? "solid" : "soft"}
|
||||||
radius="none"
|
radius="none"
|
||||||
color={tankFilters.gunType.includes("regular") ? undefined : "gray"}
|
color={tankFilters.gunType.includes("regular") ? undefined : "gray"}
|
||||||
@@ -397,6 +412,7 @@ export function FilterControl() {
|
|||||||
<GunRegularIcon style={{ width: "1em", height: "1em" }} />
|
<GunRegularIcon style={{ width: "1em", height: "1em" }} />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
<IconButton
|
<IconButton
|
||||||
|
size={size}
|
||||||
variant={
|
variant={
|
||||||
tankFilters.gunType.includes("auto_loader") ? "solid" : "soft"
|
tankFilters.gunType.includes("auto_loader") ? "solid" : "soft"
|
||||||
}
|
}
|
||||||
@@ -420,6 +436,7 @@ export function FilterControl() {
|
|||||||
<GunAutoloaderIcon style={{ width: "1em", height: "1em" }} />
|
<GunAutoloaderIcon style={{ width: "1em", height: "1em" }} />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
<IconButton
|
<IconButton
|
||||||
|
size={size}
|
||||||
variant={
|
variant={
|
||||||
tankFilters.gunType.includes("auto_reloader") ? "solid" : "soft"
|
tankFilters.gunType.includes("auto_reloader") ? "solid" : "soft"
|
||||||
}
|
}
|
||||||
@@ -460,6 +477,7 @@ export function FilterControl() {
|
|||||||
direction="column"
|
direction="column"
|
||||||
>
|
>
|
||||||
<IconButton
|
<IconButton
|
||||||
|
size={size}
|
||||||
variant={
|
variant={
|
||||||
tankFilters.types?.includes(TankType.RESEARCHABLE)
|
tankFilters.types?.includes(TankType.RESEARCHABLE)
|
||||||
? "solid"
|
? "solid"
|
||||||
@@ -487,6 +505,7 @@ export function FilterControl() {
|
|||||||
<ResearchedIcon style={{ width: "1em", height: "1em" }} />
|
<ResearchedIcon style={{ width: "1em", height: "1em" }} />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
<IconButton
|
<IconButton
|
||||||
|
size={size}
|
||||||
variant={
|
variant={
|
||||||
tankFilters.types?.includes(TankType.PREMIUM) ? "solid" : "soft"
|
tankFilters.types?.includes(TankType.PREMIUM) ? "solid" : "soft"
|
||||||
}
|
}
|
||||||
@@ -519,6 +538,7 @@ export function FilterControl() {
|
|||||||
</Text>
|
</Text>
|
||||||
</IconButton>
|
</IconButton>
|
||||||
<IconButton
|
<IconButton
|
||||||
|
size={size}
|
||||||
variant={
|
variant={
|
||||||
tankFilters.types?.includes(TankType.COLLECTOR) ? "solid" : "soft"
|
tankFilters.types?.includes(TankType.COLLECTOR) ? "solid" : "soft"
|
||||||
}
|
}
|
||||||
@@ -558,6 +578,7 @@ export function FilterControl() {
|
|||||||
direction="column"
|
direction="column"
|
||||||
>
|
>
|
||||||
<IconButton
|
<IconButton
|
||||||
|
size={size}
|
||||||
variant={tankFilters.testing === "include" ? "solid" : "soft"}
|
variant={tankFilters.testing === "include" ? "solid" : "soft"}
|
||||||
radius="none"
|
radius="none"
|
||||||
color={tankFilters.testing === "include" ? undefined : "gray"}
|
color={tankFilters.testing === "include" ? undefined : "gray"}
|
||||||
@@ -571,6 +592,7 @@ export function FilterControl() {
|
|||||||
<ComponentBooleanIcon />
|
<ComponentBooleanIcon />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
<IconButton
|
<IconButton
|
||||||
|
size={size}
|
||||||
variant={tankFilters.testing === "exclude" ? "solid" : "soft"}
|
variant={tankFilters.testing === "exclude" ? "solid" : "soft"}
|
||||||
radius="none"
|
radius="none"
|
||||||
color={tankFilters.testing === "exclude" ? undefined : "gray"}
|
color={tankFilters.testing === "exclude" ? undefined : "gray"}
|
||||||
@@ -586,6 +608,7 @@ export function FilterControl() {
|
|||||||
/>
|
/>
|
||||||
</IconButton>
|
</IconButton>
|
||||||
<IconButton
|
<IconButton
|
||||||
|
size={size}
|
||||||
variant={tankFilters.testing === "only" ? "solid" : "soft"}
|
variant={tankFilters.testing === "only" ? "solid" : "soft"}
|
||||||
radius="none"
|
radius="none"
|
||||||
color={tankFilters.testing === "only" ? undefined : "gray"}
|
color={tankFilters.testing === "only" ? undefined : "gray"}
|
||||||
|
|||||||
@@ -35,14 +35,18 @@ export const tools: Record<string, Tool> = {
|
|||||||
disabled: true,
|
disabled: true,
|
||||||
button: "bronze",
|
button: "bronze",
|
||||||
},
|
},
|
||||||
guess: {
|
playlist: {
|
||||||
id: "guess",
|
id: "playlist",
|
||||||
button: "cyan",
|
button: "tomato",
|
||||||
},
|
},
|
||||||
mixer: {
|
mixer: {
|
||||||
id: "mixer",
|
id: "mixer",
|
||||||
button: "gray",
|
button: "gray",
|
||||||
},
|
},
|
||||||
|
guess: {
|
||||||
|
id: "guess",
|
||||||
|
button: "cyan",
|
||||||
|
},
|
||||||
gallery: {
|
gallery: {
|
||||||
id: "gallery",
|
id: "gallery",
|
||||||
button: "gold",
|
button: "gold",
|
||||||
|
|||||||
@@ -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<number, IndividualTankStats> = {};
|
||||||
|
|
||||||
|
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 };
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
Whitespace-only changes.
@@ -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<number, IndividualTankStats> = {};
|
||||||
|
|
||||||
|
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;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -16,11 +16,11 @@ export interface Props {
|
|||||||
noFooter?: boolean;
|
noFooter?: boolean;
|
||||||
noNavbar?: boolean;
|
noNavbar?: boolean;
|
||||||
integrated?: boolean;
|
integrated?: boolean;
|
||||||
|
locale: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const servicing = import.meta.env.SERVICING;
|
const servicing = import.meta.env.SERVICING;
|
||||||
const { noFooter, noNavbar, integrated } = Astro.props;
|
const { noFooter, noNavbar, integrated, locale } = Astro.props;
|
||||||
const locale = Astro.params.locale;
|
|
||||||
---
|
---
|
||||||
|
|
||||||
<html lang={locale}>
|
<html lang={locale}>
|
||||||
|
|||||||
@@ -7,10 +7,12 @@ import BaseLayout from "./BaseLayout.astro";
|
|||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
tool: Tool;
|
tool: Tool;
|
||||||
|
locale: string;
|
||||||
|
noNavbar?: boolean;
|
||||||
|
noFooter?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const { tool } = Astro.props;
|
const { tool, locale, noNavbar, noFooter } = Astro.props;
|
||||||
const { locale } = Astro.params;
|
|
||||||
const strings = getStrings(locale);
|
const strings = getStrings(locale);
|
||||||
const toolStrings =
|
const toolStrings =
|
||||||
strings.website.tools[
|
strings.website.tools[
|
||||||
@@ -20,7 +22,7 @@ const toolStrings =
|
|||||||
|
|
||||||
<ToolOpenGraph {tool} />
|
<ToolOpenGraph {tool} />
|
||||||
|
|
||||||
<BaseLayout>
|
<BaseLayout {locale} {noNavbar} {noFooter}>
|
||||||
<Box
|
<Box
|
||||||
style={{
|
style={{
|
||||||
backgroundImage: `url(/assets/images/tools/${tool.id}.webp)`,
|
backgroundImage: `url(/assets/images/tools/${tool.id}.webp)`,
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ const { locale } = Astro.params;
|
|||||||
|
|
||||||
<ToolOpenGraph tool={tools.performance} />
|
<ToolOpenGraph tool={tools.performance} />
|
||||||
|
|
||||||
<BaseLayout noFooter>
|
<BaseLayout {locale} noFooter>
|
||||||
<Page client:only="react" {locale}>
|
<Page client:only="react" {locale}>
|
||||||
<Page skeleton slot="fallback" {locale} />
|
<Page skeleton slot="fallback" {locale} />
|
||||||
</Page>
|
</Page>
|
||||||
|
|||||||
@@ -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 (
|
||||||
|
<LocaleProvider locale={locale}>
|
||||||
|
<PageWrapper color="tomato" direction="row" maxWidth="unset" p="0">
|
||||||
|
{!generated && <PlaylistGenerator skeleton={skeleton} />}
|
||||||
|
{generated && <PlaylistOrder />}
|
||||||
|
</PageWrapper>
|
||||||
|
</LocaleProvider>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
---
|
||||||
|
|
||||||
|
<ToolOpenGraph tool={tools.playlist} />
|
||||||
|
|
||||||
|
<BaseLayout {locale} noFooter>
|
||||||
|
<Page {locale} client:only="react">
|
||||||
|
<Page {locale} skeleton slot="fallback" />
|
||||||
|
</Page>
|
||||||
|
</BaseLayout>
|
||||||
@@ -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>({}, "playlist");
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
"extends": ["../../tsconfig.json", "astro/tsconfigs/base"],
|
"extends": ["../../tsconfig.json", "astro/tsconfigs/base"],
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"jsx": "react-jsx",
|
"jsx": "react-jsx",
|
||||||
"jsxImportSource": "react"
|
"jsxImportSource": "react",
|
||||||
},
|
"types": ["vite/client"]
|
||||||
"include": [".astro/types.d.ts"]
|
}
|
||||||
}
|
}
|
||||||
Reference in new issue
Block a user