From cfee953dee8032be8d6a45b330d78a2dd68c7466 Mon Sep 17 00:00:00 2001 From: Aditya Gupta Date: Mon, 23 Feb 2026 13:23:59 +0530 Subject: [PATCH] feat: Implement IP to color scheme mapping closes #2 - updated page title - attatched modal to "YOU GET" pill --- README.md | 8 +- index.html | 25 ++--- src/App.tsx | 53 ++++++++++- src/components/HeroSection.tsx | 17 +--- src/components/InteractiveSystemOverview.tsx | 34 +++---- src/components/ThemeMapperModal.tsx | 96 ++++++++++++++++++++ src/theme.css | 82 +++++++++++++++++ src/themes.ts | 6 +- 8 files changed, 272 insertions(+), 49 deletions(-) create mode 100644 src/components/ThemeMapperModal.tsx diff --git a/README.md b/README.md index 64d1a2d..dcd7e77 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,9 @@ # Mi sitio personal -English or Spanish \ No newline at end of file +## 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 +``` \ No newline at end of file diff --git a/index.html b/index.html index 121994a..dd9c7c7 100644 --- a/index.html +++ b/index.html @@ -1,13 +1,16 @@ - - - - - portfolio - - -
- - - + + + + + + Portfolio - Aditya Gupta (sortedcord) + + + +
+ + + + \ No newline at end of file diff --git a/src/App.tsx b/src/App.tsx index e9bfac6..75034d3 100644 --- a/src/App.tsx +++ b/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(null); + const [deterministicIndex, setDeterministicIndex] = useState(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() { + setShowThemeModal(false)} + /> ); } \ No newline at end of file diff --git a/src/components/HeroSection.tsx b/src/components/HeroSection.tsx index 1fc8acb..2e98646 100644 --- a/src/components/HeroSection.tsx +++ b/src/components/HeroSection.tsx @@ -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({ -
+
- {showThemePillTooltip && ( -
- HAHA Made you click -
- )} + {showThemePillTooltip}
diff --git a/src/components/InteractiveSystemOverview.tsx b/src/components/InteractiveSystemOverview.tsx index ded78ec..4670c05 100644 --- a/src/components/InteractiveSystemOverview.tsx +++ b/src/components/InteractiveSystemOverview.tsx @@ -119,20 +119,20 @@ const CONTENT_MODULES = [ nas-store - Ready + Ready truenas-iscsi - + edge-rout - Ready + Ready ingress-bpf
- - Healthy + + Healthy
@@ -143,10 +143,10 @@ const CONTENT_MODULES = [ title: 'lshw -short | grep input', tty: 'tty4', content: ( -
+
{'>'} - Hardware focus -
    + Hardware focus +
    • MCU: RP2040 (C/Rust)
    • Matrix: Ortho 40%
    • Sensor: EMR Stylus
    • @@ -160,11 +160,11 @@ const CONTENT_MODULES = [ title: 'tail -f /var/log/focus', tty: 'tty5', content: ( -
      -
      Streaming...
      +
      +
      Streaming...

      [INFO] Analyzing GQL via eBPF

      [INFO] Wiring custom router

      -

      [WAIT] PCB transit delays

      +

      [WAIT] PCB transit delays

      ) } @@ -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)} `} > -
      - +
      + * {node.title} - [{node.tty}] + [{node.tty}]
      @@ -237,8 +237,8 @@ export function InteractiveSystemOverview() {
      {nextModuleIndex < CONTENT_MODULES.length && ( -
      - +
      + SPLIT_VIEW
      diff --git a/src/components/ThemeMapperModal.tsx b/src/components/ThemeMapperModal.tsx new file mode 100644 index 0000000..52ba0a6 --- /dev/null +++ b/src/components/ThemeMapperModal.tsx @@ -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 ( +
      +
      event.stopPropagation()} + > +

      + theme_mapper.sh +

      +

      + This website uses hashing functions to map your ip address to a color pallete. +
      + This color scheme is unique to your IP. Your friends will probably have a different color scheme to look at. +

      +
      +
      +
      + Your IP Address + {viewerIp ?? 'unknown'} +
      +
      + (hashing function) + +
      +
      + Hash Output + {deterministicIndex ?? '—'} +
      +
      + (theme select) + +
      +
      + Theme Name + {themeName} +
      +
      +
      + + Share on Twitter + +
      +
      + ); +} diff --git a/src/theme.css b/src/theme.css index 24f3c5e..b9f684f 100644 --- a/src/theme.css +++ b/src/theme.css @@ -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)); + } } \ No newline at end of file diff --git a/src/themes.ts b/src/themes.ts index a6a078b..592a7b8 100644 --- a/src/themes.ts +++ b/src/themes.ts @@ -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',