ui: separate trackers from projects; keep header dropdown on top
- /trackers index, Trackers section on the homepage and in the header nav; /projects lists only projects (tracker URLs unchanged) - header is its own z-50 layer and <main> is isolated, so maps, charts and sticky tables no longer paint over the nav dropdowns
This commit is contained in:
63
src/lib/components/CatalogGrid.svelte
Normal file
63
src/lib/components/CatalogGrid.svelte
Normal file
@@ -0,0 +1,63 @@
|
||||
<script lang="ts">
|
||||
// Card grid shared by the home page, /projects and /trackers.
|
||||
import { resolve } from '$app/paths';
|
||||
import ArrowRight from 'lucide-svelte/icons/arrow-right';
|
||||
import ExternalLink from 'lucide-svelte/icons/external-link';
|
||||
import * as Card from '#lib/components/ui/card/index.js';
|
||||
import ProjectIcon from '#lib/components/ProjectIcon.svelte';
|
||||
|
||||
type Item = { name: string; subtitle: string; href: string; external: boolean };
|
||||
let {
|
||||
items,
|
||||
subtitleClass = 'mt-1 block text-sm text-muted-foreground'
|
||||
}: { items: readonly Item[]; subtitleClass?: string } = $props();
|
||||
</script>
|
||||
|
||||
<ul class="grid gap-3 sm:grid-cols-2">
|
||||
{#each items as project (project.name)}
|
||||
<li>
|
||||
<Card.Root class="h-full py-0 transition-colors hover:bg-muted/50"
|
||||
><Card.Content class="p-0">
|
||||
{#if project.external}
|
||||
<a
|
||||
href={project.href}
|
||||
rel="external"
|
||||
class="flex min-h-28 items-start justify-between gap-4 p-5"
|
||||
>
|
||||
<span class="flex min-w-0 items-start gap-3">
|
||||
<span
|
||||
class="mt-0.5 flex size-10 shrink-0 items-center justify-center rounded-md border border-border bg-[var(--site-surface)] text-[var(--site-muted)]"
|
||||
>
|
||||
<ProjectIcon name={project.name} class="size-5" />
|
||||
</span>
|
||||
<span class="min-w-0"
|
||||
><span class="block font-medium">{project.name}</span><span class={subtitleClass}
|
||||
>{project.subtitle}</span
|
||||
></span
|
||||
>
|
||||
</span><ExternalLink size={18} aria-hidden="true" class="text-[var(--site-muted)]" />
|
||||
</a>
|
||||
{:else}
|
||||
<a
|
||||
href={resolve(project.href as '/')}
|
||||
class="flex min-h-28 items-start justify-between gap-4 p-5"
|
||||
>
|
||||
<span class="flex min-w-0 items-start gap-3">
|
||||
<span
|
||||
class="mt-0.5 flex size-10 shrink-0 items-center justify-center rounded-md border border-border bg-[var(--site-surface)] text-[var(--site-muted)]"
|
||||
>
|
||||
<ProjectIcon name={project.name} class="size-5" />
|
||||
</span>
|
||||
<span class="min-w-0"
|
||||
><span class="block font-medium">{project.name}</span><span class={subtitleClass}
|
||||
>{project.subtitle}</span
|
||||
></span
|
||||
>
|
||||
</span><ArrowRight size={18} aria-hidden="true" class="text-[var(--site-muted)]" />
|
||||
</a>
|
||||
{/if}
|
||||
</Card.Content></Card.Root
|
||||
>
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
@@ -15,6 +15,7 @@
|
||||
import House from 'lucide-svelte/icons/house';
|
||||
import Globe2 from 'lucide-svelte/icons/globe-2';
|
||||
import Languages from 'lucide-svelte/icons/languages';
|
||||
import ListChecks from 'lucide-svelte/icons/list-checks';
|
||||
import Music2 from 'lucide-svelte/icons/music-2';
|
||||
import PawPrint from 'lucide-svelte/icons/paw-print';
|
||||
import Trophy from 'lucide-svelte/icons/trophy';
|
||||
@@ -32,7 +33,8 @@
|
||||
'language learner': Languages,
|
||||
'money tracker': Wallet,
|
||||
'cs prices': CandlestickChart,
|
||||
'house tracker': House
|
||||
'house tracker': House,
|
||||
lists: ListChecks
|
||||
};
|
||||
|
||||
const Icon = $derived(icons[name as keyof typeof icons] ?? FolderKanban);
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
// Real projects (tools and experiments). Personal data trackers live in `trackers`.
|
||||
export const projects = [
|
||||
{
|
||||
name: 'vash esports',
|
||||
@@ -5,18 +6,6 @@ export const projects = [
|
||||
href: 'https://esports.vash.software',
|
||||
external: true
|
||||
},
|
||||
{
|
||||
name: 'game tracker',
|
||||
subtitle: 'every pokemon in every game, and call of duty camo mastery',
|
||||
href: '/projects/game-tracker',
|
||||
external: false
|
||||
},
|
||||
{
|
||||
name: 'animaldex',
|
||||
subtitle: 'add neighborhood pets to the world map',
|
||||
href: '/projects/animaldex',
|
||||
external: false
|
||||
},
|
||||
{
|
||||
name: 'osu! map finder',
|
||||
subtitle: 'find osu! maps for Spotify songs/albums/playlists',
|
||||
@@ -40,6 +29,16 @@ export const projects = [
|
||||
subtitle: 'Dutch, Korean and English, from zero to fluent',
|
||||
href: '/projects/language-learner',
|
||||
external: false
|
||||
}
|
||||
] as const;
|
||||
|
||||
// Trackers keep their existing /projects/... URLs (no moves, so no redirects needed).
|
||||
export const trackers = [
|
||||
{
|
||||
name: 'game tracker',
|
||||
subtitle: 'every pokemon in every game, and call of duty camo mastery',
|
||||
href: '/projects/game-tracker',
|
||||
external: false
|
||||
},
|
||||
{
|
||||
name: 'money tracker',
|
||||
@@ -58,5 +57,17 @@ export const projects = [
|
||||
subtitle: 'Counter-Strike market prices across time',
|
||||
href: '/projects/cs-prices',
|
||||
external: false
|
||||
},
|
||||
{
|
||||
name: 'animaldex',
|
||||
subtitle: 'add neighborhood pets to the world map',
|
||||
href: '/projects/animaldex',
|
||||
external: false
|
||||
},
|
||||
{
|
||||
name: 'lists',
|
||||
subtitle: 'books, games, places and other things worth keeping',
|
||||
href: '/lists',
|
||||
external: false
|
||||
}
|
||||
] as const;
|
||||
|
||||
@@ -32,9 +32,14 @@ const publicPages: Record<string, { title: string; description: string }> = {
|
||||
'/projects': {
|
||||
title: 'Projects',
|
||||
description:
|
||||
'Software projects by Stan Runge, including Game Tracker, Animaldex and Derby Lens.'
|
||||
'Software projects by Stan Runge, including osu! map finder, Derby Lens and Language Learner.'
|
||||
},
|
||||
'/blog': { title: 'Blog', description: 'Articles and notes by Stan Runge.' },
|
||||
'/trackers': {
|
||||
title: 'Trackers',
|
||||
description:
|
||||
'Stan Runge’s trackers: games, money, houses, Counter-Strike prices, Animaldex and lists.'
|
||||
},
|
||||
'/lists': { title: 'Lists', description: 'Lists collected and maintained by Stan Runge.' },
|
||||
'/projects/game-tracker': {
|
||||
title: 'Game Tracker',
|
||||
|
||||
@@ -17,6 +17,7 @@ export function getTitle(path: string, environment: SiteEnvironment = null): str
|
||||
const titles: Record<string, string> = {
|
||||
'/': 'Home',
|
||||
'/projects': 'Projects',
|
||||
'/trackers': 'Trackers',
|
||||
'/blog': 'Blog',
|
||||
'/lists': 'Lists',
|
||||
'/login': 'Sign in',
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
import { getSeo } from '#lib/seo.js';
|
||||
import CopyButton from '#lib/components/CopyButton.svelte';
|
||||
import ProjectIcon from '#lib/components/ProjectIcon.svelte';
|
||||
import { projects } from '#lib/projects.js';
|
||||
import { projects, trackers } from '#lib/projects.js';
|
||||
import { Button, buttonVariants } from '#lib/components/ui/button/index.js';
|
||||
import * as Sheet from '#lib/components/ui/sheet/index.js';
|
||||
import ArrowRight from 'lucide-svelte/icons/arrow-right';
|
||||
@@ -31,15 +31,24 @@
|
||||
let { data, children } = $props();
|
||||
const seo = $derived(getSeo(page.url.pathname, page.data, data.environment));
|
||||
let menuOpen = $state(false);
|
||||
let projectsMenuOpen = $state(false);
|
||||
// Which header dropdown is open (desktop): '/projects', '/trackers' or none.
|
||||
let openMenu = $state<string | null>(null);
|
||||
type ThemePreference = 'dark' | 'system' | 'light';
|
||||
let theme = $state<ThemePreference>('dark');
|
||||
const navigation = [
|
||||
{ label: 'home', href: '/' },
|
||||
{ label: 'projects', href: '/projects' },
|
||||
{ label: 'trackers', href: '/trackers' },
|
||||
{ label: 'blog', href: '/blog' },
|
||||
{ label: 'lists', href: '/lists' }
|
||||
] as const;
|
||||
const menus: Record<
|
||||
string,
|
||||
{ label: string; items: readonly (typeof projects | typeof trackers)[number][] }
|
||||
> = {
|
||||
'/projects': { label: 'projects', items: projects },
|
||||
'/trackers': { label: 'trackers', items: trackers }
|
||||
};
|
||||
const socialLinks = [
|
||||
{ icon: SiMail, label: 'email', href: 'mailto:stan@stanrunge.dev' },
|
||||
{ icon: SiGithub, label: 'github', href: 'https://github.com/stanrunge' },
|
||||
@@ -102,14 +111,14 @@
|
||||
return page.url.pathname === href || (href !== '/' && page.url.pathname.startsWith(href + '/'));
|
||||
}
|
||||
|
||||
function handleProjectsPointerEnter(event: PointerEvent) {
|
||||
function handleMenuPointerEnter(event: PointerEvent, href: string) {
|
||||
if (event.pointerType === 'touch') return;
|
||||
projectsMenuOpen = true;
|
||||
openMenu = href;
|
||||
}
|
||||
|
||||
function handleProjectsPointerLeave(event: PointerEvent) {
|
||||
function handleMenuPointerLeave(event: PointerEvent) {
|
||||
if (event.pointerType === 'touch') return;
|
||||
projectsMenuOpen = false;
|
||||
openMenu = null;
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -170,7 +179,14 @@
|
||||
class="sr-only focus:not-sr-only focus:absolute focus:z-50 focus:bg-[var(--site-bg)] focus:px-4 focus:py-2"
|
||||
>skip to content</a
|
||||
>
|
||||
<header class="border-b border-border bg-background/95 backdrop-blur">
|
||||
<!--
|
||||
Stacking: backdrop-blur makes the header its own stacking context, so the dropdown's z-50 only
|
||||
counts inside it. Without a z-index of its own the header then sits at z-auto/0 and anything
|
||||
positioned later in <main> (maps, chart tooltips, sticky headers) paints over the dropdown.
|
||||
Fix: lift the header (relative z-50) and isolate <main> so page z-indexes stay inside it.
|
||||
Dialogs/sheets/popovers are portalled to <body> and still render above both.
|
||||
-->
|
||||
<header class="relative z-50 border-b border-border bg-background/95 backdrop-blur">
|
||||
<div class="mx-auto flex max-w-6xl items-center gap-3 px-4 py-3 sm:px-6">
|
||||
<a
|
||||
href={resolve('/')}
|
||||
@@ -190,39 +206,40 @@
|
||||
</a>
|
||||
<nav class="ml-auto hidden items-center gap-1 md:flex" aria-label="Main navigation">
|
||||
{#each navigation as item (item.href)}
|
||||
{#if item.href === '/projects'}
|
||||
{#if menus[item.href]}
|
||||
{@const menu = menus[item.href]}
|
||||
<div
|
||||
class="relative"
|
||||
role="group"
|
||||
aria-label="Projects menu"
|
||||
onpointerenter={handleProjectsPointerEnter}
|
||||
onpointerleave={handleProjectsPointerLeave}
|
||||
aria-label={`${menu.label} menu`}
|
||||
onpointerenter={(event) => handleMenuPointerEnter(event, item.href)}
|
||||
onpointerleave={handleMenuPointerLeave}
|
||||
>
|
||||
<Button
|
||||
href={resolve('projects')}
|
||||
href={resolve(item.href)}
|
||||
variant={current(item.href) ? 'secondary' : 'ghost'}
|
||||
size="sm"
|
||||
class="cursor-pointer gap-1"
|
||||
aria-label="Browse projects"
|
||||
aria-label={`Browse ${menu.label}`}
|
||||
aria-haspopup="true"
|
||||
aria-expanded={projectsMenuOpen}
|
||||
onclick={() => (projectsMenuOpen = false)}
|
||||
aria-expanded={openMenu === item.href}
|
||||
onclick={() => (openMenu = null)}
|
||||
onkeydown={(event) => {
|
||||
if (event.key === 'Escape') projectsMenuOpen = false;
|
||||
if (event.key === 'Escape') openMenu = null;
|
||||
}}>{item.label}<ChevronDown size={15} /></Button
|
||||
>
|
||||
{#if projectsMenuOpen}
|
||||
{#if openMenu === item.href}
|
||||
<div
|
||||
class="absolute top-full left-0 z-50 max-h-[70vh] w-72 overflow-y-auto rounded-md border border-border bg-popover p-1 text-popover-foreground shadow-md"
|
||||
aria-label="Projects"
|
||||
aria-label={menu.label}
|
||||
>
|
||||
<a
|
||||
href={resolve('projects')}
|
||||
href={resolve(item.href)}
|
||||
class="flex items-center rounded-sm px-2 py-1.5 text-sm font-medium outline-none hover:bg-accent hover:text-accent-foreground focus-visible:bg-accent"
|
||||
>all projects <ArrowRight size={14} class="ml-auto" /></a
|
||||
>all {menu.label} <ArrowRight size={14} class="ml-auto" /></a
|
||||
>
|
||||
<div class="my-1 h-px bg-border"></div>
|
||||
{#each projects as project (project.name)}
|
||||
{#each menu.items as project (project.name)}
|
||||
<!-- Internal project links use resolve; external URLs are from the project catalog. -->
|
||||
<!-- eslint-disable svelte/no-navigation-without-resolve -->
|
||||
<a
|
||||
@@ -297,9 +314,9 @@
|
||||
class="justify-start"
|
||||
onclick={() => (menuOpen = false)}>{item.label}</Button
|
||||
>
|
||||
{#if item.href === '/projects'}
|
||||
{#if menus[item.href]}
|
||||
<div class="ml-3 flex flex-col border-l border-border pl-3">
|
||||
{#each projects as project (project.name)}
|
||||
{#each menus[item.href].items as project (project.name)}
|
||||
<Button
|
||||
href={project.external ? project.href : resolve(project.href)}
|
||||
variant="ghost"
|
||||
@@ -343,7 +360,7 @@
|
||||
</Sheet.Root>
|
||||
</div>
|
||||
</header>
|
||||
<main id="main" class="min-w-0 flex-1">{@render children()}</main>
|
||||
<main id="main" class="isolate min-w-0 flex-1">{@render children()}</main>
|
||||
<footer
|
||||
class="mt-16 border-t border-[var(--site-border)]"
|
||||
class:tracker-footer={page.route.id === '/projects/game-tracker'}
|
||||
|
||||
@@ -2,9 +2,8 @@
|
||||
import { resolve } from '$app/paths';
|
||||
import portrait from '#lib/assets/stan.jpg?url';
|
||||
import ProgressBar from '#lib/components/ProgressBar.svelte';
|
||||
import ProjectIcon from '#lib/components/ProjectIcon.svelte';
|
||||
import * as Card from '#lib/components/ui/card/index.js';
|
||||
import { projects } from '#lib/projects.js';
|
||||
import CatalogGrid from '#lib/components/CatalogGrid.svelte';
|
||||
import { projects, trackers } from '#lib/projects.js';
|
||||
import ArrowRight from 'lucide-svelte/icons/arrow-right';
|
||||
import ExternalLink from 'lucide-svelte/icons/external-link';
|
||||
import Mail from 'lucide-svelte/icons/mail';
|
||||
@@ -104,64 +103,18 @@
|
||||
>projects <ArrowRight size={20} aria-hidden="true" /></a
|
||||
>
|
||||
</h2>
|
||||
<ul class="grid gap-3 sm:grid-cols-2">
|
||||
{#each projects as project (project.name)}
|
||||
<li>
|
||||
<Card.Root class="h-full py-0 transition-colors hover:bg-muted/50"
|
||||
><Card.Content class="p-0">
|
||||
{#if project.external}
|
||||
<CatalogGrid items={projects} subtitleClass="mt-1 block text-base text-[var(--site-muted)]" />
|
||||
</section>
|
||||
|
||||
<section aria-labelledby="trackers-heading" class="border-t border-[var(--site-border)] pt-8">
|
||||
<h2 id="trackers-heading" class="mb-4 text-2xl font-semibold tracking-tight">
|
||||
<a
|
||||
href={project.href}
|
||||
rel="external"
|
||||
class="flex min-h-28 items-start justify-between gap-4 p-5"
|
||||
href={resolve('trackers')}
|
||||
class="inline-flex items-center gap-1 hover:text-[var(--site-muted)]"
|
||||
>trackers <ArrowRight size={20} aria-hidden="true" /></a
|
||||
>
|
||||
<span class="flex min-w-0 items-start gap-3">
|
||||
<span
|
||||
class="mt-0.5 flex size-10 shrink-0 items-center justify-center rounded-md border border-border bg-[var(--site-surface)] text-[var(--site-muted)]"
|
||||
>
|
||||
<ProjectIcon name={project.name} class="size-5" />
|
||||
</span>
|
||||
<span class="min-w-0">
|
||||
<span class="block font-medium">{project.name}</span>
|
||||
<span class="mt-1 block text-base text-[var(--site-muted)]"
|
||||
>{project.subtitle}</span
|
||||
>
|
||||
</span>
|
||||
</span><ExternalLink
|
||||
size={18}
|
||||
aria-hidden="true"
|
||||
class="shrink-0 text-[var(--site-muted)]"
|
||||
/>
|
||||
</a>
|
||||
{:else}
|
||||
<a
|
||||
href={resolve(project.href)}
|
||||
class="flex min-h-28 items-start justify-between gap-4 p-5"
|
||||
>
|
||||
<span class="flex min-w-0 items-start gap-3">
|
||||
<span
|
||||
class="mt-0.5 flex size-10 shrink-0 items-center justify-center rounded-md border border-border bg-[var(--site-surface)] text-[var(--site-muted)]"
|
||||
>
|
||||
<ProjectIcon name={project.name} class="size-5" />
|
||||
</span>
|
||||
<span class="min-w-0">
|
||||
<span class="block font-medium">{project.name}</span>
|
||||
<span class="mt-1 block text-base text-[var(--site-muted)]"
|
||||
>{project.subtitle}</span
|
||||
>
|
||||
</span>
|
||||
</span><ArrowRight
|
||||
size={18}
|
||||
aria-hidden="true"
|
||||
class="shrink-0 text-[var(--site-muted)]"
|
||||
/>
|
||||
</a>
|
||||
{/if}
|
||||
</Card.Content></Card.Root
|
||||
>
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
</h2>
|
||||
<CatalogGrid items={trackers} subtitleClass="mt-1 block text-base text-[var(--site-muted)]" />
|
||||
</section>
|
||||
|
||||
<section aria-labelledby="lists-heading" class="pt-14 sm:pt-20">
|
||||
@@ -205,7 +158,7 @@
|
||||
<span
|
||||
class="inline-flex min-w-16 items-center justify-end gap-1 whitespace-nowrap text-base text-[var(--site-muted)]"
|
||||
>
|
||||
{#if list.isProgress && data.user}
|
||||
{#if list.isProgress}
|
||||
{list.total ? Math.round((list.checked / list.total) * 100) : 0}%
|
||||
{:else}
|
||||
{list.total}
|
||||
|
||||
@@ -1,10 +1,7 @@
|
||||
<script lang="ts">
|
||||
import { resolve } from '$app/paths';
|
||||
import ArrowRight from 'lucide-svelte/icons/arrow-right';
|
||||
import ExternalLink from 'lucide-svelte/icons/external-link';
|
||||
import * as Card from '#lib/components/ui/card/index.js';
|
||||
import { Badge } from '#lib/components/ui/badge/index.js';
|
||||
import ProjectIcon from '#lib/components/ProjectIcon.svelte';
|
||||
import CatalogGrid from '#lib/components/CatalogGrid.svelte';
|
||||
import { projects } from '#lib/projects.js';
|
||||
|
||||
const earlier = [
|
||||
@@ -15,59 +12,12 @@
|
||||
</script>
|
||||
|
||||
<div class="mx-auto max-w-6xl px-4 py-12 sm:px-6 sm:py-16">
|
||||
<h1 class="mb-10 text-4xl font-semibold tracking-tight sm:text-5xl">projects</h1>
|
||||
<ul class="grid gap-3 sm:grid-cols-2">
|
||||
{#each projects as project (project.name)}
|
||||
<li>
|
||||
<Card.Root class="h-full py-0 transition-colors hover:bg-muted/50"
|
||||
><Card.Content class="p-0">
|
||||
{#if project.external}
|
||||
<a
|
||||
href={project.href}
|
||||
rel="external"
|
||||
class="flex min-h-28 items-start justify-between gap-4 p-5"
|
||||
>
|
||||
<span class="flex min-w-0 items-start gap-3">
|
||||
<span
|
||||
class="mt-0.5 flex size-10 shrink-0 items-center justify-center rounded-md border border-border bg-[var(--site-surface)] text-[var(--site-muted)]"
|
||||
>
|
||||
<ProjectIcon name={project.name} class="size-5" />
|
||||
</span>
|
||||
<span class="min-w-0"
|
||||
><span class="block font-medium">{project.name}</span><span
|
||||
class="mt-1 block text-sm text-muted-foreground">{project.subtitle}</span
|
||||
></span
|
||||
>
|
||||
</span><ExternalLink
|
||||
size={18}
|
||||
aria-hidden="true"
|
||||
class="text-[var(--site-muted)]"
|
||||
/>
|
||||
</a>
|
||||
{:else}
|
||||
<a
|
||||
href={resolve(project.href)}
|
||||
class="flex min-h-28 items-start justify-between gap-4 p-5"
|
||||
>
|
||||
<span class="flex min-w-0 items-start gap-3">
|
||||
<span
|
||||
class="mt-0.5 flex size-10 shrink-0 items-center justify-center rounded-md border border-border bg-[var(--site-surface)] text-[var(--site-muted)]"
|
||||
>
|
||||
<ProjectIcon name={project.name} class="size-5" />
|
||||
</span>
|
||||
<span class="min-w-0"
|
||||
><span class="block font-medium">{project.name}</span><span
|
||||
class="mt-1 block text-sm text-muted-foreground">{project.subtitle}</span
|
||||
></span
|
||||
>
|
||||
</span><ArrowRight size={18} aria-hidden="true" class="text-[var(--site-muted)]" />
|
||||
</a>
|
||||
{/if}
|
||||
</Card.Content></Card.Root
|
||||
>
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
<h1 class="mb-3 text-4xl font-semibold tracking-tight sm:text-5xl">projects</h1>
|
||||
<p class="mb-10 text-[var(--site-muted)]">
|
||||
Things I built. Looking for the money, house, game or price trackers? They live under
|
||||
<a class="underline" href={resolve('trackers')}>trackers</a>.
|
||||
</p>
|
||||
<CatalogGrid items={projects} />
|
||||
<h2 class="mt-14 mb-3 text-2xl font-semibold">other</h2>
|
||||
<ul>
|
||||
{#each earlier as name (name)}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { listSummaries } from '#lib/server/lists.js';
|
||||
import { db } from '#lib/server/db/index.js';
|
||||
import { blogArticles } from '#lib/server/db/schema/index.js';
|
||||
import { SITE_ORIGIN } from '#lib/seo.js';
|
||||
@@ -9,6 +10,7 @@ import type { RequestHandler } from './$types';
|
||||
const staticPaths = [
|
||||
'/',
|
||||
'/projects',
|
||||
'/trackers',
|
||||
'/blog',
|
||||
'/lists',
|
||||
'/projects/game-tracker',
|
||||
@@ -42,7 +44,7 @@ export const GET: RequestHandler = async () => {
|
||||
.select({ slug: blogArticles.slug, updatedAt: blogArticles.updatedAt })
|
||||
.from(blogArticles)
|
||||
.where(eq(blogArticles.status, 'published')),
|
||||
db.query.lists.findMany({ columns: { slug: true } })
|
||||
listSummaries()
|
||||
]);
|
||||
const entries: { path: string; lastmod?: string }[] = [
|
||||
...staticPaths.map((path) => ({ path })),
|
||||
|
||||
12
src/routes/trackers/+page.svelte
Normal file
12
src/routes/trackers/+page.svelte
Normal file
@@ -0,0 +1,12 @@
|
||||
<script lang="ts">
|
||||
import CatalogGrid from '#lib/components/CatalogGrid.svelte';
|
||||
import { trackers } from '#lib/projects.js';
|
||||
</script>
|
||||
|
||||
<div class="mx-auto max-w-6xl px-4 py-12 sm:px-6 sm:py-16">
|
||||
<h1 class="mb-3 text-4xl font-semibold tracking-tight sm:text-5xl">trackers</h1>
|
||||
<p class="mb-10 text-[var(--site-muted)]">
|
||||
Personal data I keep track of: games, money, houses, prices, animals and lists.
|
||||
</p>
|
||||
<CatalogGrid items={trackers} />
|
||||
</div>
|
||||
Reference in New Issue
Block a user