Files
website/src/routes/+page.svelte
Stan Runge 8275f1fb22 Add private health tracker (owner-only)
- health schema: vaccinations, body_measurements, workouts, nutrition_daily,
  sleep (generated hours), medications, hair_progress, appointments
- migration with updated_at triggers; grants to personal_app only
- /trackers/health overview with add/edit/delete, owner-gated (404 otherwise),
  noindex + private no-store, hidden from listings for non-owners
- owner-only JSON/CSV import endpoint and scripts/health-import.ts
2026-10-24 00:14:07 +00:00

215 lines
7.5 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 CatalogGrid from '#lib/components/CatalogGrid.svelte';
import { projects, 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 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">
<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>
</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)}
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>