Expand IGDB data: collections, game modes, themes, perspectives, videos, websites, age ratings, similar games; add backfill command

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Mark
2026-06-01 00:24:05 +03:00
co-authored by Claude Sonnet 4.6
parent 0eab3e8dbf
commit 2a724c0ae2
21 changed files with 768 additions and 57 deletions
+135 -2
View File
@@ -236,7 +236,7 @@
{{-- Companies --}}
@if($game->companies->isNotEmpty())
<div class="text-sm text-slate-500 space-y-1">
<div class="text-sm text-slate-500 space-y-1 mb-4">
@php
$developers = $game->companies->where('pivot.role', 'developer');
$publishers = $game->companies->where('pivot.role', 'publisher');
@@ -250,6 +250,87 @@
</div>
@endif
{{-- Extended metadata --}}
<div class="text-sm text-slate-400 space-y-2">
@if($game->collection)
<div class="flex gap-2">
<span class="text-slate-500 w-36 flex-shrink-0">Series / Collection</span>
<span class="text-slate-300">{{ $game->collection->name }}</span>
</div>
@endif
@if($game->gameModes->isNotEmpty())
<div class="flex gap-2">
<span class="text-slate-500 w-36 flex-shrink-0">Game Modes</span>
<span class="text-slate-300">{{ $game->gameModes->pluck('name')->join(', ') }}</span>
</div>
@endif
@if($game->themes->isNotEmpty())
<div class="flex gap-2">
<span class="text-slate-500 w-36 flex-shrink-0">Themes</span>
<span class="text-slate-300">{{ $game->themes->pluck('name')->join(', ') }}</span>
</div>
@endif
@if($game->playerPerspectives->isNotEmpty())
<div class="flex gap-2">
<span class="text-slate-500 w-36 flex-shrink-0">Perspective</span>
<span class="text-slate-300">{{ $game->playerPerspectives->pluck('name')->join(', ') }}</span>
</div>
@endif
@if($game->aggregated_rating)
<div class="flex gap-2 items-center">
<span class="text-slate-500 w-36 flex-shrink-0">Critics Score</span>
<span class="px-2 py-0.5 rounded text-xs font-bold
@if($game->aggregated_rating >= 75) bg-green-900/50 text-green-300
@elseif($game->aggregated_rating >= 50) bg-yellow-900/50 text-yellow-300
@else bg-red-900/50 text-red-300
@endif">
{{ round($game->aggregated_rating) }}/100
</span>
@if($game->aggregated_rating_count)
<span class="text-slate-600 text-xs">({{ $game->aggregated_rating_count }} {{ Str::plural('review', $game->aggregated_rating_count) }})</span>
@endif
</div>
@endif
@if($game->age_rating_pegi || $game->age_rating_esrb)
<div class="flex gap-2 items-center">
<span class="text-slate-500 w-36 flex-shrink-0">Age Ratings</span>
<div class="flex gap-2">
@if($game->age_rating_pegi)
<span class="px-2 py-0.5 bg-[#16161d] border border-white/10 rounded text-xs font-bold text-slate-300">PEGI {{ $game->age_rating_pegi }}</span>
@endif
@if($game->age_rating_esrb)
<span class="px-2 py-0.5 bg-[#16161d] border border-white/10 rounded text-xs font-bold text-slate-300">ESRB {{ $game->age_rating_esrb }}</span>
@endif
</div>
</div>
@endif
</div>
{{-- Websites --}}
@php
$importantCategories = [1, 13, 17, 16];
$featuredSites = $game->websites->whereIn('category', $importantCategories)->sortBy(fn($w) => array_search($w->category, $importantCategories));
@endphp
@if($featuredSites->isNotEmpty())
<div class="flex flex-wrap gap-2 mt-4">
@foreach($featuredSites as $site)
<a href="{{ $site->url }}" target="_blank" rel="noopener noreferrer"
class="inline-flex items-center gap-1.5 px-3 py-1.5 bg-[#16161d] border border-white/10 hover:border-purple-500/50 rounded-lg text-xs text-slate-300 hover:text-white transition-all">
@if($site->category === 13)
<svg class="w-3.5 h-3.5" viewBox="0 0 24 24" fill="currentColor"><path d="M11.979 0C5.678 0 .511 4.86.022 11.037l6.432 2.658c.545-.371 1.203-.59 1.912-.59.063 0 .125.004.187.006l2.861-4.142V8.91c0-2.495 2.028-4.524 4.524-4.524 2.494 0 4.524 2.029 4.524 4.524s-2.03 4.525-4.524 4.525h-.105l-4.076 2.911c0 .052.004.105.004.159 0 1.875-1.515 3.396-3.39 3.396-1.635 0-3.016-1.173-3.331-2.727L.436 15.27C1.862 20.307 6.486 24 11.979 24c6.627 0 11.999-5.373 11.999-12S18.606 0 11.979 0zM7.54 18.21l-1.473-.61c.262.543.714.999 1.314 1.25 1.297.539 2.793-.076 3.332-1.375.263-.63.264-1.319.005-1.949s-.75-1.121-1.377-1.383c-.624-.26-1.29-.249-1.878-.03l1.523.63c.956.4 1.409 1.5 1.009 2.455-.397.957-1.497 1.41-2.455 1.012H7.54zm11.415-9.303c0-1.662-1.353-3.015-3.015-3.015-1.665 0-3.015 1.353-3.015 3.015 0 1.665 1.35 3.015 3.015 3.015 1.663 0 3.015-1.35 3.015-3.015zm-5.273-.005c0-1.252 1.013-2.266 2.265-2.266 1.249 0 2.266 1.014 2.266 2.266 0 1.251-1.017 2.265-2.266 2.265-1.252 0-2.265-1.014-2.265-2.265z"/></svg>
@elseif($site->category === 17)
<svg class="w-3.5 h-3.5" viewBox="0 0 24 24" fill="currentColor"><path d="M12 0C5.37 0 0 5.37 0 12s5.37 12 12 12 12-5.37 12-12S18.63 0 12 0zm5.61 17.26c-.2.2-.52.2-.72 0l-4.89-4.89-4.89 4.89c-.2.2-.52.2-.72 0-.2-.2-.2-.52 0-.72l4.89-4.89-4.89-4.89c-.2-.2-.2-.52 0-.72.2-.2.52-.2.72 0l4.89 4.89 4.89-4.89c.2-.2.52-.2.72 0 .2.2.2.52 0 .72l-4.89 4.89 4.89 4.89c.2.2.2.52 0 .72z"/></svg>
@elseif($site->category === 16)
<svg class="w-3.5 h-3.5" viewBox="0 0 24 24" fill="currentColor"><path d="M0 0h24v24H0z" fill="none"/><path d="M21 6.5C21 4.6 19.5 3 17.5 3c-1.9 0-3.5 1.6-3.5 3.5C14 8.4 15.6 10 17.5 10c1.9 0 3.5-1.5 3.5-3.5zM6.5 3C4.6 3 3 4.6 3 6.5S4.6 10 6.5 10 10 8.4 10 6.5 8.4 3 6.5 3zM17.5 14c-1.9 0-3.5 1.6-3.5 3.5S15.6 21 17.5 21c1.9 0 3.5-1.5 3.5-3.5S19.4 14 17.5 14zm-11 0C4.6 14 3 15.6 3 17.5S4.6 21 6.5 21 10 19.4 10 17.5 8.4 14 6.5 14z"/></svg>
@else
<svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14"/></svg>
@endif
{{ $site->label }}
</a>
@endforeach
</div>
@endif
</div>
</div>
@@ -261,6 +342,52 @@
</div>
@endif
{{-- Storyline (only if different from summary) --}}
@if($game->storyline && $game->storyline !== $game->summary)
<div class="mt-8">
<h2 class="text-lg font-semibold text-white mb-3">Storyline</h2>
<p class="text-slate-400 leading-relaxed max-w-3xl">{{ $game->storyline }}</p>
</div>
@endif
{{-- Trailers --}}
@if($game->videos->isNotEmpty())
<div class="mt-10 pt-8 border-t border-white/5"
x-data="{
activeVideo: '{{ $game->videos->first()->video_id }}',
activeName: {{ json_encode($game->videos->first()->name ?? 'Trailer') }},
}">
<h2 class="text-lg font-semibold text-white mb-4">Videos</h2>
<div class="flex flex-col lg:flex-row gap-4">
<div class="flex-1 min-w-0">
<div class="aspect-video rounded-xl overflow-hidden bg-black">
<iframe
:src="'https://www.youtube.com/embed/' + activeVideo + '?rel=0'"
class="w-full h-full"
loading="lazy"
allowfullscreen
title="Game video"
></iframe>
</div>
<p class="text-sm text-slate-500 mt-2" x-text="activeName"></p>
</div>
@if($game->videos->count() > 1)
<div class="flex lg:flex-col gap-2 overflow-x-auto lg:overflow-y-auto lg:max-h-72 lg:w-48 flex-shrink-0">
@foreach($game->videos as $video)
<button @click="activeVideo = '{{ $video->video_id }}'; activeName = {{ json_encode($video->name ?? 'Video') }}"
class="flex-shrink-0 lg:flex-shrink rounded-lg overflow-hidden border-2 transition-all w-32 lg:w-full aspect-video bg-black"
:class="activeVideo === '{{ $video->video_id }}' ? 'border-purple-500' : 'border-transparent hover:border-white/20'">
<img src="https://img.youtube.com/vi/{{ $video->video_id }}/mqdefault.jpg"
alt="{{ $video->name ?? 'Video' }}"
class="w-full h-full object-cover">
</button>
@endforeach
</div>
@endif
</div>
</div>
@endif
{{-- Screenshots --}}
@if($game->screenshots->isNotEmpty())
<div class="mt-10 pt-8 border-t border-white/5"
@@ -554,7 +681,13 @@
@if($similarGames->isNotEmpty())
<div class="mt-10 pt-8 border-t border-white/5">
<h2 class="text-lg font-semibold text-white mb-4">You Might Also Like</h2>
<h2 class="text-lg font-semibold text-white mb-4">
@if($game->similarGames->isNotEmpty())
Similar Games
@else
You Might Also Like
@endif
</h2>
<div class="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-6 gap-4">
@foreach($similarGames as $similar)
<x-game-card :game="$similar" />