diff --git a/apps/web/src/components/portfolio/bento-grid.tsx b/apps/web/src/components/portfolio/bento-grid.tsx index f7eb86f..13b3d32 100644 --- a/apps/web/src/components/portfolio/bento-grid.tsx +++ b/apps/web/src/components/portfolio/bento-grid.tsx @@ -3,65 +3,80 @@ import Image from "next/image"; export const PortfolioGrid = () => { return ( -
+
{/* Hero Project: r/CryptoCurrency */} - -
-

r/CryptoCurrency

-

Governance dashboard for 10M+ members. Real-time DAO metrics.

+ +
+

r/CryptoCurrency

+

Governance dashboard for 10M+ members. Real-time DAO metrics.

+
+
+ r/CryptoCurrency
- r/CryptoCurrency Governance Dashboard
{/* Secondary: Occupy Wallets */} - -
-

Occupy Wallets

-

High-fidelity gallery for fiscal graffiti.

+ +
+

Occupy Wallets

+

High-fidelity gallery for fiscal graffiti.

+
+
+ Occupy Wallets
- Occupy Wallets Gallery
{/* Tertiary: Monerogue & Wowne.ro (Grouped) */} - -
+ +

Privacy Labs

-
    -
  • Monerogue (Game)
  • -
  • Wowne.ro (WASM)
  • +
      +
    • Monerogue (Game)
    • +
    • Wowne.ro (WASM)
- Privacy Labs Projects +
+ Privacy Labs +
{/* Quaternary: MoonPlace */} - -
+ +

MoonPlace

-

Collaborative pixel canvas engine.

+

Collaborative pixel canvas engine.

- MoonPlace Pixel Canvas +
+ MoonPlace +
); +}; +
+ ); }; \ No newline at end of file diff --git a/package.json b/package.json index 6c95095..98ae158 100644 --- a/package.json +++ b/package.json @@ -9,13 +9,16 @@ "format": "prettier --write \"**/*.{ts,tsx,md}\"" }, "devDependencies": { - "turbo": "latest", - "prettier": "^3.2.0", "eslint": "^9.0.0", + "prettier": "^3.2.0", + "turbo": "latest", "typescript": "^5.4.0" }, "packageManager": "pnpm@9.0.0", "engines": { "node": ">=20.0.0" + }, + "dependencies": { + "@types/react": "^18.0.0" } } \ No newline at end of file diff --git a/packages/ui/package.json b/packages/ui/package.json index cbdac8f..aaa4567 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -13,6 +13,7 @@ }, "dependencies": { "@radix-ui/react-slot": "^1.2.4", + "@types/react": "^18.0.0", "class-variance-authority": "^0.7.1", "clsx": "^2.1.0", "framer-motion": "^11.0.0", diff --git a/packages/ui/src/procedural/nebula-field.tsx b/packages/ui/src/procedural/nebula-field.tsx index de93061..6b6c201 100644 --- a/packages/ui/src/procedural/nebula-field.tsx +++ b/packages/ui/src/procedural/nebula-field.tsx @@ -1,14 +1,14 @@ 'use client'; -import { motion, useAnimation } from "framer-motion"; -import { useEffect, useState, useCallback } from "react"; +import { motion, useAnimation, useScroll, useTransform } from "framer-motion"; +import { useEffect, useState, useCallback, useMemo } from "react"; import { cn } from "../lib/utils"; import { Monitor } from "lucide-react"; interface NebulaProps { className?: string; density?: number; - themeMode?: 'light' | 'dark'; // customized prop + themeMode?: 'light' | 'dark'; } interface StreamLineProps { @@ -24,27 +24,35 @@ const StreamLine = ({ d, themeMode, onHit, width }: StreamLineProps) => { useEffect(() => { let isMounted = true; const animate = async () => { - // Random initial delay + // Staggered start delay (0-4s) await new Promise(resolve => setTimeout(resolve, Math.random() * 4000)); while (isMounted) { - const duration = 2 + Math.random() * 4; + const duration = 10 + Math.random() * 8; // Slow streamers - controls.set({ pathLength: 0, opacity: 0 }); + // Ensure starting state is definitely 0 + controls.set({ pathLength: 0, pathOffset: 0, opacity: 0 }); + + // Trigger onHit when the tip reaches the end (at 75% of duration) + const hitTimeout = setTimeout(() => { + if (isMounted) onHit(); + }, duration * 750); await controls.start({ - pathLength: [0, 1], - opacity: [0, 1, 1, 0], + pathLength: [0, 0.25, 0.25, 0], + pathOffset: [0, 0, 0.75, 1], + opacity: [0, 0.5, 0.5, 0], transition: { duration, - ease: "easeInOut", - times: [0, 0.1, 0.9, 1] + ease: "linear", + times: [0, 0.25, 0.75, 1] } }); - if (isMounted) onHit(); + clearTimeout(hitTimeout); - await new Promise(resolve => setTimeout(resolve, Math.random() * 2000)); + // Continuous flow with small pause + await new Promise(resolve => setTimeout(resolve, 1000 + Math.random() * 2000)); } }; animate(); @@ -57,57 +65,74 @@ const StreamLine = ({ d, themeMode, onHit, width }: StreamLineProps) => { stroke="currentColor" strokeWidth={width} fill="none" + initial={{ pathLength: 0, pathOffset: 0, opacity: 0 }} animate={controls} className={cn( - "transition-colors duration-500", - themeMode === 'light' ? "text-slate-400" : "text-emerald-500" + "transition-colors duration-1000", + themeMode === 'light' ? "text-slate-300" : "text-emerald-500/50" )} /> ); }; -export const NebulaField = ({ className, density = 20, themeMode = 'dark' }: NebulaProps) => { - const [paths, setPaths] = useState([]); +export const NebulaField = ({ className, density = 8, themeMode = 'dark' }: NebulaProps) => { const [mounted, setMounted] = useState(false); const monitorControls = useAnimation(); + const [monitorColor, setMonitorColor] = useState(themeMode === 'light' ? "text-slate-400" : "text-emerald-500"); - // Configuration for convergence point (percent of container) - const targetX = 25; // moved right - const targetY = 20; // moved up - + // Sync color with theme if no impacts have happened yet useEffect(() => { - setMounted(true); - const newPaths = Array.from({ length: density }).map(() => { - // Determine random edge start point - const edge = Math.floor(Math.random() * 4); // 0: top, 1: right, 2: bottom, 3: left + setMonitorColor(themeMode === 'light' ? "text-slate-300" : "text-emerald-500/50"); + }, [themeMode]); + + // Parallax Scroll for Computer Icon + const { scrollY } = useScroll(); + const yRange = useTransform(scrollY, [0, 1000], [0, 200]); + + // Configuration for convergence point + const targetX = 25; + const targetY = 20; + + const paths = useMemo(() => { + return Array.from({ length: density }).map(() => { + const edge = Math.floor(Math.random() * 4); let startX = 50, startY = 50; - // Push start point well outside viewbox to ensure full coverage switch(edge) { - case 0: startX = Math.random() * 100; startY = -20; break; // Top - case 1: startX = 120; startY = Math.random() * 100; break; // Right - case 2: startX = Math.random() * 100; startY = 120; break; // Bottom - case 3: startX = -20; startY = Math.random() * 100; break; // Left + case 0: startX = Math.random() * 100; startY = -20; break; + case 1: startX = 120; startY = Math.random() * 100; break; + case 2: startX = Math.random() * 100; startY = 120; break; + case 3: startX = -20; startY = Math.random() * 100; break; } - // End at target point with small variance - const endX = targetX + (Math.random() - 0.5) * 5; - const endY = targetY + (Math.random() - 0.5) * 5; - - // Control point for curve + const endX = targetX + (Math.random() - 0.5) * 4; + const endY = targetY + (Math.random() - 0.5) * 4; const cpX = (startX + endX) / 2 + (Math.random() - 0.5) * 50; const cpY = (startY + endY) / 2 + (Math.random() - 0.5) * 50; return `M ${startX} ${startY} Q ${cpX} ${cpY} ${endX} ${endY}`; }); - setPaths(newPaths); - }, [density]); // Removed targetX/Y dep as constant + }, [density, themeMode]); // Regenerate on theme switch + + useEffect(() => { + setMounted(true); + }, []); const handleHit = useCallback(() => { + const colors = [ + "text-emerald-500", "text-blue-500", "text-purple-500", + "text-indigo-500", "text-cyan-500", "text-rose-500", "text-amber-500" + ]; + setMonitorColor(colors[Math.floor(Math.random() * colors.length)]); + monitorControls.start({ - scale: [1, 1.15, 1], - filter: ["brightness(1)", "brightness(1.5)", "brightness(1)"], - transition: { duration: 0.3 } + scale: [1, 1.2, 1], + filter: [ + "brightness(1) drop-shadow(0 0 0px rgba(0,0,0,0))", + "brightness(1.5) drop-shadow(0 0 20px currentColor)", + "brightness(1) drop-shadow(0 0 0px rgba(0,0,0,0))" + ], + transition: { duration: 4, ease: "easeInOut" } }); }, [monitorControls]); @@ -116,17 +141,20 @@ export const NebulaField = ({ className, density = 20, themeMode = 'dark' }: Neb return (
- {/* "Computer" Icon positioned via inline styles to match physics target exactly */} @@ -134,18 +162,18 @@ export const NebulaField = ({ className, density = 20, themeMode = 'dark' }: Neb - {paths.map((d, i) => ( + {paths.map((d: string, i: number) => ( ))}
); -}; \ No newline at end of file +}; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d456d64..1cfe333 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -7,6 +7,10 @@ settings: importers: .: + dependencies: + '@types/react': + specifier: ^18.0.0 + version: 18.3.27 devDependencies: eslint: specifier: ^9.0.0 @@ -105,6 +109,9 @@ importers: '@radix-ui/react-slot': specifier: ^1.2.4 version: 1.2.4(@types/react@18.3.27)(react@18.3.1) + '@types/react': + specifier: ^18.0.0 + version: 18.3.27 class-variance-authority: specifier: ^0.7.1 version: 0.7.1