TypeScript · 3200 bytes Raw Blame History
1 import { useState, useEffect } from "react";
2 import { getGames, launchGame } from "../hooks/useApi";
3 import type { GameInfo } from "../types";
4 import GameCard from "../components/GameCard";
5
6 export default function GamesView() {
7 const [games, setGames] = useState<GameInfo[]>([]);
8 const [loading, setLoading] = useState(true);
9 const [error, setError] = useState<string | null>(null);
10 const [search, setSearch] = useState("");
11 const [launching, setLaunching] = useState<number | null>(null);
12
13 useEffect(() => {
14 loadGames();
15 }, []);
16
17 async function loadGames() {
18 try {
19 setLoading(true);
20 const gamesList = await getGames();
21 // Sort by name
22 gamesList.sort((a, b) => a.name.localeCompare(b.name));
23 setGames(gamesList);
24 } catch (err) {
25 setError(String(err));
26 } finally {
27 setLoading(false);
28 }
29 }
30
31 async function handleLaunch(appId: number, withWemod: boolean) {
32 setLaunching(appId);
33 try {
34 await launchGame(appId, withWemod);
35 } catch (err) {
36 setError(String(err));
37 } finally {
38 setLaunching(null);
39 }
40 }
41
42 const filteredGames = games.filter((game) =>
43 game.name.toLowerCase().includes(search.toLowerCase())
44 );
45
46 if (loading) {
47 return (
48 <div>
49 <div className="page-header">
50 <h1 className="page-title">Games</h1>
51 </div>
52 <div style={{ textAlign: "center", padding: "50px" }}>
53 <div className="spinner" style={{ margin: "0 auto" }} />
54 <p style={{ marginTop: "20px", color: "var(--text-secondary)" }}>Loading games...</p>
55 </div>
56 </div>
57 );
58 }
59
60 return (
61 <div>
62 <div className="page-header">
63 <h1 className="page-title">Games</h1>
64 <button className="btn btn-secondary" onClick={loadGames}>
65 Refresh
66 </button>
67 </div>
68
69 {error && (
70 <div
71 className="card"
72 style={{ backgroundColor: "rgba(248, 113, 113, 0.1)", marginBottom: "20px" }}
73 >
74 <p style={{ color: "var(--error)" }}>{error}</p>
75 </div>
76 )}
77
78 <div className="form-group">
79 <input
80 type="text"
81 className="form-input"
82 placeholder="Search games..."
83 value={search}
84 onChange={(e) => setSearch(e.target.value)}
85 />
86 </div>
87
88 {filteredGames.length === 0 ? (
89 <div className="card" style={{ textAlign: "center" }}>
90 <p style={{ color: "var(--text-secondary)" }}>
91 {search ? "No games match your search" : "No Proton games found"}
92 </p>
93 </div>
94 ) : (
95 <div className="game-grid">
96 {filteredGames.map((game) => (
97 <GameCard
98 key={game.app_id}
99 game={game}
100 onLaunch={handleLaunch}
101 launching={launching === game.app_id}
102 />
103 ))}
104 </div>
105 )}
106
107 <p
108 style={{
109 marginTop: "20px",
110 fontSize: "0.8rem",
111 color: "var(--text-secondary)",
112 textAlign: "center",
113 }}
114 >
115 Showing {filteredGames.length} of {games.length} Proton games
116 </p>
117 </div>
118 );
119 }