skeletal layout, theme functionality, mobile/desktop responsive, autoscroll
This commit is contained in:
32
.gitignore
vendored
Normal file
32
.gitignore
vendored
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
node_modules/
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
pnpm-debug.log*
|
||||||
|
|
||||||
|
# Build output
|
||||||
|
dist/
|
||||||
|
build/
|
||||||
|
|
||||||
|
# Vite cache
|
||||||
|
.vite/
|
||||||
|
|
||||||
|
# VSCode settings
|
||||||
|
.vscode/
|
||||||
|
|
||||||
|
# Log files
|
||||||
|
*.log
|
||||||
|
|
||||||
|
# Environment variables
|
||||||
|
.env
|
||||||
|
.env.*.local
|
||||||
|
|
||||||
|
# OS generated
|
||||||
|
.DS_Store
|
||||||
|
Thumbs.db
|
||||||
|
|
||||||
|
# Optional: lock files (if you use one package manager)
|
||||||
|
# Uncomment the ones you're not using
|
||||||
|
# yarn.lock
|
||||||
|
# package-lock.json
|
||||||
|
# pnpm-lock.yaml
|
||||||
83
README.md
Normal file
83
README.md
Normal file
@@ -0,0 +1,83 @@
|
|||||||
|
# Jody Holt | Developer Portfolio
|
||||||
|
|
||||||
|
This is the source code for my personal portfolio.
|
||||||
|
Created using React//Tailwind//TypeScript
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
This site demonstrates my personality, skills, and presents the tone I provide through all my collaborative/business endeavors.
|
||||||
|
|
||||||
|
**STACK**
|
||||||
|
|
||||||
|
-SPA with React (functonal components, hooks)
|
||||||
|
-TailwindCSS (custom tokens and theme system)
|
||||||
|
-Theme Engine (5 themes with automated color adaption per user preference )
|
||||||
|
-IntersectionObserver (based scroll nav)
|
||||||
|
-Modular component layout
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Project structure
|
||||||
|
|
||||||
|
```bash
|
||||||
|
src/
|
||||||
|
|-assets/ # Images
|
||||||
|
|-components/ #Reusable UI components
|
||||||
|
|-hooks/ #Custom React Hooks
|
||||||
|
|-styles/ #Tailwind + custom theme CSS
|
||||||
|
|App.tsx
|
||||||
|
|index.css
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Theme System
|
||||||
|
|
||||||
|
within index:
|
||||||
|
-5 unique themes are layed out by:
|
||||||
|
*color-bg
|
||||||
|
*color-primary
|
||||||
|
*color-text
|
||||||
|
*color-secondary
|
||||||
|
*color-tertiary
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
-All social links are dummy values as of now.
|
||||||
|
-Section layout is controlleted via <Section id="...."></Section> wrappers.
|
||||||
|
-Images are outdated and will be replaced.
|
||||||
|
-UI is mobile-oriented, but device friendly.
|
||||||
|
|
||||||
|
|
||||||
|
## TODO
|
||||||
|
|
||||||
|
-Add links to projects within cards
|
||||||
|
-Change out experiance tab for resume/skills
|
||||||
|
-Add animations
|
||||||
|
-more ways to contact
|
||||||
|
-Deploy site via personal service
|
||||||
|
|
||||||
|
|
||||||
|
## View Progress
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm i
|
||||||
|
|
||||||
|
npm run dev
|
||||||
|
|
||||||
|
## Local: http://localhost:5173/ (control + click local host url)
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
## Author
|
||||||
|
|
||||||
|
Jody Holt
|
||||||
|
Frontend Developer • Passion Pioneer
|
||||||
|
[GitHub](https://github.com/Ricearoni1245) • [LinkedIn](https://www.linkedin.com/in/jody-holt-9b19b0256) • [Email](mailto:jholt1008@gmail.com)
|
||||||
|
|
||||||
23
eslint.config.js
Normal file
23
eslint.config.js
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
import js from '@eslint/js'
|
||||||
|
import globals from 'globals'
|
||||||
|
import reactHooks from 'eslint-plugin-react-hooks'
|
||||||
|
import reactRefresh from 'eslint-plugin-react-refresh'
|
||||||
|
import tseslint from 'typescript-eslint'
|
||||||
|
import { defineConfig, globalIgnores } from 'eslint/config'
|
||||||
|
|
||||||
|
export default defineConfig([
|
||||||
|
globalIgnores(['dist']),
|
||||||
|
{
|
||||||
|
files: ['**/*.{ts,tsx}'],
|
||||||
|
extends: [
|
||||||
|
js.configs.recommended,
|
||||||
|
tseslint.configs.recommended,
|
||||||
|
reactHooks.configs['recommended-latest'],
|
||||||
|
reactRefresh.configs.vite,
|
||||||
|
],
|
||||||
|
languageOptions: {
|
||||||
|
ecmaVersion: 2020,
|
||||||
|
globals: globals.browser,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
])
|
||||||
13
index.html
Normal file
13
index.html
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>jody-holt-portfolio</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="root"></div>
|
||||||
|
<script type="module" src="/src/main.tsx"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
3932
package-lock.json
generated
Normal file
3932
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
32
package.json
Normal file
32
package.json
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
{
|
||||||
|
"name": "jody-holt-portfolio",
|
||||||
|
"private": true,
|
||||||
|
"version": "0.0.0",
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "vite",
|
||||||
|
"build": "tsc -b && vite build",
|
||||||
|
"lint": "eslint .",
|
||||||
|
"preview": "vite preview"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@tailwindcss/vite": "^4.1.16",
|
||||||
|
"react": "^19.1.1",
|
||||||
|
"react-dom": "^19.1.1",
|
||||||
|
"tailwindcss": "^4.1.16"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@eslint/js": "^9.36.0",
|
||||||
|
"@types/node": "^24.6.0",
|
||||||
|
"@types/react": "^19.1.16",
|
||||||
|
"@types/react-dom": "^19.1.9",
|
||||||
|
"@vitejs/plugin-react": "^5.0.4",
|
||||||
|
"eslint": "^9.36.0",
|
||||||
|
"eslint-plugin-react-hooks": "^5.2.0",
|
||||||
|
"eslint-plugin-react-refresh": "^0.4.22",
|
||||||
|
"globals": "^16.4.0",
|
||||||
|
"typescript": "~5.9.3",
|
||||||
|
"typescript-eslint": "^8.45.0",
|
||||||
|
"vite": "^7.1.7"
|
||||||
|
}
|
||||||
|
}
|
||||||
1
public/vite.svg
Normal file
1
public/vite.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>
|
||||||
|
After Width: | Height: | Size: 1.5 KiB |
42
src/App.css
Normal file
42
src/App.css
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
#root {
|
||||||
|
max-width: 1280px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 2rem;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo {
|
||||||
|
height: 6em;
|
||||||
|
padding: 1.5em;
|
||||||
|
will-change: filter;
|
||||||
|
transition: filter 300ms;
|
||||||
|
}
|
||||||
|
.logo:hover {
|
||||||
|
filter: drop-shadow(0 0 2em #646cffaa);
|
||||||
|
}
|
||||||
|
.logo.react:hover {
|
||||||
|
filter: drop-shadow(0 0 2em #61dafbaa);
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes logo-spin {
|
||||||
|
from {
|
||||||
|
transform: rotate(0deg);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
transform: rotate(360deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (prefers-reduced-motion: no-preference) {
|
||||||
|
a:nth-of-type(2) .logo {
|
||||||
|
animation: logo-spin infinite 20s linear;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.card {
|
||||||
|
padding: 2em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.read-the-docs {
|
||||||
|
color: #888;
|
||||||
|
}
|
||||||
69
src/App.tsx
Normal file
69
src/App.tsx
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
import React, { useEffect, useMemo, useRef, useState } from "react";
|
||||||
|
import { Navbar } from "./components/Navbar";
|
||||||
|
import { Section } from "./components/Section";
|
||||||
|
import { Hero } from "./components/Hero";
|
||||||
|
import { Placeholder } from "./components/Placeholder";
|
||||||
|
import { Footer } from "./components/Footer";
|
||||||
|
|
||||||
|
export default function App() {
|
||||||
|
const sections = useMemo(() => ["home", "projects", "experience"], []);
|
||||||
|
const refs = useRef<Record<string, HTMLElement | null>>({});
|
||||||
|
const [active, setActive] = useState<string>(sections[0]);
|
||||||
|
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const map: Record<string, HTMLElement | null> = {};
|
||||||
|
sections.forEach((id) => (map[id] = document.getElementById(id)));
|
||||||
|
refs.current = map;
|
||||||
|
|
||||||
|
|
||||||
|
const io = new IntersectionObserver(
|
||||||
|
(entries) => {
|
||||||
|
entries.forEach((e) => {
|
||||||
|
if (e.isIntersecting) setActive(e.target.id);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
{ rootMargin: "-40% 0px -55% 0px", threshold: [0, 0.2, 1] }
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
Object.values(map).forEach((el) => el && io.observe(el));
|
||||||
|
return () => io.disconnect();
|
||||||
|
}, [sections]);
|
||||||
|
|
||||||
|
|
||||||
|
const handleNav = (id: string) => {
|
||||||
|
const el = refs.current[id];
|
||||||
|
if (el) el.scrollIntoView({ behavior: "smooth", block: "start" });
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen bg-bg text-text">
|
||||||
|
<Navbar onNav={handleNav} />
|
||||||
|
|
||||||
|
|
||||||
|
<main>
|
||||||
|
<Section id="home"><Hero /></Section>
|
||||||
|
<GradientBand />
|
||||||
|
<Section id="projects"><Placeholder title="Projects" /></Section>
|
||||||
|
<GradientBand />
|
||||||
|
<Section id="experience"><Placeholder title="Experience" /></Section>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
|
||||||
|
<Footer />
|
||||||
|
|
||||||
|
|
||||||
|
{/* Active section indicator (optional) */}
|
||||||
|
<div className="fixed bottom-4 right-4 rounded-full border border-secondary bg-bg/80 px-3 py-1 text-sm text-text/80 shadow">{active.toUpperCase()}</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function GradientBand() {
|
||||||
|
return <div className="h-px bg-gradient-to-r from-secondary via-primary/60 to-secondary" />;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
BIN
src/assets/Jody-mobile.png
Normal file
BIN
src/assets/Jody-mobile.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 444 KiB |
BIN
src/assets/Jody.png
Normal file
BIN
src/assets/Jody.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.4 MiB |
64
src/components/Footer.tsx
Normal file
64
src/components/Footer.tsx
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
import React from "react";
|
||||||
|
|
||||||
|
type Social = { label: string; href: string; icon?: React.ReactNode };
|
||||||
|
|
||||||
|
export function Footer({
|
||||||
|
year = new Date().getFullYear(),
|
||||||
|
socials = [
|
||||||
|
{ label: "GitHub", href: "#" },
|
||||||
|
{ label: "LinkedIn", href: "#" },
|
||||||
|
{ label: "Email", href: "#" },
|
||||||
|
],
|
||||||
|
showBackToTop = true,
|
||||||
|
}: {
|
||||||
|
year?: number;
|
||||||
|
socials?: Social[];
|
||||||
|
showBackToTop?: boolean;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<footer className="border-t border-secondary bg-bg px-4 py-10">
|
||||||
|
<div className="mx-auto flex max-w-7xl flex-col items-center justify-between gap-6 md:flex-row">
|
||||||
|
{/* Left: Brand + tagline */}
|
||||||
|
<div className="text-center md:text-left">
|
||||||
|
<div className="text-xl font-extrabold tracking-wide text-text">Jody Holt</div>
|
||||||
|
<p className="text-sm text-text/70">Design • Develop • Deliver</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Middle: Links */}
|
||||||
|
<nav className="flex items-center gap-5">
|
||||||
|
<a className="text-text hover:text-primary" href="#projects">Projects</a>
|
||||||
|
<a className="text-text hover:text-primary" href="#experience">Experience</a>
|
||||||
|
<a className="text-text hover:text-primary" href="#home">Background</a>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
{/* Right: Socials */}
|
||||||
|
<div className="flex items-center gap-4 text-text">
|
||||||
|
{socials.map((s) => (
|
||||||
|
<a
|
||||||
|
key={s.label}
|
||||||
|
href={s.href}
|
||||||
|
aria-label={s.label}
|
||||||
|
className="inline-flex h-10 w-10 items-center justify-center rounded-lg border border-secondary hover:border-primary hover:text-primary"
|
||||||
|
title={s.label}
|
||||||
|
>
|
||||||
|
{/* replace with real SVGs later */}
|
||||||
|
{s.icon ?? <span className="h-2.5 w-2.5 rounded-full bg-current" />}
|
||||||
|
</a>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mx-auto mt-6 flex max-w-7xl items-center justify-center gap-4">
|
||||||
|
<div className="text-center text-xs text-text/60">© {year} Jody Holt • All rights reserved</div>
|
||||||
|
{showBackToTop && (
|
||||||
|
<button
|
||||||
|
onClick={() => window.scrollTo({ top: 0, behavior: "smooth" })}
|
||||||
|
className="rounded px-3 py-1 text-xs text-text/70 hover:text-primary border border-secondary hover:border-primary"
|
||||||
|
>
|
||||||
|
Back to top
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
);
|
||||||
|
}
|
||||||
158
src/components/Hero.tsx
Normal file
158
src/components/Hero.tsx
Normal file
@@ -0,0 +1,158 @@
|
|||||||
|
import React from "react";
|
||||||
|
import profileImage from "../assets/jody.png";
|
||||||
|
import jodyMobile from "../assets/Jody-mobile.png";
|
||||||
|
|
||||||
|
export function Hero() {
|
||||||
|
return (
|
||||||
|
<section className="relative w-full bg-hero">
|
||||||
|
<div className="md:hidden flex flex-col items-center text-center gap-2 min-h-[calc(100vh-64px)] py-6">
|
||||||
|
<h1
|
||||||
|
className="font-extrabold tracking-wide leading-tight text-text
|
||||||
|
text-2xl underline md:decoration-secondary decoration-primary"
|
||||||
|
>
|
||||||
|
Design. Develop. Deliver.
|
||||||
|
</h1>
|
||||||
|
|
||||||
|
<p className="text-sm text-text/80">
|
||||||
|
Driven by a genuine passion for creation through code.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div className="relative h-68 w-68 rounded-full overflow-hidden mb-2">
|
||||||
|
|
||||||
|
<div className="absolute inset-0 rounded-full img-glow" />
|
||||||
|
|
||||||
|
|
||||||
|
<img
|
||||||
|
src={jodyMobile}
|
||||||
|
alt="Jody Holt"
|
||||||
|
className="relative z-[1] h-full w-full object-cover select-none pointer-events-none"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h2 className="mt- font-extrabold text-text leading-tight tracking-wide text-3xl">
|
||||||
|
Hello, I’m Jody Holt
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
<p className=" mt-5 text-lg text-base text-text/85">
|
||||||
|
Turning concepts into clean, functional code.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p className="text-2xl font-semibold text-text mt-4">It’s What I Do.</p>
|
||||||
|
|
||||||
|
<p className="mt-8 text-2xl text-text">I would love to connect!</p>
|
||||||
|
|
||||||
|
<div className="mt-2 mb-4 flex items-center justify-center gap-4">
|
||||||
|
{[
|
||||||
|
{ label: "GitHub", href: "#" },
|
||||||
|
{ label: "LinkedIn", href: "#" },
|
||||||
|
{ label: "Email", href: "#" },
|
||||||
|
].map((a) => (
|
||||||
|
<a
|
||||||
|
key={a.label}
|
||||||
|
href={a.href}
|
||||||
|
aria-label={a.label}
|
||||||
|
className="inline-flex h-12 w-12 items-center justify-center rounded-lg border border-secondary/70 bg-secondary/20 text-text hover:border-primary hover:text-primary transition"
|
||||||
|
>
|
||||||
|
<span className="h-3 w-3 rounded-full bg-current" />
|
||||||
|
</a>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<div className="hidden md:block md:mx-auto max-w-7xl px-4">
|
||||||
|
<div
|
||||||
|
className="
|
||||||
|
min-h-[calc(100vh-64px)]
|
||||||
|
md:min-h-[calc(100vh-80px)]
|
||||||
|
flex flex-col md:flex-row items-start gap-10 lg:gap-10
|
||||||
|
py-8 md:py-1
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<div className="shrink-0 self-start lg:pl-20">
|
||||||
|
<img
|
||||||
|
src={profileImage}
|
||||||
|
alt="Jody Holt"
|
||||||
|
className="w-[240px] sm:h-[280px] md:h-[700px] lg:h-[780px] xl:g-[800px] h-auto object-contain select-none pointer-events-none"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex-1 self-start md:pt-10 items-center text-center">
|
||||||
|
<h1
|
||||||
|
className="text-text font-extrabold tracking-wide leading-tight
|
||||||
|
text-3xl sm:text-4xl md:text-3xl lg:text-5xl xl:text-6xl underline md:decoration-secondary decoration-primary"
|
||||||
|
>
|
||||||
|
Design. Develop. Deliver.
|
||||||
|
</h1>
|
||||||
|
|
||||||
|
<p
|
||||||
|
className="mb-10 text-text/80
|
||||||
|
text-sm sm:text-base md:text-lg lg:text-xl xl:text-2"
|
||||||
|
>
|
||||||
|
Driven by a genuine passion for creation through code.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<h2
|
||||||
|
className="font-extrabold text-text leading-tight tracking-wide mb-5
|
||||||
|
text-2xl sm:text-3xl md:text-4xl lg:text-5xl xl:text-6xl"
|
||||||
|
>
|
||||||
|
Hello, I’m Jody Holt
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
<p
|
||||||
|
className="mb-3 text-text/85 md:mt-10 md:mb-5
|
||||||
|
text-base md:text-xl lg:text-2xl xl:text-3xl"
|
||||||
|
>
|
||||||
|
Turning concepts into clean, functional code.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p
|
||||||
|
className="mb-30 text-text/85
|
||||||
|
text-base md:text-3xl lg:text-4xl xl:text-5xl
|
||||||
|
font-semibold"
|
||||||
|
>
|
||||||
|
It’s What I Do.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p
|
||||||
|
className="mb-8 text-text
|
||||||
|
text-lg md:text-4xl lg:text-5xl"
|
||||||
|
>
|
||||||
|
I would love to connect!
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div className="flex items-center justify-center gap-4 md:gap-6">
|
||||||
|
{[
|
||||||
|
{ label: "GitHub", href: "#" },
|
||||||
|
{ label: "LinkedIn", href: "#" },
|
||||||
|
{ label: "Email", href: "#" },
|
||||||
|
].map((a) => (
|
||||||
|
<a
|
||||||
|
key={a.label}
|
||||||
|
href={a.href}
|
||||||
|
className="inline-flex items-center justify-center rounded-xl border border-secondary/70 bg-secondary/20 text-text transition
|
||||||
|
h-10 w-10 sm:h-12 sm:w-12 md:h-14 md:w-14 lg:h-16 lg:w-16
|
||||||
|
hover:border-primary hover:text-primary"
|
||||||
|
aria-label={a.label}
|
||||||
|
title={a.label}
|
||||||
|
>
|
||||||
|
<span className="h-2.5 w-2.5 rounded-full bg-current" />
|
||||||
|
</a>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
90
src/components/Navbar.tsx
Normal file
90
src/components/Navbar.tsx
Normal file
@@ -0,0 +1,90 @@
|
|||||||
|
import React, { useState } from "react";
|
||||||
|
import { ThemeToggle } from "./ThemeToggle";
|
||||||
|
|
||||||
|
export function Navbar({ onNav }: { onNav: (id: string) => void }) {
|
||||||
|
const [open, setOpen] = useState(false);
|
||||||
|
const links = [
|
||||||
|
{ id: "home", label: "Background" },
|
||||||
|
{ id: "projects", label: "Projects" },
|
||||||
|
{ id: "experience", label: "Experience" },
|
||||||
|
];
|
||||||
|
|
||||||
|
const handleNav = (id: string) => {
|
||||||
|
onNav(id);
|
||||||
|
setOpen(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<header className="sticky top-0 z-50 border-b border-secondary bg-bg/90 backdrop-blur h-16 md:h-20">
|
||||||
|
<div className="mx-auto flex h-full max-w-7xl items-center justify-between px-4">
|
||||||
|
{/* Brand (stacked) */}
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<div className="leading-tight">
|
||||||
|
<div className="text-xl md:text-2xl font-extrabold tracking-wide text-text">
|
||||||
|
Jody Holt
|
||||||
|
</div>
|
||||||
|
<div className="text-[11px] md:text-sm text-text/70">
|
||||||
|
Passion Pioneer
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Desktop nav */}
|
||||||
|
<nav className="hidden items-center gap-6 md:flex">
|
||||||
|
{links.map((l) => (
|
||||||
|
<button
|
||||||
|
key={l.id}
|
||||||
|
className="text-text hover:text-primary"
|
||||||
|
onClick={() => handleNav(l.id)}
|
||||||
|
>
|
||||||
|
{l.label}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
<ThemeToggle />
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
{/* Mobile controls */}
|
||||||
|
<div className="md:hidden">
|
||||||
|
<button
|
||||||
|
aria-expanded={open}
|
||||||
|
aria-label="Toggle menu"
|
||||||
|
className="rounded px-3 py-2 text-text hover:bg-secondary/60"
|
||||||
|
onClick={() => setOpen((v) => !v)}
|
||||||
|
>
|
||||||
|
<svg viewBox="0 0 24 24" width="28" height="28" fill="currentColor">
|
||||||
|
<path
|
||||||
|
d="M3 6h18M3 12h18M3 18h18"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeWidth="2"
|
||||||
|
strokeLinecap="round"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Mobile dropdown */}
|
||||||
|
{/* Mobile dropdown */}
|
||||||
|
<div
|
||||||
|
className={`md:hidden transition-[max-height] duration-300 ${
|
||||||
|
open ? "max-h-96 overflow-visible" : "max-h-0 overflow-hidden"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<div className="space-y-2 border-t border-secondary bg-bg px-4 py-3">
|
||||||
|
{links.map((l) => (
|
||||||
|
<button
|
||||||
|
key={l.id}
|
||||||
|
className="block w-full rounded px-3 py-2 text-left text-text hover:bg-secondary/60"
|
||||||
|
onClick={() => handleNav(l.id)}
|
||||||
|
>
|
||||||
|
{l.label}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
<div className="pt-2">
|
||||||
|
<ThemeToggle compact />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
);
|
||||||
|
}
|
||||||
18
src/components/Placeholder.tsx
Normal file
18
src/components/Placeholder.tsx
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
// =====================================
|
||||||
|
import React from "react";
|
||||||
|
|
||||||
|
|
||||||
|
export function Placeholder({ title }: { title: string }) {
|
||||||
|
return (
|
||||||
|
<div className="mx-auto max-w-7xl px-4 py-24">
|
||||||
|
<h3 className="mb-6 text-3xl font-bold text-text">{title}</h3>
|
||||||
|
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
|
||||||
|
{Array.from({ length: 6 }).map((_, i) => (
|
||||||
|
<div key={i} className="rounded-xl border border-secondary bg-secondary/30 p-6 text-text/85">
|
||||||
|
Card {i + 1}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
10
src/components/Section.tsx
Normal file
10
src/components/Section.tsx
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
import React from "react";
|
||||||
|
|
||||||
|
|
||||||
|
export function Section({ id, children }: React.PropsWithChildren<{ id: string }>) {
|
||||||
|
return (
|
||||||
|
<section id={id} className="scroll-mt-24">
|
||||||
|
{children}
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
48
src/components/ThemeToggle.tsx
Normal file
48
src/components/ThemeToggle.tsx
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
import React from "react";
|
||||||
|
import { useTheme } from "../hooks/useTheme";
|
||||||
|
|
||||||
|
export function ThemeToggle({ compact = false }: { compact?: boolean }) {
|
||||||
|
const { theme, setTheme } = useTheme();
|
||||||
|
const themes = ["a", "b", "c", "d", "e"] as const;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="relative inline-block text-text">
|
||||||
|
<details className="group">
|
||||||
|
<summary className="cursor-pointer select-none list-none inline-flex items-center gap-2 rounded px-3 py-1.5 bg-secondary/70 hover:bg-secondary focus:outline-none">
|
||||||
|
<span className="font-medium">
|
||||||
|
{compact ? "Theme" : "Toggle Theme"}
|
||||||
|
</span>
|
||||||
|
<span aria-hidden>▾</span>
|
||||||
|
</summary>
|
||||||
|
<div
|
||||||
|
className="
|
||||||
|
absolute top-full mt-2
|
||||||
|
left-0 right-0 w-[calc(100vw-10rem)]
|
||||||
|
md:left-auto md:right- md:mx-0 md:w-44
|
||||||
|
rounded-lg border border-secondary bg-bg/95 p-2 shadow-xl backdrop-blur z-[70]
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<ul className="space-y-1">
|
||||||
|
{themes.map((t) => (
|
||||||
|
<li key={t}>
|
||||||
|
<button
|
||||||
|
onClick={() => setTheme(t as any)}
|
||||||
|
className={`w-full rounded px-3 py-2 text-left hover:bg-secondary/60 ${
|
||||||
|
theme === t ? "outline outline-1 outline-primary" : ""
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{/* preview dot uses theme accent variables you defined per theme (optional) */}
|
||||||
|
<span
|
||||||
|
className="mr-2 inline-block h-3 w-3 rounded-full align-middle"
|
||||||
|
style={{ background: `var(--color-accent-${t})` }}
|
||||||
|
/>
|
||||||
|
Theme {t.toUpperCase()}
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</details>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
21
src/hooks/useTheme.ts
Normal file
21
src/hooks/useTheme.ts
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
import { useEffect, useState } from "react";
|
||||||
|
|
||||||
|
|
||||||
|
export type ThemeKey = "a" | "b" | "c" | "d" | "e";
|
||||||
|
|
||||||
|
|
||||||
|
export function useTheme() {
|
||||||
|
const [theme, setTheme] = useState<ThemeKey>(() => {
|
||||||
|
const saved = (typeof window !== "undefined" && localStorage.getItem("theme")) as ThemeKey | null;
|
||||||
|
return saved ?? "a";
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
document.documentElement.setAttribute("data-theme", theme);
|
||||||
|
localStorage.setItem("theme", theme);
|
||||||
|
}, [theme]);
|
||||||
|
|
||||||
|
|
||||||
|
return { theme, setTheme };
|
||||||
|
}
|
||||||
232
src/index.css
Normal file
232
src/index.css
Normal file
@@ -0,0 +1,232 @@
|
|||||||
|
@import "tailwindcss";
|
||||||
|
|
||||||
|
/* Base design tokens that generate classes like bg-primary, text-text, etc. */
|
||||||
|
@theme {
|
||||||
|
--color-bg: #0e1116; /* defaults = Theme A */
|
||||||
|
--color-text: #e1e8ee;
|
||||||
|
--color-primary: #3d8eff;
|
||||||
|
--color-secondary: #1a1f26;
|
||||||
|
--color-tertiary: #00c9a7;
|
||||||
|
--color-contrast: #9ca3af;
|
||||||
|
|
||||||
|
--font-main: ui-sans-serif, system-ui, "Inter", "Segoe UI", sans-serif;
|
||||||
|
--font-title: "Nunito Sans", ui-sans-serif, system-ui, sans-serif;
|
||||||
|
--font-bold: "Bebas Neue", ui-sans-serif, system-ui, sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* A–E themes: override the same tokens inside an attribute scope */
|
||||||
|
html[data-theme="a"] {
|
||||||
|
--color-bg: #0e1116;
|
||||||
|
--color-secondary: #1a1f26;
|
||||||
|
--color-text: #e1e8ee;
|
||||||
|
--color-primary: #3d8eff;
|
||||||
|
--color-tertiary: #00c9a7;
|
||||||
|
--color-contrast: #9ca3af;
|
||||||
|
}
|
||||||
|
|
||||||
|
html[data-theme="b"] {
|
||||||
|
--color-bg: #120e0e;
|
||||||
|
--color-secondary: #1c1818;
|
||||||
|
--color-text: #fefcfb;
|
||||||
|
--color-primary: #ff7043;
|
||||||
|
--color-tertiary: #ffd166;
|
||||||
|
--color-contrast: #a3948c;
|
||||||
|
}
|
||||||
|
|
||||||
|
html[data-theme="c"] {
|
||||||
|
--color-bg: #0d1318;
|
||||||
|
--color-secondary: #1b242c;
|
||||||
|
--color-text: #e8ecef;
|
||||||
|
--color-primary: #00a3c4;
|
||||||
|
--color-tertiary: #ff8a70;
|
||||||
|
--color-contrast: #9ca3af;
|
||||||
|
}
|
||||||
|
|
||||||
|
html[data-theme="d"] {
|
||||||
|
--color-bg: #0f1014;
|
||||||
|
--color-secondary: #1d1f24;
|
||||||
|
--color-text: #eaecef;
|
||||||
|
--color-primary: #6c78ff;
|
||||||
|
--color-tertiary: #a97bff;
|
||||||
|
--color-contrast: #9ca3af;
|
||||||
|
}
|
||||||
|
|
||||||
|
html[data-theme="e"] {
|
||||||
|
--color-bg: #0c1114;
|
||||||
|
--color-secondary: #182127;
|
||||||
|
--color-text: #edeff1;
|
||||||
|
--color-primary: #00d2a2;
|
||||||
|
--color-tertiary: #ffca57;
|
||||||
|
--color-contrast: #9ca3af;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* theme-aware hero color follows --color-primary */
|
||||||
|
:root {
|
||||||
|
--hero-core: var(--color-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
@layer utilities {
|
||||||
|
/* Mobile / default */
|
||||||
|
.bg-hero {
|
||||||
|
background:
|
||||||
|
/* Top-right radial accent, similar to desktop */ radial-gradient(
|
||||||
|
120% 100% at 80% 10%,
|
||||||
|
color-mix(in oklab, var(--color-primary) 32%, transparent) 0%,
|
||||||
|
transparent 60%
|
||||||
|
),
|
||||||
|
/* Slight linear sweep from top to bottom */
|
||||||
|
linear-gradient(
|
||||||
|
180deg,
|
||||||
|
#0a0d13 0%,
|
||||||
|
var(--color-bg) 40%,
|
||||||
|
color-mix(in oklab, var(--color-primary) 10%, var(--color-bg) 90%)
|
||||||
|
100%
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/* Desktop override */
|
||||||
|
@media (min-width: 768px) {
|
||||||
|
.bg-hero {
|
||||||
|
background:
|
||||||
|
/* small, softer highlight lower than the portrait rim */ radial-gradient(
|
||||||
|
95% 70% at 50% 28%,
|
||||||
|
color-mix(in oklab, var(--hero-core) 18%, transparent 82%) 0%,
|
||||||
|
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 */
|
||||||
|
linear-gradient(
|
||||||
|
185deg,
|
||||||
|
#0b0f15 0%,
|
||||||
|
var(--color-bg) 40%,
|
||||||
|
color-mix(in oklab, var(--hero-core) 12%, var(--color-bg) 88%) 100%
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 768px) {
|
||||||
|
/* Theme A – deep blue */
|
||||||
|
html[data-theme="a"] .bg-hero {
|
||||||
|
background: radial-gradient(
|
||||||
|
135% 120% at 80% 48%,
|
||||||
|
color-mix(in oklab, var(--color-primary) 65%, black 35%) 0%,
|
||||||
|
color-mix(in oklab, var(--color-primary) 45%, black 55%) 38%,
|
||||||
|
transparent 74%
|
||||||
|
),
|
||||||
|
linear-gradient(
|
||||||
|
165deg,
|
||||||
|
#080b10 0%,
|
||||||
|
color-mix(in oklab, var(--color-bg) 70%, black 30%) 46%,
|
||||||
|
#0a1324 100%
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
html[data-theme="b"] .bg-hero {
|
||||||
|
background: radial-gradient(
|
||||||
|
140% 110% at 76% 46%,
|
||||||
|
color-mix(in oklab, var(--color-primary) 60%, black 40%) 0%,
|
||||||
|
color-mix(in oklab, var(--color-primary) 40%, black 60%) 36%,
|
||||||
|
transparent 70%
|
||||||
|
),
|
||||||
|
linear-gradient(
|
||||||
|
185deg,
|
||||||
|
#140c0b 0%,
|
||||||
|
var(--color-bg) 40%,
|
||||||
|
color-mix(in oklab, var(--color-tertiary) 6%, var(--color-bg) 94%) 100%
|
||||||
|
);
|
||||||
|
}
|
||||||
|
/* Theme C – teal/cyan */
|
||||||
|
html[data-theme="c"] .bg-hero {
|
||||||
|
background: radial-gradient(
|
||||||
|
140% 120% at 76% 48%,
|
||||||
|
color-mix(in oklab, var(--color-primary) 58%, black 42%) 0%,
|
||||||
|
color-mix(in oklab, var(--color-primary) 40%, black 60%) 36%,
|
||||||
|
transparent 72%
|
||||||
|
),
|
||||||
|
linear-gradient(
|
||||||
|
165deg,
|
||||||
|
#081016 0%,
|
||||||
|
color-mix(in oklab, var(--color-bg) 62%, black 38%) 44%,
|
||||||
|
#0a1822 100%
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Theme D – indigo/violet */
|
||||||
|
html[data-theme="d"] .bg-hero {
|
||||||
|
background: radial-gradient(
|
||||||
|
135% 120% at 80% 48%,
|
||||||
|
color-mix(in oklab, var(--color-primary) 60%, black 40%) 0%,
|
||||||
|
color-mix(in oklab, var(--color-primary) 38%, black 62%) 36%,
|
||||||
|
transparent 72%
|
||||||
|
),
|
||||||
|
linear-gradient(
|
||||||
|
165deg,
|
||||||
|
#090a10 0%,
|
||||||
|
color-mix(in oklab, var(--color-bg) 68%, black 32%) 46%,
|
||||||
|
#111328 100%
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Theme E – emerald */
|
||||||
|
html[data-theme="e"] .bg-hero {
|
||||||
|
background: radial-gradient(
|
||||||
|
140% 120% at 78% 48%,
|
||||||
|
color-mix(in oklab, var(--color-primary) 58%, black 42%) 0%,
|
||||||
|
color-mix(in oklab, var(--color-primary) 38%, black 62%) 34%,
|
||||||
|
transparent 70%
|
||||||
|
),
|
||||||
|
linear-gradient(
|
||||||
|
165deg,
|
||||||
|
#07100e 0%,
|
||||||
|
color-mix(in oklab, var(--color-bg) 64%, black 36%) 44%,
|
||||||
|
#0a1c1a 100%
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@media (min-width: 768px) {
|
||||||
|
.bg-hero::before {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
pointer-events: none;
|
||||||
|
/* left-to-right fade of darkness */
|
||||||
|
background: linear-gradient(
|
||||||
|
90deg,
|
||||||
|
rgba(0, 0, 0, 0.5) 0%,
|
||||||
|
rgba(0, 0, 0, 0.34) 30%,
|
||||||
|
rgba(0, 0, 0, 0.18) 42%,
|
||||||
|
rgba(0, 0, 0, 0) 50%
|
||||||
|
);
|
||||||
|
z-index: 0;
|
||||||
|
}
|
||||||
|
/* keep content above the overlay */
|
||||||
|
.bg-hero > * {
|
||||||
|
position: relative;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@layer utilities {
|
||||||
|
.img-glow {
|
||||||
|
background:
|
||||||
|
radial-gradient(
|
||||||
|
68% 68% at 50% 42%,
|
||||||
|
color-mix(in oklab, var(--color-primary) 100%, black 0%) 0%,
|
||||||
|
color-mix(in oklab, var(--color-primary) 70%, black 30%) 40%,
|
||||||
|
color-mix(in oklab, var(--color-primary) 45%, black 55%) 70%,
|
||||||
|
color-mix(in oklab, var(--color-primary) 20%, black 80%) 85%
|
||||||
|
),
|
||||||
|
radial-gradient(
|
||||||
|
80% 80% at 50% 50%,
|
||||||
|
rgba(0,0,0,0) 58%,
|
||||||
|
rgba(0,0,0,0.35) 78%,
|
||||||
|
rgba(0,0,0,0.55) 100%
|
||||||
|
);
|
||||||
|
box-shadow: inset 0 0 24px rgba(0, 0, 0, 0.35);
|
||||||
|
}
|
||||||
|
}
|
||||||
10
src/main.tsx
Normal file
10
src/main.tsx
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
import { StrictMode } from 'react'
|
||||||
|
import { createRoot } from 'react-dom/client'
|
||||||
|
import './index.css'
|
||||||
|
import App from './App.tsx'
|
||||||
|
|
||||||
|
createRoot(document.getElementById('root')!).render(
|
||||||
|
<StrictMode>
|
||||||
|
<App />
|
||||||
|
</StrictMode>,
|
||||||
|
)
|
||||||
28
tsconfig.app.json
Normal file
28
tsconfig.app.json
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
|
||||||
|
"target": "ES2022",
|
||||||
|
"useDefineForClassFields": true,
|
||||||
|
"lib": ["ES2022", "DOM", "DOM.Iterable"],
|
||||||
|
"module": "ESNext",
|
||||||
|
"types": ["vite/client"],
|
||||||
|
"skipLibCheck": true,
|
||||||
|
|
||||||
|
/* Bundler mode */
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"allowImportingTsExtensions": true,
|
||||||
|
"verbatimModuleSyntax": true,
|
||||||
|
"moduleDetection": "force",
|
||||||
|
"noEmit": true,
|
||||||
|
"jsx": "react-jsx",
|
||||||
|
|
||||||
|
/* Linting */
|
||||||
|
"strict": true,
|
||||||
|
"noUnusedLocals": true,
|
||||||
|
"noUnusedParameters": true,
|
||||||
|
"erasableSyntaxOnly": true,
|
||||||
|
"noFallthroughCasesInSwitch": true,
|
||||||
|
"noUncheckedSideEffectImports": true
|
||||||
|
},
|
||||||
|
"include": ["src"]
|
||||||
|
}
|
||||||
7
tsconfig.json
Normal file
7
tsconfig.json
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"files": [],
|
||||||
|
"references": [
|
||||||
|
{ "path": "./tsconfig.app.json" },
|
||||||
|
{ "path": "./tsconfig.node.json" }
|
||||||
|
]
|
||||||
|
}
|
||||||
26
tsconfig.node.json
Normal file
26
tsconfig.node.json
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
|
||||||
|
"target": "ES2023",
|
||||||
|
"lib": ["ES2023"],
|
||||||
|
"module": "ESNext",
|
||||||
|
"types": ["node"],
|
||||||
|
"skipLibCheck": true,
|
||||||
|
|
||||||
|
/* Bundler mode */
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"allowImportingTsExtensions": true,
|
||||||
|
"verbatimModuleSyntax": true,
|
||||||
|
"moduleDetection": "force",
|
||||||
|
"noEmit": true,
|
||||||
|
|
||||||
|
/* Linting */
|
||||||
|
"strict": true,
|
||||||
|
"noUnusedLocals": true,
|
||||||
|
"noUnusedParameters": true,
|
||||||
|
"erasableSyntaxOnly": true,
|
||||||
|
"noFallthroughCasesInSwitch": true,
|
||||||
|
"noUncheckedSideEffectImports": true
|
||||||
|
},
|
||||||
|
"include": ["vite.config.ts"]
|
||||||
|
}
|
||||||
11
vite.config.ts
Normal file
11
vite.config.ts
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
import { defineConfig } from 'vite'
|
||||||
|
import react from '@vitejs/plugin-react'
|
||||||
|
import tailwindcss from '@tailwindcss/vite'
|
||||||
|
|
||||||
|
// https://vite.dev/config/
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [
|
||||||
|
react(),
|
||||||
|
tailwindcss(),
|
||||||
|
],
|
||||||
|
})
|
||||||
Reference in New Issue
Block a user