213 lines
4.5 KiB
Svelte
213 lines
4.5 KiB
Svelte
<script lang="ts">
|
|
import Icon from '@iconify/svelte';
|
|
import type { Game } from '#lib/content';
|
|
import { storeFor } from '#lib/library/stores';
|
|
|
|
let { game }: { game: Game } = $props();
|
|
|
|
const link = $derived(game.url ?? game.igdbUrl ?? '');
|
|
const store = $derived(storeFor(link));
|
|
const releaseYear = $derived(game.released?.slice(0, 4) ?? null);
|
|
const metaLine = $derived([releaseYear, game.developers[0]].filter(Boolean).join(' · '));
|
|
</script>
|
|
|
|
<a class="game-card" href={link || undefined} target="_blank" rel="noopener noreferrer" title={game.summary ?? game.name}>
|
|
<div class="cover">
|
|
{#if game.image}
|
|
<img src={game.image} alt="" loading="lazy" />
|
|
{:else}
|
|
<span class="placeholder"><Icon icon="mdi:gamepad-variant-outline" width="36" /></span>
|
|
{/if}
|
|
{#if game.favorite}
|
|
<span class="favorite"><Icon icon="mdi:star" width="13" />favorite</span>
|
|
{/if}
|
|
{#if game.rating !== undefined}
|
|
<span class="score"><Icon icon="mdi:star" width="13" />{game.rating}/10</span>
|
|
{:else if game.score !== null}
|
|
<span class="score community" title="IGDB rating"><Icon icon="mdi:star-outline" width="13" />{game.score}</span>
|
|
{/if}
|
|
{#if game.status === 'playing'}
|
|
<span class="status playing"><Icon icon="mdi:play" width="13" />playing</span>
|
|
{:else if game.status === 'planning'}
|
|
<span class="status planning"><Icon icon="mdi:bookmark-outline" width="13" />planning</span>
|
|
{:else if game.status === 'replayable'}
|
|
<span class="status replayable"><Icon icon="mdi:repeat" width="13" />replayable</span>
|
|
{/if}
|
|
{#if link}
|
|
<span class="play"><Icon icon="mdi:play" width="16" /> Play</span>
|
|
{/if}
|
|
</div>
|
|
<div class="details">
|
|
<span class="name">{game.name}</span>
|
|
{#if link}
|
|
<span class="store"><Icon icon={store.icon} width="14" />{store.label}</span>
|
|
{/if}
|
|
</div>
|
|
{#if metaLine}
|
|
<span class="meta">{metaLine}</span>
|
|
{/if}
|
|
{#if game.genres.length}
|
|
<span class="meta">{game.genres.slice(0, 3).join(' · ')}</span>
|
|
{/if}
|
|
</a>
|
|
|
|
<style>
|
|
.game-card {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 0.45rem;
|
|
min-width: 0;
|
|
}
|
|
|
|
.cover {
|
|
position: relative;
|
|
aspect-ratio: 16 / 9;
|
|
overflow: hidden;
|
|
background: var(--dark-background);
|
|
border: 1px solid color-mix(in srgb, var(--foreground) 20%, transparent);
|
|
transition: border-color 0.15s ease;
|
|
}
|
|
|
|
.game-card:hover .cover {
|
|
border-color: var(--accent);
|
|
}
|
|
|
|
.cover img {
|
|
width: 100%;
|
|
height: 100%;
|
|
object-fit: cover;
|
|
transition: transform 0.3s ease;
|
|
}
|
|
|
|
.game-card:hover .cover img {
|
|
transform: scale(1.05);
|
|
}
|
|
|
|
.placeholder {
|
|
display: grid;
|
|
place-items: center;
|
|
height: 100%;
|
|
color: var(--muted);
|
|
}
|
|
|
|
.score {
|
|
position: absolute;
|
|
top: 0.4rem;
|
|
right: 0.4rem;
|
|
display: inline-flex;
|
|
align-items: center;
|
|
gap: 0.2rem;
|
|
padding: 0 0.4rem;
|
|
background: var(--background);
|
|
color: var(--yellow);
|
|
font-size: 0.78rem;
|
|
}
|
|
|
|
.meta {
|
|
overflow: hidden;
|
|
color: var(--muted);
|
|
font-size: 0.75rem;
|
|
white-space: nowrap;
|
|
text-overflow: ellipsis;
|
|
}
|
|
|
|
.favorite {
|
|
position: absolute;
|
|
top: 0.4rem;
|
|
left: 0.4rem;
|
|
display: inline-flex;
|
|
align-items: center;
|
|
gap: 0.2rem;
|
|
padding: 0 0.4rem;
|
|
background: var(--yellow);
|
|
color: var(--background);
|
|
font-size: 0.75rem;
|
|
font-weight: 700;
|
|
}
|
|
|
|
.score.community {
|
|
color: var(--muted);
|
|
}
|
|
|
|
.status {
|
|
position: absolute;
|
|
bottom: 0.4rem;
|
|
left: 0.4rem;
|
|
display: inline-flex;
|
|
align-items: center;
|
|
gap: 0.2rem;
|
|
padding: 0 0.4rem;
|
|
color: var(--background);
|
|
font-size: 0.75rem;
|
|
font-weight: 700;
|
|
}
|
|
|
|
.status.playing {
|
|
background: var(--blue);
|
|
}
|
|
|
|
.status.planning {
|
|
background: var(--magenta);
|
|
}
|
|
|
|
.status.replayable {
|
|
background: var(--cyan);
|
|
}
|
|
|
|
.play {
|
|
position: absolute;
|
|
right: 0.5rem;
|
|
bottom: 0.5rem;
|
|
display: inline-flex;
|
|
align-items: center;
|
|
gap: 0.25rem;
|
|
padding: 0.1rem 0.55rem;
|
|
background: var(--accent);
|
|
color: var(--background);
|
|
font-size: 0.8rem;
|
|
opacity: 0;
|
|
transform: translateY(4px);
|
|
transition:
|
|
opacity 0.2s ease,
|
|
transform 0.2s ease;
|
|
}
|
|
|
|
.game-card:hover .play,
|
|
.game-card:focus-visible .play {
|
|
opacity: 1;
|
|
transform: translateY(0);
|
|
}
|
|
|
|
.details {
|
|
display: flex;
|
|
align-items: baseline;
|
|
justify-content: space-between;
|
|
gap: 0.5rem;
|
|
min-width: 0;
|
|
}
|
|
|
|
.name {
|
|
overflow: hidden;
|
|
color: var(--accent);
|
|
font-weight: 700;
|
|
white-space: nowrap;
|
|
text-overflow: ellipsis;
|
|
}
|
|
|
|
.store {
|
|
display: inline-flex;
|
|
flex-shrink: 0;
|
|
align-items: center;
|
|
gap: 0.25rem;
|
|
color: var(--muted);
|
|
font-size: 0.75rem;
|
|
}
|
|
|
|
@media (prefers-reduced-motion: reduce) {
|
|
.cover img,
|
|
.play {
|
|
transition: none;
|
|
}
|
|
}
|
|
</style>
|