feat: Implement useCardTouch hook to standardize card interaction and touch event handling across components.
Some checks failed
Build and Deploy / build (push) Failing after 56s
Some checks failed
Build and Deploy / build (push) Failing after 56s
This commit is contained in:
@@ -144,7 +144,7 @@ export const CardHoverWrapper: React.FC<{ card: DraftCard; children: React.React
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleTouchStart = (e: React.TouchEvent) => {
|
const handleTouchStart = (e: React.TouchEvent) => {
|
||||||
if (!hasImage || !isMobile) return;
|
if (!hasImage || !isMobile || preventPreview) return;
|
||||||
const touch = e.touches[0];
|
const touch = e.touches[0];
|
||||||
const { clientX, clientY } = touch;
|
const { clientX, clientY } = touch;
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import React, { useMemo } from 'react';
|
import React, { useMemo } from 'react';
|
||||||
import { DraftCard } from '../services/PackGeneratorService';
|
import { DraftCard } from '../services/PackGeneratorService';
|
||||||
import { FoilOverlay, CardHoverWrapper } from './CardPreview';
|
import { FoilOverlay, CardHoverWrapper } from './CardPreview';
|
||||||
|
import { useCardTouch } from '../utils/interaction';
|
||||||
|
|
||||||
interface StackViewProps {
|
interface StackViewProps {
|
||||||
cards: DraftCard[];
|
cards: DraftCard[];
|
||||||
@@ -80,30 +81,18 @@ export const StackView: React.FC<StackViewProps> = ({ cards, cardWidth = 150, on
|
|||||||
const displayImage = useArtCrop ? card.imageArtCrop : card.image;
|
const displayImage = useArtCrop ? card.imageArtCrop : card.image;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<StackCardItem
|
||||||
key={card.id}
|
key={card.id}
|
||||||
className="relative w-full z-0 hover:z-50 transition-all duration-200 group"
|
card={card}
|
||||||
onMouseEnter={() => onHover && onHover(card)}
|
cardWidth={cardWidth}
|
||||||
onMouseLeave={() => onHover && onHover(null)}
|
isLast={isLast}
|
||||||
onClick={() => onCardClick && onCardClick(card)}
|
useArtCrop={useArtCrop}
|
||||||
>
|
displayImage={displayImage}
|
||||||
<CardHoverWrapper card={card} preventPreview={disableHoverPreview || cardWidth >= 200}>
|
onHover={onHover}
|
||||||
<div
|
onCardClick={onCardClick}
|
||||||
className={`relative w-full rounded-lg bg-slate-800 shadow-md border border-slate-950 overflow-hidden cursor-pointer group-hover:ring-2 group-hover:ring-purple-400`}
|
disableHoverPreview={disableHoverPreview}
|
||||||
style={{
|
/>
|
||||||
// Aspect ratio is maintained by image or div dimensions
|
);
|
||||||
// With overlap, we just render them one after another with negative margin
|
|
||||||
marginBottom: isLast ? '0' : (useArtCrop ? '-85%' : '-125%'), // Negative margin to show header. Square cards need less negative margin.
|
|
||||||
aspectRatio: useArtCrop ? '1/1' : '2.5/3.5'
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<img src={displayImage} alt={card.name} className="w-full h-full object-cover" />
|
|
||||||
{/* Optional: Shine effect for foils if visible? */}
|
|
||||||
{card.finish === 'foil' && <FoilOverlay />}
|
|
||||||
</div>
|
|
||||||
</CardHoverWrapper>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -112,3 +101,32 @@ export const StackView: React.FC<StackViewProps> = ({ cards, cardWidth = 150, on
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const StackCardItem = ({ card, cardWidth, isLast, useArtCrop, displayImage, onHover, onCardClick, disableHoverPreview }: any) => {
|
||||||
|
const { onTouchStart, onTouchEnd, onTouchMove, onClick } = useCardTouch(onHover || (() => { }), () => onCardClick && onCardClick(card), card);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className="relative w-full z-0 hover:z-50 transition-all duration-200 group"
|
||||||
|
onMouseEnter={() => onHover && onHover(card)}
|
||||||
|
onMouseLeave={() => onHover && onHover(null)}
|
||||||
|
onClick={onClick}
|
||||||
|
onTouchStart={onTouchStart}
|
||||||
|
onTouchEnd={onTouchEnd}
|
||||||
|
onTouchMove={onTouchMove}
|
||||||
|
>
|
||||||
|
<CardHoverWrapper card={card} preventPreview={disableHoverPreview || cardWidth >= 200}>
|
||||||
|
<div
|
||||||
|
className={`relative w-full rounded-lg bg-slate-800 shadow-md border border-slate-950 overflow-hidden cursor-pointer group-hover:ring-2 group-hover:ring-purple-400`}
|
||||||
|
style={{
|
||||||
|
marginBottom: isLast ? '0' : (useArtCrop ? '-85%' : '-125%'),
|
||||||
|
aspectRatio: useArtCrop ? '1/1' : '2.5/3.5'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<img src={displayImage} alt={card.name} className="w-full h-full object-cover" />
|
||||||
|
{card.finish === 'foil' && <FoilOverlay />}
|
||||||
|
</div>
|
||||||
|
</CardHoverWrapper>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { Save, Layers, Clock, Columns, LayoutTemplate, List, LayoutGrid } from '
|
|||||||
import { StackView } from '../../components/StackView';
|
import { StackView } from '../../components/StackView';
|
||||||
import { FoilOverlay } from '../../components/CardPreview';
|
import { FoilOverlay } from '../../components/CardPreview';
|
||||||
import { DraftCard } from '../../services/PackGeneratorService';
|
import { DraftCard } from '../../services/PackGeneratorService';
|
||||||
|
import { useCardTouch } from '../../utils/interaction';
|
||||||
|
|
||||||
interface DeckBuilderViewProps {
|
interface DeckBuilderViewProps {
|
||||||
roomId: string;
|
roomId: string;
|
||||||
@@ -34,11 +35,16 @@ const ListItem: React.FC<{ card: DraftCard; onClick?: () => void; onHover?: (c:
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const { onTouchStart, onTouchEnd, onTouchMove, onClick: handleTouchClick } = useCardTouch(onHover || (() => { }), onClick || (() => { }), card);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
onClick={onClick}
|
onClick={handleTouchClick}
|
||||||
onMouseEnter={() => onHover && onHover(card)}
|
onMouseEnter={() => onHover && onHover(card)}
|
||||||
onMouseLeave={() => onHover && onHover(null)}
|
onMouseLeave={() => onHover && onHover(null)}
|
||||||
|
onTouchStart={onTouchStart}
|
||||||
|
onTouchEnd={onTouchEnd}
|
||||||
|
onTouchMove={onTouchMove}
|
||||||
className="flex items-center justify-between py-1 px-2 rounded hover:bg-slate-700/50 cursor-pointer transition-colors w-full group"
|
className="flex items-center justify-between py-1 px-2 rounded hover:bg-slate-700/50 cursor-pointer transition-colors w-full group"
|
||||||
>
|
>
|
||||||
<span className={`font-medium flex items-center gap-2 truncate ${card.rarity === 'mythic' ? 'text-orange-400' : card.rarity === 'rare' ? 'text-yellow-400' : card.rarity === 'uncommon' ? 'text-slate-200' : 'text-slate-400'}`}>
|
<span className={`font-medium flex items-center gap-2 truncate ${card.rarity === 'mythic' ? 'text-orange-400' : card.rarity === 'rare' ? 'text-yellow-400' : card.rarity === 'uncommon' ? 'text-slate-200' : 'text-slate-400'}`}>
|
||||||
@@ -109,28 +115,18 @@ const CardsDisplay: React.FC<{
|
|||||||
{cards.map(c => {
|
{cards.map(c => {
|
||||||
const card = normalizeCard(c);
|
const card = normalizeCard(c);
|
||||||
const useArtCrop = cardWidth < 200 && !!card.imageArtCrop;
|
const useArtCrop = cardWidth < 200 && !!card.imageArtCrop;
|
||||||
const displayImage = useArtCrop ? card.imageArtCrop : card.image;
|
|
||||||
const isFoil = card.finish === 'foil';
|
const isFoil = card.finish === 'foil';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<DeckCardItem
|
||||||
key={card.id}
|
key={card.id}
|
||||||
onClick={() => onCardClick(c)}
|
card={card}
|
||||||
onMouseEnter={() => onHover(card)}
|
useArtCrop={useArtCrop}
|
||||||
onMouseLeave={() => onHover(null)}
|
isFoil={isFoil}
|
||||||
className="relative group bg-slate-900 rounded-lg shrink-0 cursor-pointer hover:scale-105 transition-transform"
|
onCardClick={onCardClick}
|
||||||
>
|
onHover={onHover}
|
||||||
<div className={`relative ${useArtCrop ? 'aspect-square' : 'aspect-[2.5/3.5]'} overflow-hidden rounded-lg shadow-xl border transition-all duration-200 group-hover:ring-2 group-hover:ring-purple-400 group-hover:shadow-purple-500/30 ${isFoil ? 'border-purple-400 shadow-purple-500/20' : 'border-slate-800'}`}>
|
/>
|
||||||
{isFoil && <FoilOverlay />}
|
|
||||||
{isFoil && <div className="absolute top-1 right-1 z-30 text-[10px] font-bold text-white bg-purple-600/80 px-1 rounded backdrop-blur-sm">FOIL</div>}
|
|
||||||
{displayImage ? (
|
|
||||||
<img src={displayImage} alt={card.name} className="w-full h-full object-cover" />
|
|
||||||
) : (
|
|
||||||
<div className="w-full h-full flex items-center justify-center text-xs text-center p-1 text-slate-500 font-bold border-2 border-slate-700 m-1 rounded">{card.name}</div>
|
|
||||||
)}
|
|
||||||
<div className={`absolute bottom-0 left-0 right-0 h-1.5 ${card.rarity === 'mythic' ? 'bg-gradient-to-r from-orange-500 to-red-600' : card.rarity === 'rare' ? 'bg-gradient-to-r from-yellow-400 to-yellow-600' : card.rarity === 'uncommon' ? 'bg-gradient-to-r from-gray-300 to-gray-500' : 'bg-black'}`} />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
@@ -341,7 +337,7 @@ export const DeckBuilderView: React.FC<DeckBuilderViewProps> = ({ initialPool, a
|
|||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex-1 w-full flex h-full bg-slate-950 text-white overflow-hidden flex-col">
|
<div className="flex-1 w-full flex h-full bg-slate-950 text-white overflow-hidden flex-col select-none" onContextMenu={(e) => e.preventDefault()}>
|
||||||
{/* Global Toolbar - Inlined */}
|
{/* Global Toolbar - Inlined */}
|
||||||
<div className="h-14 bg-slate-800 border-b border-slate-700 flex items-center justify-between px-4 shrink-0">
|
<div className="h-14 bg-slate-800 border-b border-slate-700 flex items-center justify-between px-4 shrink-0">
|
||||||
<div className="flex items-center gap-4">
|
<div className="flex items-center gap-4">
|
||||||
@@ -491,3 +487,31 @@ export const DeckBuilderView: React.FC<DeckBuilderViewProps> = ({ initialPool, a
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const DeckCardItem = ({ card, useArtCrop, isFoil, onCardClick, onHover }: any) => {
|
||||||
|
const displayImage = useArtCrop ? card.imageArtCrop : card.image;
|
||||||
|
const { onTouchStart, onTouchEnd, onTouchMove, onClick } = useCardTouch(onHover, () => onCardClick(card), card);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
onClick={onClick}
|
||||||
|
onMouseEnter={() => onHover(card)}
|
||||||
|
onMouseLeave={() => onHover(null)}
|
||||||
|
onTouchStart={onTouchStart}
|
||||||
|
onTouchEnd={onTouchEnd}
|
||||||
|
onTouchMove={onTouchMove}
|
||||||
|
className="relative group bg-slate-900 rounded-lg shrink-0 cursor-pointer hover:scale-105 transition-transform"
|
||||||
|
>
|
||||||
|
<div className={`relative ${useArtCrop ? 'aspect-square' : 'aspect-[2.5/3.5]'} overflow-hidden rounded-lg shadow-xl border transition-all duration-200 group-hover:ring-2 group-hover:ring-purple-400 group-hover:shadow-purple-500/30 ${isFoil ? 'border-purple-400 shadow-purple-500/20' : 'border-slate-800'}`}>
|
||||||
|
{isFoil && <FoilOverlay />}
|
||||||
|
{isFoil && <div className="absolute top-1 right-1 z-30 text-[10px] font-bold text-white bg-purple-600/80 px-1.5 rounded backdrop-blur-sm">FOIL</div>}
|
||||||
|
{displayImage ? (
|
||||||
|
<img src={displayImage} alt={card.name} className="w-full h-full object-cover" />
|
||||||
|
) : (
|
||||||
|
<div className="w-full h-full flex items-center justify-center text-xs text-center p-1 text-slate-500 font-bold border-2 border-slate-700 m-1 rounded">{card.name}</div>
|
||||||
|
)}
|
||||||
|
<div className={`absolute bottom-0 left-0 right-0 h-1.5 ${card.rarity === 'mythic' ? 'bg-gradient-to-r from-orange-500 to-red-600' : card.rarity === 'rare' ? 'bg-gradient-to-r from-yellow-400 to-yellow-600' : card.rarity === 'uncommon' ? 'bg-gradient-to-r from-gray-300 to-gray-500' : 'bg-black'}`} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { socketService } from '../../services/SocketService';
|
|||||||
import { LogOut } from 'lucide-react';
|
import { LogOut } from 'lucide-react';
|
||||||
import { Modal } from '../../components/Modal';
|
import { Modal } from '../../components/Modal';
|
||||||
import { FoilOverlay } from '../../components/CardPreview';
|
import { FoilOverlay } from '../../components/CardPreview';
|
||||||
|
import { useCardTouch } from '../../utils/interaction';
|
||||||
|
|
||||||
// Helper to normalize card data for visuals
|
// Helper to normalize card data for visuals
|
||||||
const normalizeCard = (c: any) => ({
|
const normalizeCard = (c: any) => ({
|
||||||
@@ -247,34 +248,15 @@ export const DraftView: React.FC<DraftViewProps> = ({ draftState, currentPlayerI
|
|||||||
<div className="flex flex-col items-center justify-center min-h-full pb-10">
|
<div className="flex flex-col items-center justify-center min-h-full pb-10">
|
||||||
<h3 className="text-center text-slate-500 uppercase tracking-[0.2em] text-xs font-bold mb-8">Select a Card</h3>
|
<h3 className="text-center text-slate-500 uppercase tracking-[0.2em] text-xs font-bold mb-8">Select a Card</h3>
|
||||||
<div className="flex flex-wrap justify-center gap-6 [perspective:1000px]">
|
<div className="flex flex-wrap justify-center gap-6 [perspective:1000px]">
|
||||||
{activePack.cards.map((rawCard: any) => {
|
{activePack.cards.map((rawCard: any) => (
|
||||||
const card = normalizeCard(rawCard);
|
<DraftCardItem
|
||||||
const isFoil = card.finish === 'foil';
|
key={rawCard.id}
|
||||||
|
rawCard={rawCard}
|
||||||
return (
|
cardScale={cardScale}
|
||||||
<div
|
handlePick={handlePick}
|
||||||
key={card.id}
|
setHoveredCard={setHoveredCard}
|
||||||
className="group relative transition-all duration-300 hover:scale-110 hover:-translate-y-4 hover:z-50 cursor-pointer"
|
/>
|
||||||
style={{ width: `${14 * cardScale}rem` }}
|
))}
|
||||||
onClick={() => handlePick(card.id)}
|
|
||||||
onMouseEnter={() => setHoveredCard(card)}
|
|
||||||
onMouseLeave={() => setHoveredCard(null)}
|
|
||||||
>
|
|
||||||
{/* Foil Glow Effect */}
|
|
||||||
{isFoil && <div className="absolute inset-0 -m-1 rounded-xl bg-purple-500 blur-md opacity-20 group-hover:opacity-60 transition-opacity duration-300 animate-pulse"></div>}
|
|
||||||
|
|
||||||
<div className={`relative w-full rounded-xl shadow-2xl shadow-black overflow-hidden bg-slate-900 ${isFoil ? 'ring-2 ring-purple-400/50' : 'group-hover:ring-2 ring-emerald-400/50'}`}>
|
|
||||||
<img
|
|
||||||
src={card.image}
|
|
||||||
alt={card.name}
|
|
||||||
className="w-full h-full object-cover relative z-10"
|
|
||||||
/>
|
|
||||||
{isFoil && <FoilOverlay />}
|
|
||||||
{isFoil && <div className="absolute top-2 right-2 z-30 text-[10px] font-bold text-white bg-purple-600/80 px-1.5 rounded backdrop-blur-sm border border-white/20">FOIL</div>}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -303,18 +285,7 @@ export const DraftView: React.FC<DraftViewProps> = ({ draftState, currentPlayerI
|
|||||||
</div>
|
</div>
|
||||||
<div className="flex-1 overflow-x-auto flex items-center gap-2 px-6 pb-4 custom-scrollbar">
|
<div className="flex-1 overflow-x-auto flex items-center gap-2 px-6 pb-4 custom-scrollbar">
|
||||||
{pickedCards.map((card: any, idx: number) => (
|
{pickedCards.map((card: any, idx: number) => (
|
||||||
<div
|
<PoolCardItem key={`${card.id}-${idx}`} card={card} setHoveredCard={setHoveredCard} />
|
||||||
key={`${card.id}-${idx}`}
|
|
||||||
className="relative group shrink-0 transition-all h-full flex items-center"
|
|
||||||
onMouseEnter={() => setHoveredCard(card)}
|
|
||||||
onMouseLeave={() => setHoveredCard(null)}
|
|
||||||
>
|
|
||||||
<img
|
|
||||||
src={card.image || card.image_uris?.normal || card.card_faces?.[0]?.image_uris?.normal}
|
|
||||||
alt={card.name}
|
|
||||||
className="h-[90%] w-auto rounded-lg shadow-lg border border-slate-700/50 group-hover:border-emerald-500/50 group-hover:shadow-emerald-500/20 transition-all object-contain"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -331,3 +302,57 @@ export const DraftView: React.FC<DraftViewProps> = ({ draftState, currentPlayerI
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const DraftCardItem = ({ rawCard, cardScale, handlePick, setHoveredCard }: any) => {
|
||||||
|
const card = normalizeCard(rawCard);
|
||||||
|
const isFoil = card.finish === 'foil';
|
||||||
|
const { onTouchStart, onTouchEnd, onTouchMove, onClick } = useCardTouch(setHoveredCard, () => handlePick(card.id), card);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className="group relative transition-all duration-300 hover:scale-110 hover:-translate-y-4 hover:z-50 cursor-pointer"
|
||||||
|
style={{ width: `${14 * cardScale}rem` }}
|
||||||
|
onClick={onClick}
|
||||||
|
onMouseEnter={() => setHoveredCard(card)}
|
||||||
|
onMouseLeave={() => setHoveredCard(null)}
|
||||||
|
onTouchStart={onTouchStart}
|
||||||
|
onTouchEnd={onTouchEnd}
|
||||||
|
onTouchMove={onTouchMove}
|
||||||
|
>
|
||||||
|
{/* Foil Glow Effect */}
|
||||||
|
{isFoil && <div className="absolute inset-0 -m-1 rounded-xl bg-purple-500 blur-md opacity-20 group-hover:opacity-60 transition-opacity duration-300 animate-pulse"></div>}
|
||||||
|
|
||||||
|
<div className={`relative w-full rounded-xl shadow-2xl shadow-black overflow-hidden bg-slate-900 ${isFoil ? 'ring-2 ring-purple-400/50' : 'group-hover:ring-2 ring-emerald-400/50'}`}>
|
||||||
|
<img
|
||||||
|
src={card.image}
|
||||||
|
alt={card.name}
|
||||||
|
className="w-full h-full object-cover relative z-10"
|
||||||
|
/>
|
||||||
|
{isFoil && <FoilOverlay />}
|
||||||
|
{isFoil && <div className="absolute top-2 right-2 z-30 text-[10px] font-bold text-white bg-purple-600/80 px-1.5 rounded backdrop-blur-sm border border-white/20">FOIL</div>}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const PoolCardItem = ({ card, setHoveredCard }: any) => {
|
||||||
|
const { onTouchStart, onTouchEnd, onTouchMove, onClick } = useCardTouch(setHoveredCard, () => { }, card);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className="relative group shrink-0 transition-all h-full flex items-center"
|
||||||
|
onMouseEnter={() => setHoveredCard(card)}
|
||||||
|
onMouseLeave={() => setHoveredCard(null)}
|
||||||
|
onTouchStart={onTouchStart}
|
||||||
|
onTouchEnd={onTouchEnd}
|
||||||
|
onTouchMove={onTouchMove}
|
||||||
|
onClick={onClick}
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
src={card.image || card.image_uris?.normal || card.card_faces?.[0]?.image_uris?.normal}
|
||||||
|
alt={card.name}
|
||||||
|
className="h-[90%] w-auto rounded-lg shadow-lg border border-slate-700/50 group-hover:border-emerald-500/50 group-hover:shadow-emerald-500/20 transition-all object-contain"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
};
|
||||||
|
|||||||
59
src/client/src/utils/interaction.ts
Normal file
59
src/client/src/utils/interaction.ts
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
import { useRef, useCallback } from 'react';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hook to handle touch interactions for cards (Long Press for Preview).
|
||||||
|
* - Tap: Click
|
||||||
|
* - Long Press: Preview (Hover)
|
||||||
|
* - Drag/Scroll: Cancel
|
||||||
|
*/
|
||||||
|
export function useCardTouch(
|
||||||
|
onHover: (card: any | null) => void,
|
||||||
|
onClick: () => void,
|
||||||
|
cardPayload: any
|
||||||
|
) {
|
||||||
|
const timerRef = useRef<any>(null);
|
||||||
|
const isLongPress = useRef(false);
|
||||||
|
|
||||||
|
const handleTouchStart = useCallback(() => {
|
||||||
|
isLongPress.current = false;
|
||||||
|
timerRef.current = setTimeout(() => {
|
||||||
|
isLongPress.current = true;
|
||||||
|
onHover(cardPayload);
|
||||||
|
}, 400); // 400ms threshold
|
||||||
|
}, [onHover, cardPayload]);
|
||||||
|
|
||||||
|
const handleTouchEnd = useCallback((e: React.TouchEvent) => {
|
||||||
|
if (timerRef.current) clearTimeout(timerRef.current);
|
||||||
|
|
||||||
|
if (isLongPress.current) {
|
||||||
|
if (e.cancelable) e.preventDefault();
|
||||||
|
onHover(null); // Clear preview on release, mimicking "hover out"
|
||||||
|
}
|
||||||
|
}, [onHover]);
|
||||||
|
|
||||||
|
const handleTouchMove = useCallback(() => {
|
||||||
|
if (timerRef.current) {
|
||||||
|
clearTimeout(timerRef.current);
|
||||||
|
// If we were already previewing?
|
||||||
|
// If user moves finger while holding, maybe we should effectively cancel the "click" potential too?
|
||||||
|
// Usually moving means scrolling.
|
||||||
|
isLongPress.current = false; // ensure we validly cancel any queued longpress action
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleClick = useCallback((e: React.MouseEvent) => {
|
||||||
|
if (isLongPress.current) {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
onClick();
|
||||||
|
}, [onClick]);
|
||||||
|
|
||||||
|
return {
|
||||||
|
onTouchStart: handleTouchStart,
|
||||||
|
onTouchEnd: handleTouchEnd,
|
||||||
|
onTouchMove: handleTouchMove,
|
||||||
|
onClick: handleClick
|
||||||
|
};
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user