Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
153 changes: 13 additions & 140 deletions src/components/game/GameCard.tsx
Original file line number Diff line number Diff line change
@@ -1,101 +1,26 @@
import { type GameCard as GameCardType } from "@/types/game";
import {
Sparkles,
Zap,
Star,
Circle,
Coins,
Trophy,
RefreshCw,
Sword,
Leaf,
LucideIcon,
} from "lucide-react";
import { formatNumber } from "@/lib/utils";
import { resolveCardStats } from "@/store/gameStore";
import { RARITY_STYLES, getPrimaryType } from "@/config/rarityConfig";
import { CardImage } from "./card-parts/CardImage";
import { CardStatusBadge } from "./card-parts/CardStatusBadge";
import { CardStats } from "./card-parts/CardStats";
import { CardTypes } from "./card-parts/CardTypes";
import { CardRarityOverlay } from "./card-parts/CardRarityOverlay";

interface GameCardProps {
card: GameCardType;
onClick?: () => void;
isActive?: boolean;
}

type RarityOrType =
| "COMMON"
| "NORMAL"
| "RARE"
| "HOLO"
| "FULL_ART"
| "SILVER"
| "GOLD"
| "REVERT"
| "SWORD";

const typeIcons = {
COMMON: Circle,
NORMAL: Circle,
RARE: Star,
HOLO: Sparkles,
FULL_ART: Zap,
SILVER: Coins,
GOLD: Trophy,
REVERT: RefreshCw,
SWORD: Sword,
} satisfies Record<RarityOrType, LucideIcon>;

const rarityConfig = {
COMMON: { border: "border-border", bg: "bg-card" },
NORMAL: { border: "border-border", bg: "bg-card" },
RARE: { border: "border-blue-400/60 animate-rare-pulse", bg: "bg-card" },
HOLO: { border: "border-rarity-holo", bg: "bg-card" },
FULL_ART: { border: "border-rarity-fullart", bg: "bg-card" },
SILVER: { border: "border-slate-300", bg: "bg-slate-900/40" },
GOLD: { border: "border-yellow-500", bg: "bg-yellow-900/20" },
REVERT: { border: "border-red-500", bg: "bg-black" },
} satisfies Record<string, { border: string; bg: string }>;

