Skip to content

Commit b4ea843

Browse files
authored
Create GameCard.tsx
1 parent 151adf6 commit b4ea843

File tree

1 file changed

+143
-0
lines changed

1 file changed

+143
-0
lines changed

app/components/GameCard.tsx

Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
import React, { useState, useCallback, useEffect } from 'react';
2+
import { GameConfig } from '../types';
3+
4+
interface GameCardProps {
5+
game: GameConfig;
6+
}
7+
8+
const GameCard: React.FC<GameCardProps> = ({ game }) => {
9+
const [loading, setLoading] = useState(false);
10+
const [error, setError] = useState<string | null>(null);
11+
const [installed, setInstalled] = useState<boolean | null>(null);
12+
13+
// Check if installed on mount
14+
useEffect(() => {
15+
if (window.electronAPI) {
16+
window.electronAPI.checkGameExists(game.id)
17+
.then(setInstalled)
18+
.catch(() => setInstalled(false));
19+
} else {
20+
console.warn("Electron API not found - running in browser mode?");
21+
setInstalled(false);
22+
}
23+
}, [game.id]);
24+
25+
const handleLaunch = useCallback(async () => {
26+
if (installed === false) return;
27+
if (!window.electronAPI) {
28+
setError('ELECTRON_API_MISSING');
29+
return;
30+
}
31+
32+
setLoading(true);
33+
setError(null);
34+
try {
35+
await window.electronAPI.launchGame(game.id);
36+
setTimeout(() => setLoading(false), 3000); // Simulate init sequence
37+
} catch (err: any) {
38+
setError(err.message || 'EXECUTION FAILED');
39+
setLoading(false);
40+
}
41+
}, [game.id, installed]);
42+
43+
return (
44+
<div className="group relative bg-surface/80 backdrop-blur border border-neutral-800 p-1 flex flex-col h-[300px] transition-all duration-300 hover:-translate-y-1">
45+
46+
{/* Dynamic Colored Border Glow on Hover */}
47+
<div
48+
className="absolute inset-0 opacity-0 group-hover:opacity-100 transition-opacity duration-300 pointer-events-none"
49+
style={{ boxShadow: `0 0 20px ${game.color}40`, border: `1px solid ${game.color}` }}
50+
></div>
51+
52+
{/* Top Bar with decorative tech bits */}
53+
<div className="flex justify-between items-center px-2 py-1 bg-black/40 text-[10px] text-neutral-500 font-mono mb-4 border-b border-neutral-800">
54+
<span>ID: {game.id.toUpperCase().substring(0, 4)}</span>
55+
<div className="flex gap-1">
56+
<div className={`w-2 h-2 rounded-full ${installed ? 'bg-green-500' : 'bg-red-900'}`}></div>
57+
<div className="w-2 h-2 rounded-full bg-neutral-800"></div>
58+
<div className="w-2 h-2 rounded-full bg-neutral-800"></div>
59+
</div>
60+
</div>
61+
62+
<div className="flex-grow flex flex-col items-center justify-center px-4 text-center z-10">
63+
{/* Game Icon */}
64+
<div
65+
className="w-20 h-20 mb-4 flex items-center justify-center transition-all duration-300 group-hover:scale-110"
66+
>
67+
{game.icon ? (
68+
<img
69+
src={game.icon}
70+
alt={game.name}
71+
className="w-full h-full object-contain drop-shadow-[0_0_5px_rgba(255,255,255,0.3)]"
72+
onError={(e) => {
73+
// Fallback to text if image fails to load
74+
e.currentTarget.style.display = 'none';
75+
const parent = e.currentTarget.parentElement;
76+
if (parent) {
77+
const span = document.createElement('span');
78+
span.innerText = game.name.charAt(0);
79+
span.className = "text-3xl font-bold";
80+
span.style.color = game.color;
81+
parent.classList.add('rounded-full', 'border-2', 'border-dashed');
82+
parent.style.borderColor = game.color;
83+
parent.appendChild(span);
84+
}
85+
}}
86+
/>
87+
) : (
88+
<div
89+
className="w-full h-full rounded-full flex items-center justify-center border-2 border-dashed opacity-80 group-hover:opacity-100"
90+
style={{ borderColor: game.color, color: game.color }}
91+
>
92+
<span className="text-3xl font-bold">{game.name.charAt(0)}</span>
93+
</div>
94+
)}
95+
</div>
96+
97+
<h2
98+
className={`text-2xl font-bold mb-2 uppercase tracking-wider glow-text transition-colors duration-300`}
99+
style={{ color: game.color }}
100+
>
101+
{game.name}
102+
</h2>
103+
104+
<p className="text-neutral-400 text-xs leading-relaxed font-sans mb-4 border-t border-b border-neutral-800 py-2 w-full line-clamp-2">
105+
{game.description}
106+
</p>
107+
</div>
108+
109+
{/* Action Area */}
110+
<div className="p-4 z-10">
111+
{error && (
112+
<div className="text-red-500 text-[10px] text-center mb-2 font-bold bg-black/50 border border-red-900 p-1 animate-pulse">
113+
ERR: {error}
114+
</div>
115+
)}
116+
117+
<button
118+
onClick={handleLaunch}
119+
disabled={loading || installed === false}
120+
className={`
121+
w-full py-3 px-4 font-bold text-black uppercase tracking-widest text-sm
122+
transition-all duration-200 clip-path-button relative overflow-hidden
123+
${game.twBg}
124+
${loading ? 'opacity-70 cursor-wait' : 'opacity-90 hover:opacity-100'}
125+
${installed === false ? 'grayscale cursor-not-allowed opacity-30' : ''}
126+
`}
127+
style={{
128+
boxShadow: loading ? `0 0 15px ${game.color}` : 'none',
129+
}}
130+
>
131+
<span className="relative z-10">
132+
{loading ? 'INITIALIZING...' : installed === false ? 'MISSING_FILE' : 'LAUNCH_EXE'}
133+
</span>
134+
135+
{/* Scanline overlay on button */}
136+
<div className="absolute inset-0 bg-[url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAQAAAAECAYAAACp8Z5+AAAAIklEQVQIW2NkQAKrVq36zwjjgzhhZWGMYAEYB8RmROaABADeOQ8CXl/xfgAAAABJRU5ErkJggg==')] opacity-20"></div>
137+
</button>
138+
</div>
139+
</div>
140+
);
141+
};
142+
143+
export default GameCard;

0 commit comments

Comments
 (0)