2023-01-18 10:52:12 -05:00
|
|
|
import { useTransition } from "@react-spring/web";
|
2023-01-18 13:38:29 -05:00
|
|
|
import React, {
|
|
|
|
FC,
|
|
|
|
memo,
|
|
|
|
ReactNode,
|
|
|
|
useCallback,
|
|
|
|
useMemo,
|
|
|
|
useRef,
|
|
|
|
useState,
|
|
|
|
} from "react";
|
2023-01-18 10:52:12 -05:00
|
|
|
import useMeasure from "react-use-measure";
|
|
|
|
import styles from "./NewVideoGrid.module.css";
|
|
|
|
import { TileDescriptor } from "./TileDescriptor";
|
|
|
|
import { VideoGridProps as Props } from "./VideoGrid";
|
|
|
|
|
|
|
|
interface Cell {
|
|
|
|
/**
|
|
|
|
* The item held by the slot containing this cell.
|
|
|
|
*/
|
2023-01-18 11:33:40 -05:00
|
|
|
item: TileDescriptor;
|
2023-01-18 10:52:12 -05:00
|
|
|
/**
|
|
|
|
* Whether this cell is the first cell of the containing slot.
|
|
|
|
*/
|
2023-01-18 11:33:40 -05:00
|
|
|
slot: boolean;
|
2023-01-18 10:52:12 -05:00
|
|
|
/**
|
|
|
|
* The width, in columns, of the containing slot.
|
|
|
|
*/
|
2023-01-18 11:33:40 -05:00
|
|
|
columns: number;
|
2023-01-18 10:52:12 -05:00
|
|
|
/**
|
|
|
|
* The height, in rows, of the containing slot.
|
|
|
|
*/
|
2023-01-18 11:33:40 -05:00
|
|
|
rows: number;
|
2023-01-18 10:52:12 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
interface Rect {
|
2023-01-18 11:33:40 -05:00
|
|
|
x: number;
|
|
|
|
y: number;
|
|
|
|
width: number;
|
|
|
|
height: number;
|
2023-01-18 10:52:12 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
interface Tile extends Rect {
|
2023-01-18 11:33:40 -05:00
|
|
|
item: TileDescriptor;
|
2023-01-18 10:52:12 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
interface SlotsProps {
|
2023-01-18 11:33:40 -05:00
|
|
|
count: number;
|
2023-01-18 10:52:12 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Generates a number of empty slot divs.
|
|
|
|
*/
|
|
|
|
const Slots: FC<SlotsProps> = memo(({ count }) => {
|
2023-01-18 11:33:40 -05:00
|
|
|
const slots = new Array<ReactNode>(count);
|
|
|
|
for (let i = 0; i < count; i++)
|
|
|
|
slots[i] = <div className={styles.slot} key={i} />;
|
|
|
|
return <>{slots}</>;
|
|
|
|
});
|
2023-01-18 10:52:12 -05:00
|
|
|
|
2023-01-18 13:38:29 -05:00
|
|
|
export const NewVideoGrid: FC<Props> = ({
|
|
|
|
items,
|
|
|
|
disableAnimations,
|
|
|
|
children,
|
|
|
|
}) => {
|
|
|
|
const [slotGrid, setSlotGrid] = useState<HTMLDivElement | null>(null);
|
2023-01-18 10:52:12 -05:00
|
|
|
const [gridRef, gridBounds] = useMeasure();
|
|
|
|
|
|
|
|
const slotRects = useMemo(() => {
|
2023-01-18 13:38:29 -05:00
|
|
|
if (slotGrid === null) return [];
|
2023-01-18 10:52:12 -05:00
|
|
|
|
2023-01-18 13:38:29 -05:00
|
|
|
const slots = slotGrid.getElementsByClassName(styles.slot);
|
2023-01-18 11:33:40 -05:00
|
|
|
const rects = new Array<Rect>(slots.length);
|
2023-01-18 10:52:12 -05:00
|
|
|
for (let i = 0; i < slots.length; i++) {
|
2023-01-18 11:33:40 -05:00
|
|
|
const slot = slots[i] as HTMLElement;
|
2023-01-18 10:52:12 -05:00
|
|
|
rects[i] = {
|
|
|
|
x: slot.offsetLeft,
|
|
|
|
y: slot.offsetTop,
|
|
|
|
width: slot.offsetWidth,
|
|
|
|
height: slot.offsetHeight,
|
2023-01-18 11:33:40 -05:00
|
|
|
};
|
2023-01-18 10:52:12 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
return rects;
|
2023-01-18 13:38:29 -05:00
|
|
|
}, [items, gridBounds, slotGrid]);
|
2023-01-18 10:52:12 -05:00
|
|
|
|
2023-01-18 11:33:40 -05:00
|
|
|
const cells: Cell[] = useMemo(
|
|
|
|
() =>
|
|
|
|
items.map((item) => ({
|
|
|
|
item,
|
|
|
|
slot: true,
|
|
|
|
columns: 1,
|
|
|
|
rows: 1,
|
|
|
|
})),
|
|
|
|
[items]
|
|
|
|
);
|
|
|
|
|
|
|
|
const slotCells = useMemo(() => cells.filter((cell) => cell.slot), [cells]);
|
|
|
|
|
|
|
|
const tiles: Tile[] = useMemo(
|
|
|
|
() =>
|
|
|
|
slotRects.flatMap((slot, i) => {
|
|
|
|
const cell = slotCells[i];
|
|
|
|
if (cell === undefined) return [];
|
|
|
|
|
|
|
|
return [
|
|
|
|
{
|
|
|
|
item: cell.item,
|
|
|
|
x: slot.x,
|
|
|
|
y: slot.y,
|
|
|
|
width: slot.width,
|
|
|
|
height: slot.height,
|
|
|
|
},
|
|
|
|
];
|
|
|
|
}),
|
|
|
|
[slotRects, cells]
|
|
|
|
);
|
|
|
|
|
|
|
|
const [tileTransitions] = useTransition(
|
|
|
|
tiles,
|
|
|
|
() => ({
|
|
|
|
key: ({ item }: Tile) => item.id,
|
2023-01-18 13:38:29 -05:00
|
|
|
from: (({ x, y, width, height }: Tile) => ({
|
|
|
|
opacity: 0,
|
|
|
|
scale: 0,
|
|
|
|
shadow: 1,
|
2023-01-18 11:33:40 -05:00
|
|
|
x,
|
|
|
|
y,
|
|
|
|
width,
|
|
|
|
height,
|
2023-01-18 13:38:29 -05:00
|
|
|
// react-spring's types are bugged and need this to be a function with no
|
|
|
|
// parameters to infer the spring type
|
|
|
|
})) as unknown as () => {
|
|
|
|
opacity: number;
|
|
|
|
scale: number;
|
|
|
|
shadow: number;
|
|
|
|
x: number;
|
|
|
|
y: number;
|
|
|
|
width: number;
|
|
|
|
height: number;
|
|
|
|
},
|
|
|
|
enter: { opacity: 1, scale: 1 },
|
2023-01-18 11:33:40 -05:00
|
|
|
update: ({ x, y, width, height }: Tile) => ({ x, y, width, height }),
|
2023-01-18 13:38:29 -05:00
|
|
|
leave: { opacity: 0, scale: 0 },
|
|
|
|
immediate: (key: string) =>
|
|
|
|
disableAnimations || key === "zIndex" || key === "shadow",
|
|
|
|
// If we just stopped dragging a tile, give it time for the
|
|
|
|
// animation to settle before pushing its z-index back down
|
|
|
|
delay: (key: string) => (key === "zIndex" ? 500 : 0),
|
|
|
|
trail: 20,
|
2023-01-18 11:33:40 -05:00
|
|
|
}),
|
2023-01-18 13:38:29 -05:00
|
|
|
[tiles, disableAnimations]
|
2023-01-18 11:33:40 -05:00
|
|
|
);
|
2023-01-18 10:52:12 -05:00
|
|
|
|
|
|
|
const slotGridStyle = useMemo(() => {
|
|
|
|
const columnCount = gridBounds.width >= 800 ? 6 : 3;
|
|
|
|
return {
|
|
|
|
gridTemplateColumns: `repeat(${columnCount}, 1fr)`,
|
|
|
|
};
|
|
|
|
}, [gridBounds]);
|
|
|
|
|
|
|
|
// Render nothing if the bounds are not yet known
|
|
|
|
if (gridBounds.width === 0) {
|
2023-01-18 13:38:29 -05:00
|
|
|
return <div ref={gridRef} className={styles.grid} />;
|
2023-01-18 10:52:12 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
return (
|
2023-01-18 11:33:40 -05:00
|
|
|
<div ref={gridRef} className={styles.grid}>
|
2023-01-18 13:38:29 -05:00
|
|
|
<div style={slotGridStyle} ref={setSlotGrid} className={styles.slotGrid}>
|
2023-01-18 10:52:12 -05:00
|
|
|
<Slots count={items.length} />
|
|
|
|
</div>
|
2023-01-18 13:38:29 -05:00
|
|
|
{tileTransitions(({ shadow, ...style }, tile) =>
|
2023-01-18 11:33:40 -05:00
|
|
|
children({
|
|
|
|
key: tile.item.id,
|
2023-01-18 13:38:29 -05:00
|
|
|
style: {
|
|
|
|
boxShadow: shadow.to(
|
|
|
|
(s) => `rgba(0, 0, 0, 0.5) 0px ${s}px ${2 * s}px 0px`
|
|
|
|
),
|
|
|
|
...style,
|
|
|
|
},
|
2023-01-18 11:33:40 -05:00
|
|
|
width: tile.width,
|
|
|
|
height: tile.height,
|
|
|
|
item: tile.item,
|
|
|
|
})
|
|
|
|
)}
|
2023-01-18 10:52:12 -05:00
|
|
|
</div>
|
|
|
|
);
|
|
|
|
};
|