trackers: group cars, trains, planes and wildlife under one spotting tracker with /trackers/spotting landing page and category switcher
All checks were successful
Deploy production / deploy (push) Successful in 1m9s
All checks were successful
Deploy production / deploy (push) Successful in 1m9s
This commit is contained in:
@@ -14,6 +14,7 @@ const staticPaths = [
|
||||
'/blog',
|
||||
'/lists',
|
||||
'/trackers/books',
|
||||
'/trackers/spotting',
|
||||
'/trackers/cars',
|
||||
'/trackers/cars/brands',
|
||||
'/trackers/planes',
|
||||
|
||||
8
src/routes/trackers/cars/+layout.svelte
Normal file
8
src/routes/trackers/cars/+layout.svelte
Normal file
@@ -0,0 +1,8 @@
|
||||
<script lang="ts">
|
||||
import SpottingSwitcher from '#lib/components/SpottingSwitcher.svelte';
|
||||
|
||||
let { children } = $props();
|
||||
</script>
|
||||
|
||||
<SpottingSwitcher />
|
||||
{@render children()}
|
||||
8
src/routes/trackers/planes/+layout.svelte
Normal file
8
src/routes/trackers/planes/+layout.svelte
Normal file
@@ -0,0 +1,8 @@
|
||||
<script lang="ts">
|
||||
import SpottingSwitcher from '#lib/components/SpottingSwitcher.svelte';
|
||||
|
||||
let { children } = $props();
|
||||
</script>
|
||||
|
||||
<SpottingSwitcher />
|
||||
{@render children()}
|
||||
32
src/routes/trackers/spotting/+page.server.ts
Normal file
32
src/routes/trackers/spotting/+page.server.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
import { carOverview } from '#lib/server/cars.js';
|
||||
import { planeOverview } from '#lib/server/planes.js';
|
||||
import { trainOverview } from '#lib/server/trains.js';
|
||||
import { wildlifeOverview } from '#lib/server/wildlife.js';
|
||||
import type { PageServerLoad } from './$types';
|
||||
|
||||
// Headline counts per spotting tracker. Each overview is cached for 10 minutes; a failing
|
||||
// one just hides its count instead of breaking the landing page.
|
||||
const safe = <T>(p: Promise<T>) => p.catch(() => null);
|
||||
|
||||
export const load: PageServerLoad = async () => {
|
||||
const [cars, trains, planes, wildlife] = await Promise.all([
|
||||
safe(carOverview()),
|
||||
safe(trainOverview()),
|
||||
safe(planeOverview()),
|
||||
safe(wildlifeOverview())
|
||||
]);
|
||||
const counts: Record<string, { value: number; label: string } | null> = {
|
||||
cars: cars ? { value: cars.stats.models, label: 'models' } : null,
|
||||
trains: trains ? { value: trains.stats.types, label: 'types' } : null,
|
||||
planes: planes ? { value: planes.stats.types, label: 'types' } : null,
|
||||
wildlife: wildlife ? { value: wildlife.stats.species, label: 'species' } : null
|
||||
};
|
||||
return {
|
||||
counts,
|
||||
seo: {
|
||||
title: 'Spotting',
|
||||
description:
|
||||
'Stan Runge’s spotting pokedexes: cars, trains, planes and wildlife, with specs, photos and how to recognise them.'
|
||||
}
|
||||
};
|
||||
};
|
||||
99
src/routes/trackers/spotting/+page.svelte
Normal file
99
src/routes/trackers/spotting/+page.svelte
Normal file
@@ -0,0 +1,99 @@
|
||||
<script lang="ts">
|
||||
import { resolve } from '$app/paths';
|
||||
import ArrowLeft from 'lucide-svelte/icons/arrow-left';
|
||||
import ArrowRight from 'lucide-svelte/icons/arrow-right';
|
||||
import TrackerIcon from '#lib/components/TrackerIcon.svelte';
|
||||
import { spottingTrackers } from '#lib/projects.js';
|
||||
import type { PageProps } from './$types';
|
||||
|
||||
let { data }: PageProps = $props();
|
||||
</script>
|
||||
|
||||
<div class="mx-auto max-w-6xl px-4 py-12 sm:px-6 sm:py-16">
|
||||
<a
|
||||
href={resolve('/trackers')}
|
||||
class="mb-6 inline-flex items-center gap-1 text-sm text-[var(--site-muted)] hover:text-foreground"
|
||||
><ArrowLeft size={14} aria-hidden="true" /> trackers</a
|
||||
>
|
||||
<h1 class="mb-3 flex items-center gap-3 text-4xl font-semibold tracking-tight sm:text-5xl">
|
||||
<TrackerIcon name="spotting" class="size-10 sm:size-12" />spotting
|
||||
</h1>
|
||||
<p class="mb-10 text-[var(--site-muted)]">
|
||||
Cars, trains, planes and wildlife I've spotted. Pick a pokedex.
|
||||
</p>
|
||||
<ul class="grid gap-3 sm:grid-cols-2 lg:grid-cols-4">
|
||||
{#each spottingTrackers as t (t.name)}
|
||||
{@const count = data.counts[t.name]}
|
||||
<li>
|
||||
<a href={resolve(t.href as '/')} class="spot-tile flex h-full flex-col gap-4 p-5">
|
||||
<span class="flex items-start justify-between">
|
||||
<TrackerIcon name={t.name} class="spot-icon size-14" />
|
||||
<ArrowRight size={18} aria-hidden="true" class="spot-arrow text-[var(--site-muted)]" />
|
||||
</span>
|
||||
<span class="min-w-0">
|
||||
<span class="block text-xl font-semibold tracking-tight">{t.name}</span>
|
||||
{#if count}
|
||||
<span class="mt-0.5 block text-sm font-medium tabular-nums"
|
||||
>{count.value.toLocaleString('en-GB')}
|
||||
<span class="text-[var(--site-muted)]">{count.label}</span></span
|
||||
>
|
||||
{/if}
|
||||
<span class="mt-2 block text-sm text-muted-foreground">{t.subtitle}</span>
|
||||
</span>
|
||||
</a>
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.spot-tile {
|
||||
border: 1px solid color-mix(in oklab, var(--foreground) 9%, transparent);
|
||||
border-radius: 0.9rem;
|
||||
background: color-mix(in oklab, var(--foreground) 2%, transparent);
|
||||
outline: none;
|
||||
transition:
|
||||
transform 160ms cubic-bezier(0.2, 0.8, 0.2, 1),
|
||||
background-color 160ms ease,
|
||||
border-color 160ms ease,
|
||||
box-shadow 160ms ease;
|
||||
}
|
||||
.spot-tile:hover {
|
||||
background: color-mix(in oklab, var(--foreground) 6%, transparent);
|
||||
border-color: color-mix(in oklab, var(--foreground) 22%, transparent);
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 12px 28px -16px rgb(0 0 0 / 0.55);
|
||||
}
|
||||
.spot-tile:active {
|
||||
transform: translateY(0);
|
||||
box-shadow: none;
|
||||
}
|
||||
.spot-tile:focus-visible {
|
||||
outline: 2px solid var(--foreground);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
.spot-tile :global(.spot-icon),
|
||||
.spot-tile :global(.spot-arrow) {
|
||||
transition: transform 160ms cubic-bezier(0.2, 0.8, 0.2, 1);
|
||||
}
|
||||
.spot-tile:hover :global(.spot-icon) {
|
||||
transform: scale(1.06) rotate(-3deg);
|
||||
}
|
||||
.spot-tile:hover :global(.spot-arrow) {
|
||||
transform: translateX(2px);
|
||||
}
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.spot-tile,
|
||||
.spot-tile :global(.spot-icon),
|
||||
.spot-tile :global(.spot-arrow) {
|
||||
transition:
|
||||
background-color 160ms ease,
|
||||
border-color 160ms ease;
|
||||
}
|
||||
.spot-tile:hover,
|
||||
.spot-tile:hover :global(.spot-icon),
|
||||
.spot-tile:hover :global(.spot-arrow) {
|
||||
transform: none;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
8
src/routes/trackers/trains/+layout.svelte
Normal file
8
src/routes/trackers/trains/+layout.svelte
Normal file
@@ -0,0 +1,8 @@
|
||||
<script lang="ts">
|
||||
import SpottingSwitcher from '#lib/components/SpottingSwitcher.svelte';
|
||||
|
||||
let { children } = $props();
|
||||
</script>
|
||||
|
||||
<SpottingSwitcher />
|
||||
{@render children()}
|
||||
8
src/routes/trackers/wildlife/+layout.svelte
Normal file
8
src/routes/trackers/wildlife/+layout.svelte
Normal file
@@ -0,0 +1,8 @@
|
||||
<script lang="ts">
|
||||
import SpottingSwitcher from '#lib/components/SpottingSwitcher.svelte';
|
||||
|
||||
let { children } = $props();
|
||||
</script>
|
||||
|
||||
<SpottingSwitcher />
|
||||
{@render children()}
|
||||
Reference in New Issue
Block a user