mirror of
https://github.com/blitzkit/blitzkit.git
synced 2026-11-05 08:50:38 +01:00
feat: dragging
This commit is contained in:
1 parent
aee1ddb948
commit
71c1f8dac9
7 files changed
+316
-206
No files matched your search
@@ -1,5 +1,5 @@
|
|||||||
.tank-search-card {
|
.tank-search-card {
|
||||||
cursor: pointer;
|
user-select: none;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@@ -10,6 +10,10 @@
|
|||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.tank-search-card[data-provide-link='true'] {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
.tank-search-card .link {
|
.tank-search-card .link {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|||||||
@@ -1,215 +1,105 @@
|
|||||||
import {
|
import { asset, tankIcon, TankType, type TankDefinition } from '@blitzkit/core';
|
||||||
asset,
|
import { Flex, Link, Text, type TextProps } from '@radix-ui/themes';
|
||||||
fetchModelDefinitions,
|
|
||||||
normalizeBoundingBox,
|
|
||||||
resolveDpm,
|
|
||||||
tankIcon,
|
|
||||||
TankType,
|
|
||||||
unionBoundingBox,
|
|
||||||
type TankDefinition,
|
|
||||||
} from '@blitzkit/core';
|
|
||||||
import { useStore } from '@nanostores/react';
|
|
||||||
import { Flex, Link, Text } from '@radix-ui/themes';
|
|
||||||
import { uniq } from 'lodash-es';
|
import { uniq } from 'lodash-es';
|
||||||
import { useMemo } from 'react';
|
import { forwardRef } from 'react';
|
||||||
import { resolveReload } from '../../core/blitzkit/resolveReload';
|
|
||||||
import { TankopediaPersistent } from '../../stores/tankopediaPersistent';
|
import { TankopediaPersistent } from '../../stores/tankopediaPersistent';
|
||||||
import { $tankopediaSort } from '../../stores/tankopediaSort';
|
|
||||||
import { classIcons } from '../ClassIcon';
|
import { classIcons } from '../ClassIcon';
|
||||||
import { MAX_RECENTLY_VIEWED } from '../TankSearch/constants';
|
import { MAX_RECENTLY_VIEWED } from '../TankSearch/constants';
|
||||||
import './index.css';
|
import './index.css';
|
||||||
|
|
||||||
interface TankCardProps {
|
type TankCardProps = TextProps & {
|
||||||
tank: TankDefinition;
|
tank: TankDefinition;
|
||||||
onSelect?: (tank: TankDefinition) => void;
|
onTankSelect?: (tank: TankDefinition) => void;
|
||||||
}
|
discriminator?: string;
|
||||||
|
noLink?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
const modelDefinitions = await fetchModelDefinitions();
|
export const TankCard = forwardRef<HTMLSpanElement, TankCardProps>(
|
||||||
|
(
|
||||||
|
{
|
||||||
|
tank,
|
||||||
|
discriminator,
|
||||||
|
onTankSelect: onSelect,
|
||||||
|
noLink,
|
||||||
|
style,
|
||||||
|
...props
|
||||||
|
}: TankCardProps,
|
||||||
|
ref,
|
||||||
|
) => {
|
||||||
|
const provideLink = !noLink && onSelect === undefined;
|
||||||
|
const mutateTankopediaPersistent = TankopediaPersistent.useMutation();
|
||||||
|
const Icon = classIcons[tank.class];
|
||||||
|
|
||||||
export function TankCard({ tank, onSelect }: TankCardProps) {
|
return (
|
||||||
const mutateTankopediaPersistent = TankopediaPersistent.useMutation();
|
<Text
|
||||||
const Icon = classIcons[tank.class];
|
ref={ref}
|
||||||
const tankopediaSort = useStore($tankopediaSort);
|
tabIndex={onSelect ? 0 : undefined}
|
||||||
const discriminator = useMemo(() => {
|
size="1"
|
||||||
if (tankopediaSort.by.startsWith('meta')) return undefined;
|
color={
|
||||||
|
tank.type === TankType.COLLECTOR
|
||||||
const turret = tank.turrets.at(-1)!;
|
? 'blue'
|
||||||
const gun = turret.guns.at(-1)!;
|
: tank.type === TankType.PREMIUM
|
||||||
const shell0 = gun.gun_type!.value.base.shells[0];
|
? 'amber'
|
||||||
const shell1 = gun.gun_type!.value.base.shells[1];
|
: 'gray'
|
||||||
const tracks = tank.tracks.at(-1)!;
|
}
|
||||||
const engine = tank.engines.at(-1)!;
|
highContrast={tank.type === TankType.RESEARCHABLE}
|
||||||
const tankModelDefinition = modelDefinitions.models[tank.id];
|
onClick={() => {
|
||||||
const turretModelDefinition = tankModelDefinition.turrets[turret.id];
|
onSelect?.(tank);
|
||||||
const gunModelDefinition =
|
mutateTankopediaPersistent((draft) => {
|
||||||
turretModelDefinition.guns[gun.gun_type!.value.base.id];
|
draft.recentlyViewed = uniq([
|
||||||
|
tank.id,
|
||||||
switch (tankopediaSort.by) {
|
...draft.recentlyViewed,
|
||||||
case 'fire.aimTime':
|
]).slice(0, MAX_RECENTLY_VIEWED);
|
||||||
return gun.gun_type!.value.base.aim_time.toFixed(2);
|
});
|
||||||
case 'fire.caliber':
|
|
||||||
return shell0.caliber.toFixed(0);
|
|
||||||
case 'fire.damage':
|
|
||||||
return shell0.armor_damage.toFixed(0);
|
|
||||||
case 'fire.moduleDamage':
|
|
||||||
return shell0.armor_damage.toFixed(0);
|
|
||||||
case 'fire.dispersionMoving':
|
|
||||||
return tracks.dispersion_move.toFixed(3);
|
|
||||||
case 'fire.dispersionStill':
|
|
||||||
return gun.gun_type!.value.base.dispersion_base.toFixed(3);
|
|
||||||
case 'fire.dpm':
|
|
||||||
return resolveDpm(gun, shell0).toFixed(0);
|
|
||||||
case 'fire.dpmPremium':
|
|
||||||
return shell1 ? resolveDpm(gun, shell1).toFixed(0) : '--';
|
|
||||||
case 'fire.reload':
|
|
||||||
return resolveReload(gun).toFixed(2);
|
|
||||||
case 'fire.standardPenetration':
|
|
||||||
return shell0.penetration.near.toFixed(0);
|
|
||||||
case 'fire.premiumPenetration':
|
|
||||||
return shell1 ? shell1.penetration.near.toFixed(0) : '--';
|
|
||||||
case 'fire.shellVelocity':
|
|
||||||
return shell0.velocity.toFixed(0);
|
|
||||||
case 'fire.shellCapacity':
|
|
||||||
return gun.gun_type!.value.base.shell_capacity.toFixed(0);
|
|
||||||
case 'fire.shellRange':
|
|
||||||
return shell0.range.toFixed(0);
|
|
||||||
case 'fire.gunDepression':
|
|
||||||
return (
|
|
||||||
gunModelDefinition.pitch.max +
|
|
||||||
(tankModelDefinition.initial_turret_rotation?.pitch ?? 0)
|
|
||||||
).toFixed(1);
|
|
||||||
case 'fire.gunElevation':
|
|
||||||
return (
|
|
||||||
-gunModelDefinition.pitch.min -
|
|
||||||
(tankModelDefinition.initial_turret_rotation?.pitch ?? 0)
|
|
||||||
).toFixed(1);
|
|
||||||
case 'maneuverability.forwardsSpeed':
|
|
||||||
return tank.speed_forwards.toFixed(0);
|
|
||||||
case 'maneuverability.backwardsSpeed':
|
|
||||||
return tank.speed_backwards.toFixed(0);
|
|
||||||
case 'maneuverability.power':
|
|
||||||
return engine.power.toFixed(0);
|
|
||||||
case 'maneuverability.powerToWeight':
|
|
||||||
return (
|
|
||||||
(1000 * engine.power) /
|
|
||||||
(tank.weight +
|
|
||||||
engine.weight +
|
|
||||||
tracks.weight +
|
|
||||||
turret.weight +
|
|
||||||
gun.gun_type!.value.base.weight)
|
|
||||||
).toFixed(1);
|
|
||||||
case 'maneuverability.weight':
|
|
||||||
return (
|
|
||||||
(tank.weight +
|
|
||||||
engine.weight +
|
|
||||||
tracks.weight +
|
|
||||||
turret.weight +
|
|
||||||
gun.gun_type!.value.base.weight) /
|
|
||||||
1000
|
|
||||||
).toFixed(1);
|
|
||||||
case 'maneuverability.traverseSpeed':
|
|
||||||
return tracks.traverse_speed.toFixed(1);
|
|
||||||
case 'survivability.health':
|
|
||||||
return (tank.health + turret.health).toFixed(0);
|
|
||||||
case 'survivability.viewRange':
|
|
||||||
return turret.view_range.toFixed(0);
|
|
||||||
case 'survivability.camouflageStill':
|
|
||||||
return (tank.camouflage_still * 100).toFixed(0);
|
|
||||||
case 'survivability.camouflageMoving':
|
|
||||||
return (tank.camouflage_moving * 100).toFixed(0);
|
|
||||||
case 'survivability.camouflageShooting':
|
|
||||||
return (
|
|
||||||
tank.camouflage_still *
|
|
||||||
gun.gun_type!.value.base.camouflage_loss *
|
|
||||||
100
|
|
||||||
).toFixed(0);
|
|
||||||
case 'survivability.volume': {
|
|
||||||
const dimensions = normalizeBoundingBox(
|
|
||||||
unionBoundingBox(
|
|
||||||
tankModelDefinition.bounding_box,
|
|
||||||
turretModelDefinition.bounding_box,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
return (dimensions.x * dimensions.y * dimensions.z).toFixed(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
case 'survivability.length': {
|
|
||||||
const bounds = normalizeBoundingBox(
|
|
||||||
unionBoundingBox(
|
|
||||||
tankModelDefinition.bounding_box,
|
|
||||||
turretModelDefinition.bounding_box,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
return Math.max(bounds.x, bounds.y, bounds.z).toFixed(0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, [tankopediaSort.by]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Text
|
|
||||||
tabIndex={onSelect ? 0 : undefined}
|
|
||||||
size="1"
|
|
||||||
color={
|
|
||||||
tank.type === TankType.COLLECTOR
|
|
||||||
? 'blue'
|
|
||||||
: tank.type === TankType.PREMIUM
|
|
||||||
? 'amber'
|
|
||||||
: 'gray'
|
|
||||||
}
|
|
||||||
highContrast={tank.type === TankType.RESEARCHABLE}
|
|
||||||
onClick={() => {
|
|
||||||
onSelect?.(tank);
|
|
||||||
mutateTankopediaPersistent((draft) => {
|
|
||||||
draft.recentlyViewed = uniq([tank.id, ...draft.recentlyViewed]).slice(
|
|
||||||
0,
|
|
||||||
MAX_RECENTLY_VIEWED,
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
className="tank-search-card"
|
|
||||||
style={{
|
|
||||||
backgroundImage: `url(${asset(`flags/scratched/${tank.nation}.webp`)})`,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Link
|
|
||||||
className="link"
|
|
||||||
underline="hover"
|
|
||||||
href={onSelect ? '#' : `/tools/tankopedia/${tank.id}`}
|
|
||||||
onClick={(event) => {
|
|
||||||
if (onSelect) event.preventDefault();
|
|
||||||
}}
|
}}
|
||||||
|
className="tank-search-card"
|
||||||
|
data-provide-link={provideLink}
|
||||||
|
style={{
|
||||||
|
backgroundImage: `url(${asset(`flags/scratched/${tank.nation}.webp`)})`,
|
||||||
|
...style,
|
||||||
|
}}
|
||||||
|
{...props}
|
||||||
>
|
>
|
||||||
<img alt={tank.name} src={tankIcon(tank.id)} className="image" />
|
<Link
|
||||||
|
className="link"
|
||||||
<Flex
|
underline="hover"
|
||||||
justify="center"
|
href={provideLink ? `/tools/tankopedia/${tank.id}` : undefined}
|
||||||
gap="1"
|
|
||||||
align="center"
|
|
||||||
overflow="hidden"
|
|
||||||
width="100%"
|
|
||||||
>
|
>
|
||||||
<Icon className="class-icon" />
|
<img
|
||||||
<Text align="center" className="name">
|
alt={tank.name}
|
||||||
{tank.name}
|
src={tankIcon(tank.id)}
|
||||||
</Text>
|
className="image"
|
||||||
</Flex>
|
draggable={false}
|
||||||
|
/>
|
||||||
|
|
||||||
{discriminator && (
|
<Flex
|
||||||
<Text
|
justify="center"
|
||||||
color="gray"
|
gap="1"
|
||||||
align="center"
|
align="center"
|
||||||
style={{
|
overflow="hidden"
|
||||||
whiteSpace: 'nowrap',
|
width="100%"
|
||||||
overflow: 'hidden',
|
|
||||||
textOverflow: 'ellipsis',
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
{discriminator}
|
<Icon className="class-icon" />
|
||||||
</Text>
|
<Text align="center" className="name">
|
||||||
)}
|
{tank.name}
|
||||||
</Link>
|
</Text>
|
||||||
</Text>
|
</Flex>
|
||||||
);
|
|
||||||
}
|
{discriminator && (
|
||||||
|
<Text
|
||||||
|
color="gray"
|
||||||
|
align="center"
|
||||||
|
style={{
|
||||||
|
whiteSpace: 'nowrap',
|
||||||
|
overflow: 'hidden',
|
||||||
|
textOverflow: 'ellipsis',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{discriminator}
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
|
</Link>
|
||||||
|
</Text>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
@@ -0,0 +1,148 @@
|
|||||||
|
import {
|
||||||
|
fetchModelDefinitions,
|
||||||
|
normalizeBoundingBox,
|
||||||
|
resolveDpm,
|
||||||
|
unionBoundingBox,
|
||||||
|
type TankDefinition,
|
||||||
|
} from '@blitzkit/core';
|
||||||
|
import { useStore } from '@nanostores/react';
|
||||||
|
import { useMemo } from 'react';
|
||||||
|
import { resolveReload } from '../../../core/blitzkit/resolveReload';
|
||||||
|
import { $tankopediaSort } from '../../../stores/tankopediaSort';
|
||||||
|
import { TankCard } from '../../TankCard';
|
||||||
|
|
||||||
|
interface TankSearchCardProps {
|
||||||
|
tank: TankDefinition;
|
||||||
|
onSelect?: (tank: TankDefinition) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const modelDefinitions = await fetchModelDefinitions();
|
||||||
|
|
||||||
|
export function TankSearchCard({ tank, onSelect }: TankSearchCardProps) {
|
||||||
|
const tankopediaSort = useStore($tankopediaSort);
|
||||||
|
const discriminator = useMemo(() => {
|
||||||
|
if (tankopediaSort.by.startsWith('meta')) return undefined;
|
||||||
|
|
||||||
|
const turret = tank.turrets.at(-1)!;
|
||||||
|
const gun = turret.guns.at(-1)!;
|
||||||
|
const shell0 = gun.gun_type!.value.base.shells[0];
|
||||||
|
const shell1 = gun.gun_type!.value.base.shells[1];
|
||||||
|
const tracks = tank.tracks.at(-1)!;
|
||||||
|
const engine = tank.engines.at(-1)!;
|
||||||
|
const tankModelDefinition = modelDefinitions.models[tank.id];
|
||||||
|
const turretModelDefinition = tankModelDefinition.turrets[turret.id];
|
||||||
|
const gunModelDefinition =
|
||||||
|
turretModelDefinition.guns[gun.gun_type!.value.base.id];
|
||||||
|
|
||||||
|
switch (tankopediaSort.by) {
|
||||||
|
case 'fire.aimTime':
|
||||||
|
return gun.gun_type!.value.base.aim_time.toFixed(2);
|
||||||
|
case 'fire.caliber':
|
||||||
|
return shell0.caliber.toFixed(0);
|
||||||
|
case 'fire.damage':
|
||||||
|
return shell0.armor_damage.toFixed(0);
|
||||||
|
case 'fire.moduleDamage':
|
||||||
|
return shell0.armor_damage.toFixed(0);
|
||||||
|
case 'fire.dispersionMoving':
|
||||||
|
return tracks.dispersion_move.toFixed(3);
|
||||||
|
case 'fire.dispersionStill':
|
||||||
|
return gun.gun_type!.value.base.dispersion_base.toFixed(3);
|
||||||
|
case 'fire.dpm':
|
||||||
|
return resolveDpm(gun, shell0).toFixed(0);
|
||||||
|
case 'fire.dpmPremium':
|
||||||
|
return shell1 ? resolveDpm(gun, shell1).toFixed(0) : '--';
|
||||||
|
case 'fire.reload':
|
||||||
|
return resolveReload(gun).toFixed(2);
|
||||||
|
case 'fire.standardPenetration':
|
||||||
|
return shell0.penetration.near.toFixed(0);
|
||||||
|
case 'fire.premiumPenetration':
|
||||||
|
return shell1 ? shell1.penetration.near.toFixed(0) : '--';
|
||||||
|
case 'fire.shellVelocity':
|
||||||
|
return shell0.velocity.toFixed(0);
|
||||||
|
case 'fire.shellCapacity':
|
||||||
|
return gun.gun_type!.value.base.shell_capacity.toFixed(0);
|
||||||
|
case 'fire.shellRange':
|
||||||
|
return shell0.range.toFixed(0);
|
||||||
|
case 'fire.gunDepression':
|
||||||
|
return (
|
||||||
|
gunModelDefinition.pitch.max +
|
||||||
|
(tankModelDefinition.initial_turret_rotation?.pitch ?? 0)
|
||||||
|
).toFixed(1);
|
||||||
|
case 'fire.gunElevation':
|
||||||
|
return (
|
||||||
|
-gunModelDefinition.pitch.min -
|
||||||
|
(tankModelDefinition.initial_turret_rotation?.pitch ?? 0)
|
||||||
|
).toFixed(1);
|
||||||
|
case 'maneuverability.forwardsSpeed':
|
||||||
|
return tank.speed_forwards.toFixed(0);
|
||||||
|
case 'maneuverability.backwardsSpeed':
|
||||||
|
return tank.speed_backwards.toFixed(0);
|
||||||
|
case 'maneuverability.power':
|
||||||
|
return engine.power.toFixed(0);
|
||||||
|
case 'maneuverability.powerToWeight':
|
||||||
|
return (
|
||||||
|
(1000 * engine.power) /
|
||||||
|
(tank.weight +
|
||||||
|
engine.weight +
|
||||||
|
tracks.weight +
|
||||||
|
turret.weight +
|
||||||
|
gun.gun_type!.value.base.weight)
|
||||||
|
).toFixed(1);
|
||||||
|
case 'maneuverability.weight':
|
||||||
|
return (
|
||||||
|
(tank.weight +
|
||||||
|
engine.weight +
|
||||||
|
tracks.weight +
|
||||||
|
turret.weight +
|
||||||
|
gun.gun_type!.value.base.weight) /
|
||||||
|
1000
|
||||||
|
).toFixed(1);
|
||||||
|
case 'maneuverability.traverseSpeed':
|
||||||
|
return tracks.traverse_speed.toFixed(1);
|
||||||
|
case 'survivability.health':
|
||||||
|
return (tank.health + turret.health).toFixed(0);
|
||||||
|
case 'survivability.viewRange':
|
||||||
|
return turret.view_range.toFixed(0);
|
||||||
|
case 'survivability.camouflageStill':
|
||||||
|
return (tank.camouflage_still * 100).toFixed(0);
|
||||||
|
case 'survivability.camouflageMoving':
|
||||||
|
return (tank.camouflage_moving * 100).toFixed(0);
|
||||||
|
case 'survivability.camouflageShooting':
|
||||||
|
return (
|
||||||
|
tank.camouflage_still *
|
||||||
|
gun.gun_type!.value.base.camouflage_loss *
|
||||||
|
100
|
||||||
|
).toFixed(0);
|
||||||
|
case 'survivability.volume': {
|
||||||
|
const dimensions = normalizeBoundingBox(
|
||||||
|
unionBoundingBox(
|
||||||
|
tankModelDefinition.bounding_box,
|
||||||
|
turretModelDefinition.bounding_box,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
return (dimensions.x * dimensions.y * dimensions.z).toFixed(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'survivability.length': {
|
||||||
|
const bounds = normalizeBoundingBox(
|
||||||
|
unionBoundingBox(
|
||||||
|
tankModelDefinition.bounding_box,
|
||||||
|
turretModelDefinition.bounding_box,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
return Math.max(bounds.x, bounds.y, bounds.z).toFixed(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [tankopediaSort.by]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<TankCard
|
||||||
|
key={tank.id}
|
||||||
|
onTankSelect={onSelect}
|
||||||
|
tank={tank}
|
||||||
|
discriminator={discriminator}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -24,7 +24,7 @@ import {
|
|||||||
} from '../../stores/tankopediaPersistent/constants';
|
} from '../../stores/tankopediaPersistent/constants';
|
||||||
import { $tankopediaSort } from '../../stores/tankopediaSort';
|
import { $tankopediaSort } from '../../stores/tankopediaSort';
|
||||||
import { ExperimentIcon } from '../ExperimentIcon';
|
import { ExperimentIcon } from '../ExperimentIcon';
|
||||||
import { TankCard } from '../TankCard';
|
import { TankSearchCard } from './components/Card';
|
||||||
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 { RecentlyViewed } from './components/RecentlyViewed';
|
||||||
@@ -504,7 +504,11 @@ export const TankSearch = memo<TankSearchProps>(
|
|||||||
{tanks.length > 0 && (
|
{tanks.length > 0 && (
|
||||||
<TankCardWrapper>
|
<TankCardWrapper>
|
||||||
{tanks.map((tank) => (
|
{tanks.map((tank) => (
|
||||||
<TankCard key={tank.id} onSelect={onSelect} tank={tank} />
|
<TankSearchCard
|
||||||
|
tank={tank}
|
||||||
|
key={tank.id}
|
||||||
|
onSelect={onSelect}
|
||||||
|
/>
|
||||||
))}
|
))}
|
||||||
|
|
||||||
{times(
|
{times(
|
||||||
|
|||||||
@@ -0,0 +1,64 @@
|
|||||||
|
import type { TankDefinition } from '@blitzkit/core';
|
||||||
|
import {
|
||||||
|
useCallback,
|
||||||
|
useRef,
|
||||||
|
type PointerEvent as ReactPointerEvent,
|
||||||
|
} from 'react';
|
||||||
|
import { TankCard } from '../TankCard';
|
||||||
|
|
||||||
|
interface TierListTileProps {
|
||||||
|
tank: TankDefinition;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function TierListTile({ tank }: TierListTileProps) {
|
||||||
|
const card = useRef<HTMLDivElement>(null);
|
||||||
|
const lastPosition = useRef({ x: 0, y: 0 });
|
||||||
|
const handlePointerDown = useCallback(
|
||||||
|
(event: ReactPointerEvent<HTMLSpanElement>) => {
|
||||||
|
if (!card.current) return;
|
||||||
|
|
||||||
|
const rect = card.current.getBoundingClientRect();
|
||||||
|
|
||||||
|
card.current.style.position = 'fixed';
|
||||||
|
card.current.style.cursor = 'grabbing';
|
||||||
|
card.current.style.zIndex = '1';
|
||||||
|
card.current.style.left = `${rect.left}px`;
|
||||||
|
card.current.style.top = `${rect.top}px`;
|
||||||
|
|
||||||
|
lastPosition.current = { x: event.clientX, y: event.clientY };
|
||||||
|
|
||||||
|
window.addEventListener('pointermove', handlePointerMove);
|
||||||
|
window.addEventListener('pointerup', handlePointerUp);
|
||||||
|
},
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
const handlePointerMove = useCallback((event: PointerEvent) => {
|
||||||
|
if (!card.current) return;
|
||||||
|
|
||||||
|
const rect = card.current.getBoundingClientRect();
|
||||||
|
const dx = event.clientX - lastPosition.current.x;
|
||||||
|
const dy = event.clientY - lastPosition.current.y;
|
||||||
|
|
||||||
|
card.current.style.left = `${rect.left + dx}px`;
|
||||||
|
card.current.style.top = `${rect.top + dy}px`;
|
||||||
|
|
||||||
|
lastPosition.current = { x: event.clientX, y: event.clientY };
|
||||||
|
}, []);
|
||||||
|
const handlePointerUp = useCallback(() => {
|
||||||
|
if (!card.current) return;
|
||||||
|
|
||||||
|
card.current.style.position = 'static';
|
||||||
|
card.current.style.zIndex = 'unset';
|
||||||
|
card.current.style.cursor = 'grab';
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<TankCard
|
||||||
|
ref={card}
|
||||||
|
noLink
|
||||||
|
tank={tank}
|
||||||
|
style={{ cursor: 'grab' }}
|
||||||
|
onPointerDown={handlePointerDown}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import { fetchTankDefinitions } from '@blitzkit/core';
|
import { fetchTankDefinitions } from '@blitzkit/core';
|
||||||
import { TankCard } from '../TankCard';
|
|
||||||
import { TankCardWrapper } from '../TankSearch/components/TankCardWrapper';
|
import { TankCardWrapper } from '../TankSearch/components/TankCardWrapper';
|
||||||
|
import { TierListTile } from './Tile';
|
||||||
|
|
||||||
const tankDefinitions = await fetchTankDefinitions();
|
const tankDefinitions = await fetchTankDefinitions();
|
||||||
const tanks = Object.values(tankDefinitions.tanks);
|
const tanks = Object.values(tankDefinitions.tanks);
|
||||||
@@ -9,7 +9,7 @@ export function TierListTiles() {
|
|||||||
return (
|
return (
|
||||||
<TankCardWrapper>
|
<TankCardWrapper>
|
||||||
{tanks.map((tank) => (
|
{tanks.map((tank) => (
|
||||||
<TankCard tank={tank} />
|
<TierListTile key={tank.id} tank={tank} />
|
||||||
))}
|
))}
|
||||||
</TankCardWrapper>
|
</TankCardWrapper>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { PageWrapper } from '../../../components/PageWrapper';
|
import { PageWrapper } from '../../../components/PageWrapper';
|
||||||
import { TierListTable } from '../../../components/TierList/Table';
|
|
||||||
import { TierListTiles } from '../../../components/TierList/Tiles';
|
import { TierListTiles } from '../../../components/TierList/Tiles';
|
||||||
|
import { TierListTable } from '../../../components/TierList/Table';
|
||||||
import { TankopediaPersistent } from '../../../stores/tankopediaPersistent';
|
import { TankopediaPersistent } from '../../../stores/tankopediaPersistent';
|
||||||
|
|
||||||
export function Page() {
|
export function Page() {
|
||||||
|
|||||||
Reference in new issue
Block a user