games: official logos for all 16 game trackers (self-hosted, one map in #lib/game-logos) shown by TrackerIcon everywhere games appear; coloured tile stays as fallback; logo credits on the artwork credits page
All checks were successful
Deploy production / deploy (push) Successful in 1m24s

This commit is contained in:
2026-10-25 18:35:21 +00:00
parent eba5f9eda3
commit 5d3e8d0261
20 changed files with 254 additions and 17 deletions

View File

@@ -1,27 +1,59 @@
<script lang="ts">
import { resolve } from '$app/paths';
import { gameLogos } from '#lib/game-logos.js';
import { defaultTrackerIcon, trackerIcons } from '#lib/tracker-icons.js';
// A coloured rounded tile with a white (or dark, on light tiles) inline-SVG icon.
// Same in light and dark themes: the tile carries its own background.
// Games with an official logo (#lib/game-logos) show it on a neutral near-black tile
// instead; if the image fails to load, the game's coloured tile comes back.
let { name, class: className = 'size-8' }: { name: string; class?: string } = $props();
const icon = $derived(trackerIcons[name] ?? defaultTrackerIcon);
const logo = $derived(gameLogos[name]);
let failed = $state(false);
$effect(() => {
void name;
failed = false;
});
</script>
<span
class="flex shrink-0 items-center justify-center rounded-lg shadow-sm ring-1 ring-black/10 ring-inset {className}"
style:background-image="linear-gradient(135deg, {icon.from}, {icon.to})"
style:color={icon.ink}
aria-hidden="true"
>
<svg
viewBox="0 0 24 24"
class={icon.fill ? 'size-[54%]' : 'size-[58%]'}
fill={icon.fill ? 'currentColor' : 'none'}
stroke={icon.fill ? 'none' : 'currentColor'}
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
{#if logo && !failed}
<span
class="logo-tile flex shrink-0 items-center justify-center overflow-hidden rounded-lg shadow-sm ring-1 ring-white/10 ring-inset {className}"
aria-hidden="true"
>
{#each icon.paths as d, i (i)}<path {d} />{/each}
</svg>
</span>
<img
src={resolve(logo.src as '/')}
alt=""
draggable="false"
decoding="async"
class={logo.bleed ? 'size-full object-cover' : 'size-[80%] object-contain'}
onerror={() => (failed = true)}
/>
</span>
{:else}
<span
class="flex shrink-0 items-center justify-center rounded-lg shadow-sm ring-1 ring-black/10 ring-inset {className}"
style:background-image="linear-gradient(135deg, {icon.from}, {icon.to})"
style:color={icon.ink}
aria-hidden="true"
>
<svg
viewBox="0 0 24 24"
class={icon.fill ? 'size-[54%]' : 'size-[58%]'}
fill={icon.fill ? 'currentColor' : 'none'}
stroke={icon.fill ? 'none' : 'currentColor'}
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
{#each icon.paths as d, i (i)}<path {d} />{/each}
</svg>
</span>
{/if}
<style>
.logo-tile {
background: linear-gradient(135deg, #1c1c1f, #0a0a0b);
}
</style>

106
src/lib/game-logos.ts Normal file
View File

@@ -0,0 +1,106 @@
// Official game logos for the game tracker: one self-hosted asset per game in
// static/game-logos/icons/, keyed like `gameTrackers[].icon` (game-<slug>). TrackerIcon uses
// this map everywhere games show up (header flyout, home and /trackers tiles, /start chips,
// the games switcher and each game page); a game without an entry keeps its tracker-icons tile.
// Square app icons/emblems are used instead of wide wordmarks so they stay legible at chip size.
export type GameLogo = {
/** Path under static/ (no leading slash), resolved with `asset`/`resolve` at render time. */
src: string;
/** Opaque square app icon: fill the whole tile instead of sitting padded on the dark tile. */
bleed?: boolean;
/** Where the asset came from (shown on /projects/game-tracker/credits). */
credit: string;
source: string;
};
const steamIcon = (appid: number) => `https://store.steampowered.com/app/${appid}/`;
const commons = (file: string) =>
`https://commons.wikimedia.org/wiki/File:${encodeURIComponent(file.replaceAll(' ', '_'))}`;
export const gameLogos: Record<string, GameLogo> = {
'game-osu': {
src: 'game-logos/icons/osu.svg',
credit: 'osu! logo (2016), Wikimedia Commons',
source: commons('Osu! Logo 2016.svg')
},
'game-cs2': {
src: 'game-logos/icons/cs2.png',
bleed: true,
credit: 'Counter-Strike 2 app icon, Steam',
source: steamIcon(730)
},
'game-pubg': {
src: 'game-logos/icons/pubg.png',
credit: 'PUBG: Battlegrounds icon, pubg.com',
source: 'https://pubg.com/'
},
'game-r6': {
src: 'game-logos/icons/r6.png',
bleed: true,
credit: 'Rainbow Six Siege app icon, Steam',
source: steamIcon(359550)
},
'game-tarkov': {
src: 'game-logos/icons/tarkov.png',
bleed: true,
credit: 'Escape from Tarkov app icon, Steam',
source: steamIcon(3932890)
},
'game-bo2': {
src: 'game-logos/icons/bo2.png',
bleed: true,
credit: 'Call of Duty: Black Ops II app icon, Steam',
source: steamIcon(202970)
},
'game-valorant': {
src: 'game-logos/icons/valorant.svg',
credit: 'Valorant logo (V mark, pink), Wikimedia Commons',
source: commons('Valorant logo - pink color version.svg')
},
'game-lol': {
src: 'game-logos/icons/lol.svg',
credit: 'League of Legends logomark, Simple Icons (Hextech gold)',
source: 'https://simpleicons.org/?q=league+of+legends'
},
'game-overwatch': {
src: 'game-logos/icons/overwatch.svg',
credit: 'Overwatch 2 logo (light emblem for dark tiles), Wikimedia Commons',
source: commons('Overwatch 2 logo.svg')
},
'game-minecraft': {
src: 'game-logos/icons/minecraft.svg',
credit: 'Minecraft grass block icon, Wikimedia Commons',
source: commons('Minecraft cube.svg')
},
'game-factorio': {
src: 'game-logos/icons/factorio.png',
credit: 'Factorio wheel logo, official Factorio press kit',
source: 'https://factorio.com/support/press-kit'
},
'game-ksp': {
src: 'game-logos/icons/ksp.png',
credit: 'Kerbal Space Program app icon, Steam',
source: steamIcon(220200)
},
'game-smash': {
src: 'game-logos/icons/smash.svg',
credit: 'Super Smash Bros. emblem, Wikimedia Commons (flame-coloured)',
source: commons('Smash Ball.svg')
},
'game-cubing': {
src: 'game-logos/icons/cubing.svg',
credit: 'World Cube Association logo, Wikimedia Commons',
source: commons('World Cube Association.svg')
},
'game-pokemon': {
src: 'game-logos/icons/pokemon.svg',
credit: 'Poké Ball icon, Wikimedia Commons',
source: commons('Poké Ball icon.svg')
},
'game-sot': {
src: 'game-logos/icons/sot.png',
credit: 'Sea of Thieves app icon (skull), Steam',
source: steamIcon(1172620)
}
};

View File

@@ -1,4 +1,6 @@
import { describe, expect, it } from 'vitest';
import { existsSync } from 'node:fs';
import { gameLogos } from './game-logos';
import { trackerIcons } from './tracker-icons';
import { gameTrackers, spottingTrackers, visibleTrackers } from './projects';
@@ -16,6 +18,17 @@ describe('tracker icons', () => {
expect(new Set(keys.map((k) => trackerIcons[k].to)).size).toBe(keys.length);
});
it('gives every game a self-hosted official logo', () => {
const keys = gameTrackers.map((g) => g.icon ?? g.name);
for (const key of keys) {
const logo = gameLogos[key];
expect(logo, key).toBeDefined();
expect(logo.src, key).toMatch(/^game-logos\/icons\/[a-z0-9-]+\.(svg|png)$/);
expect(existsSync(`static/${logo.src}`), logo.src).toBe(true);
}
expect(Object.keys(gameLogos).sort()).toEqual([...keys].sort());
});
it('links every game to its own page under /trackers/games', () => {
const slugs = gameTrackers.map((g) => g.href.replace('/trackers/games/', ''));
for (const slug of slugs) expect(slug).toMatch(/^[a-z0-9-]+$/);