export function GameCard({ card, onClick, isActive }: GameCardProps) {
const stats = resolveCardStats(card);
const { character, income, power } = stats;

const types = card.types || [];
const isHolo = types.includes("HOLO");
const isFullArt = types.includes("FULL_ART");
const isGold = types.includes("GOLD");
const isSilver = types.includes("SILVER");
const isRevert = types.includes("REVERT");

const primaryType = types.includes("REVERT")
? "REVERT"
: types.includes("GOLD")
? "GOLD"
: types.includes("SILVER")
? "SILVER"
: types.includes("FULL_ART")
? "FULL_ART"
: types.includes("HOLO")
? "HOLO"
: types.includes("RARE")
? "RARE"
: types.includes("NORMAL")
? "NORMAL"
: "COMMON";

const config = rarityConfig[primaryType] || rarityConfig.COMMON;

let imgSrc = `https://rickandmortyapi.com/api/character/avatar/${character.avatarId}.jpeg`;

if (character.customImage) {
imgSrc = character.customImage;
}

const imageFilter = isRevert
? "invert(1) hue-rotate(180deg)"
: isGold
? "sepia(1) saturate(5) brightness(0.8) hue-rotate(-15deg)"
: isSilver
? "grayscale(1) brightness(1.2) contrast(1.1)"
: "";
const primaryType = getPrimaryType(types);
const config = RARITY_STYLES[primaryType] || RARITY_STYLES.COMMON;

return (
<button
Expand All @@ -108,44 +33,13 @@ export function GameCard({ card, onClick, isActive }: GameCardProps) {
</div>
)}

{isHolo && (
<div className="absolute inset-0 animate-holo opacity-30 mix-blend-overlay z-10 pointer-events-none rounded-xl" />
)}
{isGold && (
<div className="absolute inset-0 bg-yellow-500/10 mix-blend-color-dodge z-10 pointer-events-none" />
)}
<CardRarityOverlay types={types} />

<div className="relative h-44 overflow-hidden">
{isFullArt ? (
<img
src={imgSrc}
alt={character.name}
className="w-full h-full object-cover transition-all duration-500"
style={{ filter: imageFilter }}
/>
) : (
<div className="flex items-center justify-center h-full bg-muted/50">
<img
src={imgSrc}
alt={character.name}
className="w-24 h-24 rounded-full object-cover border-2 border-border group-hover:scale-110 transition-all duration-500"
style={{ filter: imageFilter }}
/>
</div>
)}
<CardImage character={character} types={types} isFullArt={isFullArt} />

<div className="absolute top-2 right-2 z-20">
<span
className={`text-[8px] px-1.5 py-0.5 rounded-full font-bold uppercase ${
character.status === "Alive"
? "bg-green-500/20 text-green-400 border border-green-500/40"
: character.status === "Dead"
? "bg-red-500/20 text-red-400 border border-red-500/40"
: "bg-gray-500/20 text-gray-400 border border-gray-500/40"
}`}
>
{character.status}
</span>
<CardStatusBadge status={character.status} />
</div>
</div>

Expand All @@ -159,29 +53,8 @@ export function GameCard({ card, onClick, isActive }: GameCardProps) {
<span>{character.origin}</span>
</p>
<div className="flex items-center justify-between">
<div className="flex items-center -space-x-1">
{types.map((tId) => {
const Icon = typeIcons[tId];
if (!Icon) return null;
return (
<Icon key={tId} className="w-3 h-3 text-muted-foreground" />
);
})}
</div>
<div className="flex flex-col items-end">
<div className="flex items-center gap-1">
<Leaf className="w-2.5 h-2.5 text-green-500 fill-green-500/20" />
<span className="text-xs font-bold text-primary">
{formatNumber(income)}/s
</span>
</div>
<div className="flex items-center gap-1">
<Sword className="w-2.5 h-2.5 text-red-500 fill-red-500/20" />
<span className="text-[10px] font-bold text-red-500">
{formatNumber(power)}
</span>
</div>
</div>
<CardTypes types={types} />
<CardStats income={income} power={power} />
</div>
<div className="pt-1 border-t border-border/50 flex items-center justify-between opacity-60">
<span className="text-[8px] font-body uppercase">
Expand Down
39 changes: 39 additions & 0 deletions src/components/game/card-parts/CardImage.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { getCardImageFilter } from "@/lib/utils";
import { Character } from "@/types/game";

interface CardImageProps {
character: Character;
types: string[];
isFullArt: boolean;
}

export function CardImage({ character, types, isFullArt }: CardImageProps) {
const imageFilter = getCardImageFilter(types);
let imgSrc = `https://rickandmortyapi.com/api/character/avatar/${character.avatarId}.jpeg`;

if (character.customImage) {
imgSrc = character.customImage;
}

if (isFullArt) {
return (
<img
src={imgSrc}
alt={character.name}
className="w-full h-full object-cover transition-all duration-500"
style={{ filter: imageFilter }}
/>
);
}

return (
<div className="flex items-center justify-center h-full bg-muted/50">
<img
src={imgSrc}
alt={character.name}
className="w-24 h-24 rounded-full object-cover border-2 border-border group-hover:scale-110 transition-all duration-500"
style={{ filter: imageFilter }}
/>
</div>
);
}
19 changes: 19 additions & 0 deletions src/components/game/card-parts/CardRarityOverlay.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
interface CardRarityOverlayProps {
types: string[];
}

export function CardRarityOverlay({ types }: CardRarityOverlayProps) {
const isHolo = types.includes("HOLO");
const isGold = types.includes("GOLD");

return (
<>
{isHolo && (
<div className="absolute inset-0 animate-holo opacity-30 mix-blend-overlay z-10 pointer-events-none rounded-xl" />
)}
{isGold && (
<div className="absolute inset-0 bg-yellow-500/10 mix-blend-color-dodge z-10 pointer-events-none" />
)}
</>
);
}
26 changes: 26 additions & 0 deletions src/components/game/card-parts/CardStats.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { Leaf, Sword } from "lucide-react";
import { formatNumber } from "@/lib/utils";

interface CardStatsProps {
income: number;
power: number;
}

export function CardStats({ income, power }: CardStatsProps) {
return (
<div className="flex flex-col items-end">
<div className="flex items-center gap-1">
<Leaf className="w-2.5 h-2.5 text-green-500 fill-green-500/20" />
<span className="text-xs font-bold text-primary">
{formatNumber(income)}/s
</span>
</div>
<div className="flex items-center gap-1">
<Sword className="w-2.5 h-2.5 text-red-500 fill-red-500/20" />
<span className="text-[10px] font-bold text-red-500">
{formatNumber(power)}
</span>
</div>
</div>
);
}
19 changes: 19 additions & 0 deletions src/components/game/card-parts/CardStatusBadge.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
interface CardStatusBadgeProps {
status: string;
}

export function CardStatusBadge({ status }: CardStatusBadgeProps) {
const statusStyles = {
Alive: "bg-green-500/20 text-green-400 border border-green-500/40",
Dead: "bg-red-500/20 text-red-400 border border-red-500/40",
unknown: "bg-gray-500/20 text-gray-400 border border-gray-500/40",
}[status] || "bg-gray-500/20 text-gray-400 border border-gray-500/40";

return (
<span
className={`text-[8px] px-1.5 py-0.5 rounded-full font-bold uppercase ${statusStyles}`}
>
{status}
</span>
);
}
19 changes: 19 additions & 0 deletions src/components/game/card-parts/CardTypes.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { TYPE_ICONS } from "@/config/rarityConfig";

interface CardTypesProps {
types: string[];
}

export function CardTypes({ types }: CardTypesProps) {
return (
<div className="flex items-center -space-x-1">
{types.map((tId) => {
const Icon = TYPE_ICONS[tId];
if (!Icon) return null;
return (
<Icon key={tId} className="w-3 h-3 text-muted-foreground" />
);
})}
</div>
);
}
56 changes: 56 additions & 0 deletions src/config/rarityConfig.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import {
Sparkles,
Zap,
Star,
Circle,
Coins,
Trophy,
RefreshCw,
Sword,
LucideIcon,
} from "lucide-react";

export type RarityOrType =
| "COMMON"
| "NORMAL"
| "RARE"
| "HOLO"
| "FULL_ART"
| "SILVER"
| "GOLD"
| "REVERT"
| "SWORD";

export const TYPE_ICONS: Record<string, LucideIcon> = {
COMMON: Circle,
NORMAL: Circle,
RARE: Star,
HOLO: Sparkles,
FULL_ART: Zap,
SILVER: Coins,
GOLD: Trophy,
REVERT: RefreshCw,
SWORD: Sword,
};

export const RARITY_STYLES: Record<string, { border: string; bg: string }> = {
COMMON: { border: "border-border", bg: "bg-card" },
NORMAL: { border: "border-border", bg: "bg-card" },
RARE: { border: "border-blue-400/60 animate-rare-pulse", bg: "bg-card" },
HOLO: { border: "border-rarity-holo", bg: "bg-card" },
FULL_ART: { border: "border-rarity-fullart", bg: "bg-card" },
SILVER: { border: "border-slate-300", bg: "bg-slate-900/40" },
GOLD: { border: "border-yellow-500", bg: "bg-yellow-900/20" },
REVERT: { border: "border-red-500", bg: "bg-black" },
};

export function getPrimaryType(types: string[]): string {
if (types.includes("REVERT")) return "REVERT";
if (types.includes("GOLD")) return "GOLD";
if (types.includes("SILVER")) return "SILVER";
if (types.includes("FULL_ART")) return "FULL_ART";
if (types.includes("HOLO")) return "HOLO";
if (types.includes("RARE")) return "RARE";
if (types.includes("NORMAL")) return "NORMAL";
return "COMMON";
}
Loading
Loading