1+ import * as React from "react" ;
2+ import { cn } from "../../utils/cn" ;
3+
4+ export interface TradingCardProps extends React . HTMLAttributes < HTMLDivElement > {
5+ /**
6+ * The image URL to display in the card
7+ */
8+ imageUrl : string ;
9+
10+ /**
11+ * The title of the card
12+ */
13+ title : string ;
14+
15+ /**
16+ * The subtitle or type of the card
17+ */
18+ subtitle ?: string ;
19+
20+ /**
21+ * The main attribute value (like HP in Pokémon cards)
22+ */
23+ attributeValue ?: string | number ;
24+
25+ /**
26+ * The attribute label (like "HP")
27+ */
28+ attributeLabel ?: string ;
29+
30+ /**
31+ * The main content or description
32+ */
33+ description ?: string ;
34+
35+ /**
36+ * Optional footer text
37+ */
38+ footer ?: string ;
39+
40+ /**
41+ * Card border color
42+ */
43+ borderColor ?: string ;
44+
45+ /**
46+ * Card background color
47+ */
48+ backgroundColor ?: string ;
49+
50+ /**
51+ * Whether to enable 3D perspective effect
52+ */
53+ enable3D ?: boolean ;
54+
55+ /**
56+ * Maximum rotation angle in degrees
57+ */
58+ maxRotation ?: number ;
59+
60+ /**
61+ * Glare effect intensity (0-1)
62+ */
63+ glareIntensity ?: number ;
64+ }
65+
66+ export const TradingCard = React . forwardRef < HTMLDivElement , TradingCardProps > (
67+ ( {
68+ className,
69+ imageUrl,
70+ title,
71+ subtitle,
72+ attributeValue,
73+ attributeLabel,
74+ description,
75+ footer,
76+ borderColor = "#4299e1" , // Default blue color
77+ backgroundColor = "#ebf8ff" , // Light blue background
78+ enable3D = true ,
79+ maxRotation = 10 ,
80+ glareIntensity = 0.25 ,
81+ children,
82+ ...props
83+ } , ref ) => {
84+ const cardRef = React . useRef < HTMLDivElement > ( null ) ;
85+ const glareRef = React . useRef < HTMLDivElement > ( null ) ;
86+
87+ // State to track if the card is being touched/clicked
88+ const [ isActive , setIsActive ] = React . useState ( false ) ;
89+
90+ // Handle mouse/touch movement for 3D effect
91+ const handleMovement = React . useCallback (
92+ ( e : React . MouseEvent | React . TouchEvent | MouseEvent | TouchEvent ) => {
93+ if ( ! enable3D || ! cardRef . current ) return ;
94+
95+ // Get card dimensions and position
96+ const card = cardRef . current ;
97+ const rect = card . getBoundingClientRect ( ) ;
98+ const cardWidth = rect . width ;
99+ const cardHeight = rect . height ;
100+ const cardCenterX = rect . left + cardWidth / 2 ;
101+ const cardCenterY = rect . top + cardHeight / 2 ;
102+
103+ // Get cursor/touch position
104+ let clientX : number , clientY : number ;
105+
106+ if ( 'touches' in e ) {
107+ // Touch event
108+ clientX = e . touches [ 0 ] . clientX ;
109+ clientY = e . touches [ 0 ] . clientY ;
110+ } else {
111+ // Mouse event
112+ clientX = ( e as MouseEvent ) . clientX ;
113+ clientY = ( e as MouseEvent ) . clientY ;
114+ }
115+
116+ // Calculate rotation based on cursor position relative to card center
117+ const rotateY = ( ( clientX - cardCenterX ) / ( cardWidth / 2 ) ) * maxRotation ;
118+ const rotateX = - ( ( clientY - cardCenterY ) / ( cardHeight / 2 ) ) * maxRotation ;
119+
120+ // Apply rotation transform
121+ card . style . transform = `perspective(1000px) rotateX(${ rotateX } deg) rotateY(${ rotateY } deg)` ;
122+
123+ // Update glare effect if enabled
124+ if ( glareRef . current && glareIntensity > 0 ) {
125+ const glareX = ( ( clientX - cardCenterX ) / cardWidth ) * 100 + 50 ;
126+ const glareY = ( ( clientY - cardCenterY ) / cardHeight ) * 100 + 50 ;
127+
128+ glareRef . current . style . background = `radial-gradient(
129+ circle at ${ glareX } % ${ glareY } %,
130+ rgba(255, 255, 255, ${ glareIntensity } ),
131+ transparent 50%
132+ )` ;
133+ }
134+ } ,
135+ [ enable3D , maxRotation , glareIntensity ]
136+ ) ;
137+
138+ // Reset card position when mouse leaves or touch ends
139+ const resetCardPosition = React . useCallback ( ( ) => {
140+ if ( ! enable3D || ! cardRef . current ) return ;
141+
142+ setIsActive ( false ) ;
143+
144+ // Smoothly animate back to original position
145+ cardRef . current . style . transition = 'transform 0.5s ease-out' ;
146+ cardRef . current . style . transform = 'perspective(1000px) rotateX(0deg) rotateY(0deg)' ;
147+
148+ // Reset glare effect
149+ if ( glareRef . current ) {
150+ glareRef . current . style . background = 'transparent' ;
151+ }
152+
153+ // Remove transition after animation completes
154+ setTimeout ( ( ) => {
155+ if ( cardRef . current ) {
156+ cardRef . current . style . transition = '' ;
157+ }
158+ } , 500 ) ;
159+ } , [ enable3D ] ) ;
160+
161+ // Set up event listeners
162+ React . useEffect ( ( ) => {
163+ if ( ! enable3D ) return ;
164+
165+ const card = cardRef . current ;
166+ if ( ! card ) return ;
167+
168+ // Mouse events
169+ const handleMouseMove = ( e : MouseEvent ) => handleMovement ( e ) ;
170+ const handleMouseLeave = ( ) => resetCardPosition ( ) ;
171+ const handleMouseDown = ( ) => setIsActive ( true ) ;
172+ const handleMouseUp = ( ) => setIsActive ( false ) ;
173+
174+ // Touch events
175+ const handleTouchMove = ( e : TouchEvent ) => {
176+ e . preventDefault ( ) ;
177+ handleMovement ( e ) ;
178+ } ;
179+ const handleTouchEnd = ( ) => resetCardPosition ( ) ;
180+ const handleTouchStart = ( ) => setIsActive ( true ) ;
181+
182+ // Add event listeners
183+ card . addEventListener ( 'mousemove' , handleMouseMove ) ;
184+ card . addEventListener ( 'mouseleave' , handleMouseLeave ) ;
185+ card . addEventListener ( 'mousedown' , handleMouseDown ) ;
186+ card . addEventListener ( 'mouseup' , handleMouseUp ) ;
187+ card . addEventListener ( 'touchmove' , handleTouchMove , { passive : false } ) ;
188+ card . addEventListener ( 'touchend' , handleTouchEnd ) ;
189+ card . addEventListener ( 'touchstart' , handleTouchStart ) ;
190+
191+ // Clean up
192+ return ( ) => {
193+ card . removeEventListener ( 'mousemove' , handleMouseMove ) ;
194+ card . removeEventListener ( 'mouseleave' , handleMouseLeave ) ;
195+ card . removeEventListener ( 'mousedown' , handleMouseDown ) ;
196+ card . removeEventListener ( 'mouseup' , handleMouseUp ) ;
197+ card . removeEventListener ( 'touchmove' , handleTouchMove ) ;
198+ card . removeEventListener ( 'touchend' , handleTouchEnd ) ;
199+ card . removeEventListener ( 'touchstart' , handleTouchStart ) ;
200+ } ;
201+ } , [ enable3D , handleMovement , resetCardPosition ] ) ;
202+
203+ return (
204+ < div
205+ ref = { ( node ) => {
206+ if ( typeof ref === 'function' ) {
207+ ref ( node ) ;
208+ } else if ( ref ) {
209+ ref . current = node ;
210+ }
211+ ( cardRef as React . MutableRefObject < HTMLDivElement | null > ) . current = node ;
212+ } }
213+ className = { cn (
214+ "relative rounded-xl overflow-hidden" ,
215+ "w-72 h-[400px]" , // Default size similar to trading cards
216+ "transition-shadow duration-300" ,
217+ "select-none" , // Prevent text selection during drag
218+ isActive ? "cursor-grabbing" : "cursor-grab" ,
219+ className
220+ ) }
221+ style = { {
222+ backgroundColor : backgroundColor ,
223+ boxShadow : `0 10px 30px -5px rgba(0, 0, 0, 0.3)` ,
224+ border : `8px solid ${ borderColor } ` ,
225+ transformStyle : 'preserve-3d' ,
226+ } }
227+ { ...props }
228+ >
229+ { /* Glare effect overlay */ }
230+ < div
231+ ref = { glareRef }
232+ className = "absolute inset-0 z-20 pointer-events-none"
233+ style = { {
234+ borderRadius : 'inherit' ,
235+ } }
236+ />
237+
238+ { /* Card header with title and attribute */ }
239+ < div className = "flex justify-between items-center p-3 border-b border-gray-200 bg-white/90" >
240+ < div >
241+ < h3 className = "font-bold text-lg" > { title } </ h3 >
242+ { subtitle && < p className = "text-xs text-gray-500" > { subtitle } </ p > }
243+ </ div >
244+ { attributeValue && (
245+ < div className = "flex items-center" >
246+ < span className = "font-bold text-xl" > { attributeValue } </ span >
247+ { attributeLabel && < span className = "text-xs ml-1" > { attributeLabel } </ span > }
248+ </ div >
249+ ) }
250+ </ div >
251+
252+ { /* Card image */ }
253+ < div
254+ className = "h-40 bg-cover bg-center"
255+ style = { {
256+ backgroundImage : `url(${ imageUrl } )` ,
257+ transform : 'translateZ(20px)' , // 3D effect for the image
258+ transformStyle : 'preserve-3d' ,
259+ } }
260+ />
261+
262+ { /* Card content */ }
263+ < div className = "p-4 bg-white/90" >
264+ { description && (
265+ < p className = "text-sm" > { description } </ p >
266+ ) }
267+ { children }
268+ </ div >
269+
270+ { /* Card footer */ }
271+ { footer && (
272+ < div className = "absolute bottom-0 left-0 right-0 p-2 text-xs text-center bg-gray-100 border-t border-gray-200" >
273+ { footer }
274+ </ div >
275+ ) }
276+ </ div >
277+ ) ;
278+ }
279+ ) ;
280+
281+ TradingCard . displayName = "TradingCard" ;
0 commit comments