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
All checks were successful
Deploy production / deploy (push) Successful in 1m24s
This commit is contained in:
@@ -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
106
src/lib/game-logos.ts
Normal 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)
|
||||
}
|
||||
};
|
||||
@@ -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-]+$/);
|
||||
|
||||
Reference in New Issue
Block a user