Compare commits
10 Commits
fb85b0d834
...
9181ab2fed
| Author | SHA1 | Date | |
|---|---|---|---|
| 9181ab2fed | |||
| 31b9e665b5 | |||
| 2a56bd1b3a | |||
| 5489adf81e | |||
| 6975f5aeab | |||
| da75479555 | |||
| 261bed6e25 | |||
| 0c4d1dd9c2 | |||
|
|
584a4325af | ||
|
|
fec156f6d3 |
2
.gitignore
vendored
@@ -29,4 +29,4 @@ Thumbs.db
|
|||||||
# Uncomment the ones you're not using
|
# Uncomment the ones you're not using
|
||||||
# yarn.lock
|
# yarn.lock
|
||||||
# package-lock.json
|
# package-lock.json
|
||||||
# pnpm-lock.yaml
|
# pnpm-lock.yamlw
|
||||||
48
README.md
@@ -11,11 +11,11 @@ This site demonstrates my personality, skills, and presents the tone I provide t
|
|||||||
|
|
||||||
**STACK**
|
**STACK**
|
||||||
|
|
||||||
-SPA with React (functonal components, hooks)
|
- SPA with React (functional components, hooks)
|
||||||
-TailwindCSS (custom tokens and theme system)
|
- TailwindCSS (custom tokens and theme system)
|
||||||
-Theme Engine (5 themes with automated color adaption per user preference )
|
- Theme Engine (5 themes with automated color adaption per user preference )
|
||||||
-IntersectionObserver (based scroll nav)
|
- IntersectionObserver (based scroll nav)
|
||||||
-Modular component layout
|
- Modular component layout
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -36,32 +36,32 @@ src/
|
|||||||
|
|
||||||
## Theme System
|
## Theme System
|
||||||
|
|
||||||
within index:
|
### within index:
|
||||||
# -5 unique themes are layed out by:
|
#### 5 unique themes are laid out by:
|
||||||
# *color-bg
|
- color-bg
|
||||||
# *color-primary
|
- color-primary
|
||||||
# *color-text
|
- color-text
|
||||||
# *color-secondary
|
- color-secondary
|
||||||
# *color-tertiary
|
- color-tertiary
|
||||||
|
|
||||||
These themes are applied via html[data-theme="x"] and are used across the site for all gradients, tints, accents, etc.
|
These themes are applied via html[data-theme="x"] and are used across the site for all gradients, tints, accents, etc.
|
||||||
|
|
||||||
|
|
||||||
## DEV NOTES as of 10/29/2025
|
## DEV NOTES as of 10/29/2025
|
||||||
|
|
||||||
-All social links are dummy values as of now.
|
- All social links are dummy values as of now.
|
||||||
-Section layout is controlleted via <Section id="...."></Section> wrappers.
|
- Section layout is controlled via <Section id="...."></Section> wrappers.
|
||||||
-Images are outdated and will be replaced.
|
- Images are outdated and will be replaced.
|
||||||
-UI is mobile-oriented, but device friendly.
|
- UI is mobile-oriented, but device friendly.
|
||||||
|
|
||||||
|
|
||||||
## TODO
|
## TODO
|
||||||
|
|
||||||
-Add links to projects within cards
|
- Add links to projects within cards
|
||||||
-Change out experiance tab for resume/skills
|
- Change out experience tab for resume/skills
|
||||||
-Add animations
|
- Add animations
|
||||||
-more ways to contact
|
- more ways to contact
|
||||||
-Deploy site via personal service
|
- Deploy site via personal service
|
||||||
|
|
||||||
|
|
||||||
## View Progress
|
## View Progress
|
||||||
@@ -77,7 +77,7 @@ npm run dev
|
|||||||
|
|
||||||
## Author
|
## Author
|
||||||
|
|
||||||
Jody Holt
|
### Jody Holt
|
||||||
Frontend Developer • Passion Pioneer
|
### Frontend Developer • Passion Pioneer
|
||||||
[GitHub](https://github.com/Ricearoni1245) • [LinkedIn](https://www.linkedin.com/in/jody-holt-9b19b0256) • [Email](mailto:jholt1008@gmail.com)
|
#### [GitHub](https://github.com/Ricearoni1245) • [LinkedIn](https://www.linkedin.com/in/jody-holt-9b19b0256) • [Email](mailto:jholt1008@gmail.com)
|
||||||
|
|
||||||
|
|||||||
7
package-lock.json
generated
@@ -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": {
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
20
src/App.tsx
@@ -2,11 +2,14 @@ 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";
|
||||||
|
import { AboutMe } from "./components/AboutMe";
|
||||||
|
|
||||||
export default function App() {
|
export default function App() {
|
||||||
const sections = useMemo(() => ["home", "projects", "experience"], []);
|
const sections = useMemo(() => ["home", "about", "projects", "experience"], []);
|
||||||
const refs = useRef<Record<string, HTMLElement | null>>({});
|
const refs = useRef<Record<string, HTMLElement | null>>({});
|
||||||
const [active, setActive] = useState<string>(sections[0]);
|
const [active, setActive] = useState<string>(sections[0]);
|
||||||
|
|
||||||
@@ -43,12 +46,15 @@ return (
|
|||||||
<Navbar onNav={handleNav} />
|
<Navbar onNav={handleNav} />
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<main>
|
<main>
|
||||||
<Section id="home"><Hero /></Section>
|
<Section id="home"><Hero /></Section>
|
||||||
<GradientBand />
|
<GradientBand />
|
||||||
<Section id="projects"><Placeholder title="Projects" /></Section>
|
<Section id="about"><AboutMe /></Section>
|
||||||
<GradientBand />
|
<GradientBand />
|
||||||
<Section id="experience"><Placeholder title="Experience" /></Section>
|
<Section id="projects"><Projects /></Section>
|
||||||
|
<GradientBand />
|
||||||
|
<Section id="experience"><Resume /></Section>
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 1.4 MiB |
BIN
src/assets/img/500nmain-cover-img.jpg
Normal file
|
After Width: | Height: | Size: 86 KiB |
BIN
src/assets/img/500nmain-mobile-cover-img.jpg
Normal file
|
After Width: | Height: | Size: 81 KiB |
|
Before Width: | Height: | Size: 444 KiB After Width: | Height: | Size: 444 KiB |
BIN
src/assets/img/Jody.png
Normal file
|
After Width: | Height: | Size: 375 KiB |
BIN
src/assets/img/Skymoney-cover-img.jpg
Normal file
|
After Width: | Height: | Size: 37 KiB |
BIN
src/assets/img/about-img.png
Normal file
|
After Width: | Height: | Size: 1.4 MiB |
BIN
src/assets/img/email-icon.png
Normal file
|
After Width: | Height: | Size: 1.2 KiB |
BIN
src/assets/img/facebook-icon.png
Normal file
|
After Width: | Height: | Size: 1.9 KiB |
BIN
src/assets/img/github-icon.png
Normal file
|
After Width: | Height: | Size: 3.0 KiB |
BIN
src/assets/img/linkedin-icon.png
Normal file
|
After Width: | Height: | Size: 1.3 KiB |
BIN
src/assets/img/phone-icon.png
Normal file
|
After Width: | Height: | Size: 964 B |
BIN
src/assets/img/skymoney-mobile-cover-img.jpg
Normal file
|
After Width: | Height: | Size: 40 KiB |
BIN
src/assets/video/500nmain-mobile-video.mp4
Normal file
BIN
src/assets/video/500nmain-video.mp4
Normal file
BIN
src/assets/video/Skymoney-mobile-video.mp4
Normal file
BIN
src/assets/video/Skymoney-video.mp4
Normal file
42
src/components/AboutMe.tsx
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
import React from "react";
|
||||||
|
import aboutImg from "../assets/img/about-img.png";
|
||||||
|
|
||||||
|
export function AboutMe() {
|
||||||
|
return (
|
||||||
|
<section className="mx-auto max-w-5xl px-4 py-16 md:py-24 anim-fade-in">
|
||||||
|
<h2 className="text-3xl md:text-4xl font-extrabold text-text mb-10 font-title text-center">About Me</h2>
|
||||||
|
|
||||||
|
<div className="md:flex md:gap-10 md:items-start">
|
||||||
|
{/* Text content */}
|
||||||
|
<div className="md:flex-1">
|
||||||
|
<div className="mb-10 p-6 rounded-xl bg-secondary/20 border border-secondary shadow-md md:bg-transparent md:border-0 md:shadow-none md:p-0">
|
||||||
|
<h3 className="text-xl font-bold text-primary mb-3 font-title">Background</h3>
|
||||||
|
<p className="text-text/85 leading-relaxed">
|
||||||
|
Growing up in a small Texas town, I learned the value of living simply and appreciating what matters most. I was blessed with a loving, supportive family who encouraged my passions, no matter how ambitious. Early on, I developed a strong sense of right and wrong and felt a calling to help others. For me, doing the right thing isn't just about being a good person—it's a core part of my identity. Inspired by the teachings of Christ, I've always embraced leadership roles and sought opportunities to serve wherever I'm needed.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="p-6 rounded-xl bg-secondary/10 border border-secondary shadow-md md:bg-transparent md:border-0 md:shadow-none md:p-0">
|
||||||
|
<h3 className="text-xl font-bold text-primary mb-3 font-title">My Strive</h3>
|
||||||
|
<p className="text-text/85 leading-relaxed">
|
||||||
|
As I continue my programming journey, I strive each day to expand my knowledge and skills. I have a deep passion for software development and a unique talent for designing user interfaces. I love collaborating with others to push boundaries and create innovative projects. My long-term goal is to offer consulting services using independent platforms, ensuring greater security and data protection for my family, friends, and clients. My drive for innovation motivates me to explore the latest technologies and deliver efficient, high-quality solutions. Whether working solo or as part of a team, I am committed to producing content that exceeds expectations.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Image with caption - below text on mobile, right side on desktop */}
|
||||||
|
<figure className="mt-8 md:mt-0 md:w-80 lg:w-96 md:flex-shrink-0">
|
||||||
|
<div className="overflow-hidden rounded-2xl border border-secondary shadow-lg md:border-0 md:shadow-none">
|
||||||
|
<img
|
||||||
|
src={aboutImg}
|
||||||
|
alt="Me and my loving girlfriend"
|
||||||
|
className="w-full h-auto object-cover hover:scale-105 anim-base"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<figcaption className="mt-3 text-center text-sm text-text/60 italic">
|
||||||
|
Me and my loving girlfriend
|
||||||
|
</figcaption>
|
||||||
|
</figure>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
@@ -18,47 +27,81 @@ export function Footer({
|
|||||||
return (
|
return (
|
||||||
<footer className="border-t border-secondary bg-bg px-4 py-10">
|
<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">
|
<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-center md:text-left">
|
||||||
<div className="text-xl font-extrabold tracking-wide text-text">Jody Holt</div>
|
<div className="text-xl md:text-2xl font-extrabold font-name tracking-wide text-text">
|
||||||
<p className="text-sm text-text/70">Design • Develop • Deliver</p>
|
Jody Holt
|
||||||
|
</div>
|
||||||
|
<div className="text-[11px] md:text-sm text-text/70">
|
||||||
|
Passion Pioneer
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Middle: Links */}
|
|
||||||
<nav className="flex items-center gap-5">
|
<nav className="flex items-center gap-5">
|
||||||
<a className="text-text hover:text-primary" href="#projects">Projects</a>
|
<button
|
||||||
<a className="text-text hover:text-primary" href="#experience">Experience</a>
|
className="text-text hover:text-primary anim-base"
|
||||||
<a className="text-text hover:text-primary" href="#home">Background</a>
|
onClick={() => document.getElementById("home")?.scrollIntoView({ behavior: "smooth" })}
|
||||||
</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 */}
|
Background
|
||||||
{s.icon ?? <span className="h-2.5 w-2.5 rounded-full bg-current" />}
|
</button>
|
||||||
</a>
|
<button
|
||||||
))}
|
className="text-text hover:text-primary anim-base"
|
||||||
</div>
|
onClick={() => document.getElementById("projects")?.scrollIntoView({ behavior: "smooth" })}
|
||||||
</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">© {year} Jody Holt • All rights reserved</div>
|
{socials.map((s) => (
|
||||||
{showBackToTop && (
|
<a
|
||||||
<button
|
key={s.label}
|
||||||
onClick={() => window.scrollTo({ top: 0, behavior: "smooth" })}
|
href={s.href}
|
||||||
className="rounded px-3 py-1 text-xs text-text/70 hover:text-primary border border-secondary hover:border-primary"
|
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 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>
|
||||||
|
<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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,156 +1,155 @@
|
|||||||
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 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"
|
||||||
return (
|
return (
|
||||||
<section className="relative w-full bg-hero">
|
<section key={theme} className="relative w-full bg-hero anim-fade-in">
|
||||||
<div className="md:hidden flex flex-col items-center text-center gap-2 min-h-[calc(100vh-64px)] py-6">
|
<div className="md:hidden flex flex-col items-center text-center gap-2 min-h-[calc(100vh-64px)] py-6">
|
||||||
<h1
|
<h1
|
||||||
className="font-extrabold tracking-wide leading-tight text-text
|
className="font-extrabold font-title tracking-wide leading-tight text-text
|
||||||
text-2xl underline md:decoration-secondary decoration-primary"
|
text-2xl underline md:decoration-secondary decoration-primary anim-pop-in"
|
||||||
>
|
>
|
||||||
Design. Develop. Deliver.
|
Design. Develop. Deliver.
|
||||||
</h1>
|
</h1>
|
||||||
|
|
||||||
<p className="text-sm text-text/80">
|
<p className=" font-main text-sm text-text/80 anim-fade-in">
|
||||||
Driven by a genuine passion for creation through code.
|
Driven by a genuine passion for creation through code.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<div className="relative h-68 w-68 rounded-full overflow-hidden mb-2">
|
<div className="relative h-48 w-48 rounded-full overflow-hidden mb-2 anim-pop-in float-idle">
|
||||||
|
<div className="absolute inset-0 rounded-full img-glow" />
|
||||||
|
|
||||||
<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 will-change-transform"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h2 className="mt- font-extrabold font-title text-text leading-tight tracking-wide text-3xl anim-fade-in">
|
||||||
<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
|
Hello, I’m Jody Holt
|
||||||
</h2>
|
</h2>
|
||||||
|
|
||||||
<p className=" mt-5 text-lg text-base text-text/85">
|
<p className=" font-main mt-2 text-[22px] text-base text-text/85 anim-fade-in">
|
||||||
Turning concepts into clean, functional code.
|
Turning concepts into clean, functional code.
|
||||||
</p>
|
</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-5 mb-4 flex items-center justify-center gap-4">
|
||||||
|
{socialLinks.map((a) => (
|
||||||
<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
|
<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 border border-secondary/70 bg-secondary/20 text-text hover:border-primary hover:text-primary transition"
|
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
|
||||||
|
hover:border-primary hover:text-primary focus:outline-none focus-visible:ring-2
|
||||||
|
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>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/*DESKTOP*/}
|
||||||
|
{/*____________________________________________________________________________________________________*/}
|
||||||
|
<div
|
||||||
|
className=" md:flex md:flex-col items-center hidden md: md:mx-auto px-4 w-full sm:h-[calc(100vh-80px)]
|
||||||
|
overflow-hidden"
|
||||||
|
>
|
||||||
|
<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 font-title
|
||||||
|
text-center lg:pt-2 xl:pt-7 anim-pop-in "
|
||||||
|
>
|
||||||
|
Design. Develop. Deliver.
|
||||||
|
</h1>
|
||||||
|
<p
|
||||||
|
className="text-text/80 text-sm sm:text-base md:text-lg lg:text-xl xl:text-2xl
|
||||||
|
font-main text-center anim-fade-in"
|
||||||
|
>
|
||||||
|
Driven by a genuine passion for creation through code.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<div className="hidden md:block md:mx-auto max-w-7xl px-4">
|
|
||||||
<div
|
<div
|
||||||
className="
|
className="
|
||||||
min-h-[calc(100vh-64px)]
|
flex items-center justify-evenly
|
||||||
md:min-h-[calc(100vh-80px)]
|
w-full max-h-[calc(auto-400px) lg:px-5 xl:px-20"
|
||||||
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">
|
<div className="self-end flex flex-col items-start lg:gap-1 mb-8 lg: items-center
|
||||||
<img
|
lg:pb-22 2xl:pb-30 ">
|
||||||
src={profileImage}
|
<h5
|
||||||
alt="Jody Holt"
|
className="font-semi-bold text-text
|
||||||
className="w-[240px] sm:h-[280px] md:h-[700px] lg:h-[780px] xl:g-[800px] h-auto object-contain select-none pointer-events-none"
|
text-2xl sm:text-3xl md:text-2xl lg:text-2xl xl:text-4xl
|
||||||
/>
|
font-title text-left"
|
||||||
</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.
|
Hello,
|
||||||
</h1>
|
</h5>
|
||||||
|
|
||||||
<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
|
<h2
|
||||||
className="font-extrabold text-text leading-tight tracking-wide mb-5
|
className="font-extrabold text-text leading-tight tracking-wide
|
||||||
text-2xl sm:text-3xl md:text-4xl lg:text-5xl xl:text-6xl"
|
text-2xl sm:text-3xl md:text-3xl lg:text-5xl xl:text-8xl
|
||||||
|
font-title text-left"
|
||||||
>
|
>
|
||||||
Hello, I’m Jody Holt
|
I’m Jody Holt
|
||||||
</h2>
|
</h2>
|
||||||
|
|
||||||
<p
|
<p
|
||||||
className="mb-3 text-text/85 md:mt-10 md:mb-5
|
className="mb-3 text-text/85 md:mt-2
|
||||||
text-base md:text-xl lg:text-2xl xl:text-3xl"
|
text-base md:text-xl lg:text-2xl xl:text-4xl font-main"
|
||||||
>
|
>
|
||||||
Turning concepts into clean, functional code.
|
Turning concepts into clean, functional code.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p
|
|
||||||
className="mb-30 text-text/85
|
<div className="flex self-start items-center justify-start gap-4 md:gap-6 mt-5">
|
||||||
text-base md:text-3xl lg:text-4xl xl:text-5xl
|
{socialLinks.map((a) => (
|
||||||
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
|
<a
|
||||||
key={a.label}
|
key={a.label}
|
||||||
href={a.href}
|
href={a.href}
|
||||||
className="inline-flex items-center justify-center rounded-xl border border-secondary/70 bg-secondary/20 text-text transition
|
target={a.href.startsWith("mailto:") || a.href.startsWith("tel:") ? undefined : "_blank"}
|
||||||
h-10 w-10 sm:h-12 sm:w-12 md:h-14 md:w-14 lg:h-16 lg:w-16
|
rel={a.href.startsWith("mailto:") || a.href.startsWith("tel:") ? undefined : "noopener noreferrer"}
|
||||||
hover:border-primary hover:text-primary"
|
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 anim-base icon-hover focus:outline-none
|
||||||
|
focus-visible:ring-2 focus-visible:ring-primary/60"
|
||||||
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>
|
||||||
</div>
|
</div>
|
||||||
|
<div className=" justify-start ">
|
||||||
|
<img
|
||||||
|
src={profileImage}
|
||||||
|
alt="Jody Holt"
|
||||||
|
className="lg:max-h-[78vh] sm:max-h-[50vh] h-auto object-contain select-none
|
||||||
|
pointer-events-none anim-pop-in will-change-transform"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|||||||
@@ -4,9 +4,10 @@ import { ThemeToggle } from "./ThemeToggle";
|
|||||||
export function Navbar({ onNav }: { onNav: (id: string) => void }) {
|
export function Navbar({ onNav }: { onNav: (id: string) => void }) {
|
||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
const links = [
|
const links = [
|
||||||
{ id: "home", label: "Background" },
|
{ id: "home", label: "Intro" },
|
||||||
|
{ id: "about", label: "About" },
|
||||||
{ id: "projects", label: "Projects" },
|
{ id: "projects", label: "Projects" },
|
||||||
{ id: "experience", label: "Experience" },
|
{ id: "experience", label: "Resume" },
|
||||||
];
|
];
|
||||||
|
|
||||||
const handleNav = (id: string) => {
|
const handleNav = (id: string) => {
|
||||||
@@ -15,12 +16,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 backdrop-blur h-16 md:h-20">
|
<header
|
||||||
<div className="mx-auto flex h-full max-w-7xl items-center justify-between px-4">
|
className="sticky top-0 z-50 border-b border-secondary bg-bg/90
|
||||||
{/* Brand (stacked) */}
|
backdrop-blur h-16 md:h-20 font-main w-full anim-fade-in"
|
||||||
<div className="flex items-center gap-3">
|
>
|
||||||
|
<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 tracking-wide text-text">
|
<div
|
||||||
|
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">
|
||||||
@@ -29,12 +38,11 @@ export function Navbar({ onNav }: { onNav: (id: string) => void }) {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Desktop nav */}
|
|
||||||
<nav className="hidden items-center gap-6 md:flex">
|
<nav className="hidden items-center gap-6 md:flex">
|
||||||
{links.map((l) => (
|
{links.map((l) => (
|
||||||
<button
|
<button
|
||||||
key={l.id}
|
key={l.id}
|
||||||
className="text-text hover:text-primary"
|
className="text-text/90 hover:text-primary anim-base hover:-translate-y-[1px] focus:outline-none focus-visible:ring-2 focus-visible:ring-primary/60 rounded"
|
||||||
onClick={() => handleNav(l.id)}
|
onClick={() => handleNav(l.id)}
|
||||||
>
|
>
|
||||||
{l.label}
|
{l.label}
|
||||||
@@ -43,7 +51,6 @@ export function Navbar({ onNav }: { onNav: (id: string) => void }) {
|
|||||||
<ThemeToggle />
|
<ThemeToggle />
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
{/* Mobile controls */}
|
|
||||||
<div className="md:hidden">
|
<div className="md:hidden">
|
||||||
<button
|
<button
|
||||||
aria-expanded={open}
|
aria-expanded={open}
|
||||||
@@ -63,8 +70,6 @@ export function Navbar({ onNav }: { onNav: (id: string) => void }) {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Mobile dropdown */}
|
|
||||||
{/* Mobile dropdown */}
|
|
||||||
<div
|
<div
|
||||||
className={`md:hidden transition-[max-height] duration-300 ${
|
className={`md:hidden transition-[max-height] duration-300 ${
|
||||||
open ? "max-h-96 overflow-visible" : "max-h-0 overflow-hidden"
|
open ? "max-h-96 overflow-visible" : "max-h-0 overflow-hidden"
|
||||||
@@ -74,14 +79,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 />
|
<ThemeToggle compact />{" "}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
331
src/components/Projects.tsx
Normal 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: ["HTML", "CSS", "JQuery"],
|
||||||
|
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
@@ -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: "Training Specialist", company: "Subway", location: "Canyon, TX", dates: "2024–Present" },
|
||||||
|
{ title: "Head Lifeguard", company: "Johnson Park Youth Center", location: "Borger, TX", dates: "Seasonal 2022–2025" },
|
||||||
|
{ title: "Sacker/Grocery Stocker", company: "United Supermarkets", location: "Canyon, TX", dates: "2023–2024" },
|
||||||
|
];
|
||||||
|
|
||||||
|
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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,42 +1,72 @@
|
|||||||
|
// ThemeToggle.tsx
|
||||||
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;
|
||||||
|
|
||||||
|
const crossfadeTo = (next: typeof themes[number]) => {
|
||||||
|
// 1) capture current hero computed background (all layers resolved)
|
||||||
|
const hero = document.querySelector<HTMLElement>(".bg-hero");
|
||||||
|
const prevBg = hero ? getComputedStyle(hero).backgroundImage : "";
|
||||||
|
|
||||||
|
// 2) stash it in a CSS var & flag crossfade
|
||||||
|
const root = document.documentElement;
|
||||||
|
root.style.setProperty("--hero-bg-prev", prevBg);
|
||||||
|
root.classList.add("hero-xfade");
|
||||||
|
|
||||||
|
// 3) switch theme (your existing logic)
|
||||||
|
setTheme(next as any);
|
||||||
|
|
||||||
|
// 4) remove crossfade flag after the animation
|
||||||
|
window.setTimeout(() => {
|
||||||
|
root.classList.remove("hero-xfade");
|
||||||
|
root.style.removeProperty("--hero-bg-prev");
|
||||||
|
}, 600); // a bit > .55s animation
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="relative inline-block text-text">
|
<div className="relative inline-block text-text">
|
||||||
<details className="group">
|
<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">
|
<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 anim-base hover-pop">
|
||||||
<span className="font-medium">
|
<span className="font-medium">{compact ? "Theme" : "Toggle Theme"}</span>
|
||||||
{compact ? "Theme" : "Toggle Theme"}
|
|
||||||
</span>
|
|
||||||
<span aria-hidden>▾</span>
|
<span aria-hidden>▾</span>
|
||||||
</summary>
|
</summary>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
className="
|
className="
|
||||||
absolute top-full mt-2
|
absolute top-full mt-2 z-[70] rounded-lg border border-secondary bg-bg/95 p-2 shadow-xl backdrop-blur
|
||||||
left-0 right-0 w-[calc(100vw-10rem)]
|
left-4 right-4 mx-auto w-[calc(100vw-2rem)] max-w-[18rem]
|
||||||
md:left-auto md:right- md:mx-0 md:w-44
|
md:left-auto md:right-0 md:mx-0 md:w-44 md:max-w-none
|
||||||
rounded-lg border border-secondary bg-bg/95 p-2 shadow-xl backdrop-blur z-[70]
|
origin-top scale-y-95 opacity-0 translate-y-[-4px]
|
||||||
"
|
pointer-events-none transition-all duration-300 ease-out
|
||||||
|
group-open:opacity-100 group-open:scale-y-100 group-open:translate-y-0 group-open:pointer-events-auto
|
||||||
|
"
|
||||||
>
|
>
|
||||||
<ul className="space-y-1">
|
<ul className="space-y-1">
|
||||||
{themes.map((t) => (
|
{themes.map((t) => (
|
||||||
<li key={t}>
|
<li key={t}>
|
||||||
<button
|
<button
|
||||||
onClick={() => setTheme(t as any)}
|
onClick={() => crossfadeTo(t)}
|
||||||
className={`w-full rounded px-3 py-2 text-left hover:bg-secondary/60 ${
|
className={`w-full rounded px-3 py-2 text-left hover:bg-secondary/60 anim-base ${
|
||||||
theme === t ? "outline outline-1 outline-primary" : ""
|
theme === t ? "outline outline-1 outline-primary" : ""
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
{/* preview dot uses theme accent variables you defined per theme (optional) */}
|
|
||||||
<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})` }}
|
style={{ background: themeColors[t].primary }}
|
||||||
/>
|
/>
|
||||||
Theme {t.toUpperCase()}
|
{themeColors[t].label}
|
||||||
</button>
|
</button>
|
||||||
</li>
|
</li>
|
||||||
))}
|
))}
|
||||||
|
|||||||
173
src/index.css
@@ -1,3 +1,4 @@
|
|||||||
|
@import url('https://fonts.googleapis.com/css2?family=IBM+Plex+Sans:ital,wght@0,100..700;1,100..700&family=Oxanium:wght@200..800&family=Quantico:ital,wght@0,400;0,700;1,400;1,700&display=swap');
|
||||||
@import "tailwindcss";
|
@import "tailwindcss";
|
||||||
|
|
||||||
/* Base design tokens that generate classes like bg-primary, text-text, etc. */
|
/* Base design tokens that generate classes like bg-primary, text-text, etc. */
|
||||||
@@ -9,9 +10,10 @@
|
|||||||
--color-tertiary: #00c9a7;
|
--color-tertiary: #00c9a7;
|
||||||
--color-contrast: #9ca3af;
|
--color-contrast: #9ca3af;
|
||||||
|
|
||||||
--font-main: ui-sans-serif, system-ui, "Inter", "Segoe UI", sans-serif;
|
--font-main: "IBM Plex Sans", sans-serif;
|
||||||
--font-title: "Nunito Sans", ui-sans-serif, system-ui, sans-serif;
|
--font-title: "Oxanium", sans-serif;
|
||||||
--font-bold: "Bebas Neue", ui-sans-serif, system-ui, sans-serif;
|
--font-name: "Quantico", sans-serif;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* A–E themes: override the same tokens inside an attribute scope */
|
/* A–E themes: override the same tokens inside an attribute scope */
|
||||||
@@ -46,7 +48,7 @@ html[data-theme="d"] {
|
|||||||
--color-bg: #0f1014;
|
--color-bg: #0f1014;
|
||||||
--color-secondary: #1d1f24;
|
--color-secondary: #1d1f24;
|
||||||
--color-text: #eaecef;
|
--color-text: #eaecef;
|
||||||
--color-primary: #6c78ff;
|
--color-primary: #7743d8;
|
||||||
--color-tertiary: #a97bff;
|
--color-tertiary: #a97bff;
|
||||||
--color-contrast: #9ca3af;
|
--color-contrast: #9ca3af;
|
||||||
}
|
}
|
||||||
@@ -67,7 +69,7 @@ html[data-theme="e"] {
|
|||||||
|
|
||||||
@layer utilities {
|
@layer utilities {
|
||||||
/* Mobile / default */
|
/* Mobile / default */
|
||||||
.bg-hero {
|
.bg-hero {
|
||||||
background:
|
background:
|
||||||
/* Top-right radial accent, similar to desktop */ radial-gradient(
|
/* Top-right radial accent, similar to desktop */ radial-gradient(
|
||||||
120% 100% at 80% 10%,
|
120% 100% at 80% 10%,
|
||||||
@@ -76,36 +78,37 @@ html[data-theme="e"] {
|
|||||||
),
|
),
|
||||||
/* Slight linear sweep from top to bottom */
|
/* Slight linear sweep from top to bottom */
|
||||||
linear-gradient(
|
linear-gradient(
|
||||||
180deg,
|
120deg,
|
||||||
#0a0d13 0%,
|
#0a0d13 0%,
|
||||||
var(--color-bg) 40%,
|
var(--color-bg) 20%,
|
||||||
color-mix(in oklab, var(--color-primary) 10%, var(--color-bg) 90%)
|
color-mix(in oklab, var(--color-primary) 15%, var(--color-bg) 90%)
|
||||||
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) 18%, 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%
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -114,12 +117,12 @@ html[data-theme="e"] {
|
|||||||
html[data-theme="a"] .bg-hero {
|
html[data-theme="a"] .bg-hero {
|
||||||
background: radial-gradient(
|
background: radial-gradient(
|
||||||
135% 120% at 80% 48%,
|
135% 120% at 80% 48%,
|
||||||
color-mix(in oklab, var(--color-primary) 65%, black 35%) 0%,
|
color-mix(in oklab, var(--color-primary) 50%, black 35%) 0%,
|
||||||
color-mix(in oklab, var(--color-primary) 45%, black 55%) 38%,
|
color-mix(in oklab, var(--color-primary) 70%, black 55%) 38%,
|
||||||
transparent 74%
|
transparent 90%
|
||||||
),
|
),
|
||||||
linear-gradient(
|
linear-gradient(
|
||||||
165deg,
|
230deg,
|
||||||
#080b10 0%,
|
#080b10 0%,
|
||||||
color-mix(in oklab, var(--color-bg) 70%, black 30%) 46%,
|
color-mix(in oklab, var(--color-bg) 70%, black 30%) 46%,
|
||||||
#0a1324 100%
|
#0a1324 100%
|
||||||
@@ -129,27 +132,27 @@ html[data-theme="e"] {
|
|||||||
html[data-theme="b"] .bg-hero {
|
html[data-theme="b"] .bg-hero {
|
||||||
background: radial-gradient(
|
background: radial-gradient(
|
||||||
140% 110% at 76% 46%,
|
140% 110% at 76% 46%,
|
||||||
color-mix(in oklab, var(--color-primary) 60%, black 40%) 0%,
|
color-mix(in oklab, var(--color-primary) 50%, black 40%) 0%,
|
||||||
color-mix(in oklab, var(--color-primary) 40%, black 60%) 36%,
|
color-mix(in oklab, var(--color-primary) 70%, black 60%) 36%,
|
||||||
transparent 70%
|
transparent 82%
|
||||||
),
|
),
|
||||||
linear-gradient(
|
linear-gradient(
|
||||||
185deg,
|
230deg,
|
||||||
#140c0b 0%,
|
#140c0b 0%,
|
||||||
var(--color-bg) 40%,
|
var(--color-bg) 20%,
|
||||||
color-mix(in oklab, var(--color-tertiary) 6%, var(--color-bg) 94%) 100%
|
color-mix(in oklab, var(--color-secondary) 6%, var(--color-bg) 94%) 100%
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
/* Theme C – teal/cyan */
|
/* Theme C – teal/cyan */
|
||||||
html[data-theme="c"] .bg-hero {
|
html[data-theme="c"] .bg-hero {
|
||||||
background: radial-gradient(
|
background: radial-gradient(
|
||||||
140% 120% at 76% 48%,
|
140% 120% at 76% 48%,
|
||||||
color-mix(in oklab, var(--color-primary) 58%, black 42%) 0%,
|
color-mix(in oklab, var(--color-primary) 50%, black 42%) 0%,
|
||||||
color-mix(in oklab, var(--color-primary) 40%, black 60%) 36%,
|
color-mix(in oklab, var(--color-primary) 70%, black 60%) 36%,
|
||||||
transparent 72%
|
transparent 82%
|
||||||
),
|
),
|
||||||
linear-gradient(
|
linear-gradient(
|
||||||
165deg,
|
230deg,
|
||||||
#081016 0%,
|
#081016 0%,
|
||||||
color-mix(in oklab, var(--color-bg) 62%, black 38%) 44%,
|
color-mix(in oklab, var(--color-bg) 62%, black 38%) 44%,
|
||||||
#0a1822 100%
|
#0a1822 100%
|
||||||
@@ -160,12 +163,12 @@ html[data-theme="e"] {
|
|||||||
html[data-theme="d"] .bg-hero {
|
html[data-theme="d"] .bg-hero {
|
||||||
background: radial-gradient(
|
background: radial-gradient(
|
||||||
135% 120% at 80% 48%,
|
135% 120% at 80% 48%,
|
||||||
color-mix(in oklab, var(--color-primary) 60%, black 40%) 0%,
|
color-mix(in oklab, var(--color-primary) 50%, black 40%) 0%,
|
||||||
color-mix(in oklab, var(--color-primary) 38%, black 62%) 36%,
|
color-mix(in oklab, var(--color-primary) 70%, black 62%) 36%,
|
||||||
transparent 72%
|
transparent 82%
|
||||||
),
|
),
|
||||||
linear-gradient(
|
linear-gradient(
|
||||||
165deg,
|
230deg,
|
||||||
#090a10 0%,
|
#090a10 0%,
|
||||||
color-mix(in oklab, var(--color-bg) 68%, black 32%) 46%,
|
color-mix(in oklab, var(--color-bg) 68%, black 32%) 46%,
|
||||||
#111328 100%
|
#111328 100%
|
||||||
@@ -176,12 +179,12 @@ html[data-theme="e"] {
|
|||||||
html[data-theme="e"] .bg-hero {
|
html[data-theme="e"] .bg-hero {
|
||||||
background: radial-gradient(
|
background: radial-gradient(
|
||||||
140% 120% at 78% 48%,
|
140% 120% at 78% 48%,
|
||||||
color-mix(in oklab, var(--color-primary) 58%, black 42%) 0%,
|
color-mix(in oklab, var(--color-primary) 50%, black 42%) 0%,
|
||||||
color-mix(in oklab, var(--color-primary) 38%, black 62%) 34%,
|
color-mix(in oklab, var(--color-primary) 70%, black 62%) 34%,
|
||||||
transparent 70%
|
transparent 82%
|
||||||
),
|
),
|
||||||
linear-gradient(
|
linear-gradient(
|
||||||
165deg,
|
230deg,
|
||||||
#07100e 0%,
|
#07100e 0%,
|
||||||
color-mix(in oklab, var(--color-bg) 64%, black 36%) 44%,
|
color-mix(in oklab, var(--color-bg) 64%, black 36%) 44%,
|
||||||
#0a1c1a 100%
|
#0a1c1a 100%
|
||||||
@@ -196,12 +199,19 @@ html[data-theme="e"] {
|
|||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
/* left-to-right fade of darkness */
|
/* left-to-right fade of darkness */
|
||||||
background: linear-gradient(
|
background: linear-gradient(
|
||||||
90deg,
|
270deg,
|
||||||
rgba(0, 0, 0, 0.5) 0%,
|
rgba(0, 0, 0, 0.5) 0%,
|
||||||
rgba(0, 0, 0, 0.34) 30%,
|
rgba(0, 0, 0, 0.34) 16%,
|
||||||
rgba(0, 0, 0, 0.18) 42%,
|
rgba(0, 0, 0, 0.18) 35%,
|
||||||
rgba(0, 0, 0, 0) 50%
|
rgba(0, 0, 0, 0) 50%
|
||||||
);
|
),
|
||||||
|
linear-gradient(
|
||||||
|
270deg,
|
||||||
|
rgba(0,0,0,0.6) 0%,
|
||||||
|
rgba(0,0,0,0.48) 25%,
|
||||||
|
rgba(0,0,0,0.1) 60%
|
||||||
|
);
|
||||||
|
;
|
||||||
z-index: 0;
|
z-index: 0;
|
||||||
}
|
}
|
||||||
/* keep content above the overlay */
|
/* keep content above the overlay */
|
||||||
@@ -229,4 +239,53 @@ html[data-theme="e"] {
|
|||||||
);
|
);
|
||||||
box-shadow: inset 0 0 24px rgba(0, 0, 0, 0.35);
|
box-shadow: inset 0 0 24px rgba(0, 0, 0, 0.35);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ── Keyframes ─────────────────────────────────────────── */
|
||||||
|
/* ── Keyframes (unchanged) ───────────────────────────── */
|
||||||
|
@keyframes ui-fade-in { from{opacity:0;transform:translateY(6px)} to{opacity:1;transform:translateY(0)} }
|
||||||
|
@keyframes ui-pop-in { from{opacity:0;transform:scale(.96);filter:blur(2px)} to{opacity:1;transform:scale(1);filter:blur(0)} }
|
||||||
|
@keyframes ui-fade { from{opacity:0} to{opacity:1} }
|
||||||
|
/* Old→new gradient crossfade */
|
||||||
|
@keyframes hero-xfade-out { from { opacity: 1 } to { opacity: 0 } }
|
||||||
|
|
||||||
|
/* When html has .hero-xfade, paint the OLD gradient on ::after and fade it out */
|
||||||
|
html.hero-xfade .bg-hero {
|
||||||
|
position: relative; /* anchor overlay */
|
||||||
|
}
|
||||||
|
html.hero-xfade .bg-hero::after {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
z-index: 2; /* above everything but below menus if needed */
|
||||||
|
pointer-events: none;
|
||||||
|
background-image: var(--hero-bg-prev, none);
|
||||||
|
background-position: center;
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
background-size: cover;
|
||||||
|
animation: hero-xfade-out .55s ease-out forwards; /* match your new durations */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Ensure the base gradient is behind content as usual */
|
||||||
|
.bg-hero > * { position: relative; z-index: 3; }
|
||||||
|
|
||||||
|
|
||||||
|
/* Respect reduced motion (unchanged) */
|
||||||
|
@media (prefers-reduced-motion: reduce) {
|
||||||
|
* { animation-duration: 0.001ms !important; animation-iteration-count: 1 !important; transition-duration: 0.001ms !important; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Helpers (longer durations) ──────────────────────── */
|
||||||
|
.anim-base { transition: transform .24s ease, opacity .24s ease, filter .24s ease, color .24s ease, background-color .24s ease, border-color .24s ease; }
|
||||||
|
.anim-fade-in{ animation: ui-fade-in .55s cubic-bezier(.22,.61,.36,1) both; } /* 550ms */
|
||||||
|
.anim-pop-in { animation: ui-pop-in .48s cubic-bezier(.22,.61,.36,1) both; } /* 480ms */
|
||||||
|
.anim-fade { animation: ui-fade .45s ease-out both; } /* 450ms */
|
||||||
|
|
||||||
|
/* Optional: slightly gentler hover/tap */
|
||||||
|
.hover-pop:hover { transform: translateY(-2px) scale(1.03); transition-duration: .24s; }
|
||||||
|
.hover-pop:active { transform: translateY(0) scale(.98); transition-duration: .14s; }
|
||||||
|
|
||||||
|
/* ── Theme fade-on-switch (keeps it simple) ──────────── */
|
||||||
|
/* When the theme changes (html[data-theme] switches), the hero softly fades in */
|
||||||
|
html[data-theme] .bg-hero { animation: ui-fade .5s ease-out both; } /* 500ms */
|
||||||
|
|
||||||
|
|||||||