Skip to content

Commit aaebae9

Browse files
committed
feat(v2): show provider ratings in game details header
1 parent e7c2362 commit aaebae9

1 file changed

Lines changed: 132 additions & 3 deletions

File tree

frontend/src/v2/components/GameDetails/GameHeader.vue

Lines changed: 132 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,13 @@
99
// Metadata-provider links live in the Metadata tab, not the header.
1010
// Genre/franchise belong in the Overview tab info grid.
1111
import { RIcon, RPlatformIcon, RTag, RTooltip } from "@v2/lib";
12+
import { computed } from "vue";
1213
import { useI18n } from "vue-i18n";
1314
import type { DetailedRom } from "@/stores/roms";
1415
import GameActions from "@/v2/components/GameActions/GameActions.vue";
1516
import MainSiblingToggle from "@/v2/components/GameDetails/MainSiblingToggle.vue";
1617
import PrevNextNav from "@/v2/components/GameDetails/PrevNextNav.vue";
18+
import { PROVIDERS } from "@/v2/components/GameDetails/providers";
1719
import VersionSwitcher from "@/v2/components/GameDetails/VersionSwitcher.vue";
1820
import { useGameActions } from "@/v2/composables/useGameActions";
1921
@@ -33,6 +35,88 @@ const props = defineProps<{
3335
}>();
3436
3537
const actions = useGameActions(() => props.rom);
38+
39+
type RatingChip = {
40+
key: "igdb_id" | "ss_id" | "moby_id" | "launchbox_id" | "hltb_id";
41+
name: string;
42+
logo: string;
43+
value: string;
44+
};
45+
46+
function formatRating(value: number, options?: { percent?: boolean }): string | null {
47+
if (!Number.isFinite(value)) return null;
48+
const rounded = value >= 10 ? value.toFixed(0) : value.toFixed(1);
49+
const formatted = rounded.replace(/\.0$/, "");
50+
return options?.percent ? `${formatted}%` : formatted;
51+
}
52+
53+
function normalizeToTen(value: number): number {
54+
if (!Number.isFinite(value)) return Number.NaN;
55+
return value > 10 ? value / 10 : value;
56+
}
57+
58+
function providerByKey(key: RatingChip["key"]) {
59+
return PROVIDERS.find((p) => p.key === key) ?? null;
60+
}
61+
62+
const visibleRegions = computed(() =>
63+
props.regions.filter((r) => r.trim().toLowerCase() !== "world"),
64+
);
65+
66+
const ratingChips = computed<RatingChip[]>(() => {
67+
const rom = props.rom;
68+
69+
const values: Array<{ key: RatingChip["key"]; value: string | null }> = [
70+
{
71+
key: "igdb_id",
72+
value: formatRating(
73+
normalizeToTen(parseFloat(rom.igdb_metadata?.total_rating ?? "")),
74+
),
75+
},
76+
{
77+
key: "ss_id",
78+
value: formatRating(parseFloat(rom.ss_metadata?.ss_score ?? "") * 10, {
79+
percent: true,
80+
}),
81+
},
82+
{
83+
key: "moby_id",
84+
value: formatRating(parseFloat(rom.moby_metadata?.moby_score ?? "") * 10, {
85+
percent: true,
86+
}),
87+
},
88+
{
89+
key: "launchbox_id",
90+
value: formatRating(
91+
(rom.launchbox_metadata?.community_rating ?? Number.NaN) * 20,
92+
{ percent: true },
93+
),
94+
},
95+
{
96+
key: "hltb_id",
97+
value: formatRating(rom.hltb_metadata?.review_score ?? Number.NaN, {
98+
percent: true,
99+
}),
100+
},
101+
];
102+
103+
const out: RatingChip[] = [];
104+
105+
for (const entry of values) {
106+
if (!entry.value) continue;
107+
const provider = providerByKey(entry.key);
108+
if (!provider || !provider.logo) continue;
109+
110+
out.push({
111+
key: entry.key,
112+
name: provider.name,
113+
logo: provider.logo,
114+
value: entry.value,
115+
});
116+
}
117+
118+
return out;
119+
});
36120
</script>
37121

38122
<template>
@@ -82,18 +166,18 @@ const actions = useGameActions(() => props.rom);
82166
</span>
83167

84168
<span
85-
v-if="regions.length || languages.length || tags.length"
169+
v-if="visibleRegions.length || languages.length || tags.length"
86170
class="r-v2-det-header__sep"
87171
>
88172
·
89173
</span>
90174

91175
<span
92-
v-if="regions.length || languages.length || tags.length"
176+
v-if="visibleRegions.length || languages.length || tags.length"
93177
class="r-v2-det-header__tags"
94178
>
95179
<RTag
96-
v-for="r in regions"
180+
v-for="r in visibleRegions"
97181
:key="`r-${r}`"
98182
:text="r"
99183
tone="info"
@@ -108,6 +192,22 @@ const actions = useGameActions(() => props.rom);
108192
/>
109193
<RTag v-for="t in tags" :key="`t-${t}`" :text="t" size="small" />
110194
</span>
195+
196+
<span v-if="ratingChips.length" class="r-v2-det-header__ratings">
197+
<span
198+
v-for="chip in ratingChips"
199+
:key="chip.key"
200+
class="r-v2-det-header__rating-inline"
201+
:aria-label="`${chip.name} rating ${chip.value}`"
202+
>
203+
<img
204+
class="r-v2-det-header__rating-logo"
205+
:src="chip.logo"
206+
:alt="`${chip.name} logo`"
207+
/>
208+
<span class="r-v2-det-header__rating-value">{{ chip.value }}</span>
209+
</span>
210+
</span>
111211
</div>
112212

113213
<div v-if="rom.sibling_roms.length > 0" class="r-v2-det-header__versions">
@@ -183,6 +283,35 @@ const actions = useGameActions(() => props.rom);
183283
gap: 6px;
184284
}
185285
286+
.r-v2-det-header__ratings {
287+
display: inline-flex;
288+
align-items: center;
289+
flex-wrap: wrap;
290+
gap: 6px;
291+
flex-basis: 100%;
292+
}
293+
294+
.r-v2-det-header__rating-inline {
295+
display: inline-flex;
296+
align-items: center;
297+
gap: 4px;
298+
color: inherit;
299+
}
300+
301+
.r-v2-det-header__rating-logo {
302+
inline-size: 20px;
303+
block-size: 20px;
304+
object-fit: contain;
305+
flex-shrink: 0;
306+
}
307+
308+
.r-v2-det-header__rating-value {
309+
font-variant-numeric: tabular-nums;
310+
color: inherit;
311+
font-weight: inherit;
312+
font-size: inherit;
313+
}
314+
186315
.r-v2-det-header__versions {
187316
display: flex;
188317
align-items: center;

0 commit comments

Comments
 (0)