mirror of
https://github.com/Patiun/com.gregkilmer.www.git
synced 2026-11-08 04:08:24 +00:00
Initial Commit
This commit is contained in:
@@ -0,0 +1,132 @@
|
||||
---
|
||||
/**
|
||||
* about.astro — About Me
|
||||
* ---------------------------------------------------------------------------
|
||||
* The human story bridging the professional and personal sides. Three sections
|
||||
* from the spec, all copy verbatim: the short version, the longer arc, and the
|
||||
* quick facts (spec emojis mapped to mdi icons). Ends with the SplitTransition
|
||||
* fork into Professional / Interests.
|
||||
*/
|
||||
import BaseLayout from '@layouts/BaseLayout.astro';
|
||||
import Reveal from '@components/motion/Reveal.astro';
|
||||
import Icon from '@components/Icon.astro';
|
||||
import SectionHeader from '@components/layout/SectionHeader.astro';
|
||||
import SplitTransition from '@components/layout/SplitTransition.astro';
|
||||
import CelestialImage from '@components/mystic/CelestialImage.astro';
|
||||
import AlchemicalDivider from '@components/mystic/AlchemicalDivider.astro';
|
||||
|
||||
// Spec "Quick Facts", emojis -> mdi icons.
|
||||
const quickFacts = [
|
||||
{ icon: 'map-marker', text: 'Philadelphia, PA' },
|
||||
{ icon: 'briefcase', text: 'Software Developer at Implico Inc.' },
|
||||
{ icon: 'dog', text: 'Dog dad to Kashi the mini dachshund' },
|
||||
{ icon: 'dice-multiple', text: 'Running D&D & Daggerheart campaigns' },
|
||||
{ icon: 'book-open-variant', text: 'Currently reading: Heir of Fire by Sarah J. Maas' },
|
||||
{ icon: 'tea', text: 'Fueled by loose-leaf Chinese tea and Mountain Dew' },
|
||||
];
|
||||
---
|
||||
|
||||
<BaseLayout
|
||||
title="About"
|
||||
description="The human story behind the resume: Greg Kilmer on curiosity, tabletop RPGs, resin dice, and a Philadelphia apartment with too many board games."
|
||||
>
|
||||
<!-- Header -->
|
||||
<section class="container-prose flex flex-col gap-10 pb-10 pt-28 sm:pt-32">
|
||||
<Reveal>
|
||||
<SectionHeader
|
||||
kicker="About"
|
||||
icon="account-details"
|
||||
title="The person behind the projects"
|
||||
subtitle="Two wings of the same house. This page is the bridge between them."
|
||||
/>
|
||||
</Reveal>
|
||||
</section>
|
||||
|
||||
<!-- Section 1: The Short Version -->
|
||||
<section class="container-prose flex flex-col gap-8 py-10">
|
||||
<Reveal>
|
||||
<p
|
||||
class="border-l-2 pl-5 font-sans text-xl leading-relaxed sm:text-2xl"
|
||||
style="border-color: var(--color-accent); color: var(--color-text);"
|
||||
>
|
||||
I am Greg — a software developer based in Philadelphia who spends
|
||||
his free time running tabletop RPGs, making resin dice, 3D printing
|
||||
miniatures and props, and reading his way through fantasy series at an
|
||||
alarming rate. I live with my partner Krystina, our mini dachshund
|
||||
Kashi, and more board games than any reasonable apartment should hold.
|
||||
</p>
|
||||
</Reveal>
|
||||
</section>
|
||||
|
||||
<!-- Section 2: The Longer Arc -->
|
||||
<section class="container-prose grid gap-10 py-10 lg:grid-cols-[1fr_18rem]">
|
||||
<Reveal class="flex flex-col gap-5">
|
||||
<h2>The longer arc</h2>
|
||||
<div class="flex flex-col gap-5 text-lg leading-relaxed" style="color: var(--color-text-muted);">
|
||||
<p>
|
||||
I grew up surrounded by books and games, and not much has changed.
|
||||
Technology pulled me into a career building cloud and mobile
|
||||
applications, but I never stopped collecting dice, designing
|
||||
campaigns, or losing weekends to Sanderson rereads. I am currently
|
||||
expanding into SAP's Business Technology Platform, exploring AI and
|
||||
machine learning, and trying to convince my group that yes, we
|
||||
<em>can</em> fit one more board game into game night.
|
||||
</p>
|
||||
<p>
|
||||
I believe the best work happens when curiosity isn't compartmentalized.
|
||||
The same system-thinking that makes a good developer makes a good
|
||||
gamemaster. The patience that produces a clean resin cast is the same
|
||||
patience that debugs a stubborn deployment. This site is where I don't
|
||||
have to pretend those things are separate.
|
||||
</p>
|
||||
</div>
|
||||
</Reveal>
|
||||
|
||||
<!-- Portrait + quick facts sidebar -->
|
||||
<div class="flex flex-col gap-6">
|
||||
<Reveal delay={60}>
|
||||
<CelestialImage
|
||||
src="/images/about-portrait.jpg"
|
||||
alt="Greg, a candid portrait"
|
||||
aspect="4/5"
|
||||
/>
|
||||
</Reveal>
|
||||
<Reveal delay={120}>
|
||||
<aside class="card flex flex-col gap-4 lg:sticky lg:top-24">
|
||||
<h3 class="font-mono text-xs uppercase tracking-[0.22em]" style="color: var(--color-accent);">
|
||||
Quick facts
|
||||
</h3>
|
||||
<ul class="flex flex-col gap-3">
|
||||
{quickFacts.map((f) => (
|
||||
<li class="flex items-start gap-3 text-sm" style="color: var(--color-text);">
|
||||
<Icon name={f.icon} class="mt-0.5 h-5 w-5 shrink-0" style="color: var(--color-accent);" />
|
||||
<span>{f.text}</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</aside>
|
||||
</Reveal>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<Reveal>
|
||||
<div class="container-prose">
|
||||
<AlchemicalDivider class="mx-auto h-6 w-full max-w-sm" style="color: var(--color-accent);" />
|
||||
</div>
|
||||
</Reveal>
|
||||
|
||||
<!-- Section 3: The fork -->
|
||||
<section class="container-prose flex flex-col gap-6 py-16">
|
||||
<Reveal>
|
||||
<SectionHeader
|
||||
kicker="Both doors open here"
|
||||
icon="call-split"
|
||||
title="Where curiosity goes next"
|
||||
subtitle="The professional and personal sides are connected, not siloed. Pick a door."
|
||||
/>
|
||||
</Reveal>
|
||||
<Reveal delay={100}>
|
||||
<SplitTransition />
|
||||
</Reveal>
|
||||
</section>
|
||||
</BaseLayout>
|
||||
@@ -0,0 +1,155 @@
|
||||
/**
|
||||
* /api/contact
|
||||
* ---------------------------------------------------------------------------
|
||||
* POST endpoint that receives the contact form. Pipeline:
|
||||
* 1. Reject non-POST.
|
||||
* 2. Validate JSON body with Zod (name, email, message; honeypot must be empty).
|
||||
* 3. Rate-limit per IP (in-memory windowed counter).
|
||||
* 4. Deliver: SMTP via nodemailer if configured, else POST to a webhook if
|
||||
* configured, else return 503 so delivery can be wired later.
|
||||
* 5. Always return JSON { ok } or { error }.
|
||||
*
|
||||
* Secrets come from env (see .env.example). Nothing secret ships to the client.
|
||||
* This runs server-side only via the @astrojs/node adapter.
|
||||
*/
|
||||
import type { APIRoute } from 'astro';
|
||||
import { z } from 'zod/v4';
|
||||
|
||||
// --- Validation schema ------------------------------------------------------
|
||||
const schema = z.object({
|
||||
name: z.string().min(1).max(120),
|
||||
email: z.string().email().max(200),
|
||||
message: z.string().min(10).max(5000),
|
||||
// Honeypot: must be absent/empty. Bots tend to fill every field.
|
||||
company: z.string().max(0).optional().or(z.literal('')),
|
||||
});
|
||||
|
||||
// --- In-memory rate limiter (per worker) ------------------------------------
|
||||
// Good enough for a personal site behind a reverse proxy. For multi-instance,
|
||||
// swap this for Redis/Upstash. Window = N submissions per IP per X minutes.
|
||||
const rateWindowMin = Number(process.env.CONTACT_RATE_WINDOW_MIN ?? 15);
|
||||
const rateLimit = Number(process.env.CONTACT_RATE_LIMIT ?? 5);
|
||||
const hits = new Map<string, { count: number; reset: number }>();
|
||||
|
||||
function rateLimited(ip: string): boolean {
|
||||
const now = Date.now();
|
||||
const entry = hits.get(ip);
|
||||
if (!entry || now > entry.reset) {
|
||||
hits.set(ip, { count: 1, reset: now + rateWindowMin * 60_000 });
|
||||
return false;
|
||||
}
|
||||
entry.count += 1;
|
||||
return entry.count > rateLimit;
|
||||
}
|
||||
|
||||
// --- Helpers ----------------------------------------------------------------
|
||||
function json(body: unknown, status: number) {
|
||||
return new Response(JSON.stringify(body), {
|
||||
status,
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
});
|
||||
}
|
||||
|
||||
function clientIp(request: Request): string {
|
||||
// Cosmos/Traefik typically forwards the real client IP here.
|
||||
const fwd = request.headers.get('x-forwarded-for');
|
||||
if (fwd) return fwd.split(',')[0]!.trim();
|
||||
return request.headers.get('x-real-ip') ?? 'unknown';
|
||||
}
|
||||
|
||||
// --- Route handler ----------------------------------------------------------
|
||||
export const POST: APIRoute = async ({ request }) => {
|
||||
if (request.method !== 'POST') {
|
||||
return json({ error: 'Method not allowed.' }, 405);
|
||||
}
|
||||
|
||||
// Parse + validate.
|
||||
let payload: unknown;
|
||||
try {
|
||||
payload = await request.json();
|
||||
} catch {
|
||||
return json({ error: 'Invalid JSON body.' }, 400);
|
||||
}
|
||||
const parsed = schema.safeParse(payload);
|
||||
if (!parsed.success) {
|
||||
return json({ error: 'Please complete all fields correctly.', issues: parsed.error.issues }, 400);
|
||||
}
|
||||
const { name, email, message } = parsed.data;
|
||||
|
||||
// Rate limit.
|
||||
const ip = clientIp(request);
|
||||
if (rateLimited(ip)) {
|
||||
return json({ error: 'Too many submissions. Please try again later.' }, 429);
|
||||
}
|
||||
|
||||
// Deliver.
|
||||
try {
|
||||
const delivered = await deliver({ name, email, message });
|
||||
if (!delivered) {
|
||||
// No SMTP or webhook configured. Tell the client clearly without leaking
|
||||
// server internals, but return 503 so it is visibly a config gap.
|
||||
return json({ error: 'Contact delivery is not configured yet. Please email me directly.' }, 503);
|
||||
}
|
||||
return json({ ok: true }, 200);
|
||||
} catch (err) {
|
||||
console.error('[contact] delivery failed:', err);
|
||||
return json({ error: 'Could not send your message right now. Please try again later.' }, 502);
|
||||
}
|
||||
};
|
||||
|
||||
// --- Delivery: SMTP first, webhook fallback ---------------------------------
|
||||
interface Message {
|
||||
name: string;
|
||||
email: string;
|
||||
message: string;
|
||||
}
|
||||
|
||||
async function deliver(msg: Message): Promise<boolean> {
|
||||
const smtpHost = process.env.SMTP_HOST;
|
||||
const webhook = process.env.CONTACT_WEBHOOK_URL;
|
||||
|
||||
if (smtpHost) {
|
||||
// Dynamic import keeps nodemailer out of cold-start when unused.
|
||||
const nodemailer = (await import('nodemailer')).default;
|
||||
const transport = nodemailer.createTransport({
|
||||
host: smtpHost,
|
||||
port: Number(process.env.SMTP_PORT ?? 587),
|
||||
secure: Number(process.env.SMTP_PORT ?? 587) === 465,
|
||||
auth: process.env.SMTP_USER
|
||||
? { user: process.env.SMTP_USER, pass: process.env.SMTP_PASS }
|
||||
: undefined,
|
||||
});
|
||||
const hostname = (() => {
|
||||
try {
|
||||
return new URL(process.env.SITE_URL ?? 'https://patiun.com').hostname;
|
||||
} catch {
|
||||
return 'patiun.com';
|
||||
}
|
||||
})();
|
||||
await transport.sendMail({
|
||||
from: process.env.SMTP_FROM ?? `Greg's Site <no-reply@${hostname}>`,
|
||||
to: process.env.CONTACT_TO ?? 'greg@patiun.com',
|
||||
replyTo: `${msg.name} <${msg.email}>`,
|
||||
subject: `New message from ${msg.name} via patiun.com`,
|
||||
text: `Name: ${msg.name}\nEmail: ${msg.email}\n\n${msg.message}`,
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
if (webhook) {
|
||||
const res = await fetch(webhook, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
text: `New contact from ${msg.name} (${msg.email}):\n\n${msg.message}`,
|
||||
// Structured fields for webhook consumers (Discord/Slack/n8n).
|
||||
name: msg.name,
|
||||
email: msg.email,
|
||||
message: msg.message,
|
||||
}),
|
||||
});
|
||||
return res.ok;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
@@ -0,0 +1,141 @@
|
||||
---
|
||||
/**
|
||||
* contact.astro — Contact / Connect
|
||||
* ---------------------------------------------------------------------------
|
||||
* A single place for all ways to reach Greg, professional and social, per the
|
||||
* spec's two-column layout. Also hosts the contact form (a Node backend is
|
||||
* available, so this is the natural place to use it). Closes with the spec's
|
||||
* warm sign-off line.
|
||||
*
|
||||
* Spec items Greg still needs to fill (email, Discord, etc.) appear as
|
||||
* Placeholder slots.
|
||||
*/
|
||||
import BaseLayout from '@layouts/BaseLayout.astro';
|
||||
import Reveal from '@components/motion/Reveal.astro';
|
||||
import Icon from '@components/Icon.astro';
|
||||
import SectionHeader from '@components/layout/SectionHeader.astro';
|
||||
import Placeholder from '@components/layout/Placeholder.astro';
|
||||
import ContactForm from '@components/professional/ContactForm.tsx';
|
||||
|
||||
const professional = [
|
||||
{ icon: 'email-outline', label: 'Email', hint: 'Greg professional email address.' },
|
||||
{ icon: 'linkedin', label: 'LinkedIn', hint: 'LinkedIn profile URL.' },
|
||||
{ icon: 'github', label: 'GitHub', hint: 'GitHub profile URL.' },
|
||||
];
|
||||
|
||||
const social = [
|
||||
{ icon: 'discord', label: 'Discord', hint: 'Handle or invite link.' },
|
||||
{ icon: 'twitter', label: 'Twitter / X', hint: 'Profile link, if applicable.' },
|
||||
{ icon: 'paw', label: 'Pokémon', hint: 'Showdown or Trainer name, if shared.' },
|
||||
{ icon: 'cube-outline', label: 'Foundry VTT', hint: 'Server link, if public.' },
|
||||
];
|
||||
---
|
||||
|
||||
<BaseLayout
|
||||
title="Contact"
|
||||
description="Reach Greg Kilmer for cloud development, SAP BTP, technical leadership, or just to debate the best Gundam series or find a fourth for game night."
|
||||
>
|
||||
<section class="container-prose flex flex-col gap-10 pt-28 sm:pt-32">
|
||||
<Reveal>
|
||||
<SectionHeader
|
||||
kicker="Contact"
|
||||
icon="email"
|
||||
title="Let's connect"
|
||||
subtitle="Whether you want to talk cloud architecture, debate the best Gundam series, or need a fourth for game night, reach out."
|
||||
/>
|
||||
</Reveal>
|
||||
</section>
|
||||
|
||||
<div class="container-prose grid gap-8 py-12 lg:grid-cols-[1fr_1.1fr]">
|
||||
{/* Left: contact methods */}
|
||||
<div class="flex flex-col gap-10">
|
||||
{/* Professional */}
|
||||
<Reveal>
|
||||
<section class="flex flex-col gap-4">
|
||||
<h2 class="font-mono text-xs uppercase tracking-[0.22em]" style="color: var(--color-accent);">
|
||||
Professional
|
||||
</h2>
|
||||
<ul class="flex flex-col gap-3">
|
||||
{professional.map((item) => (
|
||||
<li class="card flex items-center gap-3">
|
||||
<span
|
||||
class="inline-flex h-10 w-10 shrink-0 items-center justify-center rounded-lg"
|
||||
style="background: color-mix(in srgb, var(--color-accent) 16%, transparent); color: var(--color-accent);"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<Icon name={item.icon} class="h-5 w-5" />
|
||||
</span>
|
||||
<span class="flex flex-col gap-0.5">
|
||||
<span class="font-sans text-sm font-semibold" style="color: var(--color-text);">{item.label}</span>
|
||||
<span class="text-xs" style="color: var(--color-text-subtle);">{item.hint}</span>
|
||||
</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
<Placeholder label="Professional links" hint="Replace the slots above with your real email / LinkedIn / GitHub once ready." />
|
||||
</section>
|
||||
</Reveal>
|
||||
|
||||
{/* Social / personal */}
|
||||
<Reveal delay={80}>
|
||||
<section class="flex flex-col gap-4">
|
||||
<h2 class="font-mono text-xs uppercase tracking-[0.22em]" style="color: var(--color-secondary);">
|
||||
Personal / Social
|
||||
</h2>
|
||||
<ul class="grid gap-3 sm:grid-cols-2">
|
||||
{social.map((item) => (
|
||||
<li class="card flex items-center gap-3">
|
||||
<span
|
||||
class="inline-flex h-10 w-10 shrink-0 items-center justify-center rounded-lg"
|
||||
style="background: color-mix(in srgb, var(--color-secondary) 18%, transparent); color: var(--color-secondary);"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<Icon name={item.icon} class="h-5 w-5" />
|
||||
</span>
|
||||
<span class="flex flex-col gap-0.5">
|
||||
<span class="font-sans text-sm font-semibold" style="color: var(--color-text);">{item.label}</span>
|
||||
<span class="text-xs" style="color: var(--color-text-subtle);">{item.hint}</span>
|
||||
</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</section>
|
||||
</Reveal>
|
||||
|
||||
{/* Availability note */}
|
||||
<Reveal delay={120}>
|
||||
<p
|
||||
class="rounded-xl border-l-2 p-4 text-sm"
|
||||
style="border-color: var(--color-accent); background: color-mix(in srgb, var(--color-accent) 8%, transparent); color: var(--color-text-muted);"
|
||||
>
|
||||
<strong style="color: var(--color-text);">Availability:</strong> Open to opportunities in cloud development, SAP BTP, and technical leadership.
|
||||
</p>
|
||||
</Reveal>
|
||||
</div>
|
||||
|
||||
{/* Right: contact form */}
|
||||
<Reveal delay={100}>
|
||||
<section class="card flex flex-col gap-5">
|
||||
<div class="flex flex-col gap-1">
|
||||
<h2 class="font-sans" style="color: var(--color-text);">Send a message</h2>
|
||||
<p class="text-sm" style="color: var(--color-text-muted);">
|
||||
Prefer a form? Drop a note and it will land in my inbox.
|
||||
</p>
|
||||
</div>
|
||||
<ContactForm client:idle />
|
||||
</section>
|
||||
</Reveal>
|
||||
</div>
|
||||
|
||||
{/* Closing note */}
|
||||
<section class="container-prose py-12">
|
||||
<Reveal>
|
||||
<blockquote
|
||||
class="mx-auto max-w-2xl border-l-2 pl-5 text-center font-sans text-lg italic"
|
||||
style="border-color: var(--color-accent); color: var(--color-text-muted);"
|
||||
>
|
||||
Whether you want to talk cloud architecture, debate the best Gundam series, or need a fourth for game night, reach out.
|
||||
</blockquote>
|
||||
</Reveal>
|
||||
</section>
|
||||
</BaseLayout>
|
||||
@@ -0,0 +1,202 @@
|
||||
---
|
||||
/**
|
||||
* index.astro — Home (Landing)
|
||||
* ---------------------------------------------------------------------------
|
||||
* First impression page. Full-width hero with the animated background, the spec
|
||||
* headline/subheadline/intro paragraph, and the two CTA buttons. Below the fold
|
||||
* is the 3-column snapshot grid (Professional, Tabletop, Making) plus the wider
|
||||
* Reading & Media strip. All copy is verbatim from the spec.
|
||||
*
|
||||
* Emojis from the spec table are rendered as mdi icons (no emojis per spec).
|
||||
*/
|
||||
import BaseLayout from '@layouts/BaseLayout.astro';
|
||||
import Reveal from '@components/motion/Reveal.astro';
|
||||
import AnimatedBackground from '@components/hero/AnimatedBackground.tsx';
|
||||
import Icon from '@components/Icon.astro';
|
||||
import SectionHeader from '@components/layout/SectionHeader.astro';
|
||||
import CelestialImage from '@components/mystic/CelestialImage.astro';
|
||||
import AlchemicalDivider from '@components/mystic/AlchemicalDivider.astro';
|
||||
import SacredGeometry from '@components/mystic/SacredGeometry.astro';
|
||||
|
||||
// Snapshot grid content from the spec, with spec emojis mapped to mdi icons.
|
||||
// Each card now carries a vibe-matched stock image (swap for real photos later).
|
||||
const cards = [
|
||||
{
|
||||
icon: 'briefcase',
|
||||
title: 'Professional',
|
||||
copy: 'From SAP-adjacent cloud apps to full-stack mobile — a decade of building software that businesses actually use.',
|
||||
href: '/professional',
|
||||
image: '/images/pro-workspace.jpg',
|
||||
alt: 'Developer workspace with code on screen',
|
||||
},
|
||||
{
|
||||
icon: 'dice-multiple',
|
||||
title: 'Tabletop',
|
||||
copy: "Currently running two campaigns in Foundry VTT and playtesting MCDM's Crows. Always rolling dice.",
|
||||
href: '/interests/ttrpgs',
|
||||
image: '/images/ttrpg-dice.jpg',
|
||||
alt: 'Polyhedral dice on a dark surface',
|
||||
},
|
||||
{
|
||||
icon: 'palette',
|
||||
title: 'Making',
|
||||
copy: 'Resin dice, 3D prints, and whatever else catches my eye. If it involves layers and precision, I am in.',
|
||||
href: '/interests/making',
|
||||
image: '/images/making-resin.jpg',
|
||||
alt: 'Resin crafting in progress',
|
||||
},
|
||||
];
|
||||
---
|
||||
|
||||
<BaseLayout>
|
||||
<!-- ===================== HERO ===================== -->
|
||||
<section class="relative flex min-h-[88vh] items-center overflow-hidden pt-16">
|
||||
<AnimatedBackground client:idle />
|
||||
|
||||
<div class="container-prose relative z-10 flex flex-col items-start gap-8 py-20">
|
||||
<div class="flex w-full flex-col gap-10 lg:flex-row lg:items-center">
|
||||
<Reveal as="div" class="max-w-2xl grow">
|
||||
<span
|
||||
class="mb-5 inline-flex items-center gap-2 rounded-full border px-3 py-1 font-mono text-xs uppercase tracking-[0.18em]"
|
||||
style="border-color: var(--color-border); color: var(--color-text-muted); background: color-mix(in srgb, var(--color-surface-raised) 60%, transparent);"
|
||||
>
|
||||
<span class="h-2 w-2 rounded-full animate-pulse-glow" style="background: var(--color-accent);"></span>
|
||||
Developer. Maker. Storyteller.
|
||||
</span>
|
||||
|
||||
<h1 class="text-balance" style="color: var(--color-text);">
|
||||
Greg Kilmer
|
||||
</h1>
|
||||
|
||||
<p
|
||||
class="mt-3 font-sans text-xl font-medium sm:text-2xl"
|
||||
style="color: var(--color-accent);"
|
||||
>
|
||||
Software Developer · Maker · Tabletop Gamemaster
|
||||
</p>
|
||||
|
||||
<blockquote
|
||||
class="mt-7 max-w-2xl border-l-2 pl-5 text-lg leading-relaxed"
|
||||
style="border-color: var(--color-accent); color: var(--color-text-muted);"
|
||||
>
|
||||
I build things with code during the day and with resin, plastic, and
|
||||
imagination at night. I lead cloud and mobile development at Implico,
|
||||
run D&D campaigns that span years, and fill my shelves with Brandon
|
||||
Sanderson novels, custom dice, and board games. This is the place
|
||||
where all of those worlds meet.
|
||||
</blockquote>
|
||||
|
||||
<div class="mt-9 flex flex-wrap gap-4">
|
||||
<a href="/professional" class="btn btn-primary">
|
||||
See My Work
|
||||
<Icon name="arrow-right" class="h-5 w-5" />
|
||||
</a>
|
||||
<a href="/interests" class="btn btn-secondary">
|
||||
Explore My Interests
|
||||
<Icon name="compass-outline" class="h-5 w-5" />
|
||||
</a>
|
||||
</div>
|
||||
</Reveal>
|
||||
|
||||
{/* Hero portrait — framed in celestial ornament. Swap for a real photo of Greg. */}
|
||||
<Reveal as="div" delay={120} class="w-full max-w-sm shrink-0 lg:w-80">
|
||||
<CelestialImage
|
||||
src="/images/hero-portrait.jpg"
|
||||
alt="Greg at his desk"
|
||||
aspect="4/5"
|
||||
/>
|
||||
</Reveal>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Scroll cue -->
|
||||
<div class="pointer-events-none absolute bottom-6 left-1/2 -translate-x-1/2 animate-float" style="color: var(--color-text-subtle);" aria-hidden="true">
|
||||
<Icon name="chevron-down" class="h-7 w-7" />
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- ===================== SNAPSHOT GRID ===================== -->
|
||||
<section class="relative overflow-hidden py-20 sm:py-28">
|
||||
{/* Faint sacred-geometry ornament behind the section. */}
|
||||
<SacredGeometry
|
||||
figure="flower"
|
||||
class="pointer-events-none absolute -right-40 top-10 h-[40rem] w-[40rem]"
|
||||
style="color: var(--color-violet); opacity:.06;"
|
||||
/>
|
||||
<div class="container-prose relative flex flex-col gap-12">
|
||||
<Reveal>
|
||||
<SectionHeader
|
||||
kicker="The two wings"
|
||||
icon="home-heart"
|
||||
title="A snapshot"
|
||||
subtitle="One house, two wings. Polished and precise on the professional side; warm and inviting on the personal side."
|
||||
/>
|
||||
</Reveal>
|
||||
|
||||
<div class="grid gap-5 md:grid-cols-3">
|
||||
{cards.map((card, i) => (
|
||||
<Reveal delay={i * 90}>
|
||||
<a href={card.href} class="card card-interactive group flex h-full flex-col gap-4 overflow-hidden p-0">
|
||||
<div class="aspect-[16/10] w-full overflow-hidden">
|
||||
<img
|
||||
src={card.image}
|
||||
alt={card.alt}
|
||||
loading="lazy"
|
||||
decoding="async"
|
||||
class="h-full w-full object-cover transition-transform duration-700 group-hover:scale-105"
|
||||
/>
|
||||
</div>
|
||||
<div class="flex flex-col gap-4 px-5 pb-5">
|
||||
<span
|
||||
class="inline-flex h-12 w-12 items-center justify-center rounded-xl"
|
||||
style="background: color-mix(in srgb, var(--color-accent) 16%, transparent); color: var(--color-accent);"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<Icon name={card.icon} class="h-6 w-6" />
|
||||
</span>
|
||||
<h3 class="font-sans" style="color: var(--color-text);">{card.title}</h3>
|
||||
<p class="grow text-sm leading-relaxed" style="color: var(--color-text-muted);">
|
||||
{card.copy}
|
||||
</p>
|
||||
<span
|
||||
class="mt-1 inline-flex items-center gap-1 font-sans text-sm font-semibold transition-transform group-hover:translate-x-1"
|
||||
style="color: var(--color-accent);"
|
||||
>
|
||||
Learn more
|
||||
<Icon name="arrow-right" class="h-4 w-4" />
|
||||
</span>
|
||||
</div>
|
||||
</a>
|
||||
</Reveal>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<Reveal>
|
||||
<AlchemicalDivider class="mx-auto h-6 w-full max-w-sm" style="color: var(--color-accent);" />
|
||||
</Reveal>
|
||||
|
||||
{/* Wider strip: Reading & Media */}
|
||||
<Reveal delay={120}>
|
||||
<a
|
||||
href="/interests/reading"
|
||||
class="card card-interactive group flex flex-col gap-4 overflow-hidden p-0 sm:flex-row sm:items-center sm:justify-between"
|
||||
>
|
||||
<span class="flex items-center gap-4">
|
||||
<span class="hidden aspect-square w-32 shrink-0 overflow-hidden sm:block">
|
||||
<img src="/images/fantasy-books.jpg" alt="Fantasy bookshelf" loading="lazy" decoding="async" class="h-full w-full object-cover" />
|
||||
</span>
|
||||
<span class="flex flex-col gap-1 px-5 py-4 sm:px-0 sm:py-0">
|
||||
<span class="font-mono text-xs uppercase tracking-[0.18em]" style="color: var(--color-text-subtle);">
|
||||
Reading & Media
|
||||
</span>
|
||||
<span class="font-sans text-lg font-semibold" style="color: var(--color-text);">
|
||||
Deep in the Cosmere, catching up on anime, and always three books ahead on my to-read list.
|
||||
</span>
|
||||
</span>
|
||||
</span>
|
||||
<Icon name="arrow-right" class="h-6 w-6 shrink-0 transition-transform group-hover:translate-x-1 sm:ml-4" style="color: var(--color-accent);" />
|
||||
</a>
|
||||
</Reveal>
|
||||
</div>
|
||||
</section>
|
||||
</BaseLayout>
|
||||
@@ -0,0 +1,98 @@
|
||||
---
|
||||
/**
|
||||
* gaming.astro — Gaming
|
||||
* ---------------------------------------------------------------------------
|
||||
* Per-interest template: header + tagline, verbatim overview, platforms card
|
||||
* grid, favorite genres, currently playing placeholder, and related interests.
|
||||
*/
|
||||
import BaseLayout from '@layouts/BaseLayout.astro';
|
||||
import Reveal from '@components/motion/Reveal.astro';
|
||||
import InterestHero from '@components/layout/InterestHero.astro';
|
||||
import InterestCard from '@components/cards/InterestCard.astro';
|
||||
import SectionHeader from '@components/layout/SectionHeader.astro';
|
||||
import RelatedInterests from '@components/layout/RelatedInterests.astro';
|
||||
import Placeholder from '@components/layout/Placeholder.astro';
|
||||
import Icon from '@components/Icon.astro';
|
||||
|
||||
const platforms = [
|
||||
{ icon: 'sony-playstation', title: 'PlayStation 5', body: 'Main console for AAA titles and exclusives.' },
|
||||
{ icon: 'nintendo-switch', title: 'Nintendo Switch / Switch 2', body: 'Handheld and docked. Nintendo first-party plus indie gems.' },
|
||||
{ icon: 'steam', title: 'Steam', body: 'The primary PC library. Strategy, RPGs, and an ever-growing backlog.' },
|
||||
];
|
||||
|
||||
const genres = [
|
||||
'RPGs (both Western and JRPG)',
|
||||
'Strategy and tactics',
|
||||
'Open-world exploration',
|
||||
'Indie darlings',
|
||||
];
|
||||
|
||||
const related = [
|
||||
{ href: '/interests/pokemon', title: 'Pokémon', icon: 'pokeball', blurb: 'Nintendo console overlap and a lifelong obsession.' },
|
||||
{ href: '/interests/ttrpgs', title: 'TTRPGs & Tabletop', icon: 'dice-multiple', blurb: 'Digital RPGs as another expression of the same impulse.' },
|
||||
];
|
||||
---
|
||||
|
||||
<BaseLayout
|
||||
surface="interest"
|
||||
title="Gaming"
|
||||
description="PlayStation 5, Nintendo Switch 2, and a Steam library that has gotten politely out of hand. RPGs, strategy, and deeply layered worlds."
|
||||
>
|
||||
<InterestHero
|
||||
icon="sony-playstation"
|
||||
title="Gaming"
|
||||
tagline="If it has a screen or a controller, I have probably played it."
|
||||
image="/images/gaming-setup.jpg"
|
||||
imageAlt="A gaming setup with controller and console"
|
||||
/>
|
||||
|
||||
<div class="container-prose flex flex-col gap-20 py-12 sm:py-16">
|
||||
{/* Overview */}
|
||||
<section class="flex flex-col gap-5 text-lg leading-relaxed" style="color: var(--color-text-muted);">
|
||||
<Reveal><p>Gaming is my other constant. I span console generations with a PlayStation 5, Nintendo Switch 2, and a Steam library that has gotten politely out of hand. I'm particularly drawn to RPGs, strategy games, and anything with a deeply layered world. If a game has lore compendiums, I'm reading them.</p></Reveal>
|
||||
</section>
|
||||
|
||||
{/* Platforms */}
|
||||
<section class="flex flex-col gap-6">
|
||||
<Reveal><SectionHeader kicker="Platforms" icon="gamepad-variant" title="Where I play" /></Reveal>
|
||||
<div class="grid gap-5 sm:grid-cols-3">
|
||||
{platforms.map((p, i) => (
|
||||
<Reveal delay={i * 80}>
|
||||
<InterestCard icon={p.icon} title={p.title}>{p.body}</InterestCard>
|
||||
</Reveal>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Genres */}
|
||||
<section class="flex flex-col gap-5">
|
||||
<Reveal><SectionHeader kicker="Tastes" icon="shape" title="Favorite genres" /></Reveal>
|
||||
<Reveal delay={80}>
|
||||
<ul class="flex flex-wrap gap-3">
|
||||
{genres.map((g) => (
|
||||
<li class="pill text-sm">
|
||||
<Icon name="star-four-points" class="h-3.5 w-3.5" />
|
||||
{g}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</Reveal>
|
||||
</section>
|
||||
|
||||
{/* Currently */}
|
||||
<section class="flex flex-col gap-6">
|
||||
<Reveal><SectionHeader kicker="Currently" icon="calendar-clock" title="Now playing" /></Reveal>
|
||||
<div class="grid gap-4 sm:grid-cols-[1fr_1fr]">
|
||||
<figure
|
||||
class="relative overflow-hidden rounded-xl border"
|
||||
style="border-color: color-mix(in srgb, var(--color-accent) 35%, transparent);"
|
||||
>
|
||||
<img src="/images/gaming-setup.jpg" alt="Gaming setup" loading="lazy" decoding="async" class="aspect-[4/3] w-full object-cover" />
|
||||
</figure>
|
||||
<Placeholder label="Currently playing" hint="Add the games you are actively playing right now." />
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<RelatedInterests items={related} />
|
||||
</div>
|
||||
</BaseLayout>
|
||||
@@ -0,0 +1,142 @@
|
||||
---
|
||||
/**
|
||||
* interests/index.astro — Interests Hub
|
||||
* ---------------------------------------------------------------------------
|
||||
* The "fun wing" landing page. A warm grid of cards linking into each interest
|
||||
* sub-page, using the spec's taglines. Sets surface="interest" so the whole
|
||||
* page reads warm (cream/amber) regardless of the global light/dark toggle.
|
||||
*/
|
||||
import BaseLayout from '@layouts/BaseLayout.astro';
|
||||
import Reveal from '@components/motion/Reveal.astro';
|
||||
import Icon from '@components/Icon.astro';
|
||||
import SectionHeader from '@components/layout/SectionHeader.astro';
|
||||
import AnimatedBackground from '@components/hero/AnimatedBackground.tsx';
|
||||
import SacredGeometry from '@components/mystic/SacredGeometry.astro';
|
||||
|
||||
const interests = [
|
||||
{
|
||||
href: '/interests/ttrpgs',
|
||||
icon: 'dice-multiple',
|
||||
title: 'TTRPGs & Tabletop',
|
||||
tagline: 'Three campaigns, two systems, one rolling table.',
|
||||
blurb: 'Long-running D&D and Daggerheart campaigns on Foundry VTT, plus the Crows playtest. Worldbuilding, NPC relationships, and player-driven narrative.',
|
||||
image: '/images/ttrpg-dice.jpg',
|
||||
alt: 'Polyhedral dice on a dark surface',
|
||||
},
|
||||
{
|
||||
href: '/interests/gaming',
|
||||
icon: 'sony-playstation',
|
||||
title: 'Gaming',
|
||||
tagline: 'If it has a screen or a controller, I have probably played it.',
|
||||
blurb: 'PlayStation 5, Nintendo Switch 2, and a politely out-of-hand Steam library. RPGs, strategy, and anything with a deeply layered world.',
|
||||
image: '/images/gaming-setup.jpg',
|
||||
alt: 'Gaming setup with controller',
|
||||
},
|
||||
{
|
||||
href: '/interests/reading',
|
||||
icon: 'book-open-page-variant',
|
||||
title: 'Reading & Media',
|
||||
tagline: 'Perpetually three books ahead of my to-read list.',
|
||||
blurb: 'The Cosmere, Dune, the Pendergast saga, and Throne of Glass. Plus light novels, manga, manhwa, anime, and film.',
|
||||
image: '/images/fantasy-books.jpg',
|
||||
alt: 'Fantasy bookshelf',
|
||||
},
|
||||
{
|
||||
href: '/interests/making',
|
||||
icon: 'palette',
|
||||
title: 'Making & Creating',
|
||||
tagline: 'If it involves layers and precision, I am in.',
|
||||
blurb: 'Resin dice in a pressure pot, miniatures and terrain on the Elegoo Mars 4 and Bambu Labs P1S. Much of it ends up on the virtual table.',
|
||||
image: '/images/making-resin.jpg',
|
||||
alt: 'Resin crafting in progress',
|
||||
},
|
||||
{
|
||||
href: '/interests/pokemon',
|
||||
icon: 'pokeball',
|
||||
title: 'Pokémon',
|
||||
tagline: 'Metagross supremacy. This is not up for debate.',
|
||||
blurb: 'A lifelong obsession from Red and Blue to modern competitive. Steel/Psychic, beautiful design, absolutely devastating in battle.',
|
||||
image: '/images/pokemon-cards.jpg',
|
||||
alt: 'Trading card collection',
|
||||
},
|
||||
];
|
||||
---
|
||||
|
||||
<BaseLayout
|
||||
surface="interest"
|
||||
title="Interests"
|
||||
description="The fun wing: tabletop RPGs, gaming, reading, resin dice and 3D printing, and Pokémon. Where Greg's passions live as stories and collections."
|
||||
>
|
||||
<!-- Hero -->
|
||||
<section class="relative overflow-hidden pt-28 sm:pt-32">
|
||||
<AnimatedBackground client:idle particles={false} />
|
||||
<div class="container-prose relative z-10 flex flex-col items-start gap-6 py-12">
|
||||
<Reveal>
|
||||
<SectionHeader
|
||||
kicker="The personal wing"
|
||||
icon="heart"
|
||||
title="Interests"
|
||||
subtitle="Warmer, more visual, personality-driven. Not a list of hobbies but a collection of stories."
|
||||
/>
|
||||
</Reveal>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Interest grid -->
|
||||
<section class="relative overflow-hidden py-12 sm:py-16">
|
||||
<SacredGeometry
|
||||
figure="metatron"
|
||||
class="pointer-events-none absolute -left-48 bottom-0 h-[38rem] w-[38rem]"
|
||||
style="color: var(--color-accent); opacity:.05;"
|
||||
/>
|
||||
<div class="container-prose relative">
|
||||
<div class="grid gap-5 md:grid-cols-2 lg:grid-cols-3">
|
||||
{interests.map((item, i) => (
|
||||
<Reveal delay={i * 80}>
|
||||
<a href={item.href} class="card card-interactive group flex h-full flex-col gap-4 overflow-hidden p-0">
|
||||
<div class="aspect-[16/10] w-full overflow-hidden">
|
||||
<img
|
||||
src={item.image}
|
||||
alt={item.alt}
|
||||
loading="lazy"
|
||||
decoding="async"
|
||||
class="h-full w-full object-cover transition-transform duration-700 group-hover:scale-105"
|
||||
/>
|
||||
</div>
|
||||
<div class="flex flex-col gap-3 px-5 pb-5">
|
||||
<div class="flex items-center justify-between">
|
||||
<span
|
||||
class="inline-flex h-12 w-12 items-center justify-center rounded-xl"
|
||||
style="background: color-mix(in srgb, var(--color-accent) 16%, transparent); color: var(--color-accent);"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<Icon name={item.icon} class="h-6 w-6" />
|
||||
</span>
|
||||
<Icon name="arrow-right" class="h-5 w-5 transition-transform group-hover:translate-x-1" style="color: var(--color-accent);" />
|
||||
</div>
|
||||
<div class="flex flex-col gap-2">
|
||||
<h3 class="font-sans" style="color: var(--color-text);">{item.title}</h3>
|
||||
<p class="font-sans text-sm italic" style="color: var(--color-secondary);">{item.tagline}</p>
|
||||
</div>
|
||||
<p class="grow text-sm leading-relaxed" style="color: var(--color-text-muted);">{item.blurb}</p>
|
||||
</div>
|
||||
</a>
|
||||
</Reveal>
|
||||
))}
|
||||
|
||||
{/* Cross-link back to professional wing */}
|
||||
<Reveal delay={interests.length * 80}>
|
||||
<a
|
||||
href="/professional"
|
||||
class="card card-interactive group flex h-full flex-col justify-center gap-2 border-dashed p-5 text-center"
|
||||
style="border-color: color-mix(in srgb, var(--color-accent) 40%, transparent);"
|
||||
>
|
||||
<Icon name="briefcase" class="mx-auto h-7 w-7" style="color: var(--color-accent);" />
|
||||
<p class="font-sans font-semibold" style="color: var(--color-text);">The other wing</p>
|
||||
<p class="text-sm" style="color: var(--color-text-muted);">See where curiosity meets the day job.</p>
|
||||
</a>
|
||||
</Reveal>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</BaseLayout>
|
||||
@@ -0,0 +1,97 @@
|
||||
---
|
||||
/**
|
||||
* making.astro — Making & Creating
|
||||
* ---------------------------------------------------------------------------
|
||||
* Per-interest template with equipment card grid, a gallery placeholder, and the
|
||||
* spec's connection-to-TTRPGs callout. Related links back to TTRPGs and Gaming.
|
||||
*/
|
||||
import BaseLayout from '@layouts/BaseLayout.astro';
|
||||
import Reveal from '@components/motion/Reveal.astro';
|
||||
import InterestHero from '@components/layout/InterestHero.astro';
|
||||
import InterestCard from '@components/cards/InterestCard.astro';
|
||||
import SectionHeader from '@components/layout/SectionHeader.astro';
|
||||
import RelatedInterests from '@components/layout/RelatedInterests.astro';
|
||||
import Placeholder from '@components/layout/Placeholder.astro';
|
||||
import Icon from '@components/Icon.astro';
|
||||
import CelestialImage from '@components/mystic/CelestialImage.astro';
|
||||
|
||||
const equipment = [
|
||||
{ icon: 'palette', title: 'Resin Dice Making', body: 'Custom molds, pressure pot curing, pigment experimentation. Producing dice sets for personal use and gifts.' },
|
||||
{ icon: 'printer-3d', title: 'Elegoo Mars 4', body: 'Resin 3D printer. Miniatures, terrain, and high-detail props.' },
|
||||
{ icon: 'printer-3d-nozzle', title: 'Bambu Labs P1S', body: 'FDM 3D printer. Functional prints, terrain pieces, and larger prototypes.' },
|
||||
{ icon: 'wrench', title: 'General Making', body: 'Always experimenting. If there is a new technique or material, I want to try it.' },
|
||||
];
|
||||
|
||||
const related = [
|
||||
{ href: '/interests/ttrpgs', title: 'TTRPGs & Tabletop', icon: 'dice-multiple', blurb: 'Much of what I make ends up on the virtual table.' },
|
||||
{ href: '/interests/reading', title: 'Reading & Media', icon: 'book-open-page-variant', blurb: 'Displays and props for the stories I love.' },
|
||||
];
|
||||
---
|
||||
|
||||
<BaseLayout
|
||||
surface="interest"
|
||||
title="Making & Creating"
|
||||
description="Resin dice in a pressure pot, miniatures and terrain on the Elegoo Mars 4 and Bambu Labs P1S. If it involves layers and precision, I am in."
|
||||
>
|
||||
<InterestHero
|
||||
icon="palette"
|
||||
title="Making & Creating"
|
||||
tagline="If it involves layers and precision, I am in."
|
||||
image="/images/making-print.jpg"
|
||||
imageAlt="A 3D printer producing a detailed model"
|
||||
/>
|
||||
|
||||
<div class="container-prose flex flex-col gap-20 py-12 sm:py-16">
|
||||
{/* Overview */}
|
||||
<section class="flex flex-col gap-5 text-lg leading-relaxed" style="color: var(--color-text-muted);">
|
||||
<Reveal><p>Making is how I decompress. I cast resin dice using pressure pots and custom molds, print miniatures and props on my Elegoo Mars 4 (resin) and Bambu Labs P1S (FDM), and generally fill my workspace with projects-in-progress. There's something deeply satisfying about taking raw material through a process and holding the finished piece, whether that's a perfect polyhedral set or a printed terrain piece for the next game session.</p></Reveal>
|
||||
</section>
|
||||
|
||||
{/* Equipment */}
|
||||
<section class="flex flex-col gap-6">
|
||||
<Reveal><SectionHeader kicker="Equipment" icon="toolbox" title="What is on the bench" /></Reveal>
|
||||
<div class="grid gap-5 sm:grid-cols-2 lg:grid-cols-2">
|
||||
{equipment.map((e, i) => (
|
||||
<Reveal delay={i * 70}>
|
||||
<InterestCard icon={e.icon} title={e.title}>{e.body}</InterestCard>
|
||||
</Reveal>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Gallery */}
|
||||
<section class="flex flex-col gap-6">
|
||||
<Reveal><SectionHeader kicker="Gallery" icon="image-multiple" title="Finished pieces" /></Reveal>
|
||||
<Reveal delay={80}>
|
||||
<div class="grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
|
||||
<CelestialImage src="/images/making-resin.jpg" alt="Resin dice set" aspect="4/3" />
|
||||
<CelestialImage src="/images/making-print.jpg" alt="3D printed miniature" aspect="4/3" />
|
||||
<CelestialImage src="/images/ttrpg-dice.jpg" alt="Terrain piece and dice" aspect="4/3" />
|
||||
</div>
|
||||
</Reveal>
|
||||
<Reveal delay={120}>
|
||||
<Placeholder label="Your real photos" hint="Swap these stock stand-ins with photos of your own resin casts and prints." />
|
||||
</Reveal>
|
||||
</section>
|
||||
|
||||
{/* Connection to TTRPGs */}
|
||||
<section class="flex flex-col gap-5">
|
||||
<Reveal><SectionHeader kicker="Cross-pollination" icon="link-variant" title="Where it all connects" /></Reveal>
|
||||
<Reveal delay={80}>
|
||||
<a
|
||||
href="/interests/ttrpgs"
|
||||
class="card card-interactive group flex items-center gap-4"
|
||||
>
|
||||
<Icon name="dice-multiple" class="h-8 w-8 shrink-0" style="color: var(--color-accent);" />
|
||||
<span class="flex flex-col gap-1">
|
||||
<span class="font-sans font-semibold" style="color: var(--color-text);">Much of what I make ends up on the virtual table.</span>
|
||||
<span class="text-sm" style="color: var(--color-text-muted);">Custom tokens, terrain, and props for Foundry VTT campaigns.</span>
|
||||
</span>
|
||||
<Icon name="arrow-right" class="ml-auto h-5 w-5 shrink-0 transition-transform group-hover:translate-x-1" style="color: var(--color-accent);" />
|
||||
</a>
|
||||
</Reveal>
|
||||
</section>
|
||||
|
||||
<RelatedInterests items={related} />
|
||||
</div>
|
||||
</BaseLayout>
|
||||
@@ -0,0 +1,95 @@
|
||||
---
|
||||
/**
|
||||
* pokemon.astro — Pokémon
|
||||
* ---------------------------------------------------------------------------
|
||||
* Per-interest template: favorite callout (Metagross), games overview, a
|
||||
* Showdown-format roster placeholder (spec calls for standard Showdown text),
|
||||
* and related links to Gaming.
|
||||
*/
|
||||
import BaseLayout from '@layouts/BaseLayout.astro';
|
||||
import Reveal from '@components/motion/Reveal.astro';
|
||||
import InterestHero from '@components/layout/InterestHero.astro';
|
||||
import SectionHeader from '@components/layout/SectionHeader.astro';
|
||||
import RelatedInterests from '@components/layout/RelatedInterests.astro';
|
||||
import Placeholder from '@components/layout/Placeholder.astro';
|
||||
import Icon from '@components/Icon.astro';
|
||||
|
||||
const related = [
|
||||
{ href: '/interests/gaming', title: 'Gaming', icon: 'sony-playstation', blurb: 'Nintendo console overlap, from Game Boy to Switch 2.' },
|
||||
{ href: '/interests/making', title: 'Making & Creating', icon: 'palette', blurb: 'Displays and props for collections.' },
|
||||
];
|
||||
---
|
||||
|
||||
<BaseLayout
|
||||
surface="interest"
|
||||
title="Pokémon"
|
||||
description="A lifelong obsession from Red and Blue to modern competitive. Metagross supremacy: Steel/Psychic, beautiful design, absolutely devastating in battle."
|
||||
>
|
||||
<InterestHero
|
||||
icon="pokeball"
|
||||
title="Pokémon"
|
||||
tagline="Metagross supremacy. This is not up for debate."
|
||||
image="/images/pokemon-cards.jpg"
|
||||
imageAlt="A collection of trading cards"
|
||||
/>
|
||||
|
||||
<div class="container-prose flex flex-col gap-20 py-12 sm:py-16">
|
||||
{/* Overview */}
|
||||
<section class="flex flex-col gap-5 text-lg leading-relaxed" style="color: var(--color-text-muted);">
|
||||
<Reveal><p>Pokémon has been a lifelong obsession. From Red and Blue on the original Game Boy to modern competitive formats, I've followed the franchise through every generation. My favorite? Metagross, the iron leg Pokémon. Steel/Psychic, beautiful design, absolutely devastating in battle. No further questions.</p></Reveal>
|
||||
</section>
|
||||
|
||||
{/* Favorite callout */}
|
||||
<Reveal>
|
||||
<div
|
||||
class="card relative flex flex-col items-center gap-4 overflow-hidden text-center sm:flex-row sm:items-center sm:gap-6 sm:text-left"
|
||||
style="background: color-mix(in srgb, var(--color-accent) 8%, var(--color-surface-raised));"
|
||||
>
|
||||
<figure
|
||||
class="relative aspect-square w-full shrink-0 overflow-hidden sm:w-28"
|
||||
>
|
||||
<img src="/images/pokemon-cards.jpg" alt="Trading card collection" loading="lazy" decoding="async" class="h-full w-full object-cover" />
|
||||
</figure>
|
||||
<span
|
||||
class="inline-flex h-20 w-20 items-center justify-center rounded-2xl"
|
||||
style="background: color-mix(in srgb, var(--color-accent) 22%, transparent); color: var(--color-accent);"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<Icon name="robot" class="h-10 w-10" />
|
||||
</span>
|
||||
<div class="flex flex-col gap-1">
|
||||
<span class="font-mono text-xs uppercase tracking-[0.22em]" style="color: var(--color-accent);">Favorite Pokémon</span>
|
||||
<h2 style="color: var(--color-text);">Metagross</h2>
|
||||
<p class="text-sm" style="color: var(--color-text-muted);">Steel / Psychic. The iron leg Pokémon.</p>
|
||||
</div>
|
||||
</div>
|
||||
</Reveal>
|
||||
|
||||
{/* Roster */}
|
||||
<section class="flex flex-col gap-6">
|
||||
<Reveal><SectionHeader kicker="Battle team" icon="sword-cross" title="Roster" /></Reveal>
|
||||
<Reveal delay={80}>
|
||||
<p class="text-sm" style="color: var(--color-text-subtle);">
|
||||
Rosters use standard Pokémon Showdown text format.
|
||||
</p>
|
||||
<Placeholder label="Pokémon Showdown roster" hint="Paste your preferred roster in Showdown text format.">
|
||||
<pre
|
||||
class="mt-3 overflow-x-auto rounded-lg p-4 font-mono text-xs"
|
||||
style="background: var(--color-surface-sunken); color: var(--color-text-muted);"
|
||||
>## Pokémon
|
||||
[Greg's preferred roster in Showdown format]</pre>
|
||||
</Placeholder>
|
||||
</Reveal>
|
||||
</section>
|
||||
|
||||
{/* Games */}
|
||||
<section class="flex flex-col gap-6">
|
||||
<Reveal><SectionHeader kicker="Across the generations" icon="gamepad-variant" title="Games" /></Reveal>
|
||||
<Reveal delay={80}>
|
||||
<Placeholder label="Mainline & spinoff titles" hint="List the mainline and spinoff titles played, if desired." />
|
||||
</Reveal>
|
||||
</section>
|
||||
|
||||
<RelatedInterests items={related} />
|
||||
</div>
|
||||
</BaseLayout>
|
||||
@@ -0,0 +1,106 @@
|
||||
---
|
||||
/**
|
||||
* reading.astro — Reading & Media
|
||||
* ---------------------------------------------------------------------------
|
||||
* Per-interest template with the spec's reading highlights table (status pills
|
||||
* mapped from spec emoji statuses), anime favorites, and movies/TV note.
|
||||
*/
|
||||
import BaseLayout from '@layouts/BaseLayout.astro';
|
||||
import Reveal from '@components/motion/Reveal.astro';
|
||||
import InterestHero from '@components/layout/InterestHero.astro';
|
||||
import InterestCard from '@components/cards/InterestCard.astro';
|
||||
import SectionHeader from '@components/layout/SectionHeader.astro';
|
||||
import RelatedInterests from '@components/layout/RelatedInterests.astro';
|
||||
import Icon from '@components/Icon.astro';
|
||||
|
||||
// Spec reading highlights, with status mapped from the spec's emoji statuses.
|
||||
const reading = [
|
||||
{ icon: 'check-decagram', title: 'Brandon Sanderson — Cosmere', status: 'Complete', body: 'Complete. Every novel, every era. The gold standard for me.' },
|
||||
{ icon: 'check-decagram', title: 'Frank Herbert — Dune', status: 'Complete', body: 'Books 1-3 complete.' },
|
||||
{ icon: 'autorenew', title: 'Douglas Preston & Lincoln Child — Pendergast', status: 'Rereading', body: 'Currently rereading. Up to Reliquary.' },
|
||||
{ icon: 'autorenew', title: 'Sarah J. Maas — Throne of Glass', status: 'Reading', body: 'Currently on Heir of Fire (Book 3).' },
|
||||
{ icon: 'new-box', title: 'Wistoria: Wand and Sword', status: 'New', body: 'Recently added to the reading list.' },
|
||||
];
|
||||
|
||||
const anime = [
|
||||
'One Piece', 'Bleach', 'Gundam (all series)', 'Code Geass',
|
||||
'Magi', 'That Time I Got Reincarnated as a Slime', 'Overlord',
|
||||
];
|
||||
|
||||
const related = [
|
||||
{ href: '/interests/ttrpgs', title: 'TTRPGs & Tabletop', icon: 'dice-multiple', blurb: 'Worldbuilding inspiration flows both ways.' },
|
||||
{ href: '/interests/making', title: 'Making & Creating', icon: 'palette', blurb: 'Props and displays for the stories I love.' },
|
||||
];
|
||||
---
|
||||
|
||||
<BaseLayout
|
||||
surface="interest"
|
||||
title="Reading & Media"
|
||||
description="The Cosmere, Dune, the Pendergast saga, Throne of Glass. Plus light novels, manga, manhwa, anime, and film. If a story exists, I am probably consuming it."
|
||||
>
|
||||
<InterestHero
|
||||
icon="book-open-page-variant"
|
||||
title="Reading & Media"
|
||||
tagline="Perpetually three books ahead of my to-read list."
|
||||
image="/images/reading-nook.jpg"
|
||||
imageAlt="A cozy reading nook with shelves of books"
|
||||
/>
|
||||
|
||||
<div class="container-prose flex flex-col gap-20 py-12 sm:py-16">
|
||||
{/* Overview */}
|
||||
<section class="flex flex-col gap-5 text-lg leading-relaxed" style="color: var(--color-text-muted);">
|
||||
<Reveal><p>I read like it's a competitive sport. Fantasy is my native language, Brandon Sanderson's Cosmere is fully consumed and eagerly awaiting the next era. Frank Herbert's Dune trilogy shaped how I think about worldbuilding. Douglas Preston & Lincoln Child's Pendergast saga is my comfort reread. And I'm currently working through Sarah J. Maas's Throne of Glass series.</p></Reveal>
|
||||
<Reveal delay={80}><p>Beyond novels, I read light novels, comic books, manhwa, and manga. I watch anime and film extensively. If a story exists in any format, I'm probably consuming it.</p></Reveal>
|
||||
</section>
|
||||
|
||||
{/* Reading highlights */}
|
||||
<section class="flex flex-col gap-6">
|
||||
<Reveal><SectionHeader kicker="On the shelf" icon="bookshelf" title="Reading highlights" /></Reveal>
|
||||
<div class="grid gap-5 sm:grid-cols-2 lg:grid-cols-3">
|
||||
{reading.map((r, i) => (
|
||||
<Reveal delay={i * 70}>
|
||||
<InterestCard icon={r.icon} title={r.title} status={r.status}>{r.body}</InterestCard>
|
||||
</Reveal>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Anime */}
|
||||
<section class="flex flex-col gap-5">
|
||||
<Reveal><SectionHeader kicker="Animated" icon="television-classic" title="Anime favorites" /></Reveal>
|
||||
<Reveal delay={80}>
|
||||
<ul class="flex flex-wrap gap-3">
|
||||
{anime.map((a) => (
|
||||
<li class="pill text-sm">
|
||||
<Icon name="play-circle" class="h-3.5 w-3.5" />
|
||||
{a}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</Reveal>
|
||||
</section>
|
||||
|
||||
{/* Movies & TV */}
|
||||
<section class="flex flex-col gap-5">
|
||||
<Reveal><SectionHeader kicker="Beyond the page" icon="movie-open" title="Movies & TV" /></Reveal>
|
||||
<Reveal delay={80}>
|
||||
<div class="grid gap-5 sm:grid-cols-[1fr_18rem]">
|
||||
<blockquote
|
||||
class="border-l-2 pl-5 text-lg leading-relaxed"
|
||||
style="border-color: var(--color-secondary); color: var(--color-text-muted);"
|
||||
>
|
||||
Watches extensively across genres. Particularly drawn to sci-fi, fantasy, and animated film.
|
||||
</blockquote>
|
||||
<figure
|
||||
class="relative overflow-hidden rounded-xl border"
|
||||
style="border-color: color-mix(in srgb, var(--color-accent) 35%, transparent);"
|
||||
>
|
||||
<img src="/images/fantasy-books.jpg" alt="Fantasy and media collection" loading="lazy" decoding="async" class="aspect-[4/3] w-full object-cover" />
|
||||
</figure>
|
||||
</div>
|
||||
</Reveal>
|
||||
</section>
|
||||
|
||||
<RelatedInterests items={related} />
|
||||
</div>
|
||||
</BaseLayout>
|
||||
@@ -0,0 +1,114 @@
|
||||
---
|
||||
/**
|
||||
* ttrpgs.astro — TTRPGs & Tabletop
|
||||
* ---------------------------------------------------------------------------
|
||||
* The spec's per-interest template: Header (title + tagline), Overview
|
||||
* (narrative, verbatim), Highlights (card grid, spec emojis -> mdi), Currently,
|
||||
* then the Board Games sub-section, then RelatedInterests cross-links.
|
||||
*/
|
||||
import BaseLayout from '@layouts/BaseLayout.astro';
|
||||
import Reveal from '@components/motion/Reveal.astro';
|
||||
import InterestHero from '@components/layout/InterestHero.astro';
|
||||
import InterestCard from '@components/cards/InterestCard.astro';
|
||||
import SectionHeader from '@components/layout/SectionHeader.astro';
|
||||
import RelatedInterests from '@components/layout/RelatedInterests.astro';
|
||||
import Placeholder from '@components/layout/Placeholder.astro';
|
||||
|
||||
const highlights = [
|
||||
{ icon: 'castle', title: 'Dungeons & Dragons', body: 'Long-running campaign on Foundry VTT. Focus on rich worldbuilding, meaningful NPC relationships, and player-driven narrative.' },
|
||||
{ icon: 'sword-cross', title: 'Daggerheart', body: "Darrington Press's dice-building RPG. Currently running a campaign and exploring how its mechanics create different story rhythms than D&D." },
|
||||
{ icon: 'bird', title: 'Crows (Playtest)', body: "MCDM's tactical RPG in active playtest. Hex-based travel, revised travel roles, Miasma mechanics. Contributing playtest feedback." },
|
||||
{ icon: 'monitor', title: 'Foundry VTT', body: 'Self-hosted on the home server (Cosmos Lenovo Tiny). Building custom modules, maps, and automation for campaigns.' },
|
||||
{ icon: 'target', title: 'Draw Steel', body: "MCDM's upcoming tactical combat game. Interested in running it when it releases." },
|
||||
];
|
||||
|
||||
const related = [
|
||||
{ href: '/interests/making', title: 'Making & Creating', icon: 'palette', blurb: 'Dice and terrain made for campaigns.' },
|
||||
{ href: '/interests/gaming', title: 'Gaming', icon: 'sony-playstation', blurb: 'Digital RPGs as another expression of the same impulse.' },
|
||||
];
|
||||
---
|
||||
|
||||
<BaseLayout
|
||||
surface="interest"
|
||||
title="TTRPGs & Tabletop"
|
||||
description="Long-running D&D and Daggerheart campaigns on Foundry VTT, the MCDM Crows playtest, and a growing board game collection."
|
||||
>
|
||||
<InterestHero
|
||||
icon="dice-multiple"
|
||||
title="TTRPGs & Tabletop"
|
||||
tagline="Three campaigns, two systems, one rolling table."
|
||||
image="/images/ttrpg-table.jpg"
|
||||
imageAlt="A tabletop RPG spread with miniatures and dice"
|
||||
/>
|
||||
|
||||
<div class="container-prose flex flex-col gap-20 py-12 sm:py-16">
|
||||
{/* Overview */}
|
||||
<section class="flex flex-col gap-5 text-lg leading-relaxed" style="color: var(--color-text-muted);">
|
||||
<Reveal><p>Tabletop gaming isn't a hobby for me, it's a practice. I run Dungeons & Dragons and Daggerheart campaigns that span years of real-time play, maintaining worlds, NPCs, and narrative threads that my players remember years later. I host everything on Foundry VTT, which lets me build the kind of dynamic, visually rich experiences that keep players invested session after session.</p></Reveal>
|
||||
<Reveal delay={80}><p>Right now, I'm also playtesting MCDM's <em>Crows</em>, Matt Colville's forthcoming RPG built around hex-crawling, faction play, and hard travel rules. The playtest has already influenced how I think about overland encounters in my own campaigns.</p></Reveal>
|
||||
</section>
|
||||
|
||||
{/* Highlights */}
|
||||
<section class="flex flex-col gap-6">
|
||||
<Reveal><SectionHeader kicker="Highlights" icon="view-grid" title="Systems & tools" /></Reveal>
|
||||
<div class="grid gap-5 sm:grid-cols-2 lg:grid-cols-3">
|
||||
{highlights.map((h, i) => (
|
||||
<Reveal delay={i * 70}>
|
||||
<InterestCard icon={h.icon} title={h.title}>{h.body}</InterestCard>
|
||||
</Reveal>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Currently */}
|
||||
<section class="flex flex-col gap-6">
|
||||
<Reveal><SectionHeader kicker="Currently" icon="calendar-clock" title="On the schedule" /></Reveal>
|
||||
<div class="grid gap-4 sm:grid-cols-3">
|
||||
<Placeholder label="Campaign 1 name" hint="D&D, [day of week]s." />
|
||||
<Placeholder label="Campaign 2 name" hint="Daggerheart, [day of week]s." />
|
||||
<Placeholder label="Crows playtest" hint="Ongoing sessions." />
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Board games */}
|
||||
<section class="flex flex-col gap-6">
|
||||
<Reveal><SectionHeader kicker="Beyond RPGs" icon="cards-playing" title="Board games" /></Reveal>
|
||||
<Reveal delay={80}>
|
||||
<blockquote
|
||||
class="border-l-2 pl-5 text-lg leading-relaxed"
|
||||
style="border-color: var(--color-secondary); color: var(--color-text-muted);"
|
||||
>
|
||||
Beyond RPGs, I maintain a growing board and card game collection.
|
||||
Favorites span strategy, deck-building, and social deduction. Game night
|
||||
is a regular event, and yes, there's always room for one more.
|
||||
</blockquote>
|
||||
</Reveal>
|
||||
<Reveal delay={140}>
|
||||
<div class="grid gap-4 sm:grid-cols-[2fr_1fr]">
|
||||
<blockquote
|
||||
class="border-l-2 pl-5 text-lg leading-relaxed"
|
||||
style="border-color: var(--color-secondary); color: var(--color-text-muted);"
|
||||
>
|
||||
Beyond RPGs, I maintain a growing board and card game collection.
|
||||
Favorites span strategy, deck-building, and social deduction. Game night
|
||||
is a regular event, and yes, there's always room for one more.
|
||||
</blockquote>
|
||||
<figure
|
||||
class="relative overflow-hidden rounded-xl border"
|
||||
style="border-color: color-mix(in srgb, var(--color-accent) 35%, transparent);"
|
||||
>
|
||||
<img src="/images/ttrpg-dice.jpg" alt="Dice and game pieces" loading="lazy" decoding="async" class="h-full w-full object-cover" />
|
||||
</figure>
|
||||
</div>
|
||||
</Reveal>
|
||||
<Reveal delay={200}>
|
||||
<Placeholder
|
||||
label="Board game shelf"
|
||||
hint="Replace this with a real photo of your game shelf once you have one."
|
||||
/>
|
||||
</Reveal>
|
||||
</section>
|
||||
|
||||
<RelatedInterests items={related} />
|
||||
</div>
|
||||
</BaseLayout>
|
||||
@@ -0,0 +1,207 @@
|
||||
---
|
||||
/**
|
||||
* professional/index.astro — Professional (Resume / Portfolio)
|
||||
* ---------------------------------------------------------------------------
|
||||
* The "polished wing". A single long-scroll page whose sections read as a
|
||||
* narrative (Summary -> Experience -> Skills -> Projects -> Education ->
|
||||
* Contact), per the spec. Uses surface="professional" for the dark slate tone.
|
||||
*
|
||||
* Experience comes from the `experience` collection; projects from the
|
||||
* `projects` collection. Both are sorted by `order`. Education is a placeholder
|
||||
* block (spec leaves it to Greg).
|
||||
*/
|
||||
import BaseLayout from '@layouts/BaseLayout.astro';
|
||||
import Reveal from '@components/motion/Reveal.astro';
|
||||
import Icon from '@components/Icon.astro';
|
||||
import SectionHeader from '@components/layout/SectionHeader.astro';
|
||||
import Placeholder from '@components/layout/Placeholder.astro';
|
||||
import SectionNav from '@components/professional/SectionNav.astro';
|
||||
import Timeline from '@components/professional/Timeline.astro';
|
||||
import SkillGrid from '@components/professional/SkillGrid.astro';
|
||||
import ProjectCard from '@components/professional/ProjectCard.astro';
|
||||
import AlchemicalDivider from '@components/mystic/AlchemicalDivider.astro';
|
||||
import Astrolabe from '@components/mystic/Astrolabe.astro';
|
||||
import { getCollection } from 'astro:content';
|
||||
|
||||
const experience = (await getCollection('experience')).sort(
|
||||
(a, b) => a.data.order - b.data.order,
|
||||
);
|
||||
const projects = (await getCollection('projects')).sort(
|
||||
(a, b) => a.data.order - b.data.order,
|
||||
);
|
||||
---
|
||||
|
||||
<BaseLayout
|
||||
surface="professional"
|
||||
title="Professional"
|
||||
description="Software developer and team lead: cloud and mobile development, SAP Business Technology Platform, AI/ML, full-stack work. Resume and portfolio for Greg Kilmer."
|
||||
>
|
||||
<!-- Page header -->
|
||||
<section class="relative overflow-hidden pt-28 sm:pt-32">
|
||||
{/* Slowly rotating astrolabe ornament behind the header. */}
|
||||
<Astrolabe
|
||||
class="pointer-events-none absolute -right-48 -top-24 h-[34rem] w-[34rem] animate-spin-slow"
|
||||
style="color: var(--color-accent); opacity:.10;"
|
||||
/>
|
||||
<div class="container-prose relative flex flex-col gap-10">
|
||||
<Reveal>
|
||||
<SectionHeader
|
||||
kicker="Professional"
|
||||
icon="briefcase"
|
||||
title="Resume & portfolio"
|
||||
subtitle="Clean, confident, skimmable. Browsable here, screenshot-able for a recruiter, printable if needed."
|
||||
/>
|
||||
</Reveal>
|
||||
<Reveal delay={80}>
|
||||
<SectionNav />
|
||||
</Reveal>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div class="container-prose flex flex-col gap-20 py-16 sm:gap-24 sm:py-20">
|
||||
{/* ===== 1. Summary ===== */}
|
||||
<section id="summary" class="scroll-mt-28 flex flex-col gap-6">
|
||||
<Reveal>
|
||||
<SectionHeader kicker="01" icon="format-list-text" title="Professional summary" />
|
||||
</Reveal>
|
||||
<Reveal delay={80}>
|
||||
<blockquote
|
||||
class="border-l-2 pl-5 text-lg leading-relaxed"
|
||||
style="border-color: var(--color-accent); color: var(--color-text-muted);"
|
||||
>
|
||||
Software developer and team lead with deep experience in cloud and
|
||||
mobile application development, currently focused on SAP-adjacent
|
||||
solutions at Implico Inc. Expanding into SAP Business Technology
|
||||
Platform (BTP) while maintaining a broad technical foundation across
|
||||
AI/ML, full-stack development, and automation. Known for bridging
|
||||
complex technical requirements with practical, maintainable solutions.
|
||||
Strong communicator who thrives at the intersection of engineering and
|
||||
business needs.
|
||||
</blockquote>
|
||||
</Reveal>
|
||||
<Reveal delay={120}>
|
||||
<figure
|
||||
class="relative overflow-hidden rounded-2xl border"
|
||||
style="border-color: color-mix(in srgb, var(--color-accent) 35%, transparent);"
|
||||
>
|
||||
<img
|
||||
src="/images/pro-workspace.jpg"
|
||||
alt="Cloud development workspace"
|
||||
loading="lazy"
|
||||
decoding="async"
|
||||
class="aspect-[21/9] w-full object-cover"
|
||||
/>
|
||||
<figcaption
|
||||
class="absolute bottom-0 left-0 m-3 rounded-md px-3 py-1 font-mono text-[0.65rem] uppercase tracking-[0.18em] backdrop-blur-sm"
|
||||
style="background: color-mix(in srgb, var(--color-ink) 60%, transparent); color: var(--color-cream);"
|
||||
>
|
||||
Where the day-job work happens
|
||||
</figcaption>
|
||||
</figure>
|
||||
</Reveal>
|
||||
</section>
|
||||
|
||||
<Reveal>
|
||||
<AlchemicalDivider class="mx-auto h-6 w-full max-w-sm" style="color: var(--color-accent);" />
|
||||
</Reveal>
|
||||
|
||||
{/* ===== 2. Experience ===== */}
|
||||
<section id="experience" class="scroll-mt-28 flex flex-col gap-6">
|
||||
<Reveal>
|
||||
<SectionHeader
|
||||
kicker="02"
|
||||
icon="timeline-text"
|
||||
title="Experience"
|
||||
subtitle="Where I've proven it. Expand any role for the detail."
|
||||
/>
|
||||
</Reveal>
|
||||
<Timeline entries={experience} />
|
||||
</section>
|
||||
|
||||
{/* ===== 3. Skills ===== */}
|
||||
<section id="skills" class="scroll-mt-28 flex flex-col gap-6">
|
||||
<Reveal>
|
||||
<SectionHeader
|
||||
kicker="03"
|
||||
icon="chart-bar"
|
||||
title="Skills & technologies"
|
||||
subtitle="What I bring to the table, organized into three tiers."
|
||||
/>
|
||||
</Reveal>
|
||||
<SkillGrid />
|
||||
</section>
|
||||
|
||||
{/* ===== 4. Projects ===== */}
|
||||
<section id="projects" class="scroll-mt-28 flex flex-col gap-6">
|
||||
<Reveal>
|
||||
<SectionHeader
|
||||
kicker="04"
|
||||
icon="view-grid"
|
||||
title="Projects"
|
||||
subtitle="What I've built. Replace the placeholder with 3-6 of your own."
|
||||
/>
|
||||
</Reveal>
|
||||
<div class="grid gap-5 sm:grid-cols-2">
|
||||
{projects.map((project, i) => (
|
||||
<Reveal delay={i * 80}>
|
||||
<ProjectCard project={project} />
|
||||
</Reveal>
|
||||
))}
|
||||
</div>
|
||||
<Placeholder
|
||||
label="Add 3-6 projects"
|
||||
hint="Drop a new markdown file in src/content/projects/ for each notable project. See docs/CONTENT.md."
|
||||
/>
|
||||
</section>
|
||||
|
||||
{/* ===== 5. Education ===== */}
|
||||
<section id="education" class="scroll-mt-28 flex flex-col gap-6">
|
||||
<Reveal>
|
||||
<SectionHeader
|
||||
kicker="05"
|
||||
icon="school"
|
||||
title="Education & certifications"
|
||||
subtitle="My foundation."
|
||||
/>
|
||||
</Reveal>
|
||||
<div class="grid gap-4 sm:grid-cols-2">
|
||||
<Placeholder label="Education history" hint="Add degrees, institutions, and dates." />
|
||||
<Placeholder label="Certifications" hint="SAP, cloud, or other relevant certifications." />
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* ===== 6. Contact ===== */}
|
||||
<section id="contact" class="scroll-mt-28 flex flex-col gap-6">
|
||||
<Reveal>
|
||||
<SectionHeader
|
||||
kicker="06"
|
||||
icon="email"
|
||||
title="Let's talk"
|
||||
subtitle="Open to opportunities in cloud development, SAP BTP, and technical leadership."
|
||||
/>
|
||||
</Reveal>
|
||||
<Reveal delay={80}>
|
||||
<div class="card flex flex-col gap-6">
|
||||
<ul class="flex flex-col gap-4 sm:flex-row sm:flex-wrap sm:gap-8">
|
||||
<li class="flex items-center gap-2">
|
||||
<Icon name="email-outline" class="h-5 w-5" style="color: var(--color-accent);" />
|
||||
<span class="font-mono text-xs uppercase tracking-[0.18em]" style="color: var(--color-accent);">Email: [Greg to fill]</span>
|
||||
</li>
|
||||
<li class="flex items-center gap-2">
|
||||
<Icon name="linkedin" class="h-5 w-5" style="color: var(--color-accent);" />
|
||||
<span class="font-mono text-xs uppercase tracking-[0.18em]" style="color: var(--color-accent);">LinkedIn: [Greg to fill]</span>
|
||||
</li>
|
||||
<li class="flex items-center gap-2">
|
||||
<Icon name="github" class="h-5 w-5" style="color: var(--color-accent);" />
|
||||
<span class="font-mono text-xs uppercase tracking-[0.18em]" style="color: var(--color-accent);">GitHub: [Greg to fill]</span>
|
||||
</li>
|
||||
</ul>
|
||||
<a href="/contact" class="btn btn-primary self-start">
|
||||
Full contact page
|
||||
<Icon name="arrow-right" class="h-5 w-5" />
|
||||
</a>
|
||||
</div>
|
||||
</Reveal>
|
||||
</section>
|
||||
</div>
|
||||
</BaseLayout>
|
||||
Reference in New Issue
Block a user