mirror of
https://github.com/blitzkit/blitzkit.git
synced 2026-10-23 19:49:50 +02:00
feat: recently veiwed tanks
This commit is contained in:
1 parent
ea013c4df8
commit
387a67266f
9 files changed
+80
-14
No files matched your search
@@ -0,0 +1,30 @@
|
|||||||
|
import { fetchTankDefinitions } from '@blitzkit/core';
|
||||||
|
import { Flex, Separator, Text } from '@radix-ui/themes';
|
||||||
|
import { TankopediaPersistent } from '../../../stores/tankopediaPersistent';
|
||||||
|
import { TankCard } from './TankCard';
|
||||||
|
import { TankCardWrapper } from './TankCardWrapper';
|
||||||
|
|
||||||
|
const tankDefinitions = await fetchTankDefinitions();
|
||||||
|
|
||||||
|
export function RecentlyViewed() {
|
||||||
|
const tankopediaPersistentStore = TankopediaPersistent.useStore();
|
||||||
|
// non-reactive because it is a little weird that it updates instantly even before the page loads
|
||||||
|
const recentlyViewed = tankopediaPersistentStore.getState().recentlyViewed;
|
||||||
|
|
||||||
|
if (recentlyViewed.length === 0) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Flex direction="column" gap="2" mt="2" mb="6">
|
||||||
|
<Text color="gray" align="center">
|
||||||
|
Recently viewed
|
||||||
|
</Text>
|
||||||
|
<TankCardWrapper>
|
||||||
|
{recentlyViewed.map((id) => (
|
||||||
|
<TankCard tank={tankDefinitions.tanks[id]} key={id} />
|
||||||
|
))}
|
||||||
|
</TankCardWrapper>
|
||||||
|
|
||||||
|
<Separator size="4" />
|
||||||
|
</Flex>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -10,10 +10,13 @@ import {
|
|||||||
} from '@blitzkit/core';
|
} from '@blitzkit/core';
|
||||||
import { useStore } from '@nanostores/react';
|
import { useStore } from '@nanostores/react';
|
||||||
import { Flex, Link, Text } from '@radix-ui/themes';
|
import { Flex, Link, Text } from '@radix-ui/themes';
|
||||||
|
import { uniq } from 'lodash-es';
|
||||||
import { useMemo } from 'react';
|
import { useMemo } from 'react';
|
||||||
import { resolveReload } from '../../../../core/blitzkit/resolveReload';
|
import { resolveReload } from '../../../../core/blitzkit/resolveReload';
|
||||||
|
import { TankopediaPersistent } from '../../../../stores/tankopediaPersistent';
|
||||||
import { $tankopediaSort } from '../../../../stores/tankopediaSort';
|
import { $tankopediaSort } from '../../../../stores/tankopediaSort';
|
||||||
import { classIcons } from '../../../ClassIcon';
|
import { classIcons } from '../../../ClassIcon';
|
||||||
|
import { MAX_RECENTLY_VIEWED } from '../../constants';
|
||||||
import './index.css';
|
import './index.css';
|
||||||
|
|
||||||
interface TankCardProps {
|
interface TankCardProps {
|
||||||
@@ -24,6 +27,7 @@ interface TankCardProps {
|
|||||||
const modelDefinitions = await fetchModelDefinitions();
|
const modelDefinitions = await fetchModelDefinitions();
|
||||||
|
|
||||||
export function TankCard({ tank, onSelect }: TankCardProps) {
|
export function TankCard({ tank, onSelect }: TankCardProps) {
|
||||||
|
const mutateTankopediaPersistent = TankopediaPersistent.useMutation();
|
||||||
const Icon = classIcons[tank.class];
|
const Icon = classIcons[tank.class];
|
||||||
const tankopediaSort = useStore($tankopediaSort);
|
const tankopediaSort = useStore($tankopediaSort);
|
||||||
const discriminator = useMemo(() => {
|
const discriminator = useMemo(() => {
|
||||||
@@ -155,7 +159,15 @@ export function TankCard({ tank, onSelect }: TankCardProps) {
|
|||||||
: 'gray'
|
: 'gray'
|
||||||
}
|
}
|
||||||
highContrast={tank.type === TankType.RESEARCHABLE}
|
highContrast={tank.type === TankType.RESEARCHABLE}
|
||||||
onClick={onSelect ? () => onSelect(tank) : undefined}
|
onClick={() => {
|
||||||
|
onSelect?.(tank);
|
||||||
|
mutateTankopediaPersistent((draft) => {
|
||||||
|
draft.recentlyViewed = uniq([tank.id, ...draft.recentlyViewed]).slice(
|
||||||
|
0,
|
||||||
|
MAX_RECENTLY_VIEWED,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}}
|
||||||
className="tank-search-card"
|
className="tank-search-card"
|
||||||
style={{
|
style={{
|
||||||
backgroundImage: `url(${asset(`flags/scratched/${tank.nation}.webp`)})`,
|
backgroundImage: `url(${asset(`flags/scratched/${tank.nation}.webp`)})`,
|
||||||
|
|||||||
@@ -5,3 +5,5 @@ export const treeTypeOrder = [
|
|||||||
TankType.PREMIUM,
|
TankType.PREMIUM,
|
||||||
TankType.COLLECTOR,
|
TankType.COLLECTOR,
|
||||||
];
|
];
|
||||||
|
|
||||||
|
export const MAX_RECENTLY_VIEWED = 9;
|
||||||
@@ -12,11 +12,12 @@ import {
|
|||||||
import { useStore } from '@nanostores/react';
|
import { useStore } from '@nanostores/react';
|
||||||
import { Callout, Flex, Link, Text, type FlexProps } from '@radix-ui/themes';
|
import { Callout, Flex, Link, Text, type FlexProps } from '@radix-ui/themes';
|
||||||
import fuzzysort from 'fuzzysort';
|
import fuzzysort from 'fuzzysort';
|
||||||
import { times } from 'lodash-es';
|
import { times, uniq } from 'lodash-es';
|
||||||
import { memo, useEffect, useMemo, useState } from 'react';
|
import { memo, useEffect, useMemo, useState } from 'react';
|
||||||
import { filterTank } from '../../core/blitzkit/filterTank';
|
import { filterTank } from '../../core/blitzkit/filterTank';
|
||||||
import { resolveReload } from '../../core/blitzkit/resolveReload';
|
import { resolveReload } from '../../core/blitzkit/resolveReload';
|
||||||
import { $tankFilters } from '../../stores/tankFilters';
|
import { $tankFilters } from '../../stores/tankFilters';
|
||||||
|
import { TankopediaPersistent } from '../../stores/tankopediaPersistent';
|
||||||
import {
|
import {
|
||||||
SORT_NAMES,
|
SORT_NAMES,
|
||||||
SORT_UNITS,
|
SORT_UNITS,
|
||||||
@@ -25,11 +26,12 @@ import { $tankopediaSort } from '../../stores/tankopediaSort';
|
|||||||
import { ExperimentIcon } from '../ExperimentIcon';
|
import { ExperimentIcon } from '../ExperimentIcon';
|
||||||
import { FilterControl } from './components/FilterControl';
|
import { FilterControl } from './components/FilterControl';
|
||||||
import { NoResults } from './components/NoResults';
|
import { NoResults } from './components/NoResults';
|
||||||
|
import { RecentlyViewed } from './components/RecentlyViewed';
|
||||||
import { SearchBar } from './components/SearchBar';
|
import { SearchBar } from './components/SearchBar';
|
||||||
import { SkeletonTankCard } from './components/SkeletonTankCard';
|
import { SkeletonTankCard } from './components/SkeletonTankCard';
|
||||||
import { TankCard } from './components/TankCard';
|
import { TankCard } from './components/TankCard';
|
||||||
import { TankCardWrapper } from './components/TankCardWrapper';
|
import { TankCardWrapper } from './components/TankCardWrapper';
|
||||||
import { treeTypeOrder } from './constants';
|
import { MAX_RECENTLY_VIEWED, treeTypeOrder } from './constants';
|
||||||
|
|
||||||
type TankSearchProps = Omit<FlexProps, 'onSelect'> & {
|
type TankSearchProps = Omit<FlexProps, 'onSelect'> & {
|
||||||
compact?: boolean;
|
compact?: boolean;
|
||||||
@@ -47,10 +49,10 @@ const tankNames = await fetchTankNames();
|
|||||||
|
|
||||||
export const TankSearch = memo<TankSearchProps>(
|
export const TankSearch = memo<TankSearchProps>(
|
||||||
({ compact, onSelect, onSelectAll, ...props }) => {
|
({ compact, onSelect, onSelectAll, ...props }) => {
|
||||||
|
const mutateTankopediaPersistent = TankopediaPersistent.useMutation();
|
||||||
const awaitedTanksDefinitionsArray = Object.values(tankDefinitions.tanks);
|
const awaitedTanksDefinitionsArray = Object.values(tankDefinitions.tanks);
|
||||||
const tankFilters = useStore($tankFilters);
|
const tankFilters = useStore($tankFilters);
|
||||||
const tankopediaSort = useStore($tankopediaSort);
|
const tankopediaSort = useStore($tankopediaSort);
|
||||||
|
|
||||||
const tanksFiltered = useMemo(() => {
|
const tanksFiltered = useMemo(() => {
|
||||||
if (tankFilters.search === undefined) {
|
if (tankFilters.search === undefined) {
|
||||||
const filtered = awaitedTanksDefinitionsArray.filter((tank) =>
|
const filtered = awaitedTanksDefinitionsArray.filter((tank) =>
|
||||||
@@ -443,6 +445,8 @@ export const TankSearch = memo<TankSearchProps>(
|
|||||||
<FilterControl compact={compact} />
|
<FilterControl compact={compact} />
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
<RecentlyViewed />
|
||||||
|
|
||||||
<Flex mt="2" gap="1" align="center" justify="center" direction="column">
|
<Flex mt="2" gap="1" align="center" justify="center" direction="column">
|
||||||
<Flex gap="2">
|
<Flex gap="2">
|
||||||
<Text color="gray">
|
<Text color="gray">
|
||||||
@@ -457,6 +461,12 @@ export const TankSearch = memo<TankSearchProps>(
|
|||||||
onClick={(event) => {
|
onClick={(event) => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
onSelectAll(tanksFiltered);
|
onSelectAll(tanksFiltered);
|
||||||
|
mutateTankopediaPersistent((draft) => {
|
||||||
|
draft.recentlyViewed = uniq([
|
||||||
|
...tanksFiltered.map(({ id }) => id),
|
||||||
|
...draft.recentlyViewed,
|
||||||
|
]).slice(0, MAX_RECENTLY_VIEWED);
|
||||||
|
});
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Select all
|
Select all
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ const testTanks = tanks
|
|||||||
const mostPlayedTanks = tankAverages
|
const mostPlayedTanks = tankAverages
|
||||||
.sort((a, b) => b.samples.d_1 - a.samples.d_1)
|
.sort((a, b) => b.samples.d_1 - a.samples.d_1)
|
||||||
.map(({ id }) => tankDefinitions.tanks[id])
|
.map(({ id }) => tankDefinitions.tanks[id])
|
||||||
.filter(Boolean);
|
.filter((tank) => tank !== undefined && !tank.testing);
|
||||||
const popularTanks = [...testTanks, ...mostPlayedTanks].slice(0, 8);
|
const popularTanks = [...testTanks, ...mostPlayedTanks].slice(0, 8);
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ import { tankToCompareMember } from '../../../core/blitzkit/tankToCompareMember'
|
|||||||
import { App } from '../../../stores/app';
|
import { App } from '../../../stores/app';
|
||||||
import { CompareEphemeral } from '../../../stores/compareEphemeral';
|
import { CompareEphemeral } from '../../../stores/compareEphemeral';
|
||||||
import { ComparePersistent } from '../../../stores/comparePersistent';
|
import { ComparePersistent } from '../../../stores/comparePersistent';
|
||||||
|
import { TankopediaPersistent } from '../../../stores/tankopediaPersistent';
|
||||||
|
|
||||||
const modelDefinitions = await fetchModelDefinitions();
|
const modelDefinitions = await fetchModelDefinitions();
|
||||||
const equipmentDefinitions = await fetchEquipmentDefinitions();
|
const equipmentDefinitions = await fetchEquipmentDefinitions();
|
||||||
@@ -28,11 +29,15 @@ const tankDefinitions = await fetchTankDefinitions();
|
|||||||
export function Page() {
|
export function Page() {
|
||||||
return (
|
return (
|
||||||
<App.Provider>
|
<App.Provider>
|
||||||
<ComparePersistent.Provider>
|
<TankopediaPersistent.Provider>
|
||||||
<CompareEphemeral.Provider data={createDefaultSkills(skillDefinitions)}>
|
<ComparePersistent.Provider>
|
||||||
<Content />
|
<CompareEphemeral.Provider
|
||||||
</CompareEphemeral.Provider>
|
data={createDefaultSkills(skillDefinitions)}
|
||||||
</ComparePersistent.Provider>
|
>
|
||||||
|
<Content />
|
||||||
|
</CompareEphemeral.Provider>
|
||||||
|
</ComparePersistent.Provider>
|
||||||
|
</TankopediaPersistent.Provider>
|
||||||
</App.Provider>
|
</App.Provider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,13 +1,16 @@
|
|||||||
import { PageWrapper } from '../../../components/PageWrapper';
|
import { PageWrapper } from '../../../components/PageWrapper';
|
||||||
import { TankSearch } from '../../../components/TankSearch';
|
import { TankSearch } from '../../../components/TankSearch';
|
||||||
import { App } from '../../../stores/app';
|
import { App } from '../../../stores/app';
|
||||||
|
import { TankopediaPersistent } from '../../../stores/tankopediaPersistent';
|
||||||
|
|
||||||
export function Page() {
|
export function Page() {
|
||||||
return (
|
return (
|
||||||
<PageWrapper color="purple" size={1028}>
|
<PageWrapper color="purple" size={1028}>
|
||||||
<App.Provider>
|
<TankopediaPersistent.Provider>
|
||||||
<TankSearch />
|
<App.Provider>
|
||||||
</App.Provider>
|
<TankSearch />
|
||||||
|
</App.Provider>
|
||||||
|
</TankopediaPersistent.Provider>
|
||||||
</PageWrapper>
|
</PageWrapper>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -5,5 +5,7 @@ import { Page } from './_index';
|
|||||||
---
|
---
|
||||||
|
|
||||||
<ToolLayout tool={tankopediaTool}>
|
<ToolLayout tool={tankopediaTool}>
|
||||||
<Page client:load />
|
<Page client:only="react">
|
||||||
|
<Page slot="fallback" />
|
||||||
|
</Page>
|
||||||
</ToolLayout>
|
</ToolLayout>
|
||||||
@@ -19,6 +19,7 @@ export interface TankopediaPersistentStore {
|
|||||||
showSpacedArmor: boolean;
|
showSpacedArmor: boolean;
|
||||||
showExternalModules: boolean;
|
showExternalModules: boolean;
|
||||||
showPrimaryArmor: boolean;
|
showPrimaryArmor: boolean;
|
||||||
|
recentlyViewed: number[];
|
||||||
|
|
||||||
sort: {
|
sort: {
|
||||||
by: TankopediaSortBy;
|
by: TankopediaSortBy;
|
||||||
@@ -41,6 +42,7 @@ export const TankopediaPersistent = createContextualStore(() =>
|
|||||||
showSpacedArmor: true,
|
showSpacedArmor: true,
|
||||||
showExternalModules: true,
|
showExternalModules: true,
|
||||||
showPrimaryArmor: true,
|
showPrimaryArmor: true,
|
||||||
|
recentlyViewed: [],
|
||||||
sort: {
|
sort: {
|
||||||
by: 'meta.none',
|
by: 'meta.none',
|
||||||
direction: 'ascending',
|
direction: 'ascending',
|
||||||
|
|||||||
Reference in new issue
Block a user