diff --git a/packages/website/src/components/TierList/DropOff/constants.ts b/packages/website/src/components/TierList/DropOff/constants.ts new file mode 100644 index 000000000..553dd2dc8 --- /dev/null +++ b/packages/website/src/components/TierList/DropOff/constants.ts @@ -0,0 +1 @@ +export const tierListDropOffs = new Set(); diff --git a/packages/website/src/components/TierList/DropOff/index.css b/packages/website/src/components/TierList/DropOff/index.css new file mode 100644 index 000000000..f1feff18d --- /dev/null +++ b/packages/website/src/components/TierList/DropOff/index.css @@ -0,0 +1,9 @@ +.drop-off { + width: 0.125rem; + height: 100%; + border-radius: var(--radius-1); +} + +.drop-off[data-dragging='true'] { + background-color: var(--accent-indicator); +} diff --git a/packages/website/src/components/TierList/DropOff/index.tsx b/packages/website/src/components/TierList/DropOff/index.tsx new file mode 100644 index 000000000..46d23994c --- /dev/null +++ b/packages/website/src/components/TierList/DropOff/index.tsx @@ -0,0 +1,42 @@ +import { Box } from '@radix-ui/themes'; +import { useEffect, useRef } from 'react'; +import { TierList } from '../../../stores/tierList'; +import { tierListDropOffs } from './constants'; +import './index.css'; + +interface DropOffProps { + row: number; + index: number; +} + +export function DropOff({ row, index }: DropOffProps) { + const tierListStore = TierList.useStore(); + const ref = useRef(null); + + useEffect(() => { + const unsubscribe = tierListStore.subscribe( + (state) => state.dragging, + (dragging) => { + if (!ref.current) return; + ref.current.dataset.dragging = `${dragging}`; + }, + ); + + tierListDropOffs.add(ref.current!); + + return () => { + unsubscribe(); + tierListDropOffs.delete(ref.current!); + }; + }, []); + + return ( + + ); +} diff --git a/packages/website/src/components/TierList/Row.tsx b/packages/website/src/components/TierList/Row.tsx new file mode 100644 index 000000000..7879ef2a0 --- /dev/null +++ b/packages/website/src/components/TierList/Row.tsx @@ -0,0 +1,47 @@ +import { fetchTankDefinitions } from '@blitzkit/core'; +import { Flex, Heading, Table } from '@radix-ui/themes'; +import { Fragment } from 'react'; +import { Var } from '../../core/radix/var'; +import { TierList } from '../../stores/tierList'; +import { DropOff } from './DropOff'; +import { tierListRows } from './Table/constants'; +import { TierListTile } from './Tile'; + +interface TierListRowProps { + index: number; +} + +const tankDefinitions = await fetchTankDefinitions(); + +export function TierListRow({ index }: TierListRowProps) { + const row = tierListRows[index]; + const tanks = TierList.use((state) => state.tanks[index]); + + return ( + + + {row.name} + + + + + + + {tanks.map((id, tankIndex) => { + const tank = tankDefinitions.tanks[id]; + + return ( + + + + + ); + })} + + + + ); +} diff --git a/packages/website/src/components/TierList/Table.tsx b/packages/website/src/components/TierList/Table.tsx deleted file mode 100644 index 4b7dd5153..000000000 --- a/packages/website/src/components/TierList/Table.tsx +++ /dev/null @@ -1,34 +0,0 @@ -import { Heading, Table } from '@radix-ui/themes'; -import { Var, type VarName } from '../../core/radix/var'; - -const rows: { - name: string; - color: VarName; -}[] = [ - { name: 'S', color: 'tomato-9' }, - { name: 'A', color: 'orange-9' }, - { name: 'B', color: 'amber-9' }, - { name: 'C', color: 'green-9' }, - { name: 'D', color: 'blue-9' }, -]; - -export function TierListTable() { - return ( - - - {rows.map((row) => ( - - - {row.name} - - - - - ))} - - - ); -} diff --git a/packages/website/src/components/TierList/Table/constants.ts b/packages/website/src/components/TierList/Table/constants.ts new file mode 100644 index 000000000..1faa6e6a1 --- /dev/null +++ b/packages/website/src/components/TierList/Table/constants.ts @@ -0,0 +1,12 @@ +import type { VarName } from '../../../core/radix/var'; + +export const tierListRows: { + name: string; + color: VarName; +}[] = [ + { name: 'S', color: 'tomato-9' }, + { name: 'A', color: 'orange-9' }, + { name: 'B', color: 'amber-9' }, + { name: 'C', color: 'green-9' }, + { name: 'D', color: 'blue-9' }, +]; diff --git a/packages/website/src/components/TierList/Table/index.tsx b/packages/website/src/components/TierList/Table/index.tsx new file mode 100644 index 000000000..04d3d47cf --- /dev/null +++ b/packages/website/src/components/TierList/Table/index.tsx @@ -0,0 +1,15 @@ +import { Table } from '@radix-ui/themes'; +import { TierListRow } from '../Row'; +import { tierListRows } from './constants'; + +export function TierListTable() { + return ( + + + {tierListRows.map((_, index) => ( + + ))} + + + ); +} diff --git a/packages/website/src/components/TierList/Tile.tsx b/packages/website/src/components/TierList/Tile.tsx index c886f5e15..3ee0ddf5a 100644 --- a/packages/website/src/components/TierList/Tile.tsx +++ b/packages/website/src/components/TierList/Tile.tsx @@ -4,7 +4,9 @@ import { useRef, type PointerEvent as ReactPointerEvent, } from 'react'; +import { TierList } from '../../stores/tierList'; import { TankCard } from '../TankCard'; +import { tierListDropOffs } from './DropOff/constants'; interface TierListTileProps { tank: TankDefinition; @@ -12,11 +14,14 @@ interface TierListTileProps { export function TierListTile({ tank }: TierListTileProps) { const card = useRef(null); + const mutateTierList = TierList.useMutation(); const lastPosition = useRef({ x: 0, y: 0 }); const handlePointerDown = useCallback( (event: ReactPointerEvent) => { if (!card.current) return; + event.preventDefault(); + const rect = card.current.getBoundingClientRect(); card.current.style.position = 'fixed'; @@ -29,12 +34,18 @@ export function TierListTile({ tank }: TierListTileProps) { window.addEventListener('pointermove', handlePointerMove); window.addEventListener('pointerup', handlePointerUp); + + mutateTierList((draft) => { + draft.dragging = true; + }); }, [], ); const handlePointerMove = useCallback((event: PointerEvent) => { if (!card.current) return; + event.preventDefault(); + const rect = card.current.getBoundingClientRect(); const dx = event.clientX - lastPosition.current.x; const dy = event.clientY - lastPosition.current.y; @@ -44,12 +55,53 @@ export function TierListTile({ tank }: TierListTileProps) { lastPosition.current = { x: event.clientX, y: event.clientY }; }, []); - const handlePointerUp = useCallback(() => { + const handlePointerUp = useCallback((event: PointerEvent) => { if (!card.current) return; + const rect = card.current.getBoundingClientRect(); + let dropOff: { row: number; index: number; distance: number }; + + tierListDropOffs.forEach((element) => { + const thisRect = element.getBoundingClientRect(); + const thisX = (thisRect.left + thisRect.right) / 2; + const thisY = (thisRect.top + thisRect.bottom) / 2; + let distance = 0; + + if ( + (thisX < rect.left || thisX > rect.right) && + (thisY < rect.top || thisY > rect.bottom) + ) { + const rectX = (rect.left + rect.right) / 2; + const rectY = (rect.top + rect.bottom) / 2; + distance = Math.sqrt((thisX - rectX) ** 2 + (thisY - rectY) ** 2); + } + + if (dropOff === undefined || dropOff.distance > distance) { + dropOff = { + row: Number(element.dataset.row), + index: Number(element.dataset.index), + distance, + }; + } + }); + + mutateTierList((draft) => { + draft.dragging = false; + draft.tanks = draft.tanks.map((tanks) => + tanks.filter((id) => id !== tank.id), + ); + + if (dropOff) { + draft.tanks[dropOff.row].splice(dropOff.index, 0, tank.id); + } + }); + card.current.style.position = 'static'; card.current.style.zIndex = 'unset'; card.current.style.cursor = 'grab'; + + window.removeEventListener('pointermove', handlePointerMove); + window.removeEventListener('pointerup', handlePointerUp); }, []); return ( diff --git a/packages/website/src/pages/tools/tier-list/_index.tsx b/packages/website/src/pages/tools/tier-list/_index.tsx index 18388ff43..4c187a756 100644 --- a/packages/website/src/pages/tools/tier-list/_index.tsx +++ b/packages/website/src/pages/tools/tier-list/_index.tsx @@ -1,13 +1,16 @@ import { PageWrapper } from '../../../components/PageWrapper'; -import { TierListTiles } from '../../../components/TierList/Tiles'; import { TierListTable } from '../../../components/TierList/Table'; +import { TierListTiles } from '../../../components/TierList/Tiles'; import { TankopediaPersistent } from '../../../stores/tankopediaPersistent'; +import { TierList } from '../../../stores/tierList'; export function Page() { return ( - - - + + + + + ); } diff --git a/packages/website/src/stores/tierList.ts b/packages/website/src/stores/tierList.ts new file mode 100644 index 000000000..382a34b52 --- /dev/null +++ b/packages/website/src/stores/tierList.ts @@ -0,0 +1,18 @@ +import { create } from 'zustand'; +import { subscribeWithSelector } from 'zustand/middleware'; +import { tierListRows } from '../components/TierList/Table/constants'; +import { createContextualStore } from '../core/zustand/createContextualStore'; + +export interface TierList { + dragging: boolean; + tanks: number[][]; +} + +export const TierList = createContextualStore(() => + create()( + subscribeWithSelector(() => ({ + dragging: false, + tanks: tierListRows.map(() => []), + })), + ), +);