feat: Implement IP to color scheme mapping
closes #2 - updated page title - attatched modal to "YOU GET" pill
This commit is contained in:
@@ -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>
|
||||
53
src/App.tsx
53
src/App.tsx
@@ -10,16 +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(() => {
|
||||
@@ -58,7 +94,6 @@ 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);
|
||||
@@ -99,6 +134,7 @@ export default function App() {
|
||||
onThemePillClick={() => {
|
||||
setHasClickedThemePill(true);
|
||||
setShowThemePillTooltip(true);
|
||||
setShowThemeModal(true);
|
||||
}}
|
||||
cpuModeEnabled={cpuModeEnabled}
|
||||
/>
|
||||
@@ -117,6 +153,13 @@ export default function App() {
|
||||
</div>
|
||||
<FooterSection />
|
||||
</div>
|
||||
<ThemeMapperModal
|
||||
isOpen={showThemeModal}
|
||||
viewerIp={viewerIp}
|
||||
deterministicIndex={deterministicIndex}
|
||||
themeName={activeTheme.name}
|
||||
onClose={() => setShowThemeModal(false)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Activity, LaptopMinimalCheck } from 'lucide-react';
|
||||
import { LaptopMinimalCheck, TriangleAlert } from 'lucide-react';
|
||||
import { RevolvingPrism } from './prism/RevolvingPrism';
|
||||
import { HeroTitle } from './HeroTitle';
|
||||
import './prism/styles.css';
|
||||
@@ -53,25 +53,18 @@ export function HeroSection({
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="relative inline-flex mb-4">
|
||||
<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-(--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'}`} />
|
||||
<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-(--bg-panel) text-(--text-main) sketch-border-subtle shadow-lg"
|
||||
>
|
||||
HAHA Made you click
|
||||
</div>
|
||||
)}
|
||||
{showThemePillTooltip}
|
||||
</div>
|
||||
<div className="md:hidden">
|
||||
<HeroTitle />
|
||||
|
||||
@@ -119,20 +119,20 @@ const CONTENT_MODULES = [
|
||||
</tr>
|
||||
<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>
|
||||
@@ -143,10 +143,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>
|
||||
@@ -160,11 +160,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>
|
||||
)
|
||||
}
|
||||
@@ -218,18 +218,18 @@ export function InteractiveSystemOverview() {
|
||||
key={node.id}
|
||||
onClick={() => handleSplit(i)}
|
||||
className={`
|
||||
sketch-border bg-[var(--bg-panel)] p-4 relative group overflow-visible 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-visible custom-scrollbar pr-1">
|
||||
@@ -237,8 +237,8 @@ export function InteractiveSystemOverview() {
|
||||
</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>
|
||||
|
||||
96
src/components/ThemeMapperModal.tsx
Normal file
96
src/components/ThemeMapperModal.tsx
Normal file
@@ -0,0 +1,96 @@
|
||||
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.
|
||||
</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)">(hashing 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 @sortedcord 's website. Let's DDoS him!\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>
|
||||
);
|
||||
}
|
||||
@@ -104,6 +104,16 @@
|
||||
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;
|
||||
@@ -138,8 +148,80 @@
|
||||
}
|
||||
}
|
||||
|
||||
@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',
|
||||
|
||||
Reference in New Issue
Block a user