165 lines
6.7 KiB
TypeScript
165 lines
6.7 KiB
TypeScript
import { useState, useEffect } from 'react';
|
|
import './App.css';
|
|
import { THEMES } from './themes';
|
|
import { CatEasterEgg } from './components/CatEasterEgg';
|
|
import { FooterSection } from './components/FooterSection';
|
|
import { EducationSection } from './components/EducationSection';
|
|
import { HeroSection } from './components/HeroSection';
|
|
import { LabNotesSection } from './components/LabNotesSection';
|
|
import { NavigationBar } from './components/NavigationBar';
|
|
import { ProjectsSection } from './components/ProjectsSection';
|
|
import { ResumeSection } from './components/ResumeSection';
|
|
import { SystemOverviewSection } from './components/SystemOverviewSection';
|
|
import { deterministicIpValue } from './utils';
|
|
import { ThemeMapperModal } from './components/ThemeMapperModal';
|
|
|
|
export default function App() {
|
|
const [activeTheme, setActiveTheme] = useState(() => THEMES[0]);
|
|
const [viewerIp, setViewerIp] = useState<string | null>(null);
|
|
const [deterministicIndex, setDeterministicIndex] = useState<number | null>(null);
|
|
const [mousePos, setMousePos] = useState({ x: 0, y: 0 });
|
|
const [hasClickedThemePill, setHasClickedThemePill] = useState(false);
|
|
const [showThemePillTooltip, setShowThemePillTooltip] = useState(false);
|
|
const [cpuModeEnabled, setCpuModeEnabled] = useState(false);
|
|
const [showThemeModal, setShowThemeModal] = useState(false);
|
|
|
|
useEffect(() => {
|
|
const shouldRandomize = import.meta.env.DEV && import.meta.env.VITE_RANDOMIZE_THEME === 'true';
|
|
|
|
if (shouldRandomize) {
|
|
const idx = Math.floor(Math.random() * THEMES.length);
|
|
setActiveTheme(THEMES[idx]);
|
|
return;
|
|
}
|
|
|
|
const setDeterministicTheme = async () => {
|
|
try {
|
|
const response = await fetch('https://api.ipify.org?format=json');
|
|
if (!response.ok) {
|
|
throw new Error('Failed to fetch IP address');
|
|
}
|
|
const data = await response.json();
|
|
const ip = typeof data.ip === 'string' ? data.ip : '';
|
|
const nextDeterministicIndex = deterministicIpValue(ip, THEMES.length);
|
|
console.log('[theme] viewer ip', ip, 'deterministic index', nextDeterministicIndex);
|
|
setViewerIp(ip);
|
|
setDeterministicIndex(nextDeterministicIndex);
|
|
const themeIndex = nextDeterministicIndex - 1;
|
|
setActiveTheme(THEMES[Math.max(0, Math.min(themeIndex, THEMES.length - 1))]);
|
|
} catch {
|
|
const fallbackIndex = Math.floor(Math.random() * THEMES.length);
|
|
setViewerIp(null);
|
|
setDeterministicIndex(null);
|
|
setActiveTheme(THEMES[fallbackIndex]);
|
|
}
|
|
};
|
|
|
|
void setDeterministicTheme();
|
|
}, []);
|
|
|
|
// Update CSS variables for dynamic theming
|
|
useEffect(() => {
|
|
const root = document.documentElement;
|
|
|
|
// Central palette
|
|
root.style.setProperty('--color-accent', activeTheme.colors.accent);
|
|
root.style.setProperty('--color-accent-muted', `${activeTheme.colors.accent}33`);
|
|
|
|
root.style.setProperty('--bg-base', activeTheme.colors.bgBase);
|
|
root.style.setProperty('--bg-surface', activeTheme.colors.bgSurface);
|
|
root.style.setProperty('--bg-panel', activeTheme.colors.bgPanel);
|
|
|
|
root.style.setProperty('--text-main', activeTheme.colors.textMain);
|
|
root.style.setProperty('--text-muted', activeTheme.colors.textMuted);
|
|
root.style.setProperty('--text-dim', activeTheme.colors.textDim);
|
|
|
|
root.style.setProperty('--border-main', activeTheme.colors.borderMain);
|
|
root.style.setProperty('--grid-pattern', activeTheme.colors.gridPattern);
|
|
root.style.setProperty('--status-success', activeTheme.colors.statusSuccess);
|
|
|
|
// Cat
|
|
root.style.setProperty('--cat-body', activeTheme.colors.bgBase);
|
|
root.style.setProperty('--cat-stroke', activeTheme.colors.catStroke);
|
|
root.style.setProperty('--cat-nose', activeTheme.colors.catNose);
|
|
root.style.setProperty('--cat-eyes', activeTheme.colors.catEyes);
|
|
root.style.setProperty('--heart-color', activeTheme.colors.heartColor);
|
|
}, [activeTheme]);
|
|
|
|
// Subtle interactive background effect
|
|
useEffect(() => {
|
|
const handleMouseMove = (e: MouseEvent) => {
|
|
setMousePos({ x: e.clientX, y: e.clientY });
|
|
};
|
|
window.addEventListener('mousemove', handleMouseMove);
|
|
return () => window.removeEventListener('mousemove', handleMouseMove);
|
|
}, []);
|
|
|
|
useEffect(() => {
|
|
if (!showThemePillTooltip) return;
|
|
const t = window.setTimeout(() => setShowThemePillTooltip(false), 1400);
|
|
return () => window.clearTimeout(t);
|
|
}, [showThemePillTooltip]);
|
|
|
|
return (
|
|
<div className="min-h-screen bg-(--bg-base) text-(--text-main) font-mono selection:bg-(--color-accent) selection:text-(--bg-base) relative overflow-x-hidden">
|
|
|
|
{/* <svg className="hidden">
|
|
<filter id="noiseFilter">
|
|
<feTurbulence type="fractalNoise" baseFrequency="0.7" numOctaves="3" stitchTiles="stitch" />
|
|
</filter>
|
|
</svg> */}
|
|
<div className="fixed inset-0 z-0 pointer-events-none opacity-[0.04]" style={{ filter: 'url(#noiseFilter)' }} />
|
|
|
|
{/* Blueprint Dot Grid Background */}
|
|
<div className={`fixed inset-0 dot-grid z-0 pointer-events-none opacity-50 ${cpuModeEnabled ? 'dot-grid-static' : ''}`} />
|
|
|
|
{/* Subtle mouse glow */}
|
|
<div
|
|
className="fixed inset-0 z-0 pointer-events-none opacity-20 transition-opacity duration-300"
|
|
style={{
|
|
background: `radial-gradient(400px circle at ${mousePos.x}px ${mousePos.y}px, var(--color-accent-muted), transparent 40%)`
|
|
}}
|
|
/>
|
|
|
|
<CatEasterEgg />
|
|
<div className="relative z-10 max-w-5xl mx-auto px-6 py-12 flex flex-col gap-16">
|
|
<NavigationBar
|
|
cpuModeEnabled={cpuModeEnabled}
|
|
onCpuModeToggle={() => setCpuModeEnabled(prev => !prev)}
|
|
/>
|
|
<HeroSection
|
|
activeThemeName={activeTheme.name}
|
|
hasClickedThemePill={hasClickedThemePill}
|
|
showThemePillTooltip={showThemePillTooltip}
|
|
onThemePillClick={() => {
|
|
setHasClickedThemePill(true);
|
|
setShowThemePillTooltip(true);
|
|
setShowThemeModal(true);
|
|
}}
|
|
cpuModeEnabled={cpuModeEnabled}
|
|
/>
|
|
<SystemOverviewSection />
|
|
<div className="grid grid-cols-1 gap-12 md:grid-cols-7">
|
|
<div className="md:col-span-5">
|
|
<EducationSection />
|
|
</div>
|
|
<div className="md:col-span-2">
|
|
<ResumeSection />
|
|
</div>
|
|
</div>
|
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-12 mt-8">
|
|
<ProjectsSection />
|
|
<LabNotesSection />
|
|
</div>
|
|
<FooterSection />
|
|
</div>
|
|
<ThemeMapperModal
|
|
isOpen={showThemeModal}
|
|
viewerIp={viewerIp}
|
|
deterministicIndex={deterministicIndex}
|
|
themeName={activeTheme.name}
|
|
onClose={() => setShowThemeModal(false)}
|
|
/>
|
|
</div>
|
|
);
|
|
} |