Compare commits
16 Commits
bc92745e54
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| 37832dac1a | |||
| c5ef1bb840 | |||
| ee7f029fc5 | |||
| 784917e1d2 | |||
| 1cdbf12f26 | |||
| c220a00b89 | |||
| e494ce01ab | |||
| af591f57f0 | |||
| b36319f5f4 | |||
| cfee953dee | |||
| 804fde0051 | |||
| 246e1c7688 | |||
| 8f1215f9d4 | |||
| 174c57f463 | |||
| c3e31fc642 | |||
| 02cc795ae2 |
@@ -1,3 +1,9 @@
|
||||
# Mi sitio personal
|
||||
|
||||
English or Spanish
|
||||
## Theme Randomization
|
||||
|
||||
If you want to trigger theme randomization instead of IP based mapping, you can set the environment var when running dev:
|
||||
|
||||
```
|
||||
VITE_RANDOMIZE_THEME=true npm run dev
|
||||
```
|
||||
25
index.html
25
index.html
@@ -1,13 +1,16 @@
|
||||
<!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>portfolio</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/main.tsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
<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>Portfolio - Aditya Gupta (sortedcord)</title>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/main.tsx"></script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
10
prod.sh
Normal file
10
prod.sh
Normal file
@@ -0,0 +1,10 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
git pull
|
||||
rm -rf /root/app/dist/
|
||||
npm run build
|
||||
|
||||
rm -rf /var/www/frontend/*
|
||||
cp -r /root/app/dist/* /var/www/frontend
|
||||
chown -R www-data:www-data /var/www/frontend
|
||||
79
src/App.tsx
79
src/App.tsx
@@ -10,15 +10,52 @@ 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] = useState(() => {
|
||||
const idx = Math.floor(Math.random() * THEMES.length);
|
||||
return THEMES[idx];
|
||||
});
|
||||
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(() => {
|
||||
@@ -57,15 +94,29 @@ export default function App() {
|
||||
return () => window.removeEventListener('mousemove', handleMouseMove);
|
||||
}, []);
|
||||
|
||||
// Auto-hide the "HAHA Made you click" tooltip
|
||||
useEffect(() => {
|
||||
if (!showThemePillTooltip) return;
|
||||
const t = window.setTimeout(() => setShowThemePillTooltip(false), 1400);
|
||||
return () => window.clearTimeout(t);
|
||||
}, [showThemePillTooltip]);
|
||||
|
||||
useEffect(() => {
|
||||
const mediaQuery = window.matchMedia('(max-width: 639px)');
|
||||
|
||||
const enforceCpuMode = () => {
|
||||
setCpuModeEnabled(mediaQuery.matches);
|
||||
};
|
||||
|
||||
enforceCpuMode();
|
||||
mediaQuery.addEventListener('change', enforceCpuMode);
|
||||
|
||||
return () => {
|
||||
mediaQuery.removeEventListener('change', enforceCpuMode);
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-(--bg-base) text-(--text-main) font-mono selection:bg-(--color-accent) selection:text-[var(--bg-base)] relative overflow-x-hidden">
|
||||
<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">
|
||||
@@ -75,7 +126,7 @@ export default function App() {
|
||||
<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" />
|
||||
<div className={`fixed inset-0 dot-grid z-0 pointer-events-none opacity-50 ${cpuModeEnabled ? 'dot-grid-static' : ''}`} />
|
||||
|
||||
{/* Subtle mouse glow */}
|
||||
<div
|
||||
@@ -87,7 +138,10 @@ export default function App() {
|
||||
|
||||
<CatEasterEgg />
|
||||
<div className="relative z-10 max-w-5xl mx-auto px-6 py-12 flex flex-col gap-16">
|
||||
<NavigationBar />
|
||||
<NavigationBar
|
||||
cpuModeEnabled={cpuModeEnabled}
|
||||
onCpuModeToggle={() => setCpuModeEnabled(prev => !prev)}
|
||||
/>
|
||||
<HeroSection
|
||||
activeThemeName={activeTheme.name}
|
||||
hasClickedThemePill={hasClickedThemePill}
|
||||
@@ -95,7 +149,9 @@ export default function App() {
|
||||
onThemePillClick={() => {
|
||||
setHasClickedThemePill(true);
|
||||
setShowThemePillTooltip(true);
|
||||
setShowThemeModal(true);
|
||||
}}
|
||||
cpuModeEnabled={cpuModeEnabled}
|
||||
/>
|
||||
<SystemOverviewSection />
|
||||
<div className="grid grid-cols-1 gap-12 md:grid-cols-7">
|
||||
@@ -112,6 +168,13 @@ export default function App() {
|
||||
</div>
|
||||
<FooterSection />
|
||||
</div>
|
||||
<ThemeMapperModal
|
||||
isOpen={showThemeModal}
|
||||
viewerIp={viewerIp}
|
||||
deterministicIndex={deterministicIndex}
|
||||
themeName={activeTheme.name}
|
||||
onClose={() => setShowThemeModal(false)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -53,27 +53,27 @@ export function CatEasterEgg() {
|
||||
|
||||
return (
|
||||
<div
|
||||
className="fixed bottom-[-2px] z-50 transition-transform duration-100 ease-out"
|
||||
className="fixed -bottom-0.5 z-50 transition-transform duration-100 ease-out"
|
||||
style={{
|
||||
left: `${posX}%`,
|
||||
transform: `translateY(${150 - (reveal * 1.5)}%)`,
|
||||
}}
|
||||
>
|
||||
<div className="relative group cursor-pointer" onClick={handleCatClick}>
|
||||
<div className={`absolute left-1/2 transform -translate-x-1/2 text-[var(--heart-color)] text-4xl z-50 transition-all duration-700 ease-out pointer-events-none ${showHeart ? '-top-16 opacity-100 scale-110' : 'top-2 opacity-0 scale-50'}`}>
|
||||
<div className={`absolute left-1/2 transform -translate-x-1/2 text-(--heart-color) text-4xl z-50 transition-all duration-700 ease-out pointer-events-none ${showHeart ? '-top-16 opacity-100 scale-110' : 'top-2 opacity-0 scale-50'}`}>
|
||||
♥
|
||||
</div>
|
||||
|
||||
<div className="absolute -top-10 -left-20 font-sketch text-xs accent-text -rotate-12 opacity-0 group-hover:opacity-100 transition-opacity whitespace-nowrap bg-[var(--bg-panel)] px-3 py-1 sketch-border-subtle backdrop-blur-sm shadow-xl">
|
||||
./purr.sh
|
||||
<div className="absolute -top-10 -left-20 font-sketch text-xs accent-text -rotate-12 opacity-0 group-hover:opacity-100 transition-opacity whitespace-nowrap bg-(--bg-panel) px-3 py-1 sketch-border-subtle backdrop-blur-sm shadow-xl">
|
||||
./patamole.sh
|
||||
</div>
|
||||
|
||||
<div className="absolute -top-2 left-2 w-4 h-5 border-2 border-[var(--cat-stroke)] rounded-t-[12px] bg-[var(--cat-body)] z-10"></div>
|
||||
<div className="absolute -top-2 right-2 w-4 h-5 border-2 border-[var(--cat-stroke)] rounded-t-[12px] bg-[var(--cat-body)] z-10"></div>
|
||||
<div className="absolute -top-2 left-2 w-4 h-5 border-2 border-(--cat-stroke) rounded-t-xl bg-(--cat-body) z-10"></div>
|
||||
<div className="absolute -top-2 right-2 w-4 h-5 border-2 border-(--cat-stroke) rounded-t-xl bg-(--cat-body) z-10"></div>
|
||||
|
||||
<div className="w-16 h-14 border-2 border-[var(--cat-stroke)] border-b-0 rounded-t-[24px] bg-[var(--cat-body)] flex justify-center pt-5 gap-3 relative z-0">
|
||||
<div className="absolute -top-3 left-0 w-6 h-7 border-2 border-[var(--cat-stroke)] border-b-0 border-r-0 rounded-tl-full transform rotate-[20deg] bg-[var(--cat-body)] -z-10"></div>
|
||||
<div className="absolute -top-3 right-0 w-6 h-7 border-2 border-[var(--cat-stroke)] border-b-0 border-l-0 rounded-tr-full transform -rotate-[20deg] bg-[var(--cat-body)] -z-10"></div>
|
||||
<div className="w-16 h-14 border-2 border-(--cat-stroke) border-b-0 rounded-t-3xl bg-(--cat-body) flex justify-center pt-5 gap-3 relative z-0">
|
||||
<div className="absolute -top-3 left-0 w-6 h-7 border-2 border-(--cat-stroke) border-b-0 border-r-0 rounded-tl-full transform rotate-[20deg] bg-[var(--cat-body)] -z-10"></div>
|
||||
<div className="absolute -top-3 right-0 w-6 h-7 border-2 border-(--cat-stroke) border-b-0 border-l-0 rounded-tr-full transform -rotate-[20deg] bg-[var(--cat-body)] -z-10"></div>
|
||||
|
||||
<div className="absolute top-7 -left-3 w-4 h-[2px] bg-[var(--cat-stroke)] rounded-full transform rotate-[15deg] opacity-60"></div>
|
||||
<div className="absolute top-9 -left-3 w-4 h-[2px] bg-[var(--cat-stroke)] rounded-full transform -rotate-[5deg] opacity-60"></div>
|
||||
|
||||
@@ -1,14 +1,17 @@
|
||||
export function FooterSection() {
|
||||
return (
|
||||
<footer className="border-t-2 border-dashed border-[var(--border-main)] pt-8 pb-12 flex flex-col sm:flex-row justify-between items-center gap-4 text-xs font-mono text-[var(--text-dim)]">
|
||||
<footer className="border-t-2 border-dashed border-(--border-main) pt-8 pb-12 flex flex-col sm:flex-row justify-between items-center gap-4 text-xs font-mono text-[var(--text-dim)]">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="font-sketch text-lg transform -rotate-6 block">Aditya Gupta</span>
|
||||
<span>// 2026 // NEW_DELHI</span>
|
||||
</div>
|
||||
<div className="flex gap-4 sketch-border-subtle px-4 py-2 bg-[var(--bg-surface)]">
|
||||
<span>UPTIME: 99.9%</span>
|
||||
<span className="text-[var(--text-dim)]">|</span>
|
||||
<span className="accent-text">NOMINAL</span>
|
||||
<div className="flex gap-4 sketch-border-subtle px-4 py-2 bg-(--bg-surface)">
|
||||
<span>Wanna Look at the code? It's not on github!</span>
|
||||
<span className="text-(--text-dim)">|</span>
|
||||
<span className="accent-text">
|
||||
<a href="https://git.adityagupta.dev/sortedcord/mi-sitio-personal">
|
||||
gitea</a>
|
||||
</span>
|
||||
</div>
|
||||
</footer>
|
||||
);
|
||||
|
||||
@@ -1,10 +1,14 @@
|
||||
import { Activity } from 'lucide-react';
|
||||
import { LaptopMinimalCheck, TriangleAlert } from 'lucide-react';
|
||||
import { RevolvingPrism } from './prism/RevolvingPrism';
|
||||
import { HeroTitle } from './HeroTitle';
|
||||
import './prism/styles.css';
|
||||
|
||||
type HeroSectionProps = {
|
||||
activeThemeName: string;
|
||||
hasClickedThemePill: boolean;
|
||||
showThemePillTooltip: boolean;
|
||||
onThemePillClick: () => void;
|
||||
cpuModeEnabled: boolean;
|
||||
};
|
||||
|
||||
export function HeroSection({
|
||||
@@ -12,46 +16,79 @@ export function HeroSection({
|
||||
hasClickedThemePill,
|
||||
showThemePillTooltip,
|
||||
onThemePillClick,
|
||||
cpuModeEnabled,
|
||||
}: HeroSectionProps) {
|
||||
return (
|
||||
<header className="flex flex-col md:flex-row gap-12 items-center justify-between relative">
|
||||
<div className="space-y-6 max-w-2xl">
|
||||
<div className="relative inline-flex mb-4">
|
||||
<div className="relative space-y-6 max-w-2xl">
|
||||
<div className="absolute inset-x-0 -top-10 flex justify-end md:hidden -z-10">
|
||||
<div className="relative w-48 h-56 sketch-border bg-(--bg-panel) transform rotate-3 opacity-70">
|
||||
<div
|
||||
className="absolute inset-2 border-2 border-dashed border-(--border-main) overflow-hidden"
|
||||
style={{
|
||||
WebkitMaskImage: 'linear-gradient(90deg, rgba(0, 0, 0, 0) 0%, rgba(0, 0, 0, 0.2) 30%, rgba(0, 0, 0, 1) 85%)',
|
||||
maskImage: 'linear-gradient(90deg, rgba(0, 0, 0, 0) 0%, rgba(0, 0, 0, 0.2) 30%, rgba(0, 0, 0, 1) 85%)',
|
||||
}}
|
||||
>
|
||||
<img
|
||||
src="/img/operator.jpg"
|
||||
alt="Operator portrait"
|
||||
className="h-full w-full object-cover filter grayscale contrast-125 brightness-90"
|
||||
loading="lazy"
|
||||
/>
|
||||
<div
|
||||
className="absolute inset-0 opacity-70"
|
||||
style={{
|
||||
background: 'linear-gradient(135deg, transparent 10%, var(--color-accent) 100%)',
|
||||
mixBlendMode: 'multiply',
|
||||
}}
|
||||
/>
|
||||
<div
|
||||
className="absolute inset-0 opacity-55"
|
||||
style={{
|
||||
background: 'var(--color-accent)',
|
||||
mixBlendMode: 'color',
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className={`relative inline-flex mb-4 ${hasClickedThemePill ? '' : 'animate-bounce'}`}>
|
||||
<button
|
||||
type="button"
|
||||
onClick={onThemePillClick}
|
||||
className={`inline-flex items-center gap-2 px-4 py-1.5 sketch-border bg-[var(--bg-surface)] text-xs transform -rotate-1 cursor-pointer select-none ${hasClickedThemePill ? '' : 'animate-bounce'}`}
|
||||
className={`inline-flex items-center gap-2 px-4 py-1.5 sketch-border bg-(--bg-surface) text-xs transform -rotate-1 cursor-pointer select-none ${hasClickedThemePill ? '' : 'animate-[pill-glow_2s_ease-in-out_infinite]'}`}
|
||||
aria-label="Theme pill"
|
||||
>
|
||||
<Activity className={`w-3 h-3 accent-text ${hasClickedThemePill ? '' : 'animate-pulse'}`} />
|
||||
<span className="font-sketch text-sm">YOU GET: <span className="text-[var(--text-main)]">{activeThemeName}</span></span>
|
||||
<TriangleAlert className="w-3 h-3 accent-text" />
|
||||
<span className="font-sketch text-sm">YOU GET: <span className="text-(--text-main)">{activeThemeName}</span></span>
|
||||
</button>
|
||||
|
||||
{showThemePillTooltip && (
|
||||
<div
|
||||
role="tooltip"
|
||||
className="absolute left-1/2 top-full mt-2 -translate-x-1/2 whitespace-nowrap px-3 py-1.5 text-xs font-sketch bg-[var(--bg-panel)] text-[var(--text-main)] sketch-border-subtle shadow-lg"
|
||||
>
|
||||
HAHA Made you click
|
||||
</div>
|
||||
)}
|
||||
{showThemePillTooltip}
|
||||
</div>
|
||||
<div className="md:hidden">
|
||||
<HeroTitle />
|
||||
</div>
|
||||
|
||||
<div className="hidden md:block">
|
||||
{cpuModeEnabled ? <HeroTitle /> : <RevolvingPrism />}
|
||||
</div>
|
||||
<h1 className="text-3xl sm:text-5xl font-bold tracking-tight text-[var(--text-main)] leading-tight">
|
||||
I overdesign <span className="accent-text relative inline-block">
|
||||
controllable systems
|
||||
<svg className="absolute w-full h-3 -bottom-1 left-0 accent-text" viewBox="0 0 100 20" preserveAspectRatio="none" fill="none" stroke="currentColor" strokeWidth="3" strokeLinecap="round" strokeLinejoin="round"><path d="M2,15 Q50,-5 98,15" /></svg>
|
||||
</span>,<br /> deconstruct black boxes, and build tools.
|
||||
</h1>
|
||||
<p className="text-md text-(--text-muted) leading-relaxed max-w-xl font-mono">
|
||||
Systems engineer in the making. From window managers and game engines to self-hosted infrastructure.
|
||||
</p>
|
||||
<div className="md:hidden">
|
||||
<div className="sketch-border bg-(--bg-panel) px-4 py-3 text-sm text-(--text-muted) font-mono max-w-sm flex items-start gap-2">
|
||||
<LaptopMinimalCheck className="h-4 w-4 mt-0.5 text-(--color-accent) animate-pulse" />
|
||||
<span>This looks a lot cooler on bigger screens.</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="relative w-48 h-56 sm:w-64 sm:h-72 shrink-0 sketch-border bg-[var(--bg-panel)] transform rotate-[3deg] hover:rotate-0 transition-transform duration-500 group flex flex-col items-center justify-center mt-8 md:mt-0">
|
||||
<div className="absolute -top-3 -right-4 w-12 h-6 bg-[var(--color-accent-muted)] transform rotate-45 opacity-60 backdrop-blur-sm sketch-border-subtle border-none" />
|
||||
<div className="absolute -bottom-3 -left-4 w-12 h-6 bg-[var(--color-accent-muted)] transform rotate-45 opacity-60 backdrop-blur-sm sketch-border-subtle border-none" />
|
||||
<div className="hidden md:flex relative w-48 h-56 sm:w-64 sm:h-72 shrink-0 sketch-border bg-(--bg-panel) transform rotate-3 hover:rotate-0 transition-transform duration-500 group flex-col items-center justify-center mt-8 md:mt-0">
|
||||
<div className="absolute -top-3 -right-4 w-12 h-6 bg-(--color-accent-muted) transform rotate-45 opacity-60 backdrop-blur-sm sketch-border-subtle border-none" />
|
||||
<div className="absolute -bottom-3 -left-4 w-12 h-6 bg-(--color-accent-muted) transform rotate-45 opacity-60 backdrop-blur-sm sketch-border-subtle border-none" />
|
||||
|
||||
<div className="absolute inset-2 border-2 border-dashed border-[var(--border-main)] overflow-hidden">
|
||||
<div className="absolute inset-2 border-2 border-dashed border-(--border-main) overflow-hidden">
|
||||
<img
|
||||
src="/img/operator.jpg"
|
||||
alt="Operator portrait"
|
||||
@@ -74,7 +111,7 @@ export function HeroSection({
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="absolute -bottom-8 -left-12 font-sketch text-xs accent-text -rotate-[15deg] hidden md:flex items-center gap-2 opacity-80">
|
||||
<div className="absolute -bottom-8 -left-12 font-sketch text-xs accent-text -rotate-15 hidden md:flex items-center gap-2 opacity-80">
|
||||
<span className="whitespace-nowrap">the operator</span>
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className="transform -scale-y-100 rotate-45"><path d="M5 12h14" /><path d="m12 5 7 7-7 7" /></svg>
|
||||
</div>
|
||||
|
||||
10
src/components/HeroTitle.tsx
Normal file
10
src/components/HeroTitle.tsx
Normal file
@@ -0,0 +1,10 @@
|
||||
export function HeroTitle() {
|
||||
return (
|
||||
<h1 className="text-3xl sm:text-5xl font-bold tracking-tight text-(--text-main) leading-tight">
|
||||
I overdesign <span className="accent-text relative inline-block">
|
||||
controllable systems
|
||||
<svg className="absolute w-full h-3 -bottom-1 left-0 accent-text" viewBox="0 0 100 20" preserveAspectRatio="none" fill="none" stroke="currentColor" strokeWidth="3" strokeLinecap="round" strokeLinejoin="round"><path d="M2,15 Q50,-5 98,15" /></svg>
|
||||
</span><br /> deconstruct black boxes and build tools.
|
||||
</h1>
|
||||
);
|
||||
}
|
||||
@@ -14,15 +14,19 @@ const CONTENT_MODULES = [
|
||||
Aditya Gupta. 20. New Delhi.<br />
|
||||
Chess climbs, rhythm games, an unhealthy attachment to good audio.
|
||||
<br />
|
||||
I take things apart software, hardware, workflows.<br />
|
||||
I take *ware and workflows apart.<br />
|
||||
Curious enough to wire it back together.
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<span className="accent-text font-sketch text-xl mr-2">{'>'}</span>
|
||||
<span className="text-(--text-main)">cat mindset.md</span>
|
||||
<span className="text-(--text-main)">cat cadence.md</span>
|
||||
<div className="mt-2 ml-4 space-y-2 border-l-2 border-(--border-main) pl-4">
|
||||
<p>Treat the environment as a continuous experiment.</p>
|
||||
<p className="mt-2 text-(--text-main)">
|
||||
I work in bursts.<br />
|
||||
Obsess, prototype, refactor, repeat. <br />
|
||||
Some stuff ships. Some just teach.
|
||||
</p>
|
||||
<p className="text-xs text-(--text-dim) mt-2">Split this pane to view technical stack.</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -36,13 +40,64 @@ const CONTENT_MODULES = [
|
||||
content: (
|
||||
<div className="font-mono text-xs text-(--text-main) flex-1 content-start">
|
||||
<div className="flex flex-wrap gap-2 mt-1">
|
||||
{['Rust', 'Go', 'C/C++', 'eBPF', 'Kubernetes', 'TrueNAS', 'Linux/KWin', 'React'].map((skill) => (
|
||||
<span key={skill} className="px-2 py-1 bg-(--bg-surface) border border-(--border-main) rounded-sm hover:border-(--color-accent) hover:text-[var(--color-accent)] transition-colors cursor-default">
|
||||
{skill}
|
||||
{[
|
||||
{
|
||||
label: 'Infra & Self-Hosting',
|
||||
details:
|
||||
'Docker, Podman, Traefik, Gitea, Jellyfin, Home Assistant, systemd, Nginx, Linux (Arch), Pacman, SSH, S3-compatible storage, rclone',
|
||||
},
|
||||
{
|
||||
label: 'Desktop & Systems Tooling',
|
||||
details:
|
||||
'KDE Plasma, KWin scripting, Wayland, X11, Tauri, Electron, GTK, Qt (QML), PipeWire, PulseAudio, Krunner, supergfxctl, nbfc',
|
||||
},
|
||||
{
|
||||
label: 'Browser Extensions & Web',
|
||||
details:
|
||||
'Manifest V3, WebExtensions API, Chrome Extensions API, Firefox Add-ons, React, TypeScript, Vue, REST APIs',
|
||||
},
|
||||
{
|
||||
label: 'Simulation & Design',
|
||||
details:
|
||||
'TypeScript, Node.js, structured world state design, deterministic intent systems, JSON schema modeling',
|
||||
},
|
||||
{
|
||||
label: 'Low Level Development',
|
||||
details: 'Rust, Cargo, Tokio, Serde, Tauri, egui',
|
||||
},
|
||||
{
|
||||
label: 'Automation & Scripting',
|
||||
details: 'Python, Selenium, BeautifulSoup, Bash, cron',
|
||||
},
|
||||
{
|
||||
label: 'Hardware/Embedded',
|
||||
details:
|
||||
'ESP32, Arduino ecosystem, relays, ACPI/EC interaction, eDP controller boards, UART, I2C',
|
||||
},
|
||||
{
|
||||
label: 'Media Data',
|
||||
details:
|
||||
'FFmpeg, FLAC, Opus, AAC, AV1, H.264, H.265, SQLite, vector-based indexing',
|
||||
},
|
||||
{
|
||||
label: 'Frontend & AppDev',
|
||||
details:
|
||||
'React, TypeScript, Qt/QML, Tauri, VuePress, REST integration',
|
||||
},
|
||||
].map((skill) => (
|
||||
<span
|
||||
key={skill.label}
|
||||
className="relative px-2 py-1 bg-(--bg-surface) border border-(--border-main) rounded-sm hover:border-(--color-accent) hover:text-(--color-accent) transition-colors cursor-default group/skill"
|
||||
>
|
||||
{skill.label}
|
||||
<span className="pointer-events-none absolute left-1/2 top-0 z-50 hidden w-72 -translate-x-1/2 -translate-y-[120%] rounded-md border border-(--border-main) bg-(--bg-panel) px-3 py-2 text-xs leading-relaxed text-(--text-muted) shadow-lg group-hover/skill:block">
|
||||
<span className="block font-sketch text-(--text-main) text-sm mb-1">{skill.label}</span>
|
||||
{skill.details}
|
||||
</span>
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
<p className="mt-4 text-[10px] text-(--text-dim) animate-pulse border-t border-dashed border-(--border-main) pt-2">Split for infrastructure status...</p>
|
||||
<p className="mt-4 text-[10px] text-(--text-dim) animate-pulse border-t border-dashed border-(--border-main) pt-2">Hover for more expanded skills...</p>
|
||||
</div>
|
||||
)
|
||||
},
|
||||
@@ -61,27 +116,27 @@ const CONTENT_MODULES = [
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="leading-relaxed">
|
||||
<tr className="group/row hover:bg-[var(--bg-surface)]">
|
||||
<td className="pt-1 text-[var(--text-main)]">pve-core</td>
|
||||
<td className="pt-1"><span className="text-[var(--status-success)]">Ready</span></td>
|
||||
<tr className="group/row hover:bg-(--bg-surface)">
|
||||
<td className="pt-1 text-(--text-main)">pve-core</td>
|
||||
<td className="pt-1"><span className="text-(--status-success)">Ready</span></td>
|
||||
<td className="pt-1 accent-text">control-plane</td>
|
||||
</tr>
|
||||
<tr className="group/row hover:bg-[var(--bg-surface)]">
|
||||
<tr className="group/row hover:bg-(--bg-surface)">
|
||||
<td>nas-store</td>
|
||||
<td><span className="text-[var(--status-success)]">Ready</span></td>
|
||||
<td><span className="text-(--status-success)">Ready</span></td>
|
||||
<td>truenas-iscsi</td>
|
||||
</tr>
|
||||
<tr className="group/row hover:bg-[var(--bg-surface)]">
|
||||
<tr className="group/row hover:bg-(--bg-surface)">
|
||||
<td>edge-rout</td>
|
||||
<td><span className="text-[var(--status-success)]">Ready</span></td>
|
||||
<td><span className="text-(--status-success)">Ready</span></td>
|
||||
<td>ingress-bpf</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<div className="mt-2 flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="w-2 h-2 rounded-full bg-[var(--status-success)]"></span>
|
||||
<span className="font-sketch text-xs text-[var(--text-dim)]">Healthy</span>
|
||||
<span className="w-2 h-2 rounded-full bg-(--status-success)"></span>
|
||||
<span className="font-sketch text-xs text-(--text-dim)">Healthy</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -92,10 +147,10 @@ const CONTENT_MODULES = [
|
||||
title: 'lshw -short | grep input',
|
||||
tty: 'tty4',
|
||||
content: (
|
||||
<div className="font-mono text-xs text-[var(--text-muted)] flex-1">
|
||||
<div className="font-mono text-xs text-(--text-muted) flex-1">
|
||||
<span className="accent-text font-sketch text-lg mr-2">{'>'}</span>
|
||||
<span className="text-[var(--text-main)]">Hardware focus</span>
|
||||
<ul className="mt-2 ml-2 border-l-2 border-[var(--border-main)] pl-3 space-y-1.5">
|
||||
<span className="text-(--text-main)">Hardware focus</span>
|
||||
<ul className="mt-2 ml-2 border-l-2 border-(--border-main) pl-3 space-y-1.5">
|
||||
<li>MCU: RP2040 (C/Rust)</li>
|
||||
<li>Matrix: Ortho 40%</li>
|
||||
<li>Sensor: EMR Stylus</li>
|
||||
@@ -109,11 +164,11 @@ const CONTENT_MODULES = [
|
||||
title: 'tail -f /var/log/focus',
|
||||
tty: 'tty5',
|
||||
content: (
|
||||
<div className="font-mono text-xs text-[var(--text-muted)] flex-1">
|
||||
<div className="text-[var(--text-dim)] border-b border-dashed border-[var(--border-main)] pb-1 mb-2">Streaming...</div>
|
||||
<div className="font-mono text-xs text-(--text-muted) flex-1">
|
||||
<div className="text-(--text-dim) border-b border-dashed border-(--border-main) pb-1 mb-2">Streaming...</div>
|
||||
<p className="mb-1"><span className="accent-text">[INFO]</span> Analyzing GQL via eBPF</p>
|
||||
<p className="mb-1"><span className="accent-text">[INFO]</span> Wiring custom router</p>
|
||||
<p><span className="text-[var(--text-dim)]">[WAIT]</span> PCB transit delays</p>
|
||||
<p><span className="text-(--text-dim)">[WAIT]</span> PCB transit delays</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -167,27 +222,27 @@ export function InteractiveSystemOverview() {
|
||||
key={node.id}
|
||||
onClick={() => handleSplit(i)}
|
||||
className={`
|
||||
sketch-border bg-[var(--bg-panel)] p-4 relative group overflow-hidden flex flex-col
|
||||
sketch-border bg-(--bg-panel) p-4 relative group overflow-visible flex flex-col
|
||||
transform ${rotation} transition-all duration-500 hover:scale-[1.01] hover:z-10
|
||||
${nextModuleIndex < CONTENT_MODULES.length ? 'cursor-crosshair' : 'cursor-default'}
|
||||
${getNodeClass(activeNodes.length, i)}
|
||||
`}
|
||||
>
|
||||
<div className="flex justify-between items-center border-b-2 border-dashed border-[var(--border-main)] pb-2 mb-3 shrink-0">
|
||||
<span className="font-sketch text-xs sm:text-sm text-[var(--text-muted)] lowercase flex items-center gap-1.5 whitespace-nowrap overflow-hidden text-ellipsis">
|
||||
<div className="flex justify-between items-center border-b-2 border-dashed border-(--border-main) pb-2 mb-3 shrink-0">
|
||||
<span className="font-sketch text-xs sm:text-sm text-(--text-muted) lowercase flex items-center gap-1.5 whitespace-nowrap overflow-hidden text-ellipsis">
|
||||
<span className="text-base accent-text leading-none">*</span>
|
||||
{node.title}
|
||||
</span>
|
||||
<span className="font-sketch text-[10px] sm:text-xs text-[var(--text-dim)] shrink-0 ml-2">[{node.tty}]</span>
|
||||
<span className="font-sketch text-[10px] sm:text-xs text-(--text-dim) shrink-0 ml-2">[{node.tty}]</span>
|
||||
</div>
|
||||
|
||||
<div className="flex-1 overflow-y-auto custom-scrollbar pr-1">
|
||||
<div className="flex-1 overflow-visible custom-scrollbar pr-1">
|
||||
{node.content}
|
||||
</div>
|
||||
|
||||
{nextModuleIndex < CONTENT_MODULES.length && (
|
||||
<div className="absolute inset-0 bg-[var(--color-accent-muted)]/5 opacity-0 group-hover:opacity-100 transition-opacity flex items-center justify-center backdrop-blur-[1px] pointer-events-none">
|
||||
<span className="font-sketch text-lg sm:text-xl font-bold text-[var(--text-main)] bg-[var(--bg-panel)] px-3 py-1.5 sketch-border transform rotate-[-2deg] shadow-lg">
|
||||
<div className="absolute inset-0 bg-(--color-accent-muted)/5 opacity-0 group-hover:opacity-100 transition-opacity flex items-center justify-center backdrop-blur-[1px] pointer-events-none">
|
||||
<span className="font-sketch text-lg sm:text-xl font-bold text-(--text-main) bg-(--bg-panel) px-3 py-1.5 sketch-border transform -rotate-2 shadow-lg">
|
||||
SPLIT_VIEW
|
||||
</span>
|
||||
</div>
|
||||
|
||||
@@ -1,21 +1,46 @@
|
||||
import { Github, Mail, Terminal } from 'lucide-react';
|
||||
import { Cpu, Github, NotebookPen, Terminal, Twitter } from 'lucide-react';
|
||||
|
||||
export function NavigationBar() {
|
||||
type NavigationBarProps = {
|
||||
cpuModeEnabled: boolean;
|
||||
onCpuModeToggle: () => void;
|
||||
};
|
||||
|
||||
export function NavigationBar({ cpuModeEnabled, onCpuModeToggle }: NavigationBarProps) {
|
||||
return (
|
||||
<nav className="flex flex-col sm:flex-row justify-between items-start sm:items-center border-b-2 border-dashed border-(--border-main) pb-4 gap-4">
|
||||
<div className="flex items-center gap-2 text-sm text-(--text-muted) relative group">
|
||||
<Terminal className="w-5 h-5 accent-text transform -rotate-12 group-hover:rotate-12 transition-transform" />
|
||||
<span className="font-sketch text-2xl text-(--text-main) tracking-wide">aditya_gupta <span className="text-(--text-dim)">//</span> sortedcord</span>
|
||||
|
||||
<div className="absolute -top-6 -right-16 font-sketch text-sm accent-text rotate-12 flex items-center gap-1 opacity-80">
|
||||
<a
|
||||
href="https://discord.gg/mDvfbSTQjJ"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="absolute -top-10 left-80 -translate-y-2 z-50 font-sketch text-sm accent-text rotate-12 flex items-center gap-1 opacity-80 hover:text-(--text-main) transition-colors sm:absolute sm:-top-6 sm:-right-16 sm:mt-0 sm:translate-y-0"
|
||||
aria-label="Open root link"
|
||||
>
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className="transform -scale-x-100 rotate-45"><path d="M5 12h14" /><path d="m12 5 7 7-7 7" /></svg>
|
||||
<span>root</span>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-6">
|
||||
<a href="#" className="font-sketch text-lg hover:text-(--text-main) transition-colors flex items-center gap-2 group"><Github className="w-4 h-4 group-hover:-translate-y-1 transition-transform" /> github</a>
|
||||
<a href="#" className="font-sketch text-lg hover:text-(--text-main) transition-colors flex items-center gap-2 group"><Mail className="w-4 h-4 group-hover:-translate-y-1 transition-transform" /> contact</a>
|
||||
<div className="flex items-center gap-6 flex-wrap">
|
||||
<button
|
||||
type="button"
|
||||
onClick={onCpuModeToggle}
|
||||
className="hidden sm:flex items-center gap-2 text-(--text-muted) hover:text-(--text-main) transition-colors"
|
||||
aria-pressed={cpuModeEnabled}
|
||||
aria-label="Toggle low CPU mode"
|
||||
>
|
||||
<Cpu className={`w-4 h-4 ${cpuModeEnabled ? 'text-(--color-accent)' : ''}`} />
|
||||
<span className="font-sketch text-lg">cpu_sav</span>
|
||||
<span className={`relative inline-flex h-5 w-9 items-center rounded-full border transition-colors ${cpuModeEnabled ? 'bg-(--color-accent) border-(--color-accent)' : 'bg-(--bg-surface) border-(--border-main)'}`}>
|
||||
<span className={`inline-block h-4 w-4 transform rounded-full transition-colors ${cpuModeEnabled ? 'translate-x-4 bg-(--bg-base)' : 'translate-x-1 bg-(--color-accent)'}`} />
|
||||
</span>
|
||||
</button>
|
||||
<a href="https://patio.adityagupta.dev" className="font-sketch text-lg hover:text-(--text-main) transition-colors flex items-center gap-2 group"><NotebookPen className="w-4 h-4 group-hover:-translate-y-1 transition-transform" /> patio</a>
|
||||
<a href="https://github.com/sortedcord" className="font-sketch text-lg hover:text-(--text-main) transition-colors flex items-center gap-2 group"><Github className="w-4 h-4 group-hover:-translate-y-1 transition-transform" /> github</a>
|
||||
<a href="https://x.com/sortedcord" className="font-sketch text-lg hover:text-(--text-main) transition-colors flex items-center gap-2 group"><Twitter className="w-4 h-4 group-hover:-translate-y-1 transition-transform" /> twitter</a>
|
||||
</div>
|
||||
</nav>
|
||||
);
|
||||
|
||||
@@ -1,49 +1,217 @@
|
||||
import { ExternalLink, Layout } from 'lucide-react';
|
||||
import { useEffect, useLayoutEffect, useRef, useState } from 'react';
|
||||
import { ChevronRight, ExternalLink, GitFork, Layout, Star } from 'lucide-react';
|
||||
import { PROJECTS } from '../data/contentData';
|
||||
|
||||
export function ProjectsSection() {
|
||||
const [repoStats, setRepoStats] = useState<Record<string, { stars: number; forks: number }>>({});
|
||||
const [orderedProjects, setOrderedProjects] = useState(PROJECTS);
|
||||
const projectRefs = useRef(new Map<string, HTMLDivElement>());
|
||||
const projectPositions = useRef(new Map<string, DOMRect>());
|
||||
|
||||
const handleProjectPromote = (projectId: string) => {
|
||||
setOrderedProjects((prev) => {
|
||||
const target = prev.find((project) => project.id === projectId);
|
||||
if (!target) return prev;
|
||||
const remaining = prev.filter((project) => project.id !== projectId);
|
||||
return [target, ...remaining];
|
||||
});
|
||||
};
|
||||
|
||||
useLayoutEffect(() => {
|
||||
const nextPositions = new Map<string, DOMRect>();
|
||||
|
||||
projectRefs.current.forEach((node, id) => {
|
||||
nextPositions.set(id, node.getBoundingClientRect());
|
||||
});
|
||||
|
||||
projectRefs.current.forEach((node, id) => {
|
||||
const prev = projectPositions.current.get(id);
|
||||
const next = nextPositions.get(id);
|
||||
if (!prev || !next) return;
|
||||
|
||||
const deltaX = prev.left - next.left;
|
||||
const deltaY = prev.top - next.top;
|
||||
|
||||
if (deltaX || deltaY) {
|
||||
node.style.transform = `translate(${deltaX}px, ${deltaY}px)`;
|
||||
node.style.transition = 'transform 0s';
|
||||
requestAnimationFrame(() => {
|
||||
node.style.transform = '';
|
||||
node.style.transition = 'transform 320ms ease';
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
projectPositions.current = nextPositions;
|
||||
}, [orderedProjects]);
|
||||
|
||||
useEffect(() => {
|
||||
let isMounted = true;
|
||||
|
||||
const fetchStats = async () => {
|
||||
const entries = PROJECTS.filter(project => project.github).map(async project => {
|
||||
try {
|
||||
const url = new URL(project.github!);
|
||||
const [owner, repo] = url.pathname.replace(/^\//, '').split('/');
|
||||
|
||||
if (!owner || !repo) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const response = await fetch(`https://api.github.com/repos/${owner}/${repo}`);
|
||||
if (!response.ok) {
|
||||
return null;
|
||||
}
|
||||
const data = await response.json();
|
||||
return {
|
||||
id: project.id,
|
||||
stars: Number(data.stargazers_count ?? 0),
|
||||
forks: Number(data.forks_count ?? 0),
|
||||
};
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
});
|
||||
|
||||
const results = await Promise.all(entries);
|
||||
if (!isMounted) return;
|
||||
|
||||
const nextStats = results.reduce<Record<string, { stars: number; forks: number }>>((acc, item) => {
|
||||
if (item) {
|
||||
acc[item.id] = { stars: item.stars, forks: item.forks };
|
||||
}
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
if (Object.keys(nextStats).length > 0) {
|
||||
setRepoStats(nextStats);
|
||||
}
|
||||
};
|
||||
|
||||
fetchStats();
|
||||
|
||||
return () => {
|
||||
isMounted = false;
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<section className="md:col-span-2 space-y-6">
|
||||
<h2 className="font-sketch text-2xl text-[var(--text-main)] border-b-2 border-dashed border-[var(--border-main)] pb-2 flex items-center gap-2">
|
||||
<h2 className="font-sketch text-2xl text-(--text-main) border-b-2 border-dashed border-(--border-main) pb-2 flex items-center gap-2">
|
||||
<Layout className="w-5 h-5 accent-text" /> recent_projects/
|
||||
</h2>
|
||||
<div className="flex flex-col gap-6">
|
||||
{PROJECTS.map((project, idx) => (
|
||||
<article
|
||||
{orderedProjects.slice(0, 3).map((project, idx) => (
|
||||
<div
|
||||
key={project.id}
|
||||
className={`group relative p-6 bg-[var(--bg-surface)] sketch-border-subtle cursor-pointer overflow-hidden ${idx % 2 === 0 ? 'transform rotate-[0.5deg]' : 'transform -rotate-[0.5deg]'}`}
|
||||
ref={(node) => {
|
||||
if (node) {
|
||||
projectRefs.current.set(project.id, node);
|
||||
} else {
|
||||
projectRefs.current.delete(project.id);
|
||||
}
|
||||
}}
|
||||
className="transition-transform duration-300 ease-out"
|
||||
>
|
||||
<div className="absolute top-0 left-0 w-full h-1 bg-[var(--border-main)] group-hover:bg-[var(--color-accent)] transition-colors duration-300 opacity-50" />
|
||||
|
||||
<div className="flex justify-between items-start mb-4">
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="p-3 sketch-border-subtle bg-[var(--bg-panel)] text-[var(--text-muted)] group-hover:text-[var(--color-accent)] group-hover:scale-110 transition-all transform -rotate-3">
|
||||
{project.icon}
|
||||
<article
|
||||
className={`group relative p-6 bg-(--bg-surface) sketch-border-subtle cursor-pointer overflow-visible ${idx % 2 === 0 ? 'transform rotate-[0.5deg]' : 'transform -rotate-[0.5deg]'}`}
|
||||
>
|
||||
{project.ribbon && (
|
||||
<div className="absolute -top-2 -right-10 rotate-12 z-10">
|
||||
<div className="sketch-border-subtle bg-(--color-accent) text-(--bg-base) px-6 py-1 text-xs font-sketch uppercase tracking-wide shadow-lg">
|
||||
{project.ribbon}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="text-[var(--text-main)] font-bold text-lg flex items-center gap-2 font-mono">
|
||||
{project.title}
|
||||
<ExternalLink className="w-4 h-4 opacity-0 group-hover:opacity-100 transition-opacity accent-text" />
|
||||
</h3>
|
||||
<span className="font-sketch text-sm text-[var(--text-muted)] accent-text">{project.category}</span>
|
||||
)}
|
||||
<div className="absolute top-0 left-0 w-full h-1 bg-(--border-main) group-hover:bg-(--color-accent) transition-colors duration-300 opacity-50" />
|
||||
|
||||
<div className="flex justify-between items-start mb-4">
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="group/icon relative flex items-center gap-2 py-2.5 ps-2.5 pe-1.5 pr-6 sketch-border-subtle bg-(--bg-panel) text-(--text-muted) hover:text-(--color-accent) hover:scale-110 transition-all transform -rotate-3">
|
||||
<span className="flex items-center justify-center">
|
||||
{project.icon}
|
||||
</span>
|
||||
<ChevronRight className="absolute right-1.5 top-1/2 -translate-y-1/2 w-4 h-4 text-(--color-accent) opacity-90 animate-pulse transition-all drop-shadow-[0_0_16px_var(--color-accent)] group-hover/icon:opacity-0 group-hover/icon:scale-0 group-focus-within/icon:opacity-0 group-focus-within/icon:scale-0" />
|
||||
{project.github && (
|
||||
<div className="flex items-center gap-3 text-xs text-(--text-muted) overflow-hidden max-w-0 opacity-0 group-hover/icon:max-w-35 group-hover/icon:opacity-100 transition-all duration-300">
|
||||
<span className="flex items-center gap-1">
|
||||
<Star className="w-3 h-3 text-(--color-accent)" />
|
||||
{repoStats[project.id]?.stars ?? project.stars ?? '—'}
|
||||
</span>
|
||||
<span className="flex items-center gap-1">
|
||||
<GitFork className="w-3 h-3 text-(--color-accent)" />
|
||||
{repoStats[project.id]?.forks ?? project.forks ?? '—'}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="text-(--text-main) font-bold text-lg font-mono">
|
||||
{project.link ? (
|
||||
<a
|
||||
href={project.link}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="inline-flex items-center gap-2 hover:text-(--color-accent) transition-colors"
|
||||
>
|
||||
{project.title}
|
||||
<ExternalLink className="w-4 h-4 opacity-0 group-hover:opacity-100 transition-opacity accent-text" />
|
||||
</a>
|
||||
) : (
|
||||
<span className="inline-flex items-center gap-2">
|
||||
{project.title}
|
||||
<ExternalLink className="w-4 h-4 opacity-0 group-hover:opacity-100 transition-opacity accent-text" />
|
||||
</span>
|
||||
)}
|
||||
</h3>
|
||||
<span className="font-sketch text-sm text-(--text-muted) accent-text">{project.category}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<p className="text-sm text-(--text-muted) mb-5 leading-relaxed font-mono">
|
||||
{project.desc}
|
||||
</p>
|
||||
|
||||
<p className="text-sm text-[var(--text-muted)] mb-5 leading-relaxed font-mono">
|
||||
{project.desc}
|
||||
</p>
|
||||
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{project.tech.map((t) => (
|
||||
<span key={t} className="text-xs px-2 py-1 bg-[var(--bg-panel)] sketch-border-subtle text-[var(--text-muted)] font-mono">
|
||||
{t}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</article>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{project.tech.map((t) => (
|
||||
<span key={t} className="text-xs px-2 py-1 bg-(--bg-panel) sketch-border-subtle text-(--text-muted) font-mono">
|
||||
{t}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</article>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
{orderedProjects.length > 3 && (
|
||||
<div className="flex flex-wrap gap-3">
|
||||
{orderedProjects.slice(3).map((project) => (
|
||||
<div
|
||||
key={project.id}
|
||||
ref={(node) => {
|
||||
if (node) {
|
||||
projectRefs.current.set(project.id, node);
|
||||
} else {
|
||||
projectRefs.current.delete(project.id);
|
||||
}
|
||||
}}
|
||||
className="transition-transform duration-300 ease-out"
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => handleProjectPromote(project.id)}
|
||||
className="group/icon relative flex items-center gap-2 py-2.5 ps-2.5 pe-1.5 pr-6 sketch-border-subtle bg-(--bg-panel) text-(--text-muted) hover:text-(--color-accent) hover:scale-105 transition-all"
|
||||
aria-label={`Promote ${project.title}`}
|
||||
>
|
||||
<span className="flex items-center justify-center">
|
||||
{project.icon}
|
||||
</span>
|
||||
<ChevronRight className="absolute right-1.5 top-1/2 -translate-y-1/2 w-4 h-4 text-(--color-accent) opacity-90 animate-pulse transition-all drop-shadow-[0_0_16px_var(--color-accent)] group-hover/icon:opacity-0 group-hover/icon:scale-0 group-focus-within/icon:opacity-0 group-focus-within/icon:scale-0" />
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ export function ResumeSection() {
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
<div className="sketch-border-subtle bg-(--bg-surface) p-6 flex flex-col gap-4">
|
||||
<div className="mt-10 p-5 sketch-border bg-(--color-accent-muted)/5 transform rotate-1">
|
||||
<p className="text-(--text-muted) text-sm font-mono leading-relaxed">
|
||||
Grab the latest snapshot of my experience, projects, and research focus.
|
||||
</p>
|
||||
|
||||
98
src/components/ThemeMapperModal.tsx
Normal file
98
src/components/ThemeMapperModal.tsx
Normal file
@@ -0,0 +1,98 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { FileBracesCorner } from 'lucide-react';
|
||||
|
||||
type ThemeMapperModalProps = {
|
||||
isOpen: boolean;
|
||||
viewerIp: string | null;
|
||||
deterministicIndex: number | null;
|
||||
themeName: string;
|
||||
onClose: () => void;
|
||||
};
|
||||
|
||||
export function ThemeMapperModal({
|
||||
isOpen,
|
||||
viewerIp,
|
||||
deterministicIndex,
|
||||
themeName,
|
||||
onClose,
|
||||
}: ThemeMapperModalProps) {
|
||||
const [shouldRender, setShouldRender] = useState(isOpen);
|
||||
const [isClosing, setIsClosing] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (isOpen) {
|
||||
setShouldRender(true);
|
||||
setIsClosing(false);
|
||||
return;
|
||||
}
|
||||
|
||||
if (shouldRender) {
|
||||
setIsClosing(true);
|
||||
const timeout = window.setTimeout(() => {
|
||||
setShouldRender(false);
|
||||
setIsClosing(false);
|
||||
}, 200);
|
||||
|
||||
return () => window.clearTimeout(timeout);
|
||||
}
|
||||
}, [isOpen, shouldRender]);
|
||||
|
||||
if (!shouldRender) return null;
|
||||
|
||||
return (
|
||||
<div
|
||||
className="fixed inset-0 z-40 flex items-center justify-center bg-black/60 px-4"
|
||||
onClick={onClose}
|
||||
>
|
||||
<div
|
||||
className={`relative w-full max-w-2xl bg-(--bg-panel) sketch-border p-6 text-(--text-main) shadow-2xl ${isClosing ? 'animate-[modal-exit_0.2s_ease-in_forwards]' : 'animate-[modal-zoom_0.25s_ease-out]'}`}
|
||||
onClick={(event) => event.stopPropagation()}
|
||||
>
|
||||
<h2 className="font-sketch text-2xl text-(--text-main) flex items-center gap-2">
|
||||
<FileBracesCorner className="w-5 h-5 accent-text" /> theme_mapper.sh
|
||||
</h2>
|
||||
<p className="font-mono mt-4 text-sm text-(--text-muted) leading-relaxed">
|
||||
This website uses hashing functions to map your ip address to a color pallete.
|
||||
<br />
|
||||
This color scheme is unique to your IP. Your friends will probably have a different color scheme to look at.
|
||||
<br />
|
||||
The theme may change as I add more color schemes to this site.
|
||||
</p>
|
||||
<div className="mt-6 rounded-xl bg-(--bg-surface) sketch-border-subtle p-4 font-mono text-xs text-(--text-muted)">
|
||||
<div className="grid gap-4 md:grid-cols-[1fr_auto_1fr_auto_1fr] items-center">
|
||||
<div className="flex flex-col gap-2">
|
||||
<span className="text-(--text-main)">Your IP Address</span>
|
||||
<span className="text-(--text-dim)">{viewerIp ?? 'unknown'}</span>
|
||||
</div>
|
||||
<div className="flex flex-col items-center gap-1">
|
||||
<span className="text-[10px] uppercase tracking-wide text-(--text-dim)">fnv1a function</span>
|
||||
<span className="arrow-trail-text" aria-hidden="true">>>>>>>>>>>></span>
|
||||
</div>
|
||||
<div className="flex flex-col gap-2">
|
||||
<span className="text-(--text-main)">Hash Output</span>
|
||||
<span className="text-(--text-dim)">{deterministicIndex ?? '—'}</span>
|
||||
</div>
|
||||
<div className="flex flex-col items-center gap-1">
|
||||
<span className="text-[10px] uppercase tracking-wide text-(--text-dim)">theme select</span>
|
||||
<span className="arrow-trail-text" aria-hidden="true">>>>>>>>>>>></span>
|
||||
</div>
|
||||
<div className="flex flex-col gap-2">
|
||||
<span className="text-(--text-main)">Theme Name</span>
|
||||
<span className="text-(--text-dim)">{themeName}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<a
|
||||
href={`https://twitter.com/intent/tweet?text=${encodeURIComponent(
|
||||
`This nerd thinks I deserve to look at "${themeName}" when viewing his website. Let's dox @sortedcord!\n\nhttps://adityagupta.dev`
|
||||
)}`}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="mt-6 inline-flex items-center justify-center gap-2 sketch-border bg-(--bg-surface) px-5 py-2 text-sm font-sketch text-(--text-main) hover:text-(--color-accent) transition-colors"
|
||||
>
|
||||
Share on Twitter
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
173
src/components/prism/RevolvingPrism.tsx
Normal file
173
src/components/prism/RevolvingPrism.tsx
Normal file
@@ -0,0 +1,173 @@
|
||||
import { useRef, useState, useEffect } from "react";
|
||||
import { Activity } from "lucide-react";
|
||||
|
||||
export function RevolvingPrism() {
|
||||
const faces = [
|
||||
{
|
||||
id: 0,
|
||||
title: "01_INFRA",
|
||||
text: "Personal Infrastructure",
|
||||
desc: "Self-hosting with Docker, Traefik, Gitea, S3 stacks and Linux. Owning deployment, networking, and storage end-to-end."
|
||||
},
|
||||
{
|
||||
id: 1,
|
||||
title: "02_DESKTOP",
|
||||
text: "Desktop & Workflow Engineering",
|
||||
desc: "KDE, KWin scripting, Wayland, Tauri, tiling logic, automation. Modifying the environment instead of adapting to it."
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
title: "03_ENGINES",
|
||||
text: "Simulation & Engine Design",
|
||||
desc: "TypeScript and Rust. Structured world state, deterministic intent systems, LLM-assisted orchestration."
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
title: "04_HARDWARE",
|
||||
text: "Hardware & Embedded Experiments",
|
||||
desc: "ESP32, relays, ACPI quirks, eDP controller boards, audio chains, and soldered prototypes."
|
||||
}
|
||||
];
|
||||
|
||||
const rotationRef = useRef(0);
|
||||
const isHoveredRef = useRef(false);
|
||||
const isDraggingRef = useRef(false);
|
||||
const lastMouseX = useRef(0);
|
||||
const [, setRenderTick] = useState(0);
|
||||
|
||||
// animation loop
|
||||
useEffect(() => {
|
||||
let animationFrameId: number | null = null;
|
||||
const loop = () => {
|
||||
if (!isDraggingRef.current) {
|
||||
if (isHoveredRef.current) {
|
||||
// SNAP: Magnetically lerp to the nearest 90-degree angle
|
||||
const nearest = Math.round(rotationRef.current / 90) * 90;
|
||||
rotationRef.current += (nearest - rotationRef.current) * 0.08;
|
||||
} else {
|
||||
// CONTINUOUS: Rotate slowly (approx 16s per revolution at 60fps)
|
||||
rotationRef.current -= 0.375;
|
||||
}
|
||||
}
|
||||
// force a re-render to apply inline transform and check active states
|
||||
setRenderTick(t => t + 1);
|
||||
animationFrameId = requestAnimationFrame(loop);
|
||||
};
|
||||
animationFrameId = requestAnimationFrame(loop);
|
||||
return () => {
|
||||
if (animationFrameId !== null) {
|
||||
cancelAnimationFrame(animationFrameId);
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
|
||||
// Drag Interactions
|
||||
const handlePointerDown = (e: React.PointerEvent<HTMLDivElement>) => {
|
||||
isDraggingRef.current = true;
|
||||
lastMouseX.current = e.clientX;
|
||||
e.currentTarget.setPointerCapture(e.pointerId);
|
||||
};
|
||||
|
||||
const handlePointerMove = (e: React.PointerEvent<HTMLDivElement>) => {
|
||||
if (!isDraggingRef.current) return;
|
||||
const delta = e.clientX - lastMouseX.current;
|
||||
rotationRef.current += delta * 0.8; // Adjust drag sensitivity
|
||||
lastMouseX.current = e.clientX;
|
||||
};
|
||||
|
||||
const handlePointerUp = (e: React.PointerEvent<HTMLDivElement>) => {
|
||||
isDraggingRef.current = false;
|
||||
e.currentTarget.releasePointerCapture(e.pointerId);
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
className="w-full h-64 flex items-center justify-start perspective-1000 z-30 relative touch-none group"
|
||||
onMouseEnter={() => isHoveredRef.current = true}
|
||||
onMouseLeave={() => {
|
||||
isHoveredRef.current = false;
|
||||
isDraggingRef.current = false;
|
||||
}}
|
||||
onPointerDown={handlePointerDown}
|
||||
onPointerMove={handlePointerMove}
|
||||
onPointerUp={handlePointerUp}
|
||||
onPointerCancel={handlePointerUp}
|
||||
>
|
||||
{/* Annotation for hover interaction */}
|
||||
<div className="absolute top-0 right-10 hidden sm:flex items-center gap-2 text-(--color-accent-muted) font-sketch text-xs animate-pulse opacity-80 pointer-events-none">
|
||||
<Activity className="w-3 h-3" />
|
||||
<span>[ hover to snap | drag to rotate ]</span>
|
||||
</div>
|
||||
|
||||
<div className="relative w-14 h-55">
|
||||
<div
|
||||
className="prism-container cursor-grab active:cursor-grabbing w-full h-full"
|
||||
style={{ transform: `rotateY(${rotationRef.current}deg)` }}
|
||||
>
|
||||
<div className="prism-cap prism-cap-top" />
|
||||
<div className="prism-cap prism-cap-bottom" />
|
||||
|
||||
{faces.map((face, index) => {
|
||||
// Calculate the true world rotation of this specific face
|
||||
const worldRotation = rotationRef.current + (index * 90);
|
||||
|
||||
// Normalize between -180 and +180
|
||||
let normalized = worldRotation % 360;
|
||||
if (normalized > 180) normalized -= 360;
|
||||
if (normalized < -180) normalized += 360;
|
||||
|
||||
// Face is considered "Active" if it is within ~42 degrees of perfectly parallel
|
||||
const isActive = Math.abs(normalized) < 42;
|
||||
|
||||
// Evenly space dots along the prism height
|
||||
const prismHeight = 220;
|
||||
const dotStep = prismHeight / (faces.length + 1);
|
||||
const dotY = Math.round(dotStep * (index + 1));
|
||||
const dy = Math.round(dotStep * index); // Distance between the dot and the text
|
||||
|
||||
return (
|
||||
<div key={face.id} className={`prism-face prism-face-${index} ${isActive ? 'active' : ''}`}>
|
||||
{/* Dynamically assign height to create the staircase */}
|
||||
<div className="glowing-dot" style={{ top: `${dotY}px` }} />
|
||||
|
||||
<div className="prism-popout">
|
||||
{/* Architectural dynamic routing line bridging the gap */}
|
||||
<svg className="prism-line-svg" width="40" height={dy + 2} style={{ position: 'absolute', top: '50%', left: 0, overflow: 'visible' }}>
|
||||
<path d={`M 0,${dy} L 20,${dy} L 20,0 L 40,0`} fill="none" stroke="var(--color-accent)" strokeWidth="2" strokeDasharray="4 4" />
|
||||
</svg>
|
||||
|
||||
<div className="prism-text">
|
||||
<div className="font-mono text-xs text-(--text-dim) mb-1 leading-none prism-3d-text">{face.title}</div>
|
||||
<div className="tracking-tight text-4xl accent-text whitespace-nowrap max-w-292 prism-3d-text leading-tight">{face.text}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
{/* Static Descriptions Container */}
|
||||
<div className="absolute left-28 top-18 w-100 sm:w-120 pointer-events-none">
|
||||
{faces.map((face, index) => {
|
||||
const worldRotation = rotationRef.current + (index * 90);
|
||||
let normalized = worldRotation % 360;
|
||||
if (normalized > 180) normalized -= 360;
|
||||
if (normalized < -180) normalized += 360;
|
||||
const isActive = Math.abs(normalized) < 42;
|
||||
|
||||
return (
|
||||
<div
|
||||
key={`desc-${face.id}`}
|
||||
className={`sketch-border bg-(--color-accent-muted)/5 transform rotate-1 m-2 p-2 absolute transition-opacity duration-300 ease-in-out ${isActive ? 'opacity-100 delay-100' : 'opacity-0'}`}
|
||||
>
|
||||
<p className="font-mono text-md text-(--text-muted) leading-relaxed pl-3">
|
||||
{face.desc}
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
136
src/components/prism/styles.css
Normal file
136
src/components/prism/styles.css
Normal file
@@ -0,0 +1,136 @@
|
||||
/* 3D Prism Animation Styles */
|
||||
.perspective-1000 {
|
||||
perspective: 1000px;
|
||||
}
|
||||
|
||||
.prism-container {
|
||||
--prism-width: 56px;
|
||||
--prism-height: 220px;
|
||||
--prism-depth: 28px;
|
||||
--prism-cap-size: 56px;
|
||||
width: var(--prism-width);
|
||||
height: var(--prism-height);
|
||||
position: relative;
|
||||
transform-style: preserve-3d;
|
||||
}
|
||||
|
||||
.prism-face {
|
||||
position: absolute;
|
||||
width: var(--prism-width);
|
||||
height: var(--prism-height);
|
||||
background: var(--bg-panel);
|
||||
border: 2px solid var(--border-main);
|
||||
transform-style: preserve-3d;
|
||||
}
|
||||
|
||||
.prism-face-0 {
|
||||
transform: rotateY(0deg) translateZ(var(--prism-depth));
|
||||
}
|
||||
|
||||
.prism-face-1 {
|
||||
transform: rotateY(90deg) translateZ(var(--prism-depth));
|
||||
}
|
||||
|
||||
.prism-face-2 {
|
||||
transform: rotateY(180deg) translateZ(var(--prism-depth));
|
||||
}
|
||||
|
||||
.prism-face-3 {
|
||||
transform: rotateY(270deg) translateZ(var(--prism-depth));
|
||||
}
|
||||
|
||||
.prism-cap {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: calc((var(--prism-height) - var(--prism-cap-size)) / 2);
|
||||
width: var(--prism-cap-size);
|
||||
height: var(--prism-cap-size);
|
||||
background: var(--bg-surface);
|
||||
border: 2px solid var(--border-main);
|
||||
}
|
||||
|
||||
.prism-cap-top {
|
||||
transform: rotateX(90deg) translateZ(calc(var(--prism-height) / 2));
|
||||
}
|
||||
|
||||
.prism-cap-bottom {
|
||||
transform: rotateX(-90deg) translateZ(calc(var(--prism-height) / 2));
|
||||
}
|
||||
|
||||
.glowing-dot {
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
border-radius: 50%;
|
||||
background: var(--border-main);
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.prism-face.active .glowing-dot {
|
||||
background: var(--color-accent);
|
||||
box-shadow: 0 0 8px var(--color-accent), 0 0 16px var(--color-accent);
|
||||
}
|
||||
|
||||
.prism-popout {
|
||||
position: absolute;
|
||||
left: 100%;
|
||||
top: 32px;
|
||||
/* Aligns with the fixed height of the text */
|
||||
transform: translateY(-50%);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
pointer-events: none;
|
||||
transform-style: preserve-3d;
|
||||
}
|
||||
|
||||
/* SVG Line wiping animation */
|
||||
.prism-line-svg {
|
||||
-webkit-mask-image: linear-gradient(to right, #000 40%, transparent 60%);
|
||||
mask-image: linear-gradient(to right, #000 40%, transparent 60%);
|
||||
-webkit-mask-size: 300% 100%;
|
||||
mask-size: 300% 100%;
|
||||
-webkit-mask-position: 100% 0;
|
||||
mask-position: 100% 0;
|
||||
transition: -webkit-mask-position 0.3s ease-out, mask-position 0.3s ease-out;
|
||||
}
|
||||
|
||||
.prism-face.active .prism-line-svg {
|
||||
-webkit-mask-position: 0 0;
|
||||
mask-position: 0 0;
|
||||
transition: -webkit-mask-position 0.4s cubic-bezier(0.25, 1, 0.5, 1);
|
||||
}
|
||||
|
||||
.prism-text {
|
||||
margin-left: var(--prism-width);
|
||||
/* Reserves space for the absolute-positioned SVG line */
|
||||
padding-left: 10px;
|
||||
transform-style: preserve-3d;
|
||||
|
||||
/* Masking for directional fade (wipes left/right) */
|
||||
-webkit-mask-image: linear-gradient(to right, #000 40%, transparent 60%);
|
||||
mask-image: linear-gradient(to right, #000 40%, transparent 60%);
|
||||
-webkit-mask-size: 300% 100%;
|
||||
mask-size: 300% 100%;
|
||||
-webkit-mask-position: 100% 0;
|
||||
mask-position: 100% 0;
|
||||
|
||||
opacity: 0;
|
||||
/* Fade out right-to-left (sped up to clear out earlier) */
|
||||
transition: -webkit-mask-position 0.2s ease-out, mask-position 0.2s ease-out, opacity 0s linear 0.2s;
|
||||
}
|
||||
|
||||
.prism-face.active .prism-text {
|
||||
-webkit-mask-position: 0 0;
|
||||
mask-position: 0 0;
|
||||
opacity: 1;
|
||||
/* Fade in left-to-right (slight delay lets the line draw first) */
|
||||
transition: -webkit-mask-position 0.5s ease-out 0.1s, mask-position 0.5s ease-out 0.1s, opacity 0s linear 0.1s;
|
||||
}
|
||||
|
||||
/* Simulates 3D extrusion/thickness on flat text */
|
||||
.prism-3d-text {
|
||||
text-shadow: 1px 1px 0 var(--border-main), 2px 2px 0 var(--bg-base);
|
||||
backface-visibility: visible;
|
||||
}
|
||||
@@ -1,34 +0,0 @@
|
||||
import { Cpu, Gamepad, Layout } from 'lucide-react';
|
||||
|
||||
export const PROJECTS = [
|
||||
{
|
||||
id: 'Krohnkite',
|
||||
title: 'Krohnkite i3',
|
||||
category: 'Window Management',
|
||||
tech: ['Typescript', 'Kwin API', 'QML'],
|
||||
desc: 'A modified stripped down version of Krohnkite that implements i3-style tiling in KDE Plasma (Wayland).',
|
||||
icon: <Layout className="w-5 h-5" />,
|
||||
},
|
||||
{
|
||||
id: 'leenim',
|
||||
title: 'Leenim',
|
||||
category: 'Desktop Application Development',
|
||||
tech: ['React', 'Rust', 'Tauri', 'Python'],
|
||||
desc: 'A Cross platform Video Editor style application for creating mathematical illustrations with Manim.',
|
||||
icon: <Cpu className="w-5 h-5" />,
|
||||
},
|
||||
{
|
||||
id: 'omnia',
|
||||
title: 'Omnia',
|
||||
category: 'AI-Native Computing Systems',
|
||||
tech: ['Typescript'],
|
||||
desc: 'A game engine specification that defines an AI driven, intent-based architecture for building determinist-first interactive worlds.',
|
||||
icon: <Gamepad className="w-5 h-5" />,
|
||||
},
|
||||
];
|
||||
|
||||
export const LAB_NOTES = [
|
||||
{ date: '2026-02-14', title: 'GT730 and handling some Nvidia Voodoo Magic' },
|
||||
{ date: '2026-01-28', title: "So, how's that VPS business going?" },
|
||||
{ date: '2025-12-10', title: 'Injecting dynamic themes via CSS variable mutations' },
|
||||
];
|
||||
@@ -1,11 +1,16 @@
|
||||
import type { ReactElement } from 'react';
|
||||
import { Cpu, Gamepad, Layout } from 'lucide-react';
|
||||
import { Cpu, Gamepad, Layout, Chromium } from 'lucide-react';
|
||||
|
||||
export type Project = {
|
||||
id: string;
|
||||
github?: string;
|
||||
stars?: number;
|
||||
forks?: number;
|
||||
title: string;
|
||||
link: string;
|
||||
category: string;
|
||||
tech: string[];
|
||||
ribbon: string | null;
|
||||
desc: string;
|
||||
icon: ReactElement;
|
||||
};
|
||||
@@ -18,26 +23,46 @@ export type LabNote = {
|
||||
export const PROJECTS: Project[] = [
|
||||
{
|
||||
id: 'Krohnkite',
|
||||
github: 'https://github.com/sortedcord/krohnkite-i3',
|
||||
link: 'https://github.com/sortedcord/krohnkite-i3',
|
||||
title: 'Krohnkite i3',
|
||||
category: 'Window Management',
|
||||
tech: ['Typescript', 'Kwin API', 'QML'],
|
||||
ribbon: null,
|
||||
desc: 'A modified stripped down version of Krohnkite that implements i3-style tiling in KDE Plasma (Wayland).',
|
||||
icon: <Layout className="w-5 h-5" />,
|
||||
},
|
||||
{
|
||||
id: 'leenim',
|
||||
github: 'https://github.com/sortedcord/leenim',
|
||||
link: 'https://github.com/sortedcord/leenim',
|
||||
title: 'Leenim',
|
||||
category: 'Desktop Application Development',
|
||||
tech: ['React', 'Rust', 'Tauri', 'Python'],
|
||||
desc: 'A Cross platform Video Editor style application for creating mathematical illustrations with Manim.',
|
||||
ribbon: null,
|
||||
icon: <Cpu className="w-5 h-5" />,
|
||||
},
|
||||
{
|
||||
id: 'gippity-pruner',
|
||||
github: 'https://github.com/sortedcord/gippity-pruner',
|
||||
link: 'https://github.com/sortedcord/gippity-pruner',
|
||||
title: 'Gippity Pruner',
|
||||
category: 'Browser Extension',
|
||||
tech: ['JavaScript'],
|
||||
desc: 'Cross browser extension that introduces lazy loading in ChatGPTs UI, improving performance for long coversations without context or memory loss.',
|
||||
ribbon: "+1k Users!",
|
||||
icon: <Chromium className='w-5 h-5' />,
|
||||
},
|
||||
{
|
||||
id: 'omnia',
|
||||
github: 'https://github.com/sortedcord/omnia',
|
||||
link: 'https://github.com/sortedcord/omnia',
|
||||
title: 'Omnia',
|
||||
category: 'AI-Native Computing Systems',
|
||||
tech: ['Typescript'],
|
||||
desc: 'A game engine specification that defines an AI driven, intent-based architecture for building determinist-first interactive worlds.',
|
||||
ribbon: null,
|
||||
icon: <Gamepad className="w-5 h-5" />,
|
||||
},
|
||||
];
|
||||
|
||||
111
src/theme.css
111
src/theme.css
@@ -99,6 +99,21 @@
|
||||
animation: dot-grid-drift-x 3s linear infinite;
|
||||
}
|
||||
|
||||
.dot-grid.dot-grid-static::before {
|
||||
animation: none;
|
||||
background-position: 0px 0px;
|
||||
}
|
||||
|
||||
.arrow-trail-text {
|
||||
font-family: 'Space Mono', monospace;
|
||||
font-size: 11px;
|
||||
letter-spacing: 2px;
|
||||
color: var(--color-accent);
|
||||
text-shadow: 0 0 6px var(--color-accent), 0 0 14px var(--color-accent);
|
||||
animation: arrow-trail-glow 1s linear infinite;
|
||||
opacity: 0.85;
|
||||
}
|
||||
|
||||
@keyframes dot-grid-drift-x {
|
||||
from {
|
||||
background-position: 0px 0px;
|
||||
@@ -109,8 +124,104 @@
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes project-shake {
|
||||
|
||||
0%,
|
||||
100% {
|
||||
transform: rotate(-3deg) translateY(0);
|
||||
}
|
||||
|
||||
20% {
|
||||
transform: rotate(1deg) translateY(-1px);
|
||||
}
|
||||
|
||||
40% {
|
||||
transform: rotate(-1deg) translateY(1px);
|
||||
}
|
||||
|
||||
60% {
|
||||
transform: rotate(1.5deg) translateY(-2px);
|
||||
}
|
||||
|
||||
80% {
|
||||
transform: rotate(-1.5deg) translateY(1px);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes arrow-trail-glow {
|
||||
0% {
|
||||
opacity: 0.6;
|
||||
text-shadow: 0 0 4px var(--color-accent), 0 0 10px var(--color-accent);
|
||||
}
|
||||
|
||||
50% {
|
||||
opacity: 1;
|
||||
text-shadow: 0 0 8px var(--color-accent), 0 0 18px var(--color-accent);
|
||||
}
|
||||
|
||||
100% {
|
||||
opacity: 0.6;
|
||||
text-shadow: 0 0 4px var(--color-accent), 0 0 10px var(--color-accent);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes modal-zoom {
|
||||
0% {
|
||||
opacity: 0;
|
||||
transform: scale(0.92);
|
||||
}
|
||||
|
||||
100% {
|
||||
opacity: 1;
|
||||
transform: scale(1);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes modal-exit {
|
||||
0% {
|
||||
opacity: 1;
|
||||
transform: translateY(0) scale(1);
|
||||
}
|
||||
|
||||
100% {
|
||||
opacity: 0;
|
||||
transform: translateY(18px) scale(0.96);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes pill-glow {
|
||||
0% {
|
||||
filter: drop-shadow(0 0 4px var(--color-accent)) drop-shadow(0 0 8px var(--color-accent));
|
||||
}
|
||||
|
||||
50% {
|
||||
filter: drop-shadow(0 0 7px var(--color-accent)) drop-shadow(0 0 14px var(--color-accent));
|
||||
}
|
||||
|
||||
100% {
|
||||
filter: drop-shadow(0 0 4px var(--color-accent)) drop-shadow(0 0 8px var(--color-accent));
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.dot-grid::before {
|
||||
animation: none;
|
||||
}
|
||||
|
||||
.arrow-trail-text {
|
||||
animation: none;
|
||||
}
|
||||
|
||||
.animate-\[modal-zoom_0\.25s_ease-out\] {
|
||||
animation: none;
|
||||
}
|
||||
|
||||
.animate-\[modal-exit_0\.2s_ease-in_forwards\] {
|
||||
animation: none;
|
||||
}
|
||||
|
||||
.animate-\[pill-glow_1\.6s_ease-in-out_infinite\] {
|
||||
animation: none;
|
||||
filter: drop-shadow(0 0 8px var(--color-accent));
|
||||
}
|
||||
}
|
||||
@@ -48,7 +48,7 @@ export const THEMES: Theme[] = [
|
||||
name: 'Acme',
|
||||
variant: 'light',
|
||||
colors: {
|
||||
accent: '#aeeeee',
|
||||
accent: '#5aeeee',
|
||||
bgBase: '#ffffea',
|
||||
bgSurface: '#fcfcce',
|
||||
bgPanel: '#ffffca',
|
||||
@@ -100,8 +100,8 @@ export const THEMES: Theme[] = [
|
||||
bgPanel: 'rgba(0, 0, 164, 0.88)',
|
||||
textMain: '#FFFF4E',
|
||||
textMuted: '#FFFFCC',
|
||||
textDim: 'rgba(255, 255, 78, 0.45)',
|
||||
borderMain: 'rgba(255, 255, 78, 0.22)',
|
||||
textDim: '#96CBFE',
|
||||
borderMain: '#a2ceeb',
|
||||
gridPattern: 'rgba(255, 255, 255, 0.48)',
|
||||
statusSuccess: '#A8FF60',
|
||||
catStroke: '#C6C5FE',
|
||||
@@ -256,4 +256,52 @@ export const THEMES: Theme[] = [
|
||||
},
|
||||
}
|
||||
|
||||
,
|
||||
{
|
||||
id: 'grass',
|
||||
name: 'Grass',
|
||||
variant: 'dark',
|
||||
colors: {
|
||||
accent: '#E7B000',
|
||||
bgBase: '#13773D',
|
||||
bgSurface: 'rgba(19, 119, 61, 0.72)',
|
||||
bgPanel: 'rgba(19, 119, 61, 0.88)',
|
||||
textMain: '#FFF0A5',
|
||||
textMuted: '#BBBBBB',
|
||||
textDim: '#555555',
|
||||
borderMain: '#000000',
|
||||
gridPattern: 'rgba(255, 240, 165, 0.22)',
|
||||
statusSuccess: '#00BB00',
|
||||
catStroke: '#00BBBB',
|
||||
catNose: '#BB0000',
|
||||
catEyes: '#FFF0A5',
|
||||
heartColor: '#E7B000',
|
||||
cursor: '#FFF0A5',
|
||||
},
|
||||
}
|
||||
|
||||
,
|
||||
{
|
||||
id: 'tomorrow-night-blue',
|
||||
name: 'Tomorrow Night Blue',
|
||||
variant: 'dark',
|
||||
colors: {
|
||||
accent: '#99FFFF',
|
||||
bgBase: '#002451',
|
||||
bgSurface: 'rgba(0, 36, 81, 0.72)',
|
||||
bgPanel: 'rgba(0, 36, 81, 0.88)',
|
||||
textMain: '#FFFFFF',
|
||||
textMuted: '#BBDAFF',
|
||||
textDim: '#7285B7',
|
||||
borderMain: '#00346E',
|
||||
gridPattern: 'rgba(255, 255, 255, 0.22)',
|
||||
statusSuccess: '#D1F1A9',
|
||||
catStroke: '#BBDAFF',
|
||||
catNose: '#FF9DA4',
|
||||
catEyes: '#FFFFFF',
|
||||
heartColor: '#FF9DA4',
|
||||
cursor: '#FFFFFF',
|
||||
},
|
||||
}
|
||||
|
||||
];
|
||||
|
||||
18
src/utils.tsx
Normal file
18
src/utils.tsx
Normal file
@@ -0,0 +1,18 @@
|
||||
export function deterministicIpValue(ip: string, x: number): number {
|
||||
if (!Number.isInteger(x) || x <= 0) {
|
||||
throw new Error("x must be a positive integer");
|
||||
}
|
||||
|
||||
// fnv1a hash
|
||||
let hash = 0x811c9dc5;
|
||||
|
||||
for (let i = 0; i < ip.length; i++) {
|
||||
hash ^= ip.charCodeAt(i);
|
||||
hash = Math.imul(hash, 0x01000193); // 32b multiply
|
||||
}
|
||||
|
||||
const unsignedHash = hash >>> 0;
|
||||
|
||||
// map to range
|
||||
return (unsignedHash % x) + 1;
|
||||
}
|
||||
Reference in New Issue
Block a user