more game page development

This commit is contained in:
roormonger
2026-03-25 16:08:46 -04:00
parent d2e976a145
commit 4e6ce29f0b
5 changed files with 569 additions and 36 deletions

View File

@@ -30,11 +30,14 @@ export interface DetailedGame extends Game {
ratingIconUrl?: string; // Icon for Age Rating
sha1?: string;
collections?: string[];
favorite?: boolean;
manualUrl?: string;
}
export interface RommCollection {
id: string;
name: string;
is_favorite?: boolean;
ownerRole: 'admin' | 'user';
games: Game[];
coverUrl?: string; // RomM collections can have intrinsic covers
@@ -119,7 +122,7 @@ export const rommApiClient = {
data.append('password', password);
data.append('grant_type', 'password');
// Ensure we request the explicit permissions RomM FastAPI needs
data.append('scope', 'me.read roms.read collections.read assets.read platforms.read');
data.append('scope', 'me.read roms.read roms.user.write collections.read collections.write assets.read platforms.read');
const res = await fetch(`${this.apiBase}/token`, {
method: 'POST',
@@ -216,6 +219,11 @@ export const rommApiClient = {
return Array.from(new Set(all)).filter(Boolean);
};
const sanitize = (val: any): string => {
if (typeof val !== 'string') return String(val?.name || val?.display_name || val || '');
return val.trim().replace(/;+$/, '');
};
// Extract Age Ratings from all providers (array format for IGDB/ScreenScraper)
const ageRatings = new Set<string>();
for (const p of providers) {
@@ -254,7 +262,7 @@ export const rommApiClient = {
// 1. Root lists
const rootList = type === 'developer' ? (json.developers || json.companies) : (json.publishers || json.companies);
if (Array.isArray(rootList)) {
rootList.forEach(c => companies.add(typeof c === 'string' ? c : (c.name || c.display_name)));
rootList.forEach(c => companies.add(sanitize(c)));
}
// 2. Provider metadata
@@ -262,20 +270,20 @@ export const rommApiClient = {
// IGDB involved_companies
if (Array.isArray(p.involved_companies)) {
p.involved_companies.forEach((ic: any) => {
if (ic[type] && ic.company?.name) companies.add(ic.company.name);
if (ic[type] && ic.company?.name) companies.add(sanitize(ic.company.name));
});
}
// General companies/developers/publishers lists
const pList = p.companies || (type === 'developer' ? (p.developers || p.developer_companies) : (p.publishers || p.publisher_companies));
if (Array.isArray(pList)) {
pList.forEach((c: any) => companies.add(typeof c === 'string' ? c : (c.name || c.display_name)));
pList.forEach((c: any) => companies.add(sanitize(c)));
}
}
// 3. Root singulars
const rootSingular = type === 'developer' ? (json.developer || json.developer_name) : (json.publisher || json.publisher_name);
if (rootSingular && typeof rootSingular === 'string') companies.add(rootSingular);
if (rootSingular && typeof rootSingular === 'string') companies.add(sanitize(rootSingular));
return Array.from(companies).filter(Boolean);
};
@@ -297,8 +305,8 @@ export const rommApiClient = {
summary: json.summary || getFirst('summary') || getFirst('description'),
developers: devList,
publishers: pubList,
genres: getAll('genres').map(g => typeof g === 'string' ? g : (g.name || g.display_name)),
franchises: getAll('franchises').map(f => typeof f === 'string' ? f : (f.name || f.display_name)),
genres: getAll('genres').map(sanitize).filter(Boolean),
franchises: getAll('franchises').map(sanitize).filter(Boolean),
releaseDate: formatDate(json.release_date || getFirst('first_release_date') || getFirst('release_date')),
screenshots: (json.screenshots || []).map((s: any) => s.url ? getFullImageUrl(s.url) || '' : ''),
fsName: json.fs_name,
@@ -311,6 +319,8 @@ export const rommApiClient = {
...(json.rom_collections || []).map((c: any) => c.name),
...(getFirst('collections') || [])
])).filter(Boolean),
favorite: (json.user_collections || json.rom_collections || []).some((c: any) => c.is_favorite || c.name === 'Favorites'),
manualUrl: getFullImageUrl(json.url_manual),
};
},
@@ -341,6 +351,7 @@ export const rommApiClient = {
return {
id: String(c.id),
name: c.name,
is_favorite: c.is_favorite || false,
ownerRole: role,
games: gamesItems,
coverUrl
@@ -350,6 +361,67 @@ export const rommApiClient = {
return mapped;
},
async fetchCollectionDetails(collectionId: string): Promise<any> {
const res = await fetch(`${this.apiBase}/collections/${collectionId}`, { headers: this.headers });
if (!res.ok) throw new Error('Failed to fetch collection details.');
return res.json();
},
async updateCollection(collectionId: string, data: { name: string; description?: string; rom_ids: number[] }): Promise<any> {
const formData = new FormData();
formData.append('name', data.name);
formData.append('description', data.description || '');
formData.append('rom_ids', JSON.stringify(data.rom_ids));
const res = await fetch(`${this.apiBase}/collections/${collectionId}?is_public=false&remove_cover=false`, {
method: 'PUT',
headers: this.headers,
body: formData
});
if (!res.ok) throw new Error('Failed to update collection.');
return res.json();
},
async toggleFavorite(gameId: string, favorite: boolean): Promise<any> {
// 1. Find the Favorites collection
const collections = await this.fetchCollections();
const favCol = collections.find(c => c.is_favorite);
if (!favCol) throw new Error("Could not find Favorites collection");
// 2. Fetch current member IDs (full list for collection-tier update)
const details = await this.fetchCollectionDetails(favCol.id);
let ids: number[] = details.rom_ids || [];
// 3. Toggle membership
const numId = parseInt(gameId);
if (favorite) {
if (!ids.includes(numId)) ids.push(numId);
} else {
ids = ids.filter(id => id !== numId);
}
// 4. Persistence via Multipart-Encoded PUT
return this.updateCollection(favCol.id, {
name: favCol.name || 'Favorites',
rom_ids: ids
});
},
async createCollection(name: string, description: string = ''): Promise<any> {
const formData = new FormData();
formData.append('name', name);
formData.append('description', description);
formData.append('rom_ids', '[]'); // Start empty
const res = await fetch(`${this.apiBase}/collections?is_public=false`, {
method: 'POST',
headers: this.headers,
body: formData
});
if (!res.ok) throw new Error('Failed to create collection.');
return res.json();
},
async fetchCurrentUser(): Promise<UserProfile> {
const res = await fetch(`${this.apiBase}/users/me`, { headers: this.headers });
if (!res.ok) throw new Error('Failed to fetch user profile.');