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
+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>
);
+11 -7
View File
@@ -1,7 +1,11 @@
export default function Container({ children }: { children: React.ReactNode }) {
return (
<div className="p-3 bg-neutral-900 flex-1">
{children}
</div>
)
}
export default function Container({
children,
}: {
children: React.ReactNode;
}) {
return (
<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>
);
}
+7 -8
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>
)
}
return (
<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>
);
}
+8 -10
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">
<Link
href="/"
className="text-2xl font-semibold text-neutral-100 hover:text-white"
>
Angel Mankel
</Link>
<Nav />
</div>
<header className="mx-auto w-full max-w-2xl px-6 py-5 flex items-center justify-between">
<Link
href="/"
className="text-sm font-semibold text-neutral-100 hover:text-white"
>
Angel Mankel
</Link>
<Nav />
</header>
);
}
+30 -18
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() {
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"
>
{link.label}
</Link>
))}
</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 (
<Link
key={link.href}
href={link.href}
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>
);
}