chore: update components folder structure, add constants, update existing layout components, scaffold pages

This commit is contained in:
2026-05-27 19:52:41 -07:00
parent 6193c57655
commit 50fadddbe3
16 changed files with 228 additions and 237 deletions
+2 -84
View File
@@ -1,94 +1,12 @@
// File-based routing: this file's location IS the URL.
// /app/about/page.tsx → https://yoursite.com/about
//
// Conventions:
// page.tsx = the route's UI
// layout.tsx = a wrapper shared across this folder + any nested routes
// loading.tsx = shown while the page is loading
// not-found.tsx = shown on 404 within this segment
import Hero from "@/components/content/Hero";
import type { Metadata } from "next";
import Link from "next/link";
// Route-level metadata. Next reads this at build time and emits the
// correct <title>, <meta description>, OG tags, etc. into <head>.
// The root layout (/app/layout.tsx) had its own metadata; this overrides
// the title for /about specifically.
export const metadata: Metadata = {
title: "About — Angel Mankel",
// description:
// "Frontend engineer with deep AI-tooling fluency. Currently at Village Medical.",
};
// This is a Server Component by default. It runs on the server, sends
// HTML to the browser, and ships zero JavaScript for this page itself.
// You only add "use client" at the top of a file when you need
// interactivity (useState, onClick, etc).
//
// Because it's a server component, you could `await fetch()` here, read
// a markdown file from disk, or query a DB directly — no API route needed.
export default function AboutPage() {
return (
<main className="mx-auto max-w-2xl px-6 py-24">
<h1 className="text-3xl font-semibold mb-12">About</h1>
<section className="space-y-6 text-base leading-relaxed text-neutral-200">
<p>
I came up through healthcare IT help desk, then Salesforce admin,
then software engineer at Village Medical, where I lead React and
TypeScript redesigns of patient-facing apps and the internal
component library.
</p>
<p>
In parallel I&apos;ve gone deep on AI-assisted development. Claude
Code, custom slash commands, MCP integrations the tooling layer
that&apos;s reshaping how I work across the stack.
</p>
<p>
I&apos;m looking for remote-US product-engineering roles where
shipping React is the day job and using and building AI tooling is
first-class.
</p>
</section>
<section className="mt-16">
<div className="text-xs uppercase tracking-wider text-neutral-500 mb-3">
Timeline
</div>
<ul className="font-mono text-sm space-y-1.5 text-neutral-200">
<li>
<span className="text-neutral-500 mr-3">2024 now</span>
Village Medical · Software Engineer
</li>
<li>
<span className="text-neutral-500 mr-3">2022 2024</span>
Village Medical · Salesforce Admin
</li>
<li>
<span className="text-neutral-500 mr-3">2019 2022</span>
Gila River · Help Desk / IT
</li>
</ul>
</section>
{/* next/link does client-side navigation between routes (no full
page reload) and prefetches the target route when the link
enters the viewport. Use it for any internal nav. For external
links and file downloads, plain <a> is fine. */}
<p className="mt-10 text-sm">
<a href="/cv.pdf" className="underline underline-offset-4 text-amber-400">
Download CV (PDF)
</a>
</p>
<nav className="mt-16 pt-8 border-t border-neutral-800 flex gap-6 text-sm text-neutral-400">
<Link href="/" className="hover:text-white">
Back home
</Link>
<Link href="/projects" className="hover:text-white">
See projects
</Link>
</nav>
</main>
<Hero label="About"/>
);
}
+4 -12
View File
@@ -1,26 +1,18 @@
@import "tailwindcss";
:root {
--background: #ffffff;
--foreground: #171717;
--background: #0a0a0a;
--foreground: #ededed;
}
@theme inline {
--color-background: var(--background);
--color-foreground: var(--foreground);
--font-sans: var(--font-geist-sans);
--font-mono: var(--font-geist-mono);
}
@media (prefers-color-scheme: dark) {
:root {
--background: #0a0a0a;
--foreground: #ededed;
}
--font-sans: var(--font-inter);
--font-serif: var(--font-instrument-serif);
}
body {
background: var(--background);
color: var(--foreground);
font-family: Arial, Helvetica, sans-serif;
}
+10 -8
View File
@@ -1,22 +1,24 @@
import type { Metadata } from "next";
import { Geist, Geist_Mono } from "next/font/google";
import { Inter, Instrument_Serif } from "next/font/google";
import "./globals.css";
import Header from "@/components/layout/Header";
import Footer from "@/components/layout/Footer";
import Container from "@/components/layout/Container";
const geistSans = Geist({
variable: "--font-geist-sans",
const inter = Inter({
variable: "--font-inter",
subsets: ["latin"],
});
const geistMono = Geist_Mono({
variable: "--font-geist-mono",
const instrumentSerif = Instrument_Serif({
variable: "--font-instrument-serif",
subsets: ["latin"],
weight: "400",
style: ["normal", "italic"],
});
export const metadata: Metadata = {
title: "Angel Mankel — Software Engineer"
title: "Angel Mankel — Software Engineer",
};
export default function RootLayout({
@@ -27,9 +29,9 @@ export default function RootLayout({
return (
<html
lang="en"
className={`${geistSans.variable} ${geistMono.variable} h-full antialiased`}
className={`${inter.variable} ${instrumentSerif.variable} h-full antialiased`}
>
<body className="min-h-full flex flex-col">
<body className="min-h-full flex flex-col font-sans">
<Header />
<Container>{children}</Container>
<Footer />
+65 -1
View File
@@ -1,3 +1,67 @@
import Hero from "@/components/content/Hero";
import Testimonials from "@/components/content/Testimonials";
import SectionLabel from "@/components/layout/SectionLabel";
import AccentLink from "@/components/navigation/AccentLink";
type Testimonial = {
name: string;
title: string;
avatar?: string;
quote: string;
};
const testimonialsData: Testimonial[] = [
{
name: "Sarah Chen",
title: "Senior Software Engineer · Patient Platforms",
quote:
"Working with Angel was the first time I saw someone treat Claude Code as a real engineering tool instead of a toy. Hed built half a workflow before the rest of us figured out it was possible.",
},
{
name: "Marcus Whitfield",
title: "Staff Frontend Engineer",
quote:
"He owned the component library end-to-end and ran the migration without breaking a single screen. Quiet shipper — you only notice his work when you go looking for it.",
},
{
name: "Priya Anand",
title: "Product Designer",
quote:
"Angel is the rare engineer who actually reads designs and asks questions when something feels off. Half my polish budget got returned to me on the projects he was on.",
},
{
name: "Daniel Okafor",
title: "Engineering Manager",
quote:
"Hes at his best when the problem doesnt fit cleanly into one stack. Id hand him an ambiguous brief and a week later hed come back with a working prototype and a plan.",
},
];
export default function HomePage() {
return <p>Home</p>;
return (
<div>
<Hero label="Angel Mankel">
<div className="space-y-5">
<p className="font-serif italic text-3xl leading-[1.3]">
Im a creator at heart.
</p>
<p className="text-[19px] leading-[1.65]">
Building software people rely on and understanding what makes those
people tick is what drives my greatest ambitions.
</p>
<p className="text-[19px] leading-[1.65]">
Im the kind of engineer who picks up whatever the problem needs and
keeps digging into the code, and into the people its meant for.
The throughline isnt a stack, its the curiosity.
</p>
</div>
</Hero>
<SectionLabel label="Selected Projects" />
<AccentLink label="See all projects" link="/projects" />
<SectionLabel label="What people say" />
<Testimonials items={testimonialsData} />
</div>
);
}
+3 -75
View File
@@ -1,84 +1,12 @@
// File-based routing: this file's location IS the URL.
// /app/about/page.tsx → https://yoursite.com/about
//
// Conventions:
// page.tsx = the route's UI
// layout.tsx = a wrapper shared across this folder + any nested routes
// loading.tsx = shown while the page is loading
// not-found.tsx = shown on 404 within this segment
import Hero from "@/components/content/Hero";
import type { Metadata } from "next";
import Link from "next/link";
// Route-level metadata. Next reads this at build time and emits the
// correct <title>, <meta description>, OG tags, etc. into <head>.
// The root layout (/app/layout.tsx) had its own metadata; this overrides
// the title for /work specifically.
export const metadata: Metadata = {
title: "Work — Angel Mankel",
// description:
// "Frontend engineer with deep AI-tooling fluency. Currently at Village Medical.",
};
// This is a Server Component by default. It runs on the server, sends
// HTML to the browser, and ships zero JavaScript for this page itself.
// You only add "use client" at the top of a file when you need
// interactivity (useState, onClick, etc).
//
// Because it's a server component, you could `await fetch()` here, read
// a markdown file from disk, or query a DB directly — no API route needed.
export default function WorkPage() {
export default function ProjectsPage() {
return (
<main className="mx-auto max-w-2xl px-6 py-24">
<h1 className="text-3xl font-semibold mb-12">Work</h1>
<section className="space-y-6 text-base leading-relaxed text-neutral-200">
<p>
I came up through healthcare IT help desk, then Salesforce admin,
then software engineer at Village Medical, where I lead React and
TypeScript redesigns of patient-facing apps and the internal
component library.
</p>
<p>
In parallel I&apos;ve gone deep on AI-assisted development. Claude
Code, custom slash commands, MCP integrations the tooling layer
that&apos;s reshaping how I work across the stack.
</p>
<p>
I&apos;m looking for remote-US product-engineering roles where
shipping React is the day job and using and building AI tooling is
first-class.
</p>
</section>
<section className="mt-16">
<div className="text-xs uppercase tracking-wider text-neutral-500 mb-3">
Timeline
</div>
<ul className="font-mono text-sm space-y-1.5 text-neutral-200">
<li>
<span className="text-neutral-500 mr-3">2024 now</span>
Village Medical · Software Engineer
</li>
<li>
<span className="text-neutral-500 mr-3">2022 2024</span>
Village Medical · Salesforce Admin
</li>
<li>
<span className="text-neutral-500 mr-3">2019 2022</span>
Gila River · Help Desk / IT
</li>
</ul>
</section>
<nav className="mt-16 pt-8 border-t border-neutral-800 flex gap-6 text-sm text-neutral-400">
<Link href="/" className="hover:text-white">
Back home
</Link>
<Link href="/about" className="hover:text-white">
See about
</Link>
</nav>
</main>
<Hero label="Projects" subtitle="Selected work, written as postmortems." />
);
}
+2 -6
View File
@@ -1,5 +1,5 @@
import Hero from "@/components/content/Hero";
import type { Metadata } from "next";
import Link from "next/link";
export const metadata: Metadata = {
title: "Uses — Angel Mankel",
@@ -7,10 +7,6 @@ import Link from "next/link";
export default function UsesPage() {
return (
<main className="mx-auto max-w-2xl px-6 py-24">
<h1 className="text-3xl font-semibold mb-12">Uses</h1>
test
</main>
<Hero label="Uses" subtitle="Tools and technologies I use in my work."/>
);
}
+11
View File
@@ -0,0 +1,11 @@
export default function Hero({ label, subtitle, children }: { label: string; subtitle?: string; children?: React.ReactNode }) {
return (
<section className="space-y-8">
<h1 className="font-serif text-[88px] leading-none tracking-tight">
{label}
</h1>
{subtitle && <p className="text-xl text-neutral-400 text-muted">{subtitle}</p>}
{children}
</section>
);
}
@@ -1,6 +1,7 @@
"use client";
import { useState } from "react";
import { useEffect, useState } from "react";
import { IconPlayerPauseFilled, IconPlayerPlayFilled, IconPoint, IconPointFilled } from '@tabler/icons-react';
type Testimonial = {
name: string;
@@ -15,10 +16,21 @@ type TestimonialsProps = {
export default function Testimonials({ items }: TestimonialsProps) {
const [activeIndex, setActiveIndex] = useState(0);
const [isPlaying, setIsPlaying] = useState(true);
const active = items[activeIndex];
if (!active) return null;
useEffect(() => {
if (items.length <= 1 || !isPlaying) return;
const id = setInterval(() => {
setActiveIndex((i) => (i + 1) % items.length);
}, 5000);
return () => clearInterval(id);
}, [items.length, isPlaying]);
return (
<section className="w-full">
{/* header: avatar + name + title */}
@@ -30,26 +42,25 @@ export default function Testimonials({ items }: TestimonialsProps) {
</div>
</header>
{/* quote */}
<blockquote>{active.quote}</blockquote>
{/* controls: pagination dots + play/pause */}
<footer className="flex items-center justify-between">
<div className="flex gap-2">
<div className="flex mt-3">
{items.map((_, i) => (
<button
key={i}
onClick={() => setActiveIndex(i)}
aria-label={`Go to testimonial ${i + 1}`}
>
{/* TODO: dot styling */}
{i === activeIndex ? "●" : "○"}
{i === activeIndex ? <IconPointFilled /> : <IconPoint />}
</button>
))}
</div>
{/* TODO: play/pause button + auto-rotate timer */}
<button aria-label="Pause"></button>
<button onClick={() => setIsPlaying(!isPlaying)} aria-label={isPlaying ? "Pause" : "Play"}>
{isPlaying ? <IconPlayerPauseFilled /> : <IconPlayerPlayFilled />}
</button>
</footer>
</section>
);
+8 -4
View File
@@ -1,7 +1,11 @@
export default function Container({ children }: { children: React.ReactNode }) {
export default function Container({
children,
}: {
children: React.ReactNode;
}) {
return (
<div className="p-3 bg-neutral-900 flex-1">
{children}
<div className="flex-1 min-h-0 overflow-y-auto">
<div className="mx-auto w-full max-w-2xl px-6 py-16">{children}</div>
</div>
)
);
}
+5 -6
View File
@@ -1,9 +1,8 @@
export default function Footer() {
return (
<div className="border-t border-neutral-800 text-start p-6">
Self-hosted · Traefik · Next.js
</div>
)
<footer className="mx-auto w-full max-w-2xl px-6 py-6 flex items-center justify-between border-t border-neutral-800 text-xs text-neutral-500">
<span>© 2026 Angel Mankel</span>
<span>Self-hosted · Traefik · Next.js</span>
</footer>
);
}
+2 -4
View File
@@ -3,16 +3,14 @@ import Nav from "./Nav";
export default function Header() {
return (
<header className="border-b border-neutral-800">
<div className="p-6 justify-start flex items-center flex gap-5">
<header className="mx-auto w-full max-w-2xl px-6 py-5 flex items-center justify-between">
<Link
href="/"
className="text-2xl font-semibold text-neutral-100 hover:text-white"
className="text-sm font-semibold text-neutral-100 hover:text-white"
>
Angel Mankel
</Link>
<Nav />
</div>
</header>
);
}
+21 -9
View File
@@ -1,24 +1,36 @@
import Link from "next/link";
"use client";
import Link from "next/link";
import { usePathname } from "next/navigation";
const navLinks = [
{ href: "/projects", label: "Projects" },
{ href: "/about", label: "About" },
{ href: "/uses", label: "Uses" },
{ href: "/projects", label: "projects" },
{ href: "/about", label: "about" },
{ href: "/uses", label: "uses" },
];
export default function Nav() {
const pathname = usePathname();
return (
<nav className="flex gap-6 text-[13px]">
{navLinks.map((link) => {
const isActive =
pathname === link.href || pathname.startsWith(`${link.href}/`);
return (
<nav className="flex gap-6 font-mono text-xs text-neutral-400">
{navLinks.map((link) => (
<Link
key={link.href}
href={link.href}
className="hover:text-white"
className={
isActive
? "text-neutral-100"
: "text-neutral-500 hover:text-neutral-100"
}
>
{link.label}
</Link>
))}
);
})}
</nav>
)
);
}
+13
View File
@@ -0,0 +1,13 @@
import { IconPointFilled } from '@tabler/icons-react';
import { colors } from '@/constants/colors';
export default function SectionLabel({ label }: { label: string }) {
return (
<div>
<h2 className="text-sm font-semibold uppercase tracking-wide text-neutral-400 flex gap-1 items-center">
<IconPointFilled color={colors.accent}/>
{label}
</h2>
</div>
);
}
+11
View File
@@ -0,0 +1,11 @@
import { IconArrowNarrowRight } from '@tabler/icons-react';
import { colors } from '@/constants/colors';
export default function AccentLink({ link, label }: { link: string, label: string }) {
return (
<a href={link} className='flex gap-1 items-center'>
<p style={{ color: colors.accent }}>{label}</p>
<IconArrowNarrowRight color={colors.accent} />
</a>
);
}
+32
View File
@@ -0,0 +1,32 @@
// Neutrals
const bg = "#0a0a0a";
const bgElevated = "#1c1c1c";
const border = "#2e2e2e";
const textMuted = "#6b6b6b";
const text = "#ededed";
// Accent
const accent = "#d4a24a";
// Hover states
const bgElevatedHover = "#262626";
const borderHover = "#3d3d3d";
const textMutedHover = "#9a9a9a";
const textHover = "#ffffff";
const accentHover = "#e6b966";
export const colors = {
bg,
bgElevated,
border,
textMuted,
text,
accent,
hover: {
bgElevated: bgElevatedHover,
border: borderHover,
textMuted: textMutedHover,
text: textHover,
accent: accentHover,
},
};