2025-07-21 14:53:06 -04:00
|
|
|
|
import React, { useState } from 'react';
|
|
|
|
|
|
import { use3DTilt } from '../hooks/use3DTilt';
|
|
|
|
|
|
|
|
|
|
|
|
interface CardImageDisplayProps {
|
|
|
|
|
|
card: {
|
2025-07-22 20:51:38 -04:00
|
|
|
|
id: string;
|
2025-07-21 14:53:06 -04:00
|
|
|
|
name: string;
|
|
|
|
|
|
game: string;
|
|
|
|
|
|
stock_image_url?: string;
|
|
|
|
|
|
image_url?: string;
|
|
|
|
|
|
rarity: string;
|
|
|
|
|
|
};
|
|
|
|
|
|
userImages?: string[];
|
|
|
|
|
|
size?: 'small' | 'medium' | 'large';
|
|
|
|
|
|
showUserPhotos?: boolean;
|
|
|
|
|
|
className?: string;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const CardImageDisplay: React.FC<CardImageDisplayProps> = ({
|
|
|
|
|
|
card,
|
|
|
|
|
|
userImages = [],
|
|
|
|
|
|
size = 'medium',
|
|
|
|
|
|
showUserPhotos: initialShowUserPhotos = false,
|
|
|
|
|
|
className = ''
|
|
|
|
|
|
}) => {
|
|
|
|
|
|
const [imageError, setImageError] = useState(false);
|
|
|
|
|
|
const [currentUserImageIndex, setCurrentUserImageIndex] = useState(0);
|
|
|
|
|
|
const [showUserPhotos, setShowUserPhotos] = useState(initialShowUserPhotos);
|
|
|
|
|
|
|
|
|
|
|
|
// 3D Tilt effect (no scaling - main container handles expansion)
|
|
|
|
|
|
const { ref: tiltRef, tiltStyles } = use3DTilt({
|
|
|
|
|
|
maxTilt: size === 'large' ? 15 : 10,
|
|
|
|
|
|
scale: 1.0, // No image scaling - main container expands
|
|
|
|
|
|
speed: 400,
|
|
|
|
|
|
easing: 'cubic-bezier(0.23, 1, 0.320, 1)'
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// Determine which image to show
|
|
|
|
|
|
const getDisplayImage = () => {
|
|
|
|
|
|
if (showUserPhotos && userImages.length > 0) {
|
|
|
|
|
|
return userImages[currentUserImageIndex];
|
|
|
|
|
|
}
|
|
|
|
|
|
return card.stock_image_url || card.image_url;
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// Determine if card should have foil effects
|
|
|
|
|
|
const isFoilCard = () => {
|
|
|
|
|
|
const foilRarities = ['super rare', 'legendary', 'mythic'];
|
|
|
|
|
|
return foilRarities.includes(card.rarity.toLowerCase());
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// Get rarity border class
|
|
|
|
|
|
const getRarityBorderClass = () => {
|
|
|
|
|
|
const rarity = card.rarity.toLowerCase().replace(/\s+/g, '');
|
|
|
|
|
|
return `rarity-border-${rarity}`;
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// Size classes
|
|
|
|
|
|
const sizeClasses = {
|
|
|
|
|
|
small: 'w-16 h-22',
|
|
|
|
|
|
medium: 'w-24 h-32',
|
|
|
|
|
|
large: 'w-48 h-64'
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// Placeholder based on game
|
|
|
|
|
|
const getPlaceholder = () => {
|
|
|
|
|
|
const gameColors = {
|
|
|
|
|
|
MTG: 'from-orange-400 to-red-500',
|
|
|
|
|
|
POKEMON: 'from-yellow-400 to-red-500',
|
|
|
|
|
|
LORCANA: 'from-purple-400 to-pink-500'
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const gradientClass = gameColors[card.game as keyof typeof gameColors] || 'from-gray-400 to-gray-600';
|
|
|
|
|
|
const rarityBorderClass = getRarityBorderClass();
|
|
|
|
|
|
const foilClass = isFoilCard() ? 'foil-rainbow' : '';
|
|
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
|
<div
|
|
|
|
|
|
ref={tiltRef}
|
|
|
|
|
|
style={tiltStyles}
|
|
|
|
|
|
className={`${sizeClasses[size]} bg-gradient-to-br ${gradientClass} rounded-lg flex flex-col items-center justify-center text-white shadow-md card-transition card-depth ${rarityBorderClass} ${foilClass} ${className}`}
|
|
|
|
|
|
>
|
|
|
|
|
|
<div className="text-lg font-bold mb-1">🃏</div>
|
|
|
|
|
|
<div className="text-xs text-center px-2 leading-tight">
|
|
|
|
|
|
{card.name.split(' ').slice(0, 2).join(' ')}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div className="text-xs opacity-75 mt-1">
|
|
|
|
|
|
{card.game}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
);
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const displayImage = getDisplayImage();
|
|
|
|
|
|
|
|
|
|
|
|
if (!displayImage || imageError) {
|
|
|
|
|
|
return getPlaceholder();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const rarityBorderClass = getRarityBorderClass();
|
|
|
|
|
|
const foilClass = isFoilCard() ? 'foil-card' : '';
|
|
|
|
|
|
const legendaryHoloClass = card.rarity.toLowerCase() === 'legendary' || card.rarity.toLowerCase() === 'mythic' ? 'holographic' : '';
|
|
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
|
<div className={`relative ${className}`}>
|
|
|
|
|
|
<div
|
|
|
|
|
|
ref={tiltRef}
|
|
|
|
|
|
style={tiltStyles}
|
|
|
|
|
|
className={`card-transition card-depth ${rarityBorderClass} ${foilClass} ${legendaryHoloClass} rounded-lg`}
|
|
|
|
|
|
>
|
|
|
|
|
|
<img
|
|
|
|
|
|
src={displayImage}
|
|
|
|
|
|
alt={card.name}
|
|
|
|
|
|
className={`${sizeClasses[size]} object-cover rounded-lg`}
|
|
|
|
|
|
onError={() => setImageError(true)}
|
|
|
|
|
|
/>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
{/* User photo indicator */}
|
|
|
|
|
|
{showUserPhotos && userImages.length > 0 && (
|
|
|
|
|
|
<div className="absolute top-1 left-1 bg-blue-500 text-white text-xs px-1 py-0.5 rounded">
|
|
|
|
|
|
📸 {currentUserImageIndex + 1}/{userImages.length}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
|
|
{/* Stock/User toggle */}
|
|
|
|
|
|
{userImages.length > 0 && (
|
|
|
|
|
|
<div className="absolute bottom-1 right-1">
|
|
|
|
|
|
<button
|
|
|
|
|
|
onClick={() => setShowUserPhotos(!showUserPhotos)}
|
|
|
|
|
|
className="bg-black bg-opacity-50 text-white text-xs px-1 py-0.5 rounded hover:bg-opacity-75"
|
|
|
|
|
|
title={showUserPhotos ? 'Show stock image' : 'Show your photos'}
|
|
|
|
|
|
>
|
|
|
|
|
|
{showUserPhotos ? '📋' : '📸'}
|
|
|
|
|
|
</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
|
|
{/* User photo navigation */}
|
|
|
|
|
|
{showUserPhotos && userImages.length > 1 && (
|
|
|
|
|
|
<div className="absolute bottom-1 left-1 flex gap-1">
|
|
|
|
|
|
<button
|
|
|
|
|
|
onClick={() => setCurrentUserImageIndex((prev) => Math.max(0, prev - 1))}
|
|
|
|
|
|
disabled={currentUserImageIndex === 0}
|
|
|
|
|
|
className="bg-black bg-opacity-50 text-white text-xs px-1 py-0.5 rounded disabled:opacity-50"
|
|
|
|
|
|
>
|
|
|
|
|
|
‹
|
|
|
|
|
|
</button>
|
|
|
|
|
|
<button
|
|
|
|
|
|
onClick={() => setCurrentUserImageIndex((prev) => Math.min(userImages.length - 1, prev + 1))}
|
|
|
|
|
|
disabled={currentUserImageIndex === userImages.length - 1}
|
|
|
|
|
|
className="bg-black bg-opacity-50 text-white text-xs px-1 py-0.5 rounded disabled:opacity-50"
|
|
|
|
|
|
>
|
|
|
|
|
|
›
|
|
|
|
|
|
</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
|
|
{/* Image type indicator */}
|
|
|
|
|
|
<div className="absolute top-1 right-1">
|
|
|
|
|
|
{showUserPhotos ? (
|
|
|
|
|
|
<span className="bg-blue-500 text-white text-xs px-1 py-0.5 rounded" title="Your card photo">
|
|
|
|
|
|
👤
|
|
|
|
|
|
</span>
|
|
|
|
|
|
) : (
|
|
|
|
|
|
<span className="bg-green-500 text-white text-xs px-1 py-0.5 rounded" title="Stock image">
|
|
|
|
|
|
✓
|
|
|
|
|
|
</span>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
);
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
export default CardImageDisplay;
|