skeletal layout, theme functionality, mobile/desktop responsive, autoscroll
This commit is contained in:
64
src/components/Footer.tsx
Normal file
64
src/components/Footer.tsx
Normal file
@@ -0,0 +1,64 @@
|
||||
import React from "react";
|
||||
|
||||
type Social = { label: string; href: string; icon?: React.ReactNode };
|
||||
|
||||
export function Footer({
|
||||
year = new Date().getFullYear(),
|
||||
socials = [
|
||||
{ label: "GitHub", href: "#" },
|
||||
{ label: "LinkedIn", href: "#" },
|
||||
{ label: "Email", href: "#" },
|
||||
],
|
||||
showBackToTop = true,
|
||||
}: {
|
||||
year?: number;
|
||||
socials?: Social[];
|
||||
showBackToTop?: boolean;
|
||||
}) {
|
||||
return (
|
||||
<footer className="border-t border-secondary bg-bg px-4 py-10">
|
||||
<div className="mx-auto flex max-w-7xl flex-col items-center justify-between gap-6 md:flex-row">
|
||||
{/* Left: Brand + tagline */}
|
||||
<div className="text-center md:text-left">
|
||||
<div className="text-xl font-extrabold tracking-wide text-text">Jody Holt</div>
|
||||
<p className="text-sm text-text/70">Design • Develop • Deliver</p>
|
||||
</div>
|
||||
|
||||
{/* Middle: Links */}
|
||||
<nav className="flex items-center gap-5">
|
||||
<a className="text-text hover:text-primary" href="#projects">Projects</a>
|
||||
<a className="text-text hover:text-primary" href="#experience">Experience</a>
|
||||
<a className="text-text hover:text-primary" href="#home">Background</a>
|
||||
</nav>
|
||||
|
||||
{/* Right: Socials */}
|
||||
<div className="flex items-center gap-4 text-text">
|
||||
{socials.map((s) => (
|
||||
<a
|
||||
key={s.label}
|
||||
href={s.href}
|
||||
aria-label={s.label}
|
||||
className="inline-flex h-10 w-10 items-center justify-center rounded-lg border border-secondary hover:border-primary hover:text-primary"
|
||||
title={s.label}
|
||||
>
|
||||
{/* replace with real SVGs later */}
|
||||
{s.icon ?? <span className="h-2.5 w-2.5 rounded-full bg-current" />}
|
||||
</a>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mx-auto mt-6 flex max-w-7xl items-center justify-center gap-4">
|
||||
<div className="text-center text-xs text-text/60">© {year} Jody Holt • All rights reserved</div>
|
||||
{showBackToTop && (
|
||||
<button
|
||||
onClick={() => window.scrollTo({ top: 0, behavior: "smooth" })}
|
||||
className="rounded px-3 py-1 text-xs text-text/70 hover:text-primary border border-secondary hover:border-primary"
|
||||
>
|
||||
Back to top
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</footer>
|
||||
);
|
||||
}
|
||||
158
src/components/Hero.tsx
Normal file
158
src/components/Hero.tsx
Normal file
@@ -0,0 +1,158 @@
|
||||
import React from "react";
|
||||
import profileImage from "../assets/jody.png";
|
||||
import jodyMobile from "../assets/Jody-mobile.png";
|
||||
|
||||
export function Hero() {
|
||||
return (
|
||||
<section className="relative w-full bg-hero">
|
||||
<div className="md:hidden flex flex-col items-center text-center gap-2 min-h-[calc(100vh-64px)] py-6">
|
||||
<h1
|
||||
className="font-extrabold tracking-wide leading-tight text-text
|
||||
text-2xl underline md:decoration-secondary decoration-primary"
|
||||
>
|
||||
Design. Develop. Deliver.
|
||||
</h1>
|
||||
|
||||
<p className="text-sm text-text/80">
|
||||
Driven by a genuine passion for creation through code.
|
||||
</p>
|
||||
|
||||
<div className="relative h-68 w-68 rounded-full overflow-hidden mb-2">
|
||||
|
||||
<div className="absolute inset-0 rounded-full img-glow" />
|
||||
|
||||
|
||||
<img
|
||||
src={jodyMobile}
|
||||
alt="Jody Holt"
|
||||
className="relative z-[1] h-full w-full object-cover select-none pointer-events-none"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<h2 className="mt- font-extrabold text-text leading-tight tracking-wide text-3xl">
|
||||
Hello, I’m Jody Holt
|
||||
</h2>
|
||||
|
||||
<p className=" mt-5 text-lg text-base text-text/85">
|
||||
Turning concepts into clean, functional code.
|
||||
</p>
|
||||
|
||||
<p className="text-2xl font-semibold text-text mt-4">It’s What I Do.</p>
|
||||
|
||||
<p className="mt-8 text-2xl text-text">I would love to connect!</p>
|
||||
|
||||
<div className="mt-2 mb-4 flex items-center justify-center gap-4">
|
||||
{[
|
||||
{ label: "GitHub", href: "#" },
|
||||
{ label: "LinkedIn", href: "#" },
|
||||
{ label: "Email", href: "#" },
|
||||
].map((a) => (
|
||||
<a
|
||||
key={a.label}
|
||||
href={a.href}
|
||||
aria-label={a.label}
|
||||
className="inline-flex h-12 w-12 items-center justify-center rounded-lg border border-secondary/70 bg-secondary/20 text-text hover:border-primary hover:text-primary transition"
|
||||
>
|
||||
<span className="h-3 w-3 rounded-full bg-current" />
|
||||
</a>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<div className="hidden md:block md:mx-auto max-w-7xl px-4">
|
||||
<div
|
||||
className="
|
||||
min-h-[calc(100vh-64px)]
|
||||
md:min-h-[calc(100vh-80px)]
|
||||
flex flex-col md:flex-row items-start gap-10 lg:gap-10
|
||||
py-8 md:py-1
|
||||
"
|
||||
>
|
||||
<div className="shrink-0 self-start lg:pl-20">
|
||||
<img
|
||||
src={profileImage}
|
||||
alt="Jody Holt"
|
||||
className="w-[240px] sm:h-[280px] md:h-[700px] lg:h-[780px] xl:g-[800px] h-auto object-contain select-none pointer-events-none"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex-1 self-start md:pt-10 items-center text-center">
|
||||
<h1
|
||||
className="text-text font-extrabold tracking-wide leading-tight
|
||||
text-3xl sm:text-4xl md:text-3xl lg:text-5xl xl:text-6xl underline md:decoration-secondary decoration-primary"
|
||||
>
|
||||
Design. Develop. Deliver.
|
||||
</h1>
|
||||
|
||||
<p
|
||||
className="mb-10 text-text/80
|
||||
text-sm sm:text-base md:text-lg lg:text-xl xl:text-2"
|
||||
>
|
||||
Driven by a genuine passion for creation through code.
|
||||
</p>
|
||||
|
||||
<h2
|
||||
className="font-extrabold text-text leading-tight tracking-wide mb-5
|
||||
text-2xl sm:text-3xl md:text-4xl lg:text-5xl xl:text-6xl"
|
||||
>
|
||||
Hello, I’m Jody Holt
|
||||
</h2>
|
||||
|
||||
<p
|
||||
className="mb-3 text-text/85 md:mt-10 md:mb-5
|
||||
text-base md:text-xl lg:text-2xl xl:text-3xl"
|
||||
>
|
||||
Turning concepts into clean, functional code.
|
||||
</p>
|
||||
|
||||
<p
|
||||
className="mb-30 text-text/85
|
||||
text-base md:text-3xl lg:text-4xl xl:text-5xl
|
||||
font-semibold"
|
||||
>
|
||||
It’s What I Do.
|
||||
</p>
|
||||
|
||||
<p
|
||||
className="mb-8 text-text
|
||||
text-lg md:text-4xl lg:text-5xl"
|
||||
>
|
||||
I would love to connect!
|
||||
</p>
|
||||
|
||||
<div className="flex items-center justify-center gap-4 md:gap-6">
|
||||
{[
|
||||
{ label: "GitHub", href: "#" },
|
||||
{ label: "LinkedIn", href: "#" },
|
||||
{ label: "Email", href: "#" },
|
||||
].map((a) => (
|
||||
<a
|
||||
key={a.label}
|
||||
href={a.href}
|
||||
className="inline-flex items-center justify-center rounded-xl border border-secondary/70 bg-secondary/20 text-text transition
|
||||
h-10 w-10 sm:h-12 sm:w-12 md:h-14 md:w-14 lg:h-16 lg:w-16
|
||||
hover:border-primary hover:text-primary"
|
||||
aria-label={a.label}
|
||||
title={a.label}
|
||||
>
|
||||
<span className="h-2.5 w-2.5 rounded-full bg-current" />
|
||||
</a>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
90
src/components/Navbar.tsx
Normal file
90
src/components/Navbar.tsx
Normal file
@@ -0,0 +1,90 @@
|
||||
import React, { useState } from "react";
|
||||
import { ThemeToggle } from "./ThemeToggle";
|
||||
|
||||
export function Navbar({ onNav }: { onNav: (id: string) => void }) {
|
||||
const [open, setOpen] = useState(false);
|
||||
const links = [
|
||||
{ id: "home", label: "Background" },
|
||||
{ id: "projects", label: "Projects" },
|
||||
{ id: "experience", label: "Experience" },
|
||||
];
|
||||
|
||||
const handleNav = (id: string) => {
|
||||
onNav(id);
|
||||
setOpen(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<header className="sticky top-0 z-50 border-b border-secondary bg-bg/90 backdrop-blur h-16 md:h-20">
|
||||
<div className="mx-auto flex h-full max-w-7xl items-center justify-between px-4">
|
||||
{/* Brand (stacked) */}
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="leading-tight">
|
||||
<div className="text-xl md:text-2xl font-extrabold tracking-wide text-text">
|
||||
Jody Holt
|
||||
</div>
|
||||
<div className="text-[11px] md:text-sm text-text/70">
|
||||
Passion Pioneer
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Desktop nav */}
|
||||
<nav className="hidden items-center gap-6 md:flex">
|
||||
{links.map((l) => (
|
||||
<button
|
||||
key={l.id}
|
||||
className="text-text hover:text-primary"
|
||||
onClick={() => handleNav(l.id)}
|
||||
>
|
||||
{l.label}
|
||||
</button>
|
||||
))}
|
||||
<ThemeToggle />
|
||||
</nav>
|
||||
|
||||
{/* Mobile controls */}
|
||||
<div className="md:hidden">
|
||||
<button
|
||||
aria-expanded={open}
|
||||
aria-label="Toggle menu"
|
||||
className="rounded px-3 py-2 text-text hover:bg-secondary/60"
|
||||
onClick={() => setOpen((v) => !v)}
|
||||
>
|
||||
<svg viewBox="0 0 24 24" width="28" height="28" fill="currentColor">
|
||||
<path
|
||||
d="M3 6h18M3 12h18M3 18h18"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Mobile dropdown */}
|
||||
{/* Mobile dropdown */}
|
||||
<div
|
||||
className={`md:hidden transition-[max-height] duration-300 ${
|
||||
open ? "max-h-96 overflow-visible" : "max-h-0 overflow-hidden"
|
||||
}`}
|
||||
>
|
||||
<div className="space-y-2 border-t border-secondary bg-bg px-4 py-3">
|
||||
{links.map((l) => (
|
||||
<button
|
||||
key={l.id}
|
||||
className="block w-full rounded px-3 py-2 text-left text-text hover:bg-secondary/60"
|
||||
onClick={() => handleNav(l.id)}
|
||||
>
|
||||
{l.label}
|
||||
</button>
|
||||
))}
|
||||
<div className="pt-2">
|
||||
<ThemeToggle compact />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
);
|
||||
}
|
||||
18
src/components/Placeholder.tsx
Normal file
18
src/components/Placeholder.tsx
Normal file
@@ -0,0 +1,18 @@
|
||||
// =====================================
|
||||
import React from "react";
|
||||
|
||||
|
||||
export function Placeholder({ title }: { title: string }) {
|
||||
return (
|
||||
<div className="mx-auto max-w-7xl px-4 py-24">
|
||||
<h3 className="mb-6 text-3xl font-bold text-text">{title}</h3>
|
||||
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
|
||||
{Array.from({ length: 6 }).map((_, i) => (
|
||||
<div key={i} className="rounded-xl border border-secondary bg-secondary/30 p-6 text-text/85">
|
||||
Card {i + 1}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
10
src/components/Section.tsx
Normal file
10
src/components/Section.tsx
Normal file
@@ -0,0 +1,10 @@
|
||||
import React from "react";
|
||||
|
||||
|
||||
export function Section({ id, children }: React.PropsWithChildren<{ id: string }>) {
|
||||
return (
|
||||
<section id={id} className="scroll-mt-24">
|
||||
{children}
|
||||
</section>
|
||||
);
|
||||
}
|
||||
48
src/components/ThemeToggle.tsx
Normal file
48
src/components/ThemeToggle.tsx
Normal file
@@ -0,0 +1,48 @@
|
||||
import React from "react";
|
||||
import { useTheme } from "../hooks/useTheme";
|
||||
|
||||
export function ThemeToggle({ compact = false }: { compact?: boolean }) {
|
||||
const { theme, setTheme } = useTheme();
|
||||
const themes = ["a", "b", "c", "d", "e"] as const;
|
||||
|
||||
return (
|
||||
<div className="relative inline-block text-text">
|
||||
<details className="group">
|
||||
<summary className="cursor-pointer select-none list-none inline-flex items-center gap-2 rounded px-3 py-1.5 bg-secondary/70 hover:bg-secondary focus:outline-none">
|
||||
<span className="font-medium">
|
||||
{compact ? "Theme" : "Toggle Theme"}
|
||||
</span>
|
||||
<span aria-hidden>▾</span>
|
||||
</summary>
|
||||
<div
|
||||
className="
|
||||
absolute top-full mt-2
|
||||
left-0 right-0 w-[calc(100vw-10rem)]
|
||||
md:left-auto md:right- md:mx-0 md:w-44
|
||||
rounded-lg border border-secondary bg-bg/95 p-2 shadow-xl backdrop-blur z-[70]
|
||||
"
|
||||
>
|
||||
<ul className="space-y-1">
|
||||
{themes.map((t) => (
|
||||
<li key={t}>
|
||||
<button
|
||||
onClick={() => setTheme(t as any)}
|
||||
className={`w-full rounded px-3 py-2 text-left hover:bg-secondary/60 ${
|
||||
theme === t ? "outline outline-1 outline-primary" : ""
|
||||
}`}
|
||||
>
|
||||
{/* preview dot uses theme accent variables you defined per theme (optional) */}
|
||||
<span
|
||||
className="mr-2 inline-block h-3 w-3 rounded-full align-middle"
|
||||
style={{ background: `var(--color-accent-${t})` }}
|
||||
/>
|
||||
Theme {t.toUpperCase()}
|
||||
</button>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
</details>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user