From 71c1f8dac92070255e4ac9853b1358b711d67872 Mon Sep 17 00:00:00 2001 From: tresabhi <43380238+tresabhi@users.noreply.github.com> Date: Sat, 16 Nov 2024 23:11:31 -0600 Subject: [PATCH] feat: dragging --- .../website/src/components/TankCard/index.css | 6 +- .../website/src/components/TankCard/index.tsx | 290 ++++++------------ .../components/TankSearch/components/Card.tsx | 148 +++++++++ .../src/components/TankSearch/index.tsx | 8 +- .../website/src/components/TierList/Tile.tsx | 64 ++++ .../website/src/components/TierList/Tiles.tsx | 4 +- .../src/pages/tools/tier-list/_index.tsx | 2 +- 7 files changed, 316 insertions(+), 206 deletions(-) create mode 100644 packages/website/src/components/TankSearch/components/Card.tsx create mode 100644 packages/website/src/components/TierList/Tile.tsx diff --git a/packages/website/src/components/TankCard/index.css b/packages/website/src/components/TankCard/index.css index 492d8dc17..5dbc67f98 100644 --- a/packages/website/src/components/TankCard/index.css +++ b/packages/website/src/components/TankCard/index.css @@ -1,5 +1,5 @@ .tank-search-card { - cursor: pointer; + user-select: none; display: flex; flex-direction: column; align-items: center; @@ -10,6 +10,10 @@ overflow: hidden; } +.tank-search-card[data-provide-link='true'] { + cursor: pointer; +} + .tank-search-card .link { width: 100%; display: flex; diff --git a/packages/website/src/components/TankCard/index.tsx b/packages/website/src/components/TankCard/index.tsx index a706084ce..076c2405c 100644 --- a/packages/website/src/components/TankCard/index.tsx +++ b/packages/website/src/components/TankCard/index.tsx @@ -1,215 +1,105 @@ -import { - asset, - 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 { asset, tankIcon, TankType, type TankDefinition } from '@blitzkit/core'; +import { Flex, Link, Text, type TextProps } from '@radix-ui/themes'; import { uniq } from 'lodash-es'; -import { useMemo } from 'react'; -import { resolveReload } from '../../core/blitzkit/resolveReload'; +import { forwardRef } from 'react'; import { TankopediaPersistent } from '../../stores/tankopediaPersistent'; -import { $tankopediaSort } from '../../stores/tankopediaSort'; import { classIcons } from '../ClassIcon'; import { MAX_RECENTLY_VIEWED } from '../TankSearch/constants'; import './index.css'; -interface TankCardProps { +type TankCardProps = TextProps & { tank: TankDefinition; - onSelect?: (tank: TankDefinition) => void; -} + onTankSelect?: (tank: TankDefinition) => void; + discriminator?: string; + noLink?: boolean; +}; -const modelDefinitions = await fetchModelDefinitions(); +export const TankCard = forwardRef( + ( + { + 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) { - const mutateTankopediaPersistent = TankopediaPersistent.useMutation(); - const Icon = classIcons[tank.class]; - 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 ( - { - 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`)})`, - }} - > - { - if (onSelect) event.preventDefault(); + return ( + { + onSelect?.(tank); + mutateTankopediaPersistent((draft) => { + draft.recentlyViewed = uniq([ + tank.id, + ...draft.recentlyViewed, + ]).slice(0, MAX_RECENTLY_VIEWED); + }); }} + className="tank-search-card" + data-provide-link={provideLink} + style={{ + backgroundImage: `url(${asset(`flags/scratched/${tank.nation}.webp`)})`, + ...style, + }} + {...props} > - {tank.name} - - - - - {tank.name} - - + {tank.name} - {discriminator && ( - - {discriminator} - - )} - - - ); -} + + + {tank.name} + + + + {discriminator && ( + + {discriminator} + + )} + + + ); + }, +); diff --git a/packages/website/src/components/TankSearch/components/Card.tsx b/packages/website/src/components/TankSearch/components/Card.tsx new file mode 100644 index 000000000..e0554796c --- /dev/null +++ b/packages/website/src/components/TankSearch/components/Card.tsx @@ -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 ( + + ); +} diff --git a/packages/website/src/components/TankSearch/index.tsx b/packages/website/src/components/TankSearch/index.tsx index 5dbc1242f..a6f43a125 100644 --- a/packages/website/src/components/TankSearch/index.tsx +++ b/packages/website/src/components/TankSearch/index.tsx @@ -24,7 +24,7 @@ import { } from '../../stores/tankopediaPersistent/constants'; import { $tankopediaSort } from '../../stores/tankopediaSort'; import { ExperimentIcon } from '../ExperimentIcon'; -import { TankCard } from '../TankCard'; +import { TankSearchCard } from './components/Card'; import { FilterControl } from './components/FilterControl'; import { NoResults } from './components/NoResults'; import { RecentlyViewed } from './components/RecentlyViewed'; @@ -504,7 +504,11 @@ export const TankSearch = memo( {tanks.length > 0 && ( {tanks.map((tank) => ( - + ))} {times( diff --git a/packages/website/src/components/TierList/Tile.tsx b/packages/website/src/components/TierList/Tile.tsx new file mode 100644 index 000000000..c886f5e15 --- /dev/null +++ b/packages/website/src/components/TierList/Tile.tsx @@ -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(null); + const lastPosition = useRef({ x: 0, y: 0 }); + const handlePointerDown = useCallback( + (event: ReactPointerEvent) => { + 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 ( + + ); +} diff --git a/packages/website/src/components/TierList/Tiles.tsx b/packages/website/src/components/TierList/Tiles.tsx index 2834e0284..8e48c4dca 100644 --- a/packages/website/src/components/TierList/Tiles.tsx +++ b/packages/website/src/components/TierList/Tiles.tsx @@ -1,6 +1,6 @@ import { fetchTankDefinitions } from '@blitzkit/core'; -import { TankCard } from '../TankCard'; import { TankCardWrapper } from '../TankSearch/components/TankCardWrapper'; +import { TierListTile } from './Tile'; const tankDefinitions = await fetchTankDefinitions(); const tanks = Object.values(tankDefinitions.tanks); @@ -9,7 +9,7 @@ export function TierListTiles() { return ( {tanks.map((tank) => ( - + ))} ); diff --git a/packages/website/src/pages/tools/tier-list/_index.tsx b/packages/website/src/pages/tools/tier-list/_index.tsx index 3a9e159c7..18388ff43 100644 --- a/packages/website/src/pages/tools/tier-list/_index.tsx +++ b/packages/website/src/pages/tools/tier-list/_index.tsx @@ -1,6 +1,6 @@ import { PageWrapper } from '../../../components/PageWrapper'; -import { TierListTable } from '../../../components/TierList/Table'; import { TierListTiles } from '../../../components/TierList/Tiles'; +import { TierListTable } from '../../../components/TierList/Table'; import { TankopediaPersistent } from '../../../stores/tankopediaPersistent'; export function Page() {