@@ -10,11 +10,22 @@ interface DragData {
1010 cardIndex ?: number ;
1111}
1212
13+ interface DragState {
14+ isDragging : boolean ;
15+ cards : Card [ ] ;
16+ startX : number ;
17+ startY : number ;
18+ currentX : number ;
19+ currentY : number ;
20+ sourceElement : HTMLElement | null ;
21+ }
22+
1323function App ( ) {
1424 const [ gameState , setGameState ] = useState < GameState > ( createInitialState ( ) ) ;
1525 const [ time , setTime ] = useState ( 0 ) ;
1626 const [ dragData , setDragData ] = useState < DragData | null > ( null ) ;
1727 const [ cardImages , setCardImages ] = useState < CardImageMap | null > ( null ) ;
28+ const [ customDrag , setCustomDrag ] = useState < DragState | null > ( null ) ;
1829
1930 // Load card sprites on mount
2031 useEffect ( ( ) => {
@@ -30,6 +41,59 @@ function App() {
3041 return ( ) => clearInterval ( timer ) ;
3142 } , [ gameState . startTime ] ) ;
3243
44+ // Handle custom drag mouse move
45+ useEffect ( ( ) => {
46+ if ( ! customDrag ?. isDragging ) return ;
47+
48+ const handleMouseMove = ( e : MouseEvent ) => {
49+ setCustomDrag ( prev => prev ? {
50+ ...prev ,
51+ currentX : e . clientX ,
52+ currentY : e . clientY
53+ } : null ) ;
54+ } ;
55+
56+ const handleMouseUp = ( e : MouseEvent ) => {
57+ if ( ! customDrag ) return ;
58+
59+ // Reset cursor
60+ document . body . style . cursor = '' ;
61+
62+ // Find the drop target
63+ const target = document . elementFromPoint ( e . clientX , e . clientY ) ;
64+ const dropZone = target ?. closest ( '.tableau-column, .card-pile.foundation' ) ;
65+
66+ if ( dropZone && dragData ) {
67+ // Determine target type and index
68+ if ( dropZone . classList . contains ( 'tableau-column' ) ) {
69+ const columns = Array . from ( document . querySelectorAll ( '.tableau-column' ) ) ;
70+ const targetIndex = columns . indexOf ( dropZone ) ;
71+ if ( targetIndex >= 0 ) {
72+ handleDrop ( 'tableau' , targetIndex ) ;
73+ }
74+ } else if ( dropZone . classList . contains ( 'foundation' ) ) {
75+ const foundations = Array . from ( document . querySelectorAll ( '.card-pile.foundation' ) ) ;
76+ const targetIndex = foundations . indexOf ( dropZone ) ;
77+ if ( targetIndex >= 0 ) {
78+ handleDrop ( 'foundation' , targetIndex ) ;
79+ }
80+ }
81+ }
82+
83+ // Clear custom drag
84+ setCustomDrag ( null ) ;
85+ setDragData ( null ) ;
86+ } ;
87+
88+ window . addEventListener ( 'mousemove' , handleMouseMove ) ;
89+ window . addEventListener ( 'mouseup' , handleMouseUp ) ;
90+
91+ return ( ) => {
92+ window . removeEventListener ( 'mousemove' , handleMouseMove ) ;
93+ window . removeEventListener ( 'mouseup' , handleMouseUp ) ;
94+ } ;
95+ } , [ customDrag , dragData ] ) ;
96+
3397 const handleNewGame = ( ) => {
3498 setGameState ( createInitialState ( ) ) ;
3599 setTime ( 0 ) ;
@@ -44,6 +108,39 @@ function App() {
44108 setDragData ( { source, index, cardIndex } ) ;
45109 } ;
46110
111+ const handleCustomDragStart = (
112+ e : React . MouseEvent ,
113+ source : 'waste' | 'tableau' ,
114+ index : number ,
115+ cardIndex ?: number ,
116+ cards ?: Card [ ]
117+ ) => {
118+ e . preventDefault ( ) ;
119+
120+ if ( ! cards || cards . length === 0 ) return ;
121+
122+ // Get the position of the clicked card element
123+ const rect = ( e . currentTarget as HTMLElement ) . getBoundingClientRect ( ) ;
124+
125+ // Calculate offset from card's top-left corner to mouse position
126+ const offsetX = e . clientX - rect . left ;
127+ const offsetY = e . clientY - rect . top ;
128+
129+ setDragData ( { source, index, cardIndex } ) ;
130+ setCustomDrag ( {
131+ isDragging : true ,
132+ cards : cards ,
133+ startX : offsetX , // Store offset within the card
134+ startY : offsetY ,
135+ currentX : e . clientX ,
136+ currentY : e . clientY ,
137+ sourceElement : e . currentTarget as HTMLElement
138+ } ) ;
139+
140+ // Set cursor to grabbing
141+ document . body . style . cursor = 'grabbing' ;
142+ } ;
143+
47144 const handleDrop = ( target : 'tableau' | 'foundation' , targetIndex : number ) => {
48145 if ( ! dragData ) return ;
49146
@@ -91,7 +188,9 @@ function App() {
91188 draggable ?: boolean ,
92189 onDragStart ?: ( ) => void ,
93190 key ?: string | number ,
94- onDoubleClick ?: ( ) => void
191+ onDoubleClick ?: ( ) => void ,
192+ column ?: Card [ ] , // Optional: full column for stack preview
193+ cardIndex ?: number // Optional: card index in column
95194 ) => {
96195 if ( ! cardImages ) {
97196 // Loading fallback - show empty card
@@ -104,23 +203,19 @@ function App() {
104203 return < div key = { key } className = { className } style = { style } > </ div > ;
105204 }
106205
107- const handleDragStart = ( e : React . DragEvent < HTMLDivElement > ) => {
108- // Make the drag preview fully visible
109- if ( e . dataTransfer ) {
110- const dragImage = e . currentTarget . cloneNode ( true ) as HTMLElement ;
111- dragImage . style . opacity = '1' ;
112- document . body . appendChild ( dragImage ) ;
113- e . dataTransfer . setDragImage ( dragImage , e . currentTarget . offsetWidth / 2 , e . currentTarget . offsetHeight / 2 ) ;
114- setTimeout ( ( ) => document . body . removeChild ( dragImage ) , 0 ) ;
206+ const handleMouseDown = ( e : React . MouseEvent ) => {
207+ if ( ! draggable ) return ;
208+
209+ // Get the cards to drag (current card and all below it in the column)
210+ let cardsToMove : Card [ ] = [ card ] ;
211+ if ( column && cardIndex !== undefined && cardIndex < column . length - 1 ) {
212+ cardsToMove = column . slice ( cardIndex ) ;
115213 }
116- // Make the original card invisible
117- e . currentTarget . style . opacity = '0' ;
118- if ( onDragStart ) onDragStart ( ) ;
119- } ;
120214
121- const handleDragEnd = ( e : React . DragEvent < HTMLDivElement > ) => {
122- // Restore the original card visibility
123- e . currentTarget . style . opacity = '1' ;
215+ // Start custom drag
216+ handleCustomDragStart ( e , 'tableau' , 0 , cardIndex , cardsToMove ) ;
217+
218+ if ( onDragStart ) onDragStart ( ) ;
124219 } ;
125220
126221 const handleClick = ( e : React . MouseEvent < HTMLDivElement > ) => {
@@ -137,15 +232,22 @@ function App() {
137232 }
138233 } ;
139234
235+ // Check if this card is being dragged
236+ const isBeingDragged = customDrag ?. isDragging && customDrag . cards . some ( c =>
237+ c . suit === card . suit && c . rank === card . rank
238+ ) ;
239+
140240 return (
141241 < div
142242 key = { key }
143243 className = { className }
144244 data-suit = { card . suit }
145- style = { style }
146- draggable = { draggable }
147- onDragStart = { draggable ? handleDragStart : undefined }
148- onDragEnd = { draggable ? handleDragEnd : undefined }
245+ style = { {
246+ ...style ,
247+ opacity : isBeingDragged ? 0 : 1 , // Fully hide the original cards during drag
248+ cursor : draggable ? 'grab' : 'default'
249+ } }
250+ onMouseDown = { draggable ? handleMouseDown : undefined }
149251 onDoubleClick = { draggable ? handleClick : undefined }
150252 >
151253 < img src = { imageUrl } alt = { `${ card . rank } of ${ card . suit } ` } draggable = { false } />
@@ -190,19 +292,22 @@ function App() {
190292 </ div >
191293
192294 < div className = "card-pile waste" >
193- { gameState . waste . length > 0 && ( ( ) => {
194- const card = gameState . waste [ gameState . waste . length - 1 ] ! ;
195- const canAutoMove = canPlaceOnFoundation ( card , gameState . foundations [ card . suit ] ) ;
196- return renderCard (
197- card ,
198- 'card' ,
199- undefined ,
200- true ,
201- ( ) => handleDragStart ( 'waste' , 0 ) ,
202- undefined ,
203- canAutoMove ? ( ) => handleAutoMoveToFoundation ( 'waste' , 0 ) : undefined
204- ) ;
205- } ) ( ) }
295+ { gameState . waste . length > 0 && (
296+ // Render all cards in the waste pile (stacked with no offset)
297+ gameState . waste . map ( ( card , cardIdx ) => {
298+ const isTopCard = cardIdx === gameState . waste . length - 1 ;
299+ const canAutoMove = isTopCard && canPlaceOnFoundation ( card , gameState . foundations [ card . suit ] ) ;
300+ return renderCard (
301+ card ,
302+ 'card' ,
303+ undefined , // No offset - cards are perfectly stacked
304+ isTopCard , // Only top card is draggable
305+ isTopCard ? ( ) => handleDragStart ( 'waste' , 0 ) : undefined ,
306+ cardIdx ,
307+ canAutoMove ? ( ) => handleAutoMoveToFoundation ( 'waste' , 0 ) : undefined
308+ ) ;
309+ } )
310+ ) }
206311 </ div >
207312
208313 < div className = "spacer" > </ div >
@@ -214,10 +319,21 @@ function App() {
214319 onDragOver = { ( e ) => e . preventDefault ( ) }
215320 onDrop = { ( ) => handleDrop ( 'foundation' , idx ) }
216321 >
217- { gameState . foundations [ suit ] . length > 0 ? ( ( ) => {
218- const card = gameState . foundations [ suit ] [ gameState . foundations [ suit ] . length - 1 ] ! ;
219- return renderCard ( card ) ;
220- } ) ( ) : (
322+ { gameState . foundations [ suit ] . length > 0 ? (
323+ // Render all cards in the foundation stack
324+ gameState . foundations [ suit ] . map ( ( card , cardIdx ) => {
325+ // Only top card should be draggable from foundation (but we disable dragging in Klondike)
326+ return renderCard (
327+ card ,
328+ 'card' ,
329+ { top : `${ Math . min ( cardIdx * 2 , 20 ) } px` } , // Slight offset, max 20px
330+ false , // Foundations are not draggable in Klondike
331+ undefined ,
332+ cardIdx ,
333+ undefined
334+ ) ;
335+ } )
336+ ) : (
221337 < div className = "pile-placeholder" > { [ '♠' , '♥' , '♦' , '♣' ] [ idx ] } </ div >
222338 ) }
223339 </ div >
@@ -251,14 +367,51 @@ function App() {
251367 true ,
252368 ( ) => handleDragStart ( 'tableau' , colIdx , cardIdx ) ,
253369 cardIdx ,
254- canAutoMove ? ( ) => handleAutoMoveToFoundation ( 'tableau' , colIdx ) : undefined
370+ canAutoMove ? ( ) => handleAutoMoveToFoundation ( 'tableau' , colIdx ) : undefined ,
371+ column , // Pass the full column
372+ cardIdx // Pass the card index
255373 ) ;
256374 } )
257375 ) }
258376 </ div >
259377 ) ) }
260378 </ div >
261379 </ div >
380+
381+ { /* Custom drag overlay */ }
382+ { customDrag && customDrag . isDragging && (
383+ < div
384+ style = { {
385+ position : 'fixed' ,
386+ left : customDrag . currentX - customDrag . startX , // Subtract the offset
387+ top : customDrag . currentY - customDrag . startY ,
388+ pointerEvents : 'none' ,
389+ zIndex : 10000 ,
390+ cursor : 'grabbing'
391+ } }
392+ >
393+ { customDrag . cards . map ( ( card , idx ) => (
394+ < div
395+ key = { `${ card . suit } -${ card . rank } -drag` }
396+ className = "card"
397+ style = { {
398+ position : 'absolute' ,
399+ top : `${ idx * 25 } px` ,
400+ left : 0 ,
401+ width : 'calc(min(100vw, 100vh) * 0.095)' ,
402+ height : 'calc(min(100vw, 100vh) * 0.134)'
403+ } }
404+ >
405+ < img
406+ src = { cardImages ?. get ( card . suit , card . rank ) }
407+ alt = { `${ card . rank } of ${ card . suit } ` }
408+ draggable = { false }
409+ style = { { width : '100%' , height : '100%' , objectFit : 'contain' } }
410+ />
411+ </ div >
412+ ) ) }
413+ </ div >
414+ ) }
262415 </ div >
263416 ) ;
264417}
0 commit comments