Files
website/src/routes/+page.svelte
Stan Runge 76bfb3dcec
All checks were successful
Deploy production / deploy (push) Successful in 1m10s
stats: /stats overview, economy, energy and Gouda pages; CBS series map; nav, homepage, /start and sitemap links
- CBS_SERIES: CPI, household tariffs (2007-now), energy balance, renewables, heat pumps,
  solar, cars, population (monthly, since 1942, components), house prices, housing stock,
  municipal levies for Gouda and its neighbours
- shared StatChart/Kpi/chart helpers with per-chart source attribution
- drop Eurostat nrg_pc_20x_h (404; CBS covers that history)
2026-10-25 20:50:47 +00:00

250 lines
8.9 KiB
Svelte
Executable File
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<script lang="ts">
import { resolve } from '$app/paths';
import portrait from '#lib/assets/stan.jpg?url';
import ProgressBar from '#lib/components/ProgressBar.svelte';
import ListeningCard from '#lib/music/ListeningCard.svelte';
import CatalogGrid from '#lib/components/CatalogGrid.svelte';
import { projects, statsPages, visibleTrackers } 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';
import BriefcaseBusiness from 'lucide-svelte/icons/briefcase-business';
import GraduationCap from 'lucide-svelte/icons/graduation-cap';
import LayoutDashboard from 'lucide-svelte/icons/layout-dashboard';
import type { PageProps } from './$types';
let { data }: PageProps = $props();
const experienceImages = import.meta.glob<string>(
'/src/lib/assets/experience/*.{webp,png,jpg,jpeg,avif,svg}',
{ eager: true, query: '?url', import: 'default' }
);
function experienceImage(filename: string) {
return (
['webp', 'png', 'jpg', 'jpeg', 'avif', 'svg']
.map((extension) => experienceImages[`/src/lib/assets/experience/${filename}.${extension}`])
.find(Boolean) ??
(filename === 'cargill'
? experienceImages[
'/src/lib/assets/experience/Cargill-Logo-R-Black-and-Leaf-Green-RGB.webp'
]
: undefined)
);
}
const experience = [
{ when: '2026–now', role: 'associate software engineer', place: 'Cargill', icon: 'cargill' },
{
when: '2021–2026',
role: 'BSc computer science · software engineering',
place: 'The Hague University of Applied Sciences',
icon: 'the-hague-university'
},
{ when: '2026', role: 'software engineering intern', place: '3webapps', icon: '3webapps' },
{ when: '2023', role: 'software engineering intern', place: 'h5mag', icon: 'h5mag' },
{ when: '2021–2023', role: 'junior IT specialist', place: 'Finance Plus', icon: 'finance-plus' }
] as const;
</script>
<svelte:head>
<link rel="preload" as="image" href={portrait} fetchpriority="high" />
</svelte:head>
<div class="mx-auto max-w-6xl px-4 sm:px-6">
{#if data.isOwner}
<!-- Owner only: a shortcut to the private /start dashboard. Visitors never see it. -->
<a
href={resolve('/start')}
class="mt-4 flex items-center gap-3 rounded-md border border-[var(--site-border)] bg-[var(--site-surface)] px-4 py-3 hover:border-[var(--site-text)]"
>
<LayoutDashboard size={20} aria-hidden="true" />
<span class="min-w-0"
><span class="block font-medium">dashboard</span><span
class="block truncate text-sm text-[var(--site-muted)]"
>tasks, server, backups, notifications and links · g then d from any page</span
></span
>
<ArrowRight size={18} class="ml-auto shrink-0" aria-hidden="true" />
</a>
{/if}
<section
class="grid items-center gap-8 pt-7 pb-12 sm:py-20 md:grid-cols-[minmax(0,1fr)_minmax(0,320px)] md:gap-16"
>
<div class="min-w-0 text-center md:text-left">
<h1 class="text-5xl font-semibold tracking-[-0.055em] sm:text-6xl lg:text-7xl">stan runge</h1>
<nav
class="mt-7 grid grid-cols-2 justify-items-center gap-x-4 gap-y-3 text-base text-[var(--site-muted)] sm:flex sm:flex-wrap sm:justify-center sm:gap-x-6 md:justify-start"
aria-label="Find me online"
>
<a
href="https://git.stanrunge.dev/stan"
rel="external"
class="inline-flex items-center gap-1 hover:text-[var(--site-text)]"
>git <ExternalLink size={14} aria-hidden="true" /></a
>
<a
href="https://x.com/stanrunge"
rel="external"
class="inline-flex items-center gap-1 hover:text-[var(--site-text)]"
>twitter <ExternalLink size={14} aria-hidden="true" /></a
>
<a
href="https://esports.vash.software"
rel="external"
class="inline-flex items-center gap-1 hover:text-[var(--site-text)]"
>vash esports <ExternalLink size={14} aria-hidden="true" /></a
>
<a
href="mailto:stan@stanrunge.dev"
class="inline-flex items-center gap-1 hover:text-[var(--site-text)]"
>email <Mail size={14} aria-hidden="true" /></a
>
</nav>
{#if data.listening}
<div class="mx-auto mt-7 max-w-sm text-left md:mx-0">
<ListeningCard listening={data.listening} />
</div>
{/if}
</div>
<img
src={portrait}
alt="Stan at his desk"
width="760"
height="746"
loading="eager"
decoding="sync"
fetchpriority="high"
class="aspect-square w-full max-w-80 justify-self-center rounded-md bg-[var(--site-surface)] object-cover md:justify-self-end"
/>
</section>
<section aria-labelledby="projects-heading" class="border-t border-[var(--site-border)] pt-8">
<h2 id="projects-heading" class="mb-4 text-2xl font-semibold tracking-tight">
<a
href={resolve('projects')}
class="inline-flex items-center gap-1 hover:text-[var(--site-muted)]"
>projects <ArrowRight size={20} aria-hidden="true" /></a
>
</h2>
<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={resolve('trackers')}
class="inline-flex items-center gap-1 hover:text-[var(--site-muted)]"
>trackers <ArrowRight size={20} aria-hidden="true" /></a
>
</h2>
<CatalogGrid
items={visibleTrackers(data.isOwner, data.inactiveGames)}
variant="tracker"
subtitleClass="mt-1 block text-base text-[var(--site-muted)]"
/>
</section>
<section aria-labelledby="stats-heading" class="border-t border-[var(--site-border)] pt-8">
<h2 id="stats-heading" class="mb-4 text-2xl font-semibold tracking-tight">
<a
href={resolve('/stats')}
class="inline-flex items-center gap-1 hover:text-[var(--site-muted)]"
>stats <ArrowRight size={20} aria-hidden="true" /></a
>
</h2>
<CatalogGrid items={statsPages} subtitleClass="mt-1 block text-base text-[var(--site-muted)]" />
</section>
<section aria-labelledby="lists-heading" class="pt-14 sm:pt-20">
<h2 id="lists-heading" class="mb-4 text-2xl font-semibold tracking-tight">
<a
href={resolve('lists')}
class="inline-flex items-center gap-1 hover:text-[var(--site-muted)]"
>lists <ArrowRight size={20} aria-hidden="true" /></a
>
</h2>
<ul>
{#each data.lists as list (list.id)}
<li class="border-b border-[var(--site-border)] text-lg">
<a
href={resolve('/lists/[slug]', { slug: list.slug })}
class={list.isProgress
? 'grid grid-cols-[minmax(0,1fr)_8rem_auto] items-center gap-3 py-4 hover:text-[var(--site-muted)]'
: 'flex items-center justify-between gap-3 py-4 hover:text-[var(--site-muted)]'}
>
<span class="flex min-w-0 items-center gap-3">
{#if list.pictureUrl}<img
src={list.pictureUrl}
alt=""
loading="lazy"
width="40"
height="40"
class="h-10 w-10 rounded bg-[var(--site-surface)] object-cover"
/>{/if}
<span class="truncate">{list.name}</span>
</span>
{#if list.isProgress}
<span class="block w-32"
><ProgressBar
value={list.checked}
max={list.total}
label={`Progress on ${list.name}`}
small
/></span
>
{/if}
<span
class="inline-flex min-w-16 items-center justify-end gap-1 whitespace-nowrap text-base text-[var(--site-muted)]"
>
{#if list.isProgress}
{list.total ? Math.round((list.checked / list.total) * 100) : 0}%
{:else}
{list.total}
{/if}
<ArrowRight size={16} aria-hidden="true" />
</span>
</a>
</li>
{/each}
</ul>
</section>
<section aria-labelledby="experience-heading" class="pt-14 sm:pt-20">
<h2 id="experience-heading" class="mb-4 text-2xl font-semibold tracking-tight">experience</h2>
<ol>
{#each experience as item (item.when + item.place)}
{@const image = experienceImage(item.icon)}
<li
class="grid grid-cols-[3rem_minmax(0,1fr)] gap-x-4 gap-y-1 border-t border-[var(--site-border)] py-4 sm:grid-cols-[3rem_minmax(0,1fr)_130px]"
>
<span
class="row-span-2 flex size-12 items-center justify-center overflow-hidden rounded-md border border-border bg-[var(--site-surface)] text-muted-foreground sm:row-span-1"
>
{#if image}
<img
src={image}
alt=""
width="48"
height="48"
loading="lazy"
class="size-full object-cover"
/>
{:else if item.icon === 'the-hague-university'}
<GraduationCap class="size-5" aria-hidden="true" />
{:else}
<BriefcaseBusiness class="size-5" aria-hidden="true" />
{/if}
</span>
<div class="min-w-0">
<h3 class="font-medium">{item.role}</h3>
<p class="mt-1 text-base text-[var(--site-muted)]">{item.place}</p>
</div>
<span class="col-start-2 text-base text-[var(--site-muted)] sm:col-start-3 sm:text-right"
>{item.when}</span
>
</li>
{/each}
</ol>
</section>
</div>