mtg-deck-completion / web /src /hooks /usePreview.ts
nsaroiu's picture
Deploy 005b9d6
15c3f7a verified
Raw History Blame Contribute Delete
1.67 kB
import { useCallback, useState } from 'react'
import type { CardInfo } from '../api'
// State for the floating card-text preview (components/CardPreview.tsx):
// shown while hovering or keyboard-focusing a card name, only on devices
// with a real pointer -- touch users tap through to Scryfall instead.
export interface PreviewState {
card: CardInfo
x: number
y: number
}
export const PREVIEW_WIDTH = 300
// Estimate used only to keep the panel on screen; real height varies with text.
const PREVIEW_HEIGHT = 240
const OFFSET = 18
function place(x: number, y: number): { x: number; y: number } {
const vw = window.innerWidth
const vh = window.innerHeight
const left = x + OFFSET + PREVIEW_WIDTH > vw ? x - OFFSET - PREVIEW_WIDTH : x + OFFSET
const top = Math.min(Math.max(8, y - PREVIEW_HEIGHT / 2), vh - PREVIEW_HEIGHT - 8)
return { x: Math.max(8, left), y: top }
}
const canHover = () => window.matchMedia('(hover: hover) and (pointer: fine)').matches
export function usePreview() {
const [state, setState] = useState<PreviewState | null>(null)
const show = useCallback((card: CardInfo, e: React.MouseEvent) => {
if (canHover()) setState({ card, ...place(e.clientX, e.clientY) })
}, [])
const move = useCallback((e: React.MouseEvent) => {
setState((s) => (s ? { ...s, ...place(e.clientX, e.clientY) } : s))
}, [])
const showAt = useCallback((card: CardInfo, el: HTMLElement) => {
if (!canHover()) return
const r = el.getBoundingClientRect()
setState({ card, ...place(r.right, r.top + r.height / 2) })
}, [])
const hide = useCallback(() => setState(null), [])
return { state, show, move, showAt, hide }
}