-
-
- Tyler Koenig
-
-
- SOC Helpdesk I · Homelab Operator
-
-
-
-
- I write software and run infrastructure that goes well past what my
- job title implies. Games, AI tooling, mobile apps, and a homelab
- running 20+ self-hosted services on segmented VLANs. Continuously
- learning by building things that actually work.
+
+
+
+ ❯
+ tyler koenig
+
+ SOC Helpdesk I · Homelab Operator
+
+
-
+
+ I write software and run infrastructure that goes well past what my
+ job title implies. Games, AI tooling, mobile apps, and a homelab
+ running 20+ self-hosted services on segmented VLANs. Continuously
+ learning by building things that actually work.{' '}
+ █
+
+
+
diff --git a/src/components/Nav.tsx b/src/components/Nav.tsx
index c5ce90e7..9b020f29 100644
--- a/src/components/Nav.tsx
+++ b/src/components/Nav.tsx
@@ -2,26 +2,28 @@
import Link from "next/link";
import { usePathname } from "next/navigation";
+import { useTheme } from "@/context/ThemeContext";
const links = [
- { href: "/", label: "home" },
+ { href: "/", label: "tyler" },
{ href: "/homelab/", label: "homelab" },
{ href: "/archive/", label: "archive" },
];
export default function Nav() {
const pathname = usePathname();
+ const { isDark, toggle } = useTheme();
return (
diff --git a/src/components/ProjectCard.tsx b/src/components/ProjectCard.tsx
index 76d75a00..51958be2 100644
--- a/src/components/ProjectCard.tsx
+++ b/src/components/ProjectCard.tsx
@@ -6,7 +6,7 @@ type Props = {
export default function ProjectCard({ project }: Props) {
return (
-
+
-
+
{project.description}
-
+
{project.tags.map((tag) => (
{tag}
diff --git a/src/components/Timeline.tsx b/src/components/Timeline.tsx
new file mode 100644
index 00000000..4af12b46
--- /dev/null
+++ b/src/components/Timeline.tsx
@@ -0,0 +1,107 @@
+'use client'
+
+import { useEffect, useRef } from 'react'
+import Widget from '@/components/Widget'
+import { timeline, type TimelineType } from '@/data/timeline'
+
+const typeColor: Record = {
+ career: 'var(--color-timeline-career)',
+ education: 'var(--color-timeline-education)',
+ cert: 'var(--color-timeline-cert)',
+ project: 'var(--color-timeline-project)',
+ homelab: 'var(--color-timeline-homelab)',
+}
+
+const typeLabel: Record = {
+ career: 'career',
+ education: 'education',
+ cert: 'cert',
+ project: 'project',
+ homelab: 'homelab',
+}
+
+export default function Timeline() {
+ const listRef = useRef(null)
+
+ useEffect(() => {
+ if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) return
+
+ const entries = listRef.current?.querySelectorAll('[data-tl-entry]')
+ if (!entries) return
+
+ const observer = new IntersectionObserver(
+ (observed) => {
+ observed.forEach((entry) => {
+ if (entry.isIntersecting) {
+ ;(entry.target as HTMLElement).style.opacity = '1'
+ ;(entry.target as HTMLElement).style.transform = 'translateY(0)'
+ observer.unobserve(entry.target)
+ }
+ })
+ },
+ { threshold: 0.15 },
+ )
+
+ entries.forEach((el) => {
+ el.style.opacity = '0'
+ el.style.transform = 'translateY(8px)'
+ el.style.transition = 'opacity 240ms linear, transform 240ms linear'
+ observer.observe(el)
+ })
+
+ return () => observer.disconnect()
+ }, [])
+
+ return (
+
+
+ {timeline.map((entry, i) => (
+ -
+ {/* Spine dot */}
+
+
+ {/* Date + type badge */}
+
+ {entry.date}
+
+ {typeLabel[entry.type]}
+
+
+
+ {/* Title */}
+
+ {entry.title}
+
+
+ {/* Description */}
+
+ {entry.description}
+
+
+ {/* Tags */}
+ {entry.tags && entry.tags.length > 0 && (
+
+ {entry.tags.map((tag) => (
+
+ {tag}
+
+ ))}
+
+ )}
+
+ ))}
+
+
+ )
+}