portfolio nearly complete, needs viewtesting on different moniters and screens and needs about me section, possibly files for production.

This commit is contained in:
2026-01-05 23:46:21 -06:00
parent da75479555
commit 6975f5aeab
26 changed files with 655 additions and 95 deletions

7
package-lock.json generated
View File

@@ -19,6 +19,7 @@
"@types/react": "^19.1.16", "@types/react": "^19.1.16",
"@types/react-dom": "^19.1.9", "@types/react-dom": "^19.1.9",
"@vitejs/plugin-react": "^5.0.4", "@vitejs/plugin-react": "^5.0.4",
"baseline-browser-mapping": "^2.9.11",
"eslint": "^9.36.0", "eslint": "^9.36.0",
"eslint-plugin-react-hooks": "^5.2.0", "eslint-plugin-react-hooks": "^5.2.0",
"eslint-plugin-react-refresh": "^0.4.22", "eslint-plugin-react-refresh": "^0.4.22",
@@ -2019,9 +2020,9 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/baseline-browser-mapping": { "node_modules/baseline-browser-mapping": {
"version": "2.8.17", "version": "2.9.11",
"resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.8.17.tgz", "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.9.11.tgz",
"integrity": "sha512-j5zJcx6golJYTG6c05LUZ3Z8Gi+M62zRT/ycz4Xq4iCOdpcxwg7ngEYD4KA0eWZC7U17qh/Smq8bYbACJ0ipBA==", "integrity": "sha512-Sg0xJUNDU1sJNGdfGWhVHX0kkZ+HWcvmVymJbj6NSgZZmW/8S9Y2HQ5euytnIgakgxN6papOAWiwDo1ctFDcoQ==",
"dev": true, "dev": true,
"license": "Apache-2.0", "license": "Apache-2.0",
"bin": { "bin": {

View File

@@ -21,6 +21,7 @@
"@types/react": "^19.1.16", "@types/react": "^19.1.16",
"@types/react-dom": "^19.1.9", "@types/react-dom": "^19.1.9",
"@vitejs/plugin-react": "^5.0.4", "@vitejs/plugin-react": "^5.0.4",
"baseline-browser-mapping": "^2.9.11",
"eslint": "^9.36.0", "eslint": "^9.36.0",
"eslint-plugin-react-hooks": "^5.2.0", "eslint-plugin-react-hooks": "^5.2.0",
"eslint-plugin-react-refresh": "^0.4.22", "eslint-plugin-react-refresh": "^0.4.22",

View File

@@ -2,7 +2,8 @@ import React, { useEffect, useMemo, useRef, useState } from "react";
import { Navbar } from "./components/Navbar"; import { Navbar } from "./components/Navbar";
import { Section } from "./components/Section"; import { Section } from "./components/Section";
import { Hero } from "./components/Hero"; import { Hero } from "./components/Hero";
import { Placeholder } from "./components/Placeholder"; import { Projects } from "./components/Projects";
import { Resume } from "./components/Resume";
import { Footer } from "./components/Footer"; import { Footer } from "./components/Footer";
export default function App() { export default function App() {
@@ -46,9 +47,9 @@ return (
<main> <main>
<Section id="home"><Hero /></Section> <Section id="home"><Hero /></Section>
<GradientBand /> <GradientBand />
<Section id="projects"><Placeholder title="Projects" /></Section> <Section id="projects"><Projects /></Section>
<GradientBand /> <GradientBand />
<Section id="experience"><Placeholder title="Experience" /></Section> <Section id="experience"><Resume /></Section>
</main> </main>

Binary file not shown.

After

Width:  |  Height:  |  Size: 86 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 81 KiB

View File

Before

Width:  |  Height:  |  Size: 444 KiB

After

Width:  |  Height:  |  Size: 444 KiB

View File

Before

Width:  |  Height:  |  Size: 375 KiB

After

Width:  |  Height:  |  Size: 375 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 964 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -1,14 +1,23 @@
import React from "react"; import React from "react";
import githubIcon from "../assets/img/github-icon.png";
import linkedInIcon from "../assets/img/linkedin-icon.png";
import emailIcon from "../assets/img/email-icon.png";
import facebookIcon from "../assets/img/facebook-icon.png";
import phoneIcon from "../assets/img/phone-icon.png";
type Social = { label: string; href: string; icon?: React.ReactNode }; const defaultSocials = [
{ label: "GitHub", href: "https://github.com/Ricearoni1245", icon: githubIcon },
{ label: "LinkedIn", href: "https://www.linkedin.com/in/jody-holt-9b19b0256", icon: linkedInIcon },
{ label: "Facebook", href: "https://www.facebook.com/jody.holt.7161/", icon: facebookIcon },
{ label: "Email", href: "mailto:jholt1008@gmail.com", icon: emailIcon },
{ label: "Phone", href: "tel:8066542813", icon: phoneIcon },
];
type Social = { label: string; href: string; icon?: string };
export function Footer({ export function Footer({
year = new Date().getFullYear(), year = new Date().getFullYear(),
socials = [ socials = defaultSocials,
{ label: "GitHub", href: "#" },
{ label: "LinkedIn", href: "#" },
{ label: "Email", href: "#" },
],
showBackToTop = true, showBackToTop = true,
}: { }: {
year?: number; year?: number;
@@ -28,47 +37,71 @@ export function Footer({
</div> </div>
<nav className="flex items-center gap-5"> <nav className="flex items-center gap-5">
<a className="text-text hover:text-primary" href="#projects"> <button
Projects className="text-text hover:text-primary anim-base"
</a> onClick={() => document.getElementById("home")?.scrollIntoView({ behavior: "smooth" })}
<a className="text-text hover:text-primary" href="#experience">
Experience
</a>
<a className="text-text hover:text-primary" href="#home">
Background
</a>
</nav>
<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}
> >
{s.icon ?? ( Background
<span className="h-2.5 w-2.5 rounded-full bg-current" /> </button>
)} <button
</a> className="text-text hover:text-primary anim-base"
))} onClick={() => document.getElementById("projects")?.scrollIntoView({ behavior: "smooth" })}
</div> >
</div> Projects
</button>
<button
className="text-text hover:text-primary anim-base"
onClick={() => document.getElementById("experience")?.scrollIntoView({ behavior: "smooth" })}
>
Experience
</button>
</nav>
<div className="mx-auto mt-6 flex max-w-7xl items-center justify-center gap-4"> <div className="flex items-center gap-4 text-text">
<div className="text-center text-xs text-text/60"> {socials.map((s) => (
© {year} Jody Holt All rights reserved <a
key={s.label}
href={s.href}
target={s.href.startsWith("mailto:") || s.href.startsWith("tel:") ? undefined : "_blank"}
rel={s.href.startsWith("mailto:") || s.href.startsWith("tel:") ? undefined : "noopener noreferrer"}
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 anim-base"
title={s.label}
>
{s.icon ? (
<img src={s.icon} alt={s.label} className="h-5 w-5 invert brightness-0 invert opacity-80" />
) : (
<span className="h-2.5 w-2.5 rounded-full bg-current" />
)}
</a>
))}
</div>
</div> </div>
{showBackToTop && (
<button <div className="mx-auto mt-6 flex max-w-7xl items-center justify-center gap-4">
onClick={() => window.scrollTo({ top: 0, behavior: "smooth" })} <div className="text-center text-xs text-text/60">
className="rounded px-3 py-1 text-xs text-text/70 hover:text-primary border border-secondary hover:border-primary" © {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>
<div className="mx-auto mt-4 max-w-7xl text-center text-[10px] text-text/40">
Icons by{" "}
<a
href="https://icons8.com"
target="_blank"
rel="noopener noreferrer"
className="hover:text-primary anim-base underline"
> >
Back to top Icons8
</button> </a>
)} </div>
</div>
</footer> </footer>
); );
} }

View File

@@ -1,7 +1,20 @@
import React from "react"; import React from "react";
import profileImage from "../assets/jody.png"; import profileImage from "../assets/img/Jody.png";
import jodyMobile from "../assets/Jody-mobile.png"; import jodyMobile from "../assets/img/Jody-mobile.png";
import { useTheme } from "../hooks/useTheme"; import { useTheme } from "../hooks/useTheme";
import githubIcon from "../assets/img/github-icon.png";
import linkedInIcon from "../assets/img/linkedin-icon.png";
import emailIcon from "../assets/img/email-icon.png";
import facebookIcon from "../assets/img/facebook-icon.png";
import phoneIcon from "../assets/img/phone-icon.png";
const socialLinks = [
{ label: "GitHub", href: "https://github.com/Ricearoni1245", icon: githubIcon },
{ label: "LinkedIn", href: "https://www.linkedin.com/in/jody-holt-9b19b0256", icon: linkedInIcon },
{ label: "Facebook", href: "https://www.facebook.com/jody.holt.7161/", icon: facebookIcon },
{ label: "Email", href: "mailto:jholt1008@gmail.com", icon: emailIcon },
{ label: "Phone", href: "tel:8066542813", icon: phoneIcon },
];
export function Hero() { export function Hero() {
const { theme } = useTheme(); // "a" | "b" | "c" | "d" | "e" const { theme } = useTheme(); // "a" | "b" | "c" | "d" | "e"
return ( return (
@@ -40,21 +53,19 @@ export function Hero() {
<div className="mt-5 mb-4 flex items-center justify-center gap-4"> <div className="mt-5 mb-4 flex items-center justify-center gap-4">
{[ {socialLinks.map((a) => (
{ label: "GitHub", href: "#" },
{ label: "LinkedIn", href: "#" },
{ label: "Email", href: "#" },
].map((a) => (
<a <a
key={a.label} key={a.label}
href={a.href} href={a.href}
target={a.href.startsWith("mailto:") || a.href.startsWith("tel:") ? undefined : "_blank"}
rel={a.href.startsWith("mailto:") || a.href.startsWith("tel:") ? undefined : "noopener noreferrer"}
aria-label={a.label} aria-label={a.label}
className="inline-flex h-12 w-12 items-center justify-center rounded-lg className="inline-flex h-12 w-12 items-center justify-center rounded-lg
border border-secondary/70 bg-secondary/20 text-text anim-base icon-hover border border-secondary/70 bg-secondary/20 text-text anim-base icon-hover
hover:border-primary hover:text-primary focus:outline-none focus-visible:ring-2 hover:border-primary hover:text-primary focus:outline-none focus-visible:ring-2
focus-visible:ring-primary/60" focus-visible:ring-primary/60"
> >
<span className="h-3 w-3 rounded-full bg-current" /> <img src={a.icon} alt={a.label} className="h-6 w-6 invert brightness-0 invert opacity-90" />
</a> </a>
))} ))}
</div> </div>
@@ -112,14 +123,12 @@ export function Hero() {
<div className="flex self-start items-center justify-start gap-4 md:gap-6 mt-5"> <div className="flex self-start items-center justify-start gap-4 md:gap-6 mt-5">
{[ {socialLinks.map((a) => (
{ label: "GitHub", href: "#" },
{ label: "LinkedIn", href: "#" },
{ label: "Email", href: "#" },
].map((a) => (
<a <a
key={a.label} key={a.label}
href={a.href} href={a.href}
target={a.href.startsWith("mailto:") || a.href.startsWith("tel:") ? undefined : "_blank"}
rel={a.href.startsWith("mailto:") || a.href.startsWith("tel:") ? undefined : "noopener noreferrer"}
className="inline-flex items-center justify-center rounded-xl border className="inline-flex items-center justify-center rounded-xl border
border-secondary/70 bg-secondary/20 text-text transition h-10 w-10 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 sm:h-12 sm:w-12 md:h-14 md:w-14 lg:h-16 lg:w-16 hover:border-primary
@@ -128,7 +137,7 @@ export function Hero() {
aria-label={a.label} aria-label={a.label}
title={a.label} title={a.label}
> >
<span className="h-2.5 w-2.5 rounded-full bg-current" /> <img src={a.icon} alt={a.label} className="h-6 w-6 md:h-8 md:w-8 lg:h-10 lg:w-10 invert brightness-0 invert opacity-90" />
</a> </a>
))} ))}
</div> </div>

View File

@@ -15,14 +15,20 @@ export function Navbar({ onNav }: { onNav: (id: string) => void }) {
}; };
return ( return (
<header className="sticky top-0 z-50 border-b border-secondary bg-bg/90 <header
backdrop-blur h-16 md:h-20 font-main w-full anim-fade-in"> className="sticky top-0 z-50 border-b border-secondary bg-bg/90
<div className="flex h-full w-full items-center justify-between px-4 sm:px-6 backdrop-blur h-16 md:h-20 font-main w-full anim-fade-in"
md:px-10 lg:px-16"> >
<div className="flex items-center gap-3 hover-pop anim-base select-none"> <div
className="flex h-full w-full items-center justify-between px-4 sm:px-6
md:px-10 lg:px-16"
>
<div className="flex items-center gap-3 hover-pop anim-base select-none">
<div className="leading-tight"> <div className="leading-tight">
<div className="text-xl md:text-2xl font-extrabold font-name tracking-wide <div
text-text"> className="text-xl md:text-2xl font-extrabold font-name tracking-wide
text-text"
>
Jody Holt Jody Holt
</div> </div>
<div className="text-[11px] md:text-sm text-text/70"> <div className="text-[11px] md:text-sm text-text/70">
@@ -72,13 +78,14 @@ export function Navbar({ onNav }: { onNav: (id: string) => void }) {
{links.map((l) => ( {links.map((l) => (
<button <button
key={l.id} key={l.id}
className="block w-full rounded px-3 py-2 text-left text-text hover:bg-secondary/60" className="block w-full rounded px-3 py-2 text-left text-text hover:bg-secondary/60 hover:text-primary anim-base"
onClick={() => handleNav(l.id)} onClick={() => handleNav(l.id)}
> >
{l.label} {l.label}
</button> </button>
))} ))}
<div className="pt-2"> <div className="pt-2">
<ThemeToggle compact />{" "}
</div> </div>
</div> </div>
</div> </div>

331
src/components/Projects.tsx Normal file
View File

@@ -0,0 +1,331 @@
import React, { useState, useEffect } from "react";
import skymoneycover from "../assets/img/Skymoney-cover-img.jpg";
import skymoneycoverMobile from "../assets/img/skymoney-mobile-cover-img.jpg";
import millercover from "../assets/img/500nmain-cover-img.jpg";
import millercoverMobile from "../assets/img/500nmain-mobile-cover-img.jpg";
import skymoneyvideo from "../assets/video/Skymoney-video.mp4";
import skymoneyvideoMobile from "../assets/video/Skymoney-mobile-video.mp4";
import millervideo from "../assets/video/500nmain-video.mp4";
import millervideoMobile from "../assets/video/500nmain-mobile-video.mp4";
type Project = {
id: string;
title: string;
description: string;
coverImage: string;
coverImageMobile: string;
video: string;
videoMobile: string;
techStack: string[];
liveUrl?: string;
comingSoon?: boolean;
};
const projects: Project[] = [
{
id: "skymoney",
title: "Skymoney",
description:
"A budgeting app that simulates your bank account to ensure financial discipline.",
coverImage: skymoneycover,
coverImageMobile: skymoneycoverMobile,
video: skymoneyvideo,
videoMobile: skymoneyvideoMobile,
techStack: ["React", "TypeScript", "Node.js", "PostgreSQL"],
comingSoon: true,
},
{
id: "miller-building",
title: "Miller Building Website",
description:
"A website showcasing the historic Miller Building located in Borger, Texas.",
coverImage: millercover,
coverImageMobile: millercoverMobile,
video: millervideo,
videoMobile: millervideoMobile,
techStack: ["React", "TypeScript", "Tailwind CSS"],
liveUrl: "https://500nmain806.com",
},
];
function VideoModal({
isOpen,
onClose,
video,
videoMobile,
title,
}: {
isOpen: boolean;
onClose: () => void;
video: string;
videoMobile: string;
title: string;
}) {
const [isMobile, setIsMobile] = useState(false);
useEffect(() => {
const checkMobile = () => setIsMobile(window.innerWidth < 768);
checkMobile();
window.addEventListener("resize", checkMobile);
return () => window.removeEventListener("resize", checkMobile);
}, []);
if (!isOpen) return null;
return (
<div
className="fixed inset-0 z-50 flex items-center justify-center bg-black/80 backdrop-blur-sm anim-fade-in"
onClick={onClose}
>
<div
className="relative w-full max-w-4xl mx-4 bg-bg rounded-2xl overflow-hidden shadow-2xl border border-secondary"
onClick={(e) => e.stopPropagation()}
>
<div className="flex items-center justify-between p-4 border-b border-secondary">
<h3 className="text-xl font-bold text-text">{title} Demo</h3>
<button
onClick={onClose}
className="p-2 rounded-lg hover:bg-secondary/50 text-text/70 hover:text-text anim-base"
aria-label="Close modal"
>
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
>
<line x1="18" y1="6" x2="6" y2="18" />
<line x1="6" y1="6" x2="18" y2="18" />
</svg>
</button>
</div>
<div className={isMobile ? "aspect-[9/16] bg-black" : "aspect-video bg-black"}>
<video
src={isMobile ? videoMobile : video}
controls
autoPlay
muted
className="w-full h-full object-contain"
>
Your browser does not support the video tag.
</video>
</div>
</div>
</div>
);
}
function ProjectCard({
project,
onPlayVideo,
}: {
project: Project;
onPlayVideo: () => void;
}) {
return (
<div className="group relative rounded-2xl border border-secondary bg-secondary/20 overflow-hidden hover:border-primary/50 anim-base hover-pop">
{/* Cover Image - Desktop */}
<div className="relative aspect-video overflow-hidden hidden md:block">
<img
src={project.coverImage}
alt={project.title}
className="w-full h-full object-cover group-hover:scale-105 anim-base"
/>
{/* Play Button Overlay */}
<button
onClick={onPlayVideo}
className="absolute inset-0 flex items-center justify-center bg-black/40 opacity-0 group-hover:opacity-100 anim-base"
aria-label={`Play ${project.title} demo video`}
>
<div className="flex items-center justify-center w-16 h-16 rounded-full bg-primary/90 text-white hover:bg-primary hover:scale-110 anim-base">
<svg
xmlns="http://www.w3.org/2000/svg"
width="32"
height="32"
viewBox="0 0 24 24"
fill="currentColor"
>
<polygon points="5 3 19 12 5 21 5 3" />
</svg>
</div>
</button>
{/* Coming Soon Badge */}
{project.comingSoon && (
<div className="absolute top-3 right-3 px-3 py-1 rounded-full bg-primary text-white text-xs font-bold uppercase tracking-wider">
Coming Soon
</div>
)}
</div>
{/* Cover Image - Mobile */}
<div className="relative aspect-[9/16] overflow-hidden md:hidden mx-4 my-4 rounded-xl">
<img
src={project.coverImageMobile}
alt={project.title}
className="w-full h-full object-cover group-hover:scale-105 anim-base"
/>
{/* Play Button Overlay */}
<button
onClick={onPlayVideo}
className="absolute inset-0 flex items-center justify-center bg-black/40 opacity-0 group-hover:opacity-100 anim-base"
aria-label={`Play ${project.title} demo video`}
>
<div className="flex items-center justify-center w-16 h-16 rounded-full bg-primary/90 text-white hover:bg-primary hover:scale-110 anim-base">
<svg
xmlns="http://www.w3.org/2000/svg"
width="32"
height="32"
viewBox="0 0 24 24"
fill="currentColor"
>
<polygon points="5 3 19 12 5 21 5 3" />
</svg>
</div>
</button>
{/* Coming Soon Badge */}
{project.comingSoon && (
<div className="absolute top-3 right-3 px-3 py-1 rounded-full bg-primary text-white text-xs font-bold uppercase tracking-wider">
Coming Soon
</div>
)}
</div>
{/* Content */}
<div className="p-5">
<h3 className="text-xl font-bold text-text mb-2">{project.title}</h3>
<p className="text-text/70 text-sm mb-4 line-clamp-2">
{project.description}
</p>
{/* Tech Stack */}
<div className="flex flex-wrap gap-2 mb-4">
{project.techStack.map((tech) => (
<span
key={tech}
className="px-2 py-1 text-xs rounded-md bg-secondary/50 text-text/80 border border-secondary"
>
{tech}
</span>
))}
</div>
{/* Actions */}
<div className="flex items-center gap-3">
<button
onClick={onPlayVideo}
className="flex items-center gap-2 px-4 py-2 rounded-lg bg-primary/20 text-primary border border-primary/30 hover:bg-primary/30 anim-base text-sm font-medium"
>
<svg
xmlns="http://www.w3.org/2000/svg"
width="16"
height="16"
viewBox="0 0 24 24"
fill="currentColor"
>
<polygon points="5 3 19 12 5 21 5 3" />
</svg>
Watch Demo
</button>
{project.liveUrl && (
<a
href={project.liveUrl}
target="_blank"
rel="noopener noreferrer"
className="flex items-center gap-2 px-4 py-2 rounded-lg bg-secondary/30 text-text border border-secondary hover:border-primary hover:text-primary anim-base text-sm font-medium"
>
<svg
xmlns="http://www.w3.org/2000/svg"
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
>
<path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6" />
<polyline points="15 3 21 3 21 9" />
<line x1="10" y1="14" x2="21" y2="3" />
</svg>
Visit Site
</a>
)}
</div>
</div>
</div>
);
}
function MoreToComCard() {
return (
<div className="relative rounded-2xl border border-dashed border-secondary bg-secondary/10 overflow-hidden flex items-center justify-center min-h-[300px]">
<div className="text-center p-6">
<div className="w-16 h-16 mx-auto mb-4 rounded-full bg-secondary/30 flex items-center justify-center">
<svg
xmlns="http://www.w3.org/2000/svg"
width="32"
height="32"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
className="text-text/50"
>
<line x1="12" y1="5" x2="12" y2="19" />
<line x1="5" y1="12" x2="19" y2="12" />
</svg>
</div>
<h3 className="text-xl font-bold text-text/70 mb-2">More to Come</h3>
<p className="text-text/50 text-sm">
Exciting projects in development. Stay tuned!
</p>
</div>
</div>
);
}
export function Projects() {
const [activeVideo, setActiveVideo] = useState<Project | null>(null);
return (
<div className="mx-auto max-w-7xl px-4 sm:px-6 md:px-10 lg:px-16 py-16 md:py-24">
<div className="text-center mb-12">
<h2 className="text-3xl md:text-4xl font-extrabold text-text mb-4 font-title">
Projects
</h2>
<p className="text-text/70 max-w-2xl mx-auto">
A showcase of my work from concept to deployment. Click on any
project to watch a demo.
</p>
</div>
<div className="grid gap-6 md:grid-cols-2 lg:grid-cols-3">
{projects.map((project) => (
<ProjectCard
key={project.id}
project={project}
onPlayVideo={() => setActiveVideo(project)}
/>
))}
<MoreToComCard />
</div>
{/* Video Modal */}
<VideoModal
isOpen={activeVideo !== null}
onClose={() => setActiveVideo(null)}
video={activeVideo?.video || ""}
videoMobile={activeVideo?.videoMobile || ""}
title={activeVideo?.title || ""}
/>
</div>
);
}

167
src/components/Resume.tsx Normal file
View File

@@ -0,0 +1,167 @@
import React from "react";
const contactInfo = {
location: "Amarillo, TX",
phone: "806.654.2813",
email: "jholt1008@gmail.com",
linkedin: "https://www.linkedin.com/in/jody-holt-9b19b0256",
};
const summary = `Detail-oriented software developer skilled in building full-stack applications using React, TypeScript, Node/Express, SQL, and Docker. Experienced in designing responsive user interfaces, structuring maintainable front-end architectures, and developing reliable, modular APIs. Strong communicator with proven ability to solve problems quickly, learn new technologies efficiently, and deliver clean, scalable code across multiple projects.`;
const skills = [
{
category: "Front-End Development",
items: ["React", "TypeScript", "Responsive UI/UX", "Component Architecture", "Entity Framework Core", "TailwindCSS"],
},
{
category: "Back-End & APIs",
items: ["Node.js", "Express.js", "RESTful API", "Authentication Flows", "Data Validation", "C#", ".NET Core"],
},
{
category: "Database & Data Modeling",
items: ["SQL", "PostgreSQL", "CRUD Operations", "Query Optimization", "Object-Oriented Analysis & Design"],
},
{
category: "DevOps & Tools",
items: ["Docker Compose", "Git/GitHub", "Software Migration", "Multi-Container Setups"],
},
{
category: "Software Engineering",
items: ["Clear Communication", "Modular Code Design", "Collaboration", "Rapid Learning", "Problem-Solving"],
},
];
const accomplishments = [
"Meta's Front-End Web Development and Data Engineering certificate programs",
"Built responsive React applications featuring structured component trees & dynamic routing",
"Designed SQL databases with optimal CRUD operations & well-structured queries",
"Containerized full-stack apps with Docker Compose for optimal scaling, resolved network, environment, version control, and dependency issues",
"Created reusable UI components and interactive features that improved consistency and flow, user-friendly animations and enticing UX",
];
const workHistory = [
{ title: "Sandwich Artist", company: "Subway", location: "Canyon, TX", dates: "2024Present" },
{ title: "Head Lifeguard", company: "Johnson Park Youth Center", location: "Borger, TX", dates: "Seasonal 20222025" },
{ title: "Sacker/Grocery Stocker", company: "United Supermarkets", location: "Canyon, TX", dates: "20232024" },
];
const education = [
{ degree: "M.S. in Computer Information Systems and Business Analytics", school: "West Texas A&M University", date: "May 2027" },
{ degree: "B.S. in Computer Information Systems", school: "West Texas A&M University", date: "May 2026" },
];
export function Resume() {
return (
<div className="mx-auto max-w-5xl px-4 py-16 md:py-24">
{/* Header */}
<div className="mb-10 text-center anim-fade-in">
<h2 className="text-4xl md:text-5xl font-extrabold font-title text-text mb-3">Resume</h2>
<div className="flex flex-wrap justify-center gap-3 text-sm text-text/70">
<span>{contactInfo.location}</span>
<span className="hidden sm:inline text-primary"></span>
<a href={`tel:${contactInfo.phone.replace(/\./g, "")}`} className="hover:text-primary anim-base">
{contactInfo.phone}
</a>
<span className="hidden sm:inline text-primary"></span>
<a href={`mailto:${contactInfo.email}`} className="hover:text-primary anim-base">
{contactInfo.email}
</a>
</div>
</div>
{/* Summary */}
<section className="mb-10 anim-fade-in">
<SectionTitle>Summary</SectionTitle>
<p className="text-text/85 leading-relaxed">{summary}</p>
</section>
{/* Skills */}
<section className="mb-10 anim-fade-in">
<SectionTitle>Skills & Strengths</SectionTitle>
<div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
{skills.map((skill) => (
<div
key={skill.category}
className="rounded-xl border border-secondary bg-secondary/20 p-4 anim-base hover:border-primary/50"
>
<h4 className="font-semibold text-primary mb-2 font-title">{skill.category}</h4>
<div className="flex flex-wrap gap-2">
{skill.items.map((item) => (
<span
key={item}
className="inline-block rounded-full bg-secondary/60 px-3 py-1 text-xs text-text/80"
>
{item}
</span>
))}
</div>
</div>
))}
</div>
</section>
{/* Accomplishments */}
<section className="mb-10 anim-fade-in">
<SectionTitle>Professional Accomplishments</SectionTitle>
<ul className="space-y-3">
{accomplishments.map((item, i) => (
<li key={i} className="flex gap-3 text-text/85">
<span className="mt-2 h-2 w-2 shrink-0 rounded-full bg-primary" />
<span>{item}</span>
</li>
))}
</ul>
</section>
{/* Work History */}
<section className="mb-10 anim-fade-in">
<SectionTitle>Work History</SectionTitle>
<div className="space-y-4">
{workHistory.map((job, i) => (
<div
key={i}
className="flex flex-col sm:flex-row sm:items-center sm:justify-between rounded-lg border border-secondary bg-secondary/20 p-4 anim-base hover:border-primary/50"
>
<div>
<h4 className="font-semibold text-text">{job.title}</h4>
<p className="text-text/70 text-sm">
{job.company} {job.location}
</p>
</div>
<span className="mt-2 sm:mt-0 text-sm text-primary font-medium">{job.dates}</span>
</div>
))}
</div>
</section>
{/* Education */}
<section className="anim-fade-in">
<SectionTitle>Education</SectionTitle>
<div className="space-y-4">
{education.map((edu, i) => (
<div
key={i}
className="flex flex-col sm:flex-row sm:items-center sm:justify-between rounded-lg border border-secondary bg-secondary/20 p-4 anim-base hover:border-primary/50"
>
<div>
<h4 className="font-semibold text-text">{edu.degree}</h4>
<p className="text-text/70 text-sm">{edu.school}</p>
</div>
<span className="mt-2 sm:mt-0 text-sm text-primary font-medium">{edu.date}</span>
</div>
))}
</div>
</section>
</div>
);
}
function SectionTitle({ children }: { children: React.ReactNode }) {
return (
<div className="mb-4">
<h3 className="text-2xl font-bold font-title text-text">{children}</h3>
<div className="mt-1 h-0.5 w-16 bg-gradient-to-r from-primary to-transparent rounded-full" />
</div>
);
}

View File

@@ -2,6 +2,15 @@
import React from "react"; import React from "react";
import { useTheme } from "../hooks/useTheme"; import { useTheme } from "../hooks/useTheme";
// Actual primary colors for each theme
const themeColors: Record<string, { primary: string; label: string }> = {
a: { primary: "#3d8eff", label: "Blue" },
b: { primary: "#ff7043", label: "Ember" },
c: { primary: "#00a3c4", label: "Teal" },
d: { primary: "#7743d8", label: "Violet" },
e: { primary: "#00d2a2", label: "Emerald" },
};
export function ThemeToggle({ compact = false }: { compact?: boolean }) { export function ThemeToggle({ compact = false }: { compact?: boolean }) {
const { theme, setTheme } = useTheme(); const { theme, setTheme } = useTheme();
const themes = ["a", "b", "c", "d", "e"] as const; const themes = ["a", "b", "c", "d", "e"] as const;
@@ -55,9 +64,9 @@ export function ThemeToggle({ compact = false }: { compact?: boolean }) {
> >
<span <span
className="mr-2 inline-block h-3 w-3 rounded-full align-middle" className="mr-2 inline-block h-3 w-3 rounded-full align-middle"
style={{ background: `var(--color-accent-${t}, var(--color-primary))` }} style={{ background: themeColors[t].primary }}
/> />
Theme {t.toUpperCase()} {themeColors[t].label}
</button> </button>
</li> </li>
))} ))}

View File

@@ -85,29 +85,30 @@ html[data-theme="e"] {
100% 100%
); );
} }
}
/* Desktop override */ /* Desktop override */
@media (min-width: 768px) { @media (min-width: 768px) {
.bg-hero { .bg-hero {
background: background:
/* small, softer highlight lower than the portrait rim */ radial-gradient( /* small, softer highlight lower than the portrait rim */ radial-gradient(
95% 70% at 50% 28%, 95% 70% at 50% 28%,
color-mix(in oklab, var(--hero-core) 24%, transparent 82%) 0%, color-mix(in oklab, var(--hero-core) 24%, transparent 82%) 0%,
transparent 56% transparent 56%
),
/* gentle bottom vignette for depth */
radial-gradient(
130% 90% at 50% 120%,
rgba(0, 0, 0, 0.32) 0%,
rgba(0, 0, 0, 0) 58%
), ),
/* base linear sweep */ /* gentle bottom vignette for depth */
linear-gradient( radial-gradient(
185deg, 130% 90% at 50% 120%,
#0b0f15 0%, rgba(0, 0, 0, 0.32) 0%,
var(--color-bg) 40%, rgba(0, 0, 0, 0) 58%
color-mix(in oklab, var(--hero-core) 12%, var(--color-bg) 88%) 100% ),
); /* base linear sweep */
linear-gradient(
185deg,
#0b0f15 0%,
var(--color-bg) 40%,
color-mix(in oklab, var(--hero-core) 12%, var(--color-bg) 88%) 100%
);
}
} }
} }

0
variable-categories.ts Normal file
View File