- 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
215 lines
7.5 KiB
Svelte
Executable File
215 lines
7.5 KiB
Svelte
Executable File
<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>
|