Add pagination and game cards to about page
This commit is contained in:
1 parent
cc085be17a
commit
5d04df8d54
5 files changed
+207
-45
No files matched your search
@@ -0,0 +1,115 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import Icon from '@iconify/svelte';
|
||||||
|
import type { Game } from '#lib/content';
|
||||||
|
|
||||||
|
let { game }: { game: Game } = $props();
|
||||||
|
|
||||||
|
const storeNames: Record<string, { label: string; icon: string }> = {
|
||||||
|
'store.steampowered.com': { label: 'Steam', icon: 'mdi:steam' }
|
||||||
|
};
|
||||||
|
|
||||||
|
const store = $derived.by(() => {
|
||||||
|
const host = new URL(game.url).hostname;
|
||||||
|
return storeNames[host] ?? { label: host.replace(/^www\./, ''), icon: 'mdi:web' };
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<a class="game-card" href={game.url} target="_blank" rel="noopener noreferrer">
|
||||||
|
<div class="cover">
|
||||||
|
<img src={game.image} alt="" loading="lazy" />
|
||||||
|
<span class="play"><Icon icon="mdi:play" width="16" /> Play</span>
|
||||||
|
</div>
|
||||||
|
<div class="details">
|
||||||
|
<span class="name">{game.name}</span>
|
||||||
|
<span class="store"><Icon icon={store.icon} width="14" />{store.label}</span>
|
||||||
|
</div>
|
||||||
|
</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);
|
||||||
|
}
|
||||||
|
|
||||||
|
.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>
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import type { RatedTitle } from '#lib/content';
|
||||||
|
import type { MediaDetails } from '#lib/media/types';
|
||||||
|
import Pagination from '#lib/components/ui/Pagination.svelte';
|
||||||
|
import MediaCard from './MediaCard.svelte';
|
||||||
|
|
||||||
|
let {
|
||||||
|
items,
|
||||||
|
mediaDetails,
|
||||||
|
kind
|
||||||
|
}: { items: RatedTitle[]; mediaDetails: Record<string, MediaDetails>; kind: 'show' | 'movie' } = $props();
|
||||||
|
|
||||||
|
const itemsPerPage = 12;
|
||||||
|
|
||||||
|
let currentPage = $state(1);
|
||||||
|
|
||||||
|
const totalPages = $derived(Math.max(1, Math.ceil(items.length / itemsPerPage)));
|
||||||
|
const visibleItems = $derived(items.slice((currentPage - 1) * itemsPerPage, currentPage * itemsPerPage));
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="media-grid">
|
||||||
|
{#each visibleItems as item (item.url)}
|
||||||
|
<MediaCard {item} details={mediaDetails[item.url]} {kind} />
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Pagination bind:currentPage {totalPages} />
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.media-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
|
||||||
|
gap: 1rem 0.85rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
@container (max-width: 520px) {
|
||||||
|
.media-grid {
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(110px, 1fr));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
let { currentPage = $bindable(), totalPages }: { currentPage: number; totalPages: number } = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#if totalPages > 1}
|
||||||
|
<nav class="pagination" aria-label="Pagination">
|
||||||
|
<button disabled={currentPage === 1} onclick={() => currentPage--}>< prev</button>
|
||||||
|
<span class="muted">page {currentPage} of {totalPages}</span>
|
||||||
|
<button disabled={currentPage === totalPages} onclick={() => currentPage++}>next ></button>
|
||||||
|
</nav>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.pagination {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
gap: 1rem;
|
||||||
|
margin-top: 1.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
padding: 0.1rem 0.6rem;
|
||||||
|
background: none;
|
||||||
|
border: 1px solid color-mix(in srgb, var(--foreground) 25%, transparent);
|
||||||
|
color: var(--muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
button:hover:not(:disabled) {
|
||||||
|
border-color: var(--accent);
|
||||||
|
color: var(--foreground);
|
||||||
|
}
|
||||||
|
|
||||||
|
button:disabled {
|
||||||
|
opacity: 0.4;
|
||||||
|
cursor: default;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -5,9 +5,9 @@
|
|||||||
import SectionNav from '#lib/components/ui/SectionNav.svelte';
|
import SectionNav from '#lib/components/ui/SectionNav.svelte';
|
||||||
import Section from '#lib/components/ui/Section.svelte';
|
import Section from '#lib/components/ui/Section.svelte';
|
||||||
import Panel from '#lib/components/ui/Panel.svelte';
|
import Panel from '#lib/components/ui/Panel.svelte';
|
||||||
import LinkButton from '#lib/components/ui/LinkButton.svelte';
|
|
||||||
import AnimeSection from '#lib/components/about/AnimeSection.svelte';
|
import AnimeSection from '#lib/components/about/AnimeSection.svelte';
|
||||||
import MediaCard from '#lib/components/about/MediaCard.svelte';
|
import MediaGrid from '#lib/components/about/MediaGrid.svelte';
|
||||||
|
import GameCard from '#lib/components/about/GameCard.svelte';
|
||||||
import type { PageProps } from './$types';
|
import type { PageProps } from './$types';
|
||||||
|
|
||||||
let { data }: PageProps = $props();
|
let { data }: PageProps = $props();
|
||||||
@@ -46,23 +46,16 @@
|
|||||||
</Section>
|
</Section>
|
||||||
|
|
||||||
<Section title="Games" id="games">
|
<Section title="Games" id="games">
|
||||||
<div class="card-grid" data-image-group>
|
<div class="game-grid">
|
||||||
{#each about.games as game (game.name)}
|
{#each about.games as game (game.name)}
|
||||||
<Panel title={game.name}>
|
<GameCard {game} />
|
||||||
<img class="game-image" src={game.image} alt={game.name} loading="lazy" />
|
|
||||||
<LinkButton href={game.url} icon="proicons:game">Play</LinkButton>
|
|
||||||
</Panel>
|
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
</Section>
|
</Section>
|
||||||
|
|
||||||
{#each mediaGroups as group (group.id)}
|
{#each mediaGroups as group (group.id)}
|
||||||
<Section title={group.title} id={group.id}>
|
<Section title={group.title} id={group.id}>
|
||||||
<div class="media-grid">
|
<MediaGrid items={group.items} mediaDetails={data.mediaDetails} kind={group.kind} />
|
||||||
{#each group.items as item (item.name)}
|
|
||||||
<MediaCard {item} details={data.mediaDetails[item.url]} kind={group.kind} />
|
|
||||||
{/each}
|
|
||||||
</div>
|
|
||||||
</Section>
|
</Section>
|
||||||
{/each}
|
{/each}
|
||||||
|
|
||||||
@@ -78,21 +71,15 @@
|
|||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.media-grid {
|
.game-grid {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
|
grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
|
||||||
gap: 1rem 0.85rem;
|
gap: 1rem 0.85rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
@container (max-width: 520px) {
|
@container (max-width: 520px) {
|
||||||
.media-grid {
|
.game-grid {
|
||||||
grid-template-columns: repeat(auto-fill, minmax(110px, 1fr));
|
grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.game-image {
|
|
||||||
width: 100%;
|
|
||||||
aspect-ratio: 16 / 9;
|
|
||||||
object-fit: cover;
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
@@ -3,6 +3,7 @@
|
|||||||
import { blogSettings } from '#lib/content';
|
import { blogSettings } from '#lib/content';
|
||||||
import { formatPostDate } from '#lib/blog/date';
|
import { formatPostDate } from '#lib/blog/date';
|
||||||
import PageHeader from '#lib/components/ui/PageHeader.svelte';
|
import PageHeader from '#lib/components/ui/PageHeader.svelte';
|
||||||
|
import Pagination from '#lib/components/ui/Pagination.svelte';
|
||||||
|
|
||||||
let { data }: PageProps = $props();
|
let { data }: PageProps = $props();
|
||||||
|
|
||||||
@@ -72,13 +73,7 @@
|
|||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{#if totalPages > 1}
|
<Pagination bind:currentPage {totalPages} />
|
||||||
<nav class="pagination">
|
|
||||||
<button disabled={currentPage === 1} onclick={() => currentPage--}>< prev</button>
|
|
||||||
<span class="muted">page {currentPage} of {totalPages}</span>
|
|
||||||
<button disabled={currentPage === totalPages} onclick={() => currentPage++}>next ></button>
|
|
||||||
</nav>
|
|
||||||
{/if}
|
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -112,16 +107,14 @@
|
|||||||
margin: 0.75rem 0 1.25rem;
|
margin: 0.75rem 0 1.25rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tags button,
|
.tags button {
|
||||||
.pagination button {
|
|
||||||
padding: 0.1rem 0.6rem;
|
padding: 0.1rem 0.6rem;
|
||||||
background: none;
|
background: none;
|
||||||
border: 1px solid color-mix(in srgb, var(--foreground) 25%, transparent);
|
border: 1px solid color-mix(in srgb, var(--foreground) 25%, transparent);
|
||||||
color: var(--muted);
|
color: var(--muted);
|
||||||
}
|
}
|
||||||
|
|
||||||
.tags button:hover,
|
.tags button:hover {
|
||||||
.pagination button:hover:not(:disabled) {
|
|
||||||
border-color: var(--accent);
|
border-color: var(--accent);
|
||||||
color: var(--foreground);
|
color: var(--foreground);
|
||||||
}
|
}
|
||||||
@@ -177,16 +170,4 @@
|
|||||||
font-size: 0.8rem;
|
font-size: 0.8rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.pagination {
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
gap: 1rem;
|
|
||||||
margin-top: 1.25rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.pagination button:disabled {
|
|
||||||
opacity: 0.4;
|
|
||||||
cursor: default;
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
Reference in new issue
Block a user