175 lines
No EOL
5.6 KiB
TypeScript
175 lines
No EOL
5.6 KiB
TypeScript
import React, { useState } from 'react';
|
||
import { use3DTilt } from '../hooks/use3DTilt';
|
||
|
||
interface CardImageDisplayProps {
|
||
card: {
|
||
id: string;
|
||
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;
|