more game page development
This commit is contained in:
@@ -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.');
|
||||
|
||||
Reference in New Issue
Block a user