1+ import React , { useState } from 'react' ;
2+ import { Player } from './types' ;
3+ import CheckerPiece from './piece' ;
4+
5+ export type PointDirection = 'up' | 'down' | 'left' | 'right' ;
6+ export type CheckerStackDirection = 'vertical-base' | 'vertical-tip' | 'horizontal-base' | 'horizontal-tip' ;
7+
8+ interface PointProps {
9+ pointValue : number ;
10+ pointIndex : number ;
11+ pointDirection : PointDirection ;
12+ checkerStackDirection : CheckerStackDirection ;
13+ isLandscape : boolean ;
14+ colorIndex : number ;
15+ onClick : ( ) => void ;
16+ onDrop : ( e : React . DragEvent < HTMLDivElement > ) => void ;
17+ isHighlighted : boolean ; // Is this point a possible destination?
18+ isSelectedSource : boolean ; // Is this point the selected source?
19+ isTouchDragHovering ?: boolean ; // Is this the current touch hover target?
20+ canDragFrom : boolean ;
21+ onCheckerDragStart : ( player : Player , sourceIndex : number ) => void ; // Mouse drag
22+ onTouchStartDraggable : ( player : Player , sourceIndex : number ) => void ; // Touch drag
23+ }
24+
25+ const Point : React . FC < PointProps > = ( {
26+ pointValue,
27+ pointIndex,
28+ pointDirection,
29+ checkerStackDirection,
30+ isLandscape,
31+ colorIndex,
32+ onClick,
33+ onDrop,
34+ isHighlighted,
35+ isSelectedSource,
36+ isTouchDragHovering,
37+ canDragFrom,
38+ onCheckerDragStart,
39+ onTouchStartDraggable
40+ } ) => {
41+ const [ isMouseDragHovering , setIsMouseDragHovering ] = useState ( false ) ;
42+
43+ const numCheckers = Math . abs ( pointValue ) ;
44+ const playerOnPoint : Player | null = pointValue > 0 ? Player . Player1 : ( pointValue < 0 ? Player . Player2 : null ) ;
45+
46+ const checkersToRenderCount = numCheckers ;
47+
48+ let pointContainerClasses = 'point-container ' ;
49+
50+ if ( isLandscape ) {
51+ pointContainerClasses += pointDirection === 'up' ? 'point-shape-up ' : 'point-shape-down ' ;
52+ } else {
53+ pointContainerClasses += pointDirection === 'left' ? 'point-shape-left ' : 'point-shape-right ' ;
54+ }
55+
56+ const showDragOverHighlight = ( isMouseDragHovering || isTouchDragHovering ) && isHighlighted ;
57+
58+ if ( showDragOverHighlight ) {
59+ pointContainerClasses += 'point-color-highlight-hover ' ;
60+ } else if ( isSelectedSource ) {
61+ pointContainerClasses += 'point-color-selected-source ' ;
62+ } else if ( isHighlighted ) {
63+ pointContainerClasses += 'point-color-highlight-move ' ;
64+ } else {
65+ pointContainerClasses += colorIndex % 2 === 0 ? 'point-color-default-0 ' : 'point-color-default-1 ' ;
66+ }
67+
68+ let checkerLayoutClasses = 'absolute flex z-10 overflow-hidden' ;
69+ const checkerSpacing = 'space-x-0.5 md:space-x-1' ;
70+ const verticalCheckerSpacing = 'space-y-0.5 md:space-y-1' ;
71+
72+ if ( isLandscape ) {
73+ checkerLayoutClasses += ` w-full h-[85%] items-center ${ verticalCheckerSpacing } ` ;
74+ if ( pointDirection === 'up' ) {
75+ checkerLayoutClasses += ' bottom-[5%] flex-col-reverse justify-start' ;
76+ } else {
77+ checkerLayoutClasses += ' top-[5%] flex-col justify-start' ;
78+ }
79+ checkerLayoutClasses += ' left-1/2 -translate-x-1/2' ;
80+ } else {
81+ checkerLayoutClasses += ` h-full w-[85%] items-center ${ checkerSpacing } ` ;
82+ if ( pointDirection === 'left' ) {
83+ checkerLayoutClasses += ' right-[5%] flex-row-reverse justify-start' ;
84+ } else {
85+ checkerLayoutClasses += ' left-[5%] flex-row justify-start' ;
86+ }
87+ checkerLayoutClasses += ' top-1/2 -translate-y-1/2' ;
88+ }
89+
90+ const handleMouseDragStartPoint = ( e : React . DragEvent < HTMLDivElement > ) => {
91+ if ( canDragFrom && playerOnPoint ) {
92+ onCheckerDragStart ( playerOnPoint , pointIndex ) ;
93+ e . dataTransfer . setData ( "text/plain" , JSON . stringify ( { type : 'point_checker' , sourceIndex : pointIndex , player : playerOnPoint } ) ) ;
94+ e . dataTransfer . effectAllowed = "move" ;
95+
96+ const checkerVisualEl = e . currentTarget . querySelector ( '.checker-piece-visual' ) as HTMLElement ;
97+ if ( checkerVisualEl ) {
98+ const xOffset = checkerVisualEl . offsetWidth / 2 ;
99+ const yOffset = checkerVisualEl . offsetHeight / 2 ;
100+ e . dataTransfer . setDragImage ( checkerVisualEl , xOffset , yOffset ) ;
101+ }
102+ } else {
103+ e . preventDefault ( ) ;
104+ }
105+ } ;
106+
107+ const handleTouchStartPoint = ( e : React . TouchEvent < HTMLDivElement > ) => {
108+ if ( canDragFrom && playerOnPoint ) {
109+ e . preventDefault ( ) ; // Prevent mouse event emulation, scrolling, and other default gestures.
110+ onTouchStartDraggable ( playerOnPoint , pointIndex ) ;
111+ }
112+ } ;
113+
114+
115+ const getCheckerKey = ( idx : number ) => `checker-${ pointIndex } -${ idx } ` ;
116+
117+ const handleDragOverPoint = ( e : React . DragEvent < HTMLDivElement > ) => {
118+ if ( isHighlighted ) {
119+ e . preventDefault ( ) ;
120+ e . dataTransfer . dropEffect = "move" ;
121+ }
122+ } ;
123+
124+ const handleDragEnterPoint = ( e : React . DragEvent < HTMLDivElement > ) => {
125+ if ( isHighlighted ) {
126+ setIsMouseDragHovering ( true ) ;
127+ e . preventDefault ( ) ;
128+ }
129+ } ;
130+
131+ const handleDragLeavePoint = ( e : React . DragEvent < HTMLDivElement > ) => {
132+ setIsMouseDragHovering ( false ) ;
133+ } ;
134+
135+ return (
136+ < div
137+ className = { `${ pointContainerClasses . trim ( ) } cursor-pointer` }
138+ onClick = { onClick }
139+ onDrop = { ( e ) => { e . stopPropagation ( ) ; onDrop ( e ) ; setIsMouseDragHovering ( false ) ; } }
140+ onDragOver = { handleDragOverPoint }
141+ onDragEnter = { handleDragEnterPoint }
142+ onDragLeave = { handleDragLeavePoint }
143+ draggable = { canDragFrom && playerOnPoint != null } // For mouse drag
144+ onDragStart = { handleMouseDragStartPoint } // For mouse drag
145+ onTouchStart = { handleTouchStartPoint } // For touch drag
146+ aria-label = { `Point ${ pointIndex + 1 } , Checkers: ${ numCheckers } , Player: ${ playerOnPoint || 'None' } ` }
147+ >
148+ { playerOnPoint && (
149+ < div className = { checkerLayoutClasses } >
150+ { Array . from ( { length : checkersToRenderCount } ) . map ( ( _ , idx ) => (
151+ < div
152+ key = { getCheckerKey ( idx ) }
153+ className = "flex-shrink-0"
154+ >
155+ < CheckerPiece
156+ player = { playerOnPoint }
157+ small = { numCheckers > 1 }
158+ // No direct touch handlers on checker piece within point, point handles it.
159+ />
160+ </ div >
161+ ) ) }
162+ </ div >
163+ ) }
164+ </ div >
165+ ) ;
166+ } ;
167+
168+ export default Point ;
0 commit comments