feat: drop

This commit is contained in:
tresabhi committed 2024-11-17 16:34:18 -06:00
1 parent 0db4be4d20
commit d851a05f53
10 files changed
+204 -39

No files matched your search

@@ -0,0 +1 @@
export const tierListDropOffs = new Set<HTMLDivElement>();
@@ -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);
}
@@ -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<HTMLDivElement>(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 (
<Box
className="drop-off"
data-dragging="false"
data-row={row}
data-index={index}
ref={ref}
/>
);
}
@@ -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 (
<Table.Row key={row.name}>
<Table.RowHeaderCell
width="0"
style={{ backgroundColor: Var(row.color) }}
>
<Heading>{row.name}</Heading>
</Table.RowHeaderCell>
<Table.Cell>
<Flex wrap="wrap" height="100%" gap="2">
<DropOff row={index} index={0} />
{tanks.map((id, tankIndex) => {
const tank = tankDefinitions.tanks[id];
return (
<Fragment key={id}>
<TierListTile tank={tank} />
<DropOff row={index} index={tankIndex + 1} />
</Fragment>
);
})}
</Flex>
</Table.Cell>
</Table.Row>
);
}
@@ -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 (
<Table.Root variant="surface">
<Table.Body>
{rows.map((row) => (
<Table.Row key={row.name}>
<Table.RowHeaderCell
width="0"
style={{ backgroundColor: Var(row.color) }}
>
<Heading>{row.name}</Heading>
</Table.RowHeaderCell>
<Table.Cell></Table.Cell>
</Table.Row>
))}
</Table.Body>
</Table.Root>
);
}
@@ -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' },
];
@@ -0,0 +1,15 @@
import { Table } from '@radix-ui/themes';
import { TierListRow } from '../Row';
import { tierListRows } from './constants';
export function TierListTable() {
return (
<Table.Root variant="surface">
<Table.Body>
{tierListRows.map((_, index) => (
<TierListRow index={index} key={index} />
))}
</Table.Body>
</Table.Root>
);
}
@@ -4,7 +4,9 @@ import {
useRef, useRef,
type PointerEvent as ReactPointerEvent, type PointerEvent as ReactPointerEvent,
} from 'react'; } from 'react';
import { TierList } from '../../stores/tierList';
import { TankCard } from '../TankCard'; import { TankCard } from '../TankCard';
import { tierListDropOffs } from './DropOff/constants';
interface TierListTileProps { interface TierListTileProps {
tank: TankDefinition; tank: TankDefinition;
@@ -12,11 +14,14 @@ interface TierListTileProps {
export function TierListTile({ tank }: TierListTileProps) { export function TierListTile({ tank }: TierListTileProps) {
const card = useRef<HTMLDivElement>(null); const card = useRef<HTMLDivElement>(null);
const mutateTierList = TierList.useMutation();
const lastPosition = useRef({ x: 0, y: 0 }); const lastPosition = useRef({ x: 0, y: 0 });
const handlePointerDown = useCallback( const handlePointerDown = useCallback(
(event: ReactPointerEvent<HTMLSpanElement>) => { (event: ReactPointerEvent<HTMLSpanElement>) => {
if (!card.current) return; if (!card.current) return;
event.preventDefault();
const rect = card.current.getBoundingClientRect(); const rect = card.current.getBoundingClientRect();
card.current.style.position = 'fixed'; card.current.style.position = 'fixed';
@@ -29,12 +34,18 @@ export function TierListTile({ tank }: TierListTileProps) {
window.addEventListener('pointermove', handlePointerMove); window.addEventListener('pointermove', handlePointerMove);
window.addEventListener('pointerup', handlePointerUp); window.addEventListener('pointerup', handlePointerUp);
mutateTierList((draft) => {
draft.dragging = true;
});
}, },
[], [],
); );
const handlePointerMove = useCallback((event: PointerEvent) => { const handlePointerMove = useCallback((event: PointerEvent) => {
if (!card.current) return; if (!card.current) return;
event.preventDefault();
const rect = card.current.getBoundingClientRect(); const rect = card.current.getBoundingClientRect();
const dx = event.clientX - lastPosition.current.x; const dx = event.clientX - lastPosition.current.x;
const dy = event.clientY - lastPosition.current.y; const dy = event.clientY - lastPosition.current.y;
@@ -44,12 +55,53 @@ export function TierListTile({ tank }: TierListTileProps) {
lastPosition.current = { x: event.clientX, y: event.clientY }; lastPosition.current = { x: event.clientX, y: event.clientY };
}, []); }, []);
const handlePointerUp = useCallback(() => { const handlePointerUp = useCallback((event: PointerEvent) => {
if (!card.current) return; 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.position = 'static';
card.current.style.zIndex = 'unset'; card.current.style.zIndex = 'unset';
card.current.style.cursor = 'grab'; card.current.style.cursor = 'grab';
window.removeEventListener('pointermove', handlePointerMove);
window.removeEventListener('pointerup', handlePointerUp);
}, []); }, []);
return ( return (
@@ -1,13 +1,16 @@
import { PageWrapper } from '../../../components/PageWrapper'; import { PageWrapper } from '../../../components/PageWrapper';
import { TierListTiles } from '../../../components/TierList/Tiles';
import { TierListTable } from '../../../components/TierList/Table'; import { TierListTable } from '../../../components/TierList/Table';
import { TierListTiles } from '../../../components/TierList/Tiles';
import { TankopediaPersistent } from '../../../stores/tankopediaPersistent'; import { TankopediaPersistent } from '../../../stores/tankopediaPersistent';
import { TierList } from '../../../stores/tierList';
export function Page() { export function Page() {
return ( return (
<TankopediaPersistent.Provider> <TierList.Provider>
<Content /> <TankopediaPersistent.Provider>
</TankopediaPersistent.Provider> <Content />
</TankopediaPersistent.Provider>
</TierList.Provider>
); );
} }
+18
View File
@@ -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<TierList>()(
subscribeWithSelector<TierList>(() => ({
dragging: false,
tanks: tierListRows.map(() => []),
})),
),
);