feat: ratings ui, import giantbomb ratings

This commit is contained in:
DecDuck
2025-05-30 22:07:50 +10:00
parent 185f37f135
commit 3fbe514f65
7 changed files with 64 additions and 25 deletions
+1 -2
View File
@@ -120,12 +120,11 @@ import {
ArrowUpRightIcon, ArrowUpRightIcon,
} from "@heroicons/vue/20/solid"; } from "@heroicons/vue/20/solid";
import { micromark } from "micromark"; import { micromark } from "micromark";
import type { Game } from "~/prisma/client";
const route = useRoute(); const route = useRoute();
const id = route.params.id.toString(); const id = route.params.id.toString();
const rawGame = await $dropFetch<Game>(`/api/v1/games/${id}`); const { game: rawGame } = await $dropFetch(`/api/v1/games/${id}`);
const game = computed(() => { const game = computed(() => {
if (!rawGame) { if (!rawGame) {
throw createError({ statusCode: 404, message: "Game not found" }); throw createError({ statusCode: 404, message: "Game not found" });
+6 -8
View File
@@ -103,7 +103,9 @@
'w-4 h-4', 'w-4 h-4',
]" ]"
/> />
<span class="text-zinc-600">({{ 0 }} reviews)</span> <span class="text-zinc-600"
>({{ rating._sum.mReviewCount ?? 0 }} reviews)</span
>
</td> </td>
</tr> </tr>
</tbody> </tbody>
@@ -174,10 +176,8 @@
<script setup lang="ts"> <script setup lang="ts">
import { ArrowTopRightOnSquareIcon } from "@heroicons/vue/24/outline"; import { ArrowTopRightOnSquareIcon } from "@heroicons/vue/24/outline";
import { StarIcon } from "@heroicons/vue/24/solid"; import { StarIcon } from "@heroicons/vue/24/solid";
import type { Game, GameVersion } from "~/prisma/client";
import { micromark } from "micromark"; import { micromark } from "micromark";
import { DateTime } from "luxon"; import { DateTime } from "luxon";
import type { SerializeObject } from "nitropack";
import type { PlatformClient } from "~/composables/types"; import type { PlatformClient } from "~/composables/types";
const route = useRoute(); const route = useRoute();
@@ -185,9 +185,7 @@ const gameId = route.params.id.toString();
const user = useUser(); const user = useUser();
const game = await $dropFetch< const { game, rating } = await $dropFetch(`/api/v1/games/${gameId}`);
SerializeObject<Game> & { versions: GameVersion[] }
>(`/api/v1/games/${gameId}`);
// Preview description (first 30 lines) // Preview description (first 30 lines)
const showPreview = ref(true); const showPreview = ref(true);
@@ -219,10 +217,10 @@ const platforms = game.versions
.filter((e, i, u) => u.indexOf(e) === i); .filter((e, i, u) => u.indexOf(e) === i);
// const rating = Math.round(game.mReviewRating * 5); // const rating = Math.round(game.mReviewRating * 5);
const rating = Math.round(0 * 5); const averageRating = Math.round((rating._avg.mReviewRating ?? 0) * 5);
const ratingArray = Array(5) const ratingArray = Array(5)
.fill(null) .fill(null)
.map((_, i) => i + 1 <= rating); .map((_, i) => i + 1 <= averageRating);
useHead({ useHead({
title: game.mName, title: game.mName,
+1 -1
View File
@@ -56,7 +56,7 @@ model GameRating {
mReviewHref String? mReviewHref String?
Game Game @relation(fields: [gameId], references: [id], onDelete: Cascade) game Game @relation(fields: [gameId], references: [id], onDelete: Cascade)
gameId String gameId String
@@unique([metadataSource, metadataId], name: "metadataKey") @@unique([metadataSource, metadataId], name: "metadataKey")
+13 -1
View File
@@ -22,5 +22,17 @@ export default defineEventHandler(async (h3) => {
if (!game) if (!game)
throw createError({ statusCode: 404, statusMessage: "Game not found" }); throw createError({ statusCode: 404, statusMessage: "Game not found" });
return game; const rating = await prisma.gameRating.aggregate({
where: {
gameId: game.id,
},
_avg: {
mReviewRating: true,
},
_sum: {
mReviewCount: true,
},
});
return { game, rating };
}); });
+28 -1
View File
@@ -7,6 +7,7 @@ import type {
GameMetadata, GameMetadata,
_FetchCompanyMetadataParams, _FetchCompanyMetadataParams,
CompanyMetadata, CompanyMetadata,
GameMetadataRating,
} from "./types"; } from "./types";
import axios, { type AxiosRequestConfig } from "axios"; import axios, { type AxiosRequestConfig } from "axios";
import TurndownService from "turndown"; import TurndownService from "turndown";
@@ -58,6 +59,17 @@ interface GameResult {
tags: string; // If it's "All Images", art, otherwise screenshot tags: string; // If it's "All Images", art, otherwise screenshot
original: string; original: string;
}>; }>;
reviews: Array<{
api_detail_url: string;
}>;
}
interface ReviewResult {
deck: string;
score: number; // Out of 5
reviewer: string;
site_detail_url: string;
} }
interface CompanySearchResult { interface CompanySearchResult {
@@ -198,6 +210,21 @@ export class GiantBombProvider implements MetadataProvider {
}-${gameData.expected_release_day ?? 1}`, }-${gameData.expected_release_day ?? 1}`,
).toJSDate(); ).toJSDate();
const reviews: GameMetadataRating[] = [];
for (const { api_detail_url } of gameData.reviews) {
const reviewId = api_detail_url.split("/").at(-2);
if (!reviewId) continue;
const review = await this.request<ReviewResult>("review", reviewId, {});
console.log(review.data);
reviews.push({
metadataSource: MetadataSource.GiantBomb,
metadataId: reviewId,
mReviewCount: 1,
mReviewRating: review.data.results.score / 5,
mReviewHref: review.data.results.site_detail_url,
});
}
const metadata: GameMetadata = { const metadata: GameMetadata = {
id: gameData.guid, id: gameData.guid,
name: gameData.name, name: gameData.name,
@@ -207,7 +234,7 @@ export class GiantBombProvider implements MetadataProvider {
tags: [], tags: [],
reviews: [], reviews,
publishers, publishers,
developers, developers,
+14 -11
View File
@@ -355,8 +355,11 @@ export class IGDBProvider implements MetadataProvider {
const response = await this.request<IGDBGameFull>("games", body); const response = await this.request<IGDBGameFull>("games", body);
for (let i = 0; i < response.length; i++) { for (let i = 0; i < response.length; i++) {
const currentGame = response[i];
if(!currentGame) continue;
let iconRaw; let iconRaw;
const cover = response[i].cover; const cover = currentGame.cover;
if (cover !== undefined) { if (cover !== undefined) {
iconRaw = await this.getCoverURL(cover); iconRaw = await this.getCoverURL(cover);
} else { } else {
@@ -366,7 +369,7 @@ export class IGDBProvider implements MetadataProvider {
let banner = ""; let banner = "";
const images = [icon]; const images = [icon];
for (const art of response[i]?.artworks ?? []) { for (const art of currentGame.artworks ?? []) {
// if banner not set // if banner not set
if (banner.length <= 0) { if (banner.length <= 0) {
banner = createObject(await this.getArtworkURL(art)); banner = createObject(await this.getArtworkURL(art));
@@ -378,7 +381,7 @@ export class IGDBProvider implements MetadataProvider {
const publishers: Company[] = []; const publishers: Company[] = [];
const developers: Company[] = []; const developers: Company[] = [];
for (const involvedCompany of response[i]?.involved_companies ?? []) { for (const involvedCompany of currentGame.involved_companies ?? []) {
// get details about the involved company // get details about the involved company
const involved_company_response = const involved_company_response =
await this.request<IGDBInvolvedCompany>( await this.request<IGDBInvolvedCompany>(
@@ -408,13 +411,13 @@ export class IGDBProvider implements MetadataProvider {
} }
} }
const firstReleaseDate = response[i].first_release_date; const firstReleaseDate = currentGame.first_release_date;
return { return {
id: "" + response[i].id, id: "" + response[i].id,
name: response[i].name, name: response[i].name,
shortDescription: this.trimMessage(response[i].summary, 280), shortDescription: this.trimMessage(currentGame.summary, 280),
description: response[i].summary, description: currentGame.summary,
released: released:
firstReleaseDate === undefined firstReleaseDate === undefined
? new Date() ? new Date()
@@ -422,18 +425,18 @@ export class IGDBProvider implements MetadataProvider {
reviews: [ reviews: [
{ {
metadataId: "" + response[i].id, metadataId: "" + currentGame.id,
metadataSource: MetadataSource.IGDB, metadataSource: MetadataSource.IGDB,
mReviewCount: response[i]?.total_rating_count ?? 0, mReviewCount: currentGame.total_rating_count ?? 0,
mReviewRating: (response[i]?.total_rating ?? 0) / 100, mReviewRating: (currentGame.total_rating ?? 0) / 100,
mReviewHref: response[i].url, mReviewHref: currentGame.url,
}, },
], ],
publishers: [], publishers: [],
developers: [], developers: [],
tags: await this.getGenres(response[i].genres), tags: await this.getGenres(currentGame.genres),
icon, icon,
bannerId: banner, bannerId: banner,
+1 -1
View File
@@ -764,7 +764,7 @@
resolved "https://registry.yarnpkg.com/@kwsites/promise-deferred/-/promise-deferred-1.1.1.tgz#8ace5259254426ccef57f3175bc64ed7095ed919" resolved "https://registry.yarnpkg.com/@kwsites/promise-deferred/-/promise-deferred-1.1.1.tgz#8ace5259254426ccef57f3175bc64ed7095ed919"
integrity sha512-GaHYm+c0O9MjZRu0ongGBRbinu8gVAMd2UZjji6jVmqKtZluZnptXGWhz1E8j8D2HJ3f/yMxKAUC0b+57wncIw== integrity sha512-GaHYm+c0O9MjZRu0ongGBRbinu8gVAMd2UZjji6jVmqKtZluZnptXGWhz1E8j8D2HJ3f/yMxKAUC0b+57wncIw==
"@lobomfz/prismark@^0.0.3": "@lobomfz/prismark@0.0.3":
version "0.0.3" version "0.0.3"
resolved "https://registry.yarnpkg.com/@lobomfz/prismark/-/prismark-0.0.3.tgz#8b4eb34963e96cf3fff01432e62de22f184f2f2d" resolved "https://registry.yarnpkg.com/@lobomfz/prismark/-/prismark-0.0.3.tgz#8b4eb34963e96cf3fff01432e62de22f184f2f2d"
integrity sha512-g2xfR/F+sRBRUhWYlpUkafqZjqsQBetjfzdWvQndRU4wdoavn3zblM3OQwb7vrsrKB6Wmbs+DtLGaD5XBQ2v8A== integrity sha512-g2xfR/F+sRBRUhWYlpUkafqZjqsQBetjfzdWvQndRU4wdoavn3zblM3OQwb7vrsrKB6Wmbs+DtLGaD5XBQ2v8A==