mirror of
https://github.com/Patiun/com.gregkilmer.www.git
synced 2026-11-08 04:08:24 +00:00
Content update and Professional details
This commit is contained in:
@@ -0,0 +1,77 @@
|
||||
---
|
||||
/**
|
||||
* Certifications.astro
|
||||
* ---------------------------------------------------------------------------
|
||||
* Card grid for the Certifications section of the Professional page. Each entry
|
||||
* is a cert with name, issuer, year, and optional status. Seeded with SAP/cloud
|
||||
* placeholders (Greg to fill); swap the array below.
|
||||
*
|
||||
* Card aesthetic matches EducationHistory / ProjectCard.
|
||||
*/
|
||||
import Icon from "@components/Icon.astro";
|
||||
|
||||
interface CertEntry {
|
||||
icon: string;
|
||||
name: string;
|
||||
issuer: string;
|
||||
year: string;
|
||||
status?: "Active" | "Expired" | "In progress";
|
||||
}
|
||||
|
||||
// Placeholder entries — replace with Greg's real certifications.
|
||||
const entries: CertEntry[] = [
|
||||
/*{
|
||||
icon: "microsoft-azure-devops",
|
||||
name: "SAP BTP Certification",
|
||||
issuer: "SAP",
|
||||
year: "[Year]",
|
||||
status: "Active",
|
||||
},*/
|
||||
{
|
||||
icon: "cloud",
|
||||
name: "Planet 9 Devlopment",
|
||||
issuer: "Neptune",
|
||||
year: "2019",
|
||||
status: "Active",
|
||||
},
|
||||
/*{
|
||||
icon: "robot",
|
||||
name: "[AI / ML cert]",
|
||||
issuer: "[Issuer]",
|
||||
year: "[Year]",
|
||||
},*/
|
||||
];
|
||||
---
|
||||
|
||||
<div class="grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
|
||||
{
|
||||
entries.map((c) => (
|
||||
<article class="card flex flex-col gap-3">
|
||||
<div class="flex items-start justify-between 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={c.icon} class="h-5 w-5" />
|
||||
</span>
|
||||
{c.status && <span class="pill">{c.status}</span>}
|
||||
</div>
|
||||
<div class="flex flex-col gap-1">
|
||||
<h3 class="font-sans text-base" style="color: var(--color-text);">
|
||||
{c.name}
|
||||
</h3>
|
||||
<p class="text-sm font-medium" style="color: var(--color-accent);">
|
||||
{c.issuer}
|
||||
</p>
|
||||
<span
|
||||
class="font-mono text-xs uppercase tracking-[0.18em]"
|
||||
style="color: var(--color-text-subtle);"
|
||||
>
|
||||
{c.year}
|
||||
</span>
|
||||
</div>
|
||||
</article>
|
||||
))
|
||||
}
|
||||
</div>
|
||||
@@ -0,0 +1,76 @@
|
||||
---
|
||||
/**
|
||||
* EducationHistory.astro
|
||||
* ---------------------------------------------------------------------------
|
||||
* Card grid for the Education section of the Professional page. Each entry is a
|
||||
* degree/diploma with institution, period, and optional detail. Data is seeded
|
||||
* with placeholder rows (Greg to fill); swap the array below with real entries.
|
||||
*
|
||||
* Mirrors the Timeline/ProjectCard card aesthetic: raised surface, hairline
|
||||
* border, gold accent for the credential, mono period label.
|
||||
*/
|
||||
import Icon from "@components/Icon.astro";
|
||||
|
||||
interface EduEntry {
|
||||
icon: string;
|
||||
credential: string;
|
||||
institution: string;
|
||||
period: string;
|
||||
detail?: string;
|
||||
}
|
||||
|
||||
// Placeholder entries — replace with Greg's real education history.
|
||||
const entries: EduEntry[] = [
|
||||
{
|
||||
icon: "school",
|
||||
credential: "B.S., Computer Science",
|
||||
institution: "Drexel University",
|
||||
period: "2014 – 2018",
|
||||
detail:
|
||||
"Foundational coursework in software engineering, algorithms, and systems. Focuses in AI and human-computer interaction.",
|
||||
},
|
||||
];
|
||||
---
|
||||
|
||||
<div class="grid gap-4 sm:grid-cols-2">
|
||||
{
|
||||
entries.map((e) => (
|
||||
<article class="card flex flex-col gap-3">
|
||||
<div class="flex items-start 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={e.icon} class="h-5 w-5" />
|
||||
</span>
|
||||
<div class="flex flex-col gap-1">
|
||||
<h3 class="font-sans text-lg" style="color: var(--color-text);">
|
||||
{e.credential}
|
||||
</h3>
|
||||
<p
|
||||
class="font-sans font-semibold"
|
||||
style="color: var(--color-accent);"
|
||||
>
|
||||
{e.institution}
|
||||
</p>
|
||||
<span
|
||||
class="font-mono text-xs uppercase tracking-[0.18em]"
|
||||
style="color: var(--color-text-subtle);"
|
||||
>
|
||||
{e.period}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
{e.detail && (
|
||||
<p
|
||||
class="text-sm leading-relaxed"
|
||||
style="color: var(--color-text-muted);"
|
||||
>
|
||||
{e.detail}
|
||||
</p>
|
||||
)}
|
||||
</article>
|
||||
))
|
||||
}
|
||||
</div>
|
||||
@@ -2,15 +2,23 @@
|
||||
/**
|
||||
* SectionNav.astro
|
||||
* ---------------------------------------------------------------------------
|
||||
* Fixed LEFT-SIDE vertical rail for the Professional page. Shows the six
|
||||
* sections as a stacked icon+label list, highlights the section currently in
|
||||
* view, and lets the user click any item to smooth-scroll to it. A vertical
|
||||
* progress line beside the items fills as the user scrolls down the page.
|
||||
* Fixed LEFT-SIDE vertical rail for the Professional page. Six sections as a
|
||||
* stacked icon+label list; highlights the current section; click to smooth-
|
||||
* scroll. A vertical progress line LEFT of the icons fills as you scroll.
|
||||
*
|
||||
* Stylistically it mirrors the page's `.card` language (same raised surface,
|
||||
* border, radius) so it reads as a sibling of the content cards rather than a
|
||||
* separate chrome element. Icons + labels use bright tokens so they pop on the
|
||||
* dark surface. Hidden below `lg`.
|
||||
* Three bugs previously fixed here:
|
||||
* 1. Contrast: the rail used surface-relative tokens (--color-surface-raised /
|
||||
* --color-cream) which collapsed to the same tone on light surfaces (the
|
||||
* interest-surface cream made icons/text invisible). The rail now uses
|
||||
* FIXED dark colors regardless of the page's surface tone.
|
||||
* 2. Progress line clipping: the line sat at the icon column's horizontal
|
||||
* center, running THROUGH the circles. It now sits in a dedicated left
|
||||
* gutter, clear of the icons.
|
||||
* 3. Last section (Contact) not activating: the IntersectionObserver band was
|
||||
* too thin for the short final section to land in. The rootMargin is
|
||||
* widened + click handlers set active immediately so feedback is instant.
|
||||
*
|
||||
* Hidden below `lg`.
|
||||
*/
|
||||
import Icon from "@components/Icon.astro";
|
||||
const sections = [
|
||||
@@ -28,70 +36,79 @@ const sections = [
|
||||
class="pro-section-nav fixed left-4 top-1/2 z-40 hidden -translate-y-1/2 flex-col lg:flex"
|
||||
data-pro-nav
|
||||
>
|
||||
{/* Rail container — matches the .card aesthetic: raised surface, hairline
|
||||
border, 1rem radius. Bright icons + labels inside. */}
|
||||
{/* Rail container. FIXED dark colors (not surface tokens) so it's always
|
||||
legible regardless of the page's data-surface tone. Layout: a left gutter
|
||||
for the progress line, then the icon+label list. */}
|
||||
<div
|
||||
class="card relative flex flex-col gap-1 p-3"
|
||||
style="border-color: var(--color-border); background: var(--color-surface-raised); backdrop-filter: blur(10px); -webkit-backdrop-filter: blur(10px); box-shadow: 0 18px 40px -24px rgba(11, 16, 38, 0.6);"
|
||||
class="relative flex gap-2 rounded-2xl border p-3 shadow-2xl"
|
||||
style="border-color: rgba(232, 181, 74, 0.22); background: linear-gradient(180deg, rgba(12, 14, 40, 0.97), rgba(8, 10, 32, 0.97)); backdrop-filter: blur(10px); -webkit-backdrop-filter: blur(10px); box-shadow: 0 18px 40px -24px rgba(11, 16, 38, 0.7);"
|
||||
>
|
||||
{/* Vertical progress track behind the dots. */}
|
||||
<div
|
||||
class="pointer-events-none absolute left-[1.55rem] top-6 bottom-6 w-px"
|
||||
style="background: color-mix(in srgb, var(--color-border) 80%, transparent);"
|
||||
aria-hidden="true"
|
||||
>
|
||||
</div>
|
||||
{/* The fill bar — height set by JS as the user scrolls. */}
|
||||
<div
|
||||
class="pointer-events-none absolute left-[1.55rem] top-6 w-px origin-top"
|
||||
style="background: var(--color-accent); height: 0%; box-shadow: 0 0 8px color-mix(in srgb, var(--color-accent) 60%, transparent);"
|
||||
data-pro-fill
|
||||
aria-hidden="true"
|
||||
>
|
||||
{/* --- Left gutter: the progress track + fill. Sits LEFT of the icons. --- */}
|
||||
<div class="relative my-1 w-2 shrink-0">
|
||||
{/* Track (full height of the gutter). */}
|
||||
<div
|
||||
class="pointer-events-none absolute left-1/2 top-0 h-full w-px -translate-x-1/2"
|
||||
style="background: rgba(255, 255, 255, 0.12);"
|
||||
aria-hidden="true"
|
||||
>
|
||||
</div>
|
||||
{/* Fill — height set by JS. Anchored to the top of the track. */}
|
||||
<div
|
||||
class="pointer-events-none absolute left-1/2 top-0 w-px -translate-x-1/2 origin-top"
|
||||
style="background: var(--color-accent); height: 0%; box-shadow: 0 0 8px color-mix(in srgb, var(--color-accent) 60%, transparent);"
|
||||
data-pro-fill
|
||||
aria-hidden="true"
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{sections.map((s) => (
|
||||
<a
|
||||
href={`#${s.id}`}
|
||||
data-section-link={s.id}
|
||||
class="group relative flex items-center gap-3 rounded-lg px-2.5 py-2 font-sans text-sm font-semibold transition-all"
|
||||
style="color: var(--color-cream); opacity: 0.85;"
|
||||
>
|
||||
<span
|
||||
class="relative z-10 inline-flex h-7 w-7 shrink-0 items-center justify-center rounded-lg transition-colors"
|
||||
style="background: color-mix(in srgb, var(--color-accent) 14%, transparent); color: var(--color-cream);"
|
||||
data-section-dot={s.id}
|
||||
{/* --- The icon+label list. --- */}
|
||||
<div class="flex flex-col gap-1">
|
||||
{sections.map((s) => (
|
||||
<a
|
||||
href={`#${s.id}`}
|
||||
data-section-link={s.id}
|
||||
class="group relative flex items-center gap-2.5 rounded-lg px-2.5 py-2 font-sans text-sm font-semibold transition-all"
|
||||
style="color: #e8e6f5;"
|
||||
>
|
||||
<Icon name={s.icon} class="h-[1.15rem] w-[1.15rem]" />
|
||||
</span>
|
||||
<span class="relative z-10 tracking-wide">{s.label}</span>
|
||||
</a>
|
||||
))}
|
||||
<span
|
||||
class="relative z-10 inline-flex h-7 w-7 shrink-0 items-center justify-center rounded-lg transition-colors"
|
||||
style="background: rgba(232, 181, 74, 0.14); color: #f4cd72;"
|
||||
data-section-dot={s.id}
|
||||
>
|
||||
<Icon name={s.icon} class="h-[1.15rem] w-[1.15rem]" />
|
||||
</span>
|
||||
<span class="relative z-10 tracking-wide">{s.label}</span>
|
||||
</a>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<style>
|
||||
/* Active link: full opacity + accent text + subtle accent wash. */
|
||||
/* Inactive items: readable but slightly dimmed. */
|
||||
.pro-section-nav a[data-section-link] {
|
||||
opacity: 0.82;
|
||||
}
|
||||
.pro-section-nav a[data-section-link]:hover {
|
||||
opacity: 1;
|
||||
background: rgba(232, 181, 74, 0.08);
|
||||
}
|
||||
/* Active link: bright accent text + accent wash. */
|
||||
.pro-section-nav a[data-active="true"] {
|
||||
opacity: 1 !important;
|
||||
color: var(--color-accent) !important;
|
||||
background: color-mix(in srgb, var(--color-accent) 14%, transparent);
|
||||
background: rgba(232, 181, 74, 0.14);
|
||||
}
|
||||
/* Active dot: filled accent, bright icon, glow ring. */
|
||||
/* Active dot: filled accent, dark icon, glow ring. */
|
||||
.pro-section-nav a[data-active="true"] [data-section-dot] {
|
||||
background: color-mix(in srgb, var(--color-accent) 80%, transparent) !important;
|
||||
color: var(--color-ink) !important;
|
||||
box-shadow: 0 0 12px color-mix(in srgb, var(--color-accent) 65%, transparent);
|
||||
}
|
||||
/* Hover lift on inactive items. */
|
||||
.pro-section-nav a:hover {
|
||||
opacity: 1 !important;
|
||||
background: color-mix(in srgb, var(--color-accent) 8%, transparent);
|
||||
background: var(--color-accent) !important;
|
||||
color: #0b1026 !important;
|
||||
box-shadow: 0 0 12px rgba(232, 181, 74, 0.65);
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
// Spy: highlight the link for whichever section is in view + fill the rail.
|
||||
function setupSectionSpy() {
|
||||
const nav = document.querySelector<HTMLElement>("[data-pro-nav]");
|
||||
if (!nav) return;
|
||||
@@ -107,40 +124,81 @@ const sections = [
|
||||
|
||||
const setActive = (id: string) => {
|
||||
for (const l of links) {
|
||||
const active = l.dataset.sectionLink === id;
|
||||
l.setAttribute("data-active", active ? "true" : "false");
|
||||
l.setAttribute("data-active", l.dataset.sectionLink === id ? "true" : "false");
|
||||
}
|
||||
};
|
||||
|
||||
// Highlight the most-visible section.
|
||||
// Manual-scroll suppression. When the user clicks a nav link we set active
|
||||
// immediately AND suppress IO updates until the smooth-scroll settles. This
|
||||
// is what stops the flicker: without it, the IO callback and the scroll
|
||||
// handler fight during the animated scroll, toggling active state rapidly.
|
||||
let suppressIo = false;
|
||||
let suppressTimer: number | undefined;
|
||||
|
||||
// Click → set active immediately + suppress IO until scrolling settles.
|
||||
for (const l of links) {
|
||||
l.addEventListener("click", () => {
|
||||
const id = l.dataset.sectionLink!;
|
||||
setActive(id);
|
||||
suppressIo = true;
|
||||
window.clearTimeout(suppressTimer);
|
||||
// Re-enable IO once the smooth-scroll has had time to finish.
|
||||
suppressTimer = window.setTimeout(() => {
|
||||
suppressIo = false;
|
||||
}, 900);
|
||||
});
|
||||
}
|
||||
|
||||
// The IntersectionObserver is the SINGLE source of truth for the active
|
||||
// section during free scrolling. (Previously the scroll handler also set
|
||||
// active, which caused the two systems to thrash.)
|
||||
const io = new IntersectionObserver(
|
||||
(entries) => {
|
||||
if (suppressIo) return;
|
||||
const visible = entries
|
||||
.filter((e) => e.isIntersecting)
|
||||
.sort((a, b) => b.intersectionRatio - a.intersectionRatio)[0];
|
||||
if (visible) setActive(visible.target.id);
|
||||
},
|
||||
{ rootMargin: "-30% 0px -55% 0px", threshold: [0, 0.25, 0.5] },
|
||||
// Band spans roughly the middle 50% of the viewport vertically.
|
||||
{ rootMargin: "-25% 0px -25% 0px", threshold: [0, 0.1, 0.25, 0.5] },
|
||||
);
|
||||
sections.forEach((s) => io.observe(s));
|
||||
|
||||
// Progress fill: fraction of the page scrolled.
|
||||
const trackTop = 24; // px — matches top-6 on the track/fill
|
||||
const trackBottom = 24; // px — matches bottom-6
|
||||
// Progress fill ONLY. The scroll handler must never set active state — that
|
||||
// was the other half of the flicker. Last-section-at-bottom is handled by
|
||||
// a dedicated, debounced check below instead of on every scroll tick.
|
||||
const updateFill = () => {
|
||||
if (!fill) return;
|
||||
const track = fill.parentElement; // the .w-2 gutter
|
||||
if (!track) return;
|
||||
const doc = document.documentElement;
|
||||
const max = doc.scrollHeight - doc.clientHeight;
|
||||
const frac = max > 0 ? Math.max(0, Math.min(1, window.scrollY / max)) : 0;
|
||||
// Fill height as a percentage of the track length (which spans the card
|
||||
// minus top/bottom padding).
|
||||
const cardEl = fill.parentElement;
|
||||
if (!cardEl) return;
|
||||
const trackPx = cardEl.clientHeight - trackTop - trackBottom;
|
||||
fill.style.height = `${(frac * trackPx).toFixed(1)}px`;
|
||||
fill.style.height = `${(frac * track.clientHeight).toFixed(1)}px`;
|
||||
};
|
||||
|
||||
// Debounced at-bottom check: pin the last section active when truly at the
|
||||
// bottom, but only after scrolling has paused (not on every tick).
|
||||
let bottomTimer: number | undefined;
|
||||
const checkBottom = () => {
|
||||
window.clearTimeout(bottomTimer);
|
||||
bottomTimer = window.setTimeout(() => {
|
||||
if (suppressIo) return;
|
||||
const doc = document.documentElement;
|
||||
const atBottom =
|
||||
window.innerHeight + window.scrollY >= doc.scrollHeight - 4;
|
||||
if (atBottom && sections.length) {
|
||||
setActive(sections[sections.length - 1].id);
|
||||
}
|
||||
}, 120);
|
||||
};
|
||||
|
||||
updateFill();
|
||||
window.addEventListener("scroll", updateFill, { passive: true });
|
||||
window.addEventListener("scroll", () => {
|
||||
updateFill();
|
||||
checkBottom();
|
||||
}, { passive: true });
|
||||
window.addEventListener("resize", updateFill);
|
||||
}
|
||||
setupSectionSpy();
|
||||
|
||||
@@ -32,25 +32,10 @@ const rendered = await Promise.all(
|
||||
);
|
||||
---
|
||||
|
||||
<ol class="relative flex flex-col gap-10">
|
||||
{/* The vertical line. */}
|
||||
<span
|
||||
class="absolute left-[1.15rem] top-2 bottom-2 w-px"
|
||||
style="background: var(--color-border);"
|
||||
aria-hidden="true"
|
||||
></span>
|
||||
|
||||
<ol class="flex flex-col gap-10">
|
||||
{rendered.map(({ entry, Content }, i) => (
|
||||
<li class="relative pl-12">
|
||||
<li class="relative">
|
||||
<Reveal delay={i * 80}>
|
||||
{/* Marker dot on the timeline line (kept small; the icon now lives inline
|
||||
with the company name inside the card so it never clips the card). */}
|
||||
<span
|
||||
class="absolute left-[1.15rem] top-7 h-3 w-3 -translate-x-1/2 rounded-full"
|
||||
style="background: var(--color-accent); box-shadow: 0 0 0 4px var(--color-surface);"
|
||||
aria-hidden="true"
|
||||
>
|
||||
</span>
|
||||
|
||||
<div class="card flex flex-col gap-3">
|
||||
{/* Header row: icon + company name inline, period on the right. */}
|
||||
|
||||
Reference in New Issue
Block a user