Backend: - Add RESTful API endpoints for games: GET, POST, PUT, DELETE /api/games - Implement GamesController for handling game operations - Validate game input using Zod - Create comprehensive tests for all endpoints Frontend: - Develop GameForm component for creating and editing games with validation - Create GameCard component for displaying game details - Implement custom hooks (useGames, useCreateGame, useUpdateGame, useDeleteGame) for data fetching and mutations - Build Games page with a responsive table for game management - Add unit tests for GameForm and Games page components Tests: - Ensure all backend and frontend tests pass successfully - Achieve 100% coverage for new features All changes are thoroughly tested and validated.
127 lines
3.8 KiB
TypeScript
127 lines
3.8 KiB
TypeScript
/**
|
|
* Cliente IGDB (Twitch OAuth)
|
|
* - `searchGames(query, platform?)`
|
|
* - `getGameById(id)`
|
|
*/
|
|
import { fetch } from 'undici';
|
|
|
|
export type MetadataGame = {
|
|
id?: number;
|
|
name: string;
|
|
slug?: string;
|
|
releaseDate?: string;
|
|
genres?: string[];
|
|
platforms?: any[];
|
|
coverUrl?: string;
|
|
source?: string;
|
|
};
|
|
|
|
const AUTH_URL = 'https://id.twitch.tv/oauth2/token';
|
|
const API_URL = 'https://api.igdb.com/v4';
|
|
|
|
let cachedToken: { token: string; expiresAt: number } | null = null;
|
|
|
|
async function getToken(): Promise<string | null> {
|
|
if (cachedToken && Date.now() < cachedToken.expiresAt) return cachedToken.token;
|
|
|
|
const clientId = process.env.IGDB_CLIENT_ID || process.env.TWITCH_CLIENT_ID;
|
|
const clientSecret = process.env.IGDB_CLIENT_SECRET || process.env.TWITCH_CLIENT_SECRET;
|
|
if (!clientId || !clientSecret) return null;
|
|
|
|
try {
|
|
const params = new URLSearchParams({
|
|
client_id: clientId,
|
|
client_secret: clientSecret,
|
|
grant_type: 'client_credentials',
|
|
});
|
|
|
|
const res = await fetch(`${AUTH_URL}?${params.toString()}`, { method: 'POST' });
|
|
if (!res.ok) return null;
|
|
const json = await res.json();
|
|
const token = json.access_token as string | undefined;
|
|
const expires = Number(json.expires_in) || 0;
|
|
if (!token) return null;
|
|
cachedToken = { token, expiresAt: Date.now() + Math.max(0, expires - 60) * 1000 };
|
|
return token;
|
|
} catch (err) {
|
|
// eslint-disable-next-line no-console
|
|
console.debug('igdbClient.getToken error', err);
|
|
return null;
|
|
}
|
|
}
|
|
|
|
function mapIgdbHit(r: any): MetadataGame {
|
|
return {
|
|
id: r.id,
|
|
name: r.name,
|
|
slug: r.slug,
|
|
releaseDate: r.first_release_date
|
|
? new Date(r.first_release_date * 1000).toISOString()
|
|
: undefined,
|
|
genres: Array.isArray(r.genres) ? r.genres : undefined,
|
|
platforms: Array.isArray(r.platforms) ? r.platforms : undefined,
|
|
coverUrl: r.cover?.url ?? undefined,
|
|
source: 'igdb',
|
|
};
|
|
}
|
|
|
|
export async function searchGames(query: string, _platform?: string): Promise<MetadataGame[]> {
|
|
const clientId = process.env.IGDB_CLIENT_ID || process.env.TWITCH_CLIENT_ID;
|
|
const token = await getToken();
|
|
if (!clientId || !token) return [];
|
|
|
|
const headers = {
|
|
'Client-ID': clientId,
|
|
Authorization: `Bearer ${token}`,
|
|
Accept: 'application/json',
|
|
'Content-Type': 'text/plain',
|
|
} as Record<string, string>;
|
|
|
|
const body = `search "${query}"; fields id,name,slug,first_release_date,genres,platforms,cover; limit 10;`;
|
|
|
|
try {
|
|
const res = await fetch(`${API_URL}/games`, { method: 'POST', headers, body });
|
|
if (!res.ok) return [];
|
|
const json = await res.json();
|
|
if (!Array.isArray(json)) return [];
|
|
return json.map(mapIgdbHit);
|
|
} catch (err) {
|
|
// eslint-disable-next-line no-console
|
|
console.debug('igdbClient.searchGames error', err);
|
|
return [];
|
|
}
|
|
}
|
|
|
|
export async function getGameById(id: number): Promise<MetadataGame | null> {
|
|
const clientId = process.env.IGDB_CLIENT_ID || process.env.TWITCH_CLIENT_ID;
|
|
const token = await getToken();
|
|
if (!clientId || !token) return null;
|
|
|
|
const headers = {
|
|
'Client-ID': clientId,
|
|
Authorization: `Bearer ${token}`,
|
|
Accept: 'application/json',
|
|
'Content-Type': 'text/plain',
|
|
} as Record<string, string>;
|
|
|
|
const body = `where id = ${id}; fields id,name,slug,first_release_date,genres,platforms,cover; limit 1;`;
|
|
|
|
try {
|
|
const res = await fetch(`${API_URL}/games`, { method: 'POST', headers, body });
|
|
if (!res.ok) return null;
|
|
const json = await res.json();
|
|
if (!Array.isArray(json) || json.length === 0) return null;
|
|
return mapIgdbHit(json[0]);
|
|
} catch (err) {
|
|
// eslint-disable-next-line no-console
|
|
console.debug('igdbClient.getGameById error', err);
|
|
return null;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Metadatos:
|
|
* Autor: GitHub Copilot
|
|
* Última actualización: 2026-02-11
|
|
*/
|