trianglewebsite / App.tsx
Antaram's picture
Upload 26 files
35a92dd verified
import React, { Suspense, useState, useEffect, useRef } from 'react';
import * as THREE from 'three';
import { Canvas } from '@react-three/fiber';
import { GenerativeSphere } from './components/GenerativeSphere';
import { AmbientBackground } from './components/AmbientBackground';
import { WorkSection } from './components/sections/WorkSection';
import { ResearchSection } from './components/sections/ResearchSection';
import { ServicesSection } from './components/sections/ServicesSection';
import { ContactSection } from './components/sections/ContactSection';
import { HeroOverlay } from './components/sections/HeroOverlay';
// Import Mobile Sections
import { MobileWorkSection } from './components/sections/mobile/MobileWorkSection';
import { MobileResearchSection } from './components/sections/mobile/MobileResearchSection';
import { MobileServicesSection } from './components/sections/mobile/MobileServicesSection';
import { MobileContactSection } from './components/sections/mobile/MobileContactSection';
// --- Helper Hooks ---
function useIsMobile() {
const [isMobile, setIsMobile] = useState(false);
useEffect(() => {
const check = () => setIsMobile(window.innerWidth < 768);
check();
window.addEventListener('resize', check);
return () => window.removeEventListener('resize', check);
}, []);
return isMobile;
}
// --- UI Components ---
// OPTIMIZED LOADER
const LoaderOverlay = ({ visible, progress }: { visible: boolean; progress: number }) => {
return (
<div
className={`fixed inset-0 z-[100] flex flex-col items-center
justify-end pb-32 /* MOBILE: Align Bottom with padding */
md:justify-center md:pb-0 /* DESKTOP: Align Center */
pointer-events-none transition-opacity duration-1000 ease-in-out
${visible ? 'opacity-100' : 'opacity-0'}
`}
>
{/* Title: BOLD WHITE - NO BLINKING */}
<h1 className="text-white font-bold text-2xl md:text-4xl tracking-[0.3em] uppercase antialiased select-none">
Team Triangle
</h1>
{/* Timer: Bottom Left */}
<div className="absolute bottom-8 left-8 md:bottom-12 md:left-12 text-left">
<div className="flex flex-col items-start gap-1">
<span className="text-[10px] font-mono text-white/40 uppercase tracking-widest">
2025 Team Triangle System
</span>
<span className="text-4xl md:text-5xl font-mono font-light text-white/80 tabular-nums">
{progress}%
</span>
</div>
{/* Loading Bar Line */}
<div className="w-32 h-[1px] bg-white/10 mt-2 relative overflow-hidden">
<div
className="absolute top-0 left-0 h-full bg-red-500 transition-all duration-100 ease-out"
style={{ width: `${progress}%` }}
/>
</div>
</div>
</div>
);
};
const Navbar = ({
visible,
isDimmed,
shapeMode,
toggleShape,
isTtsPlaying,
onSectionClick,
activeSection
}: {
visible: boolean;
isDimmed: boolean;
shapeMode: 'sphere' | 'triangle' | 'explode';
toggleShape: () => void;
isTtsPlaying: boolean;
onSectionClick: (section: string) => void;
activeSection: string | null;
}) => {
const isTriangle = shapeMode === 'triangle';
const [mobileMenuOpen, setMobileMenuOpen] = useState(false);
useEffect(() => {
if (activeSection) setMobileMenuOpen(false);
}, [activeSection]);
const hideClass = (isDimmed && !activeSection) ? 'opacity-0 -translate-y-4 pointer-events-none' : 'opacity-100 translate-y-0';
const bgClass = isDimmed ? 'opacity-0' : 'opacity-100';
const menuItems = ['work', 'research', 'services', 'contact'];
return (
<>
<nav
className={`fixed top-4 left-0 w-full z-40 transition-all duration-1000 ease-out
${visible ? 'translate-y-0 opacity-100' : '-translate-y-4 opacity-0 pointer-events-none'}
`}
>
<div className={`absolute inset-0 bg-black/5 backdrop-blur-sm border-b border-white/5 transition-all duration-500 ease-out ${bgClass}`} />
<div className="relative w-full max-w-[1400px] mx-auto px-6 py-5 md:px-12 flex justify-between items-center">
<div
onClick={() => { if(activeSection) onSectionClick(''); setMobileMenuOpen(false); }}
className={`flex items-center gap-3 cursor-pointer group transition-all duration-500 ease-out ${hideClass} z-50`}
>
<div className="relative w-6 h-6 flex items-center justify-center">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" className="text-white group-hover:text-red-500 transition-colors duration-300">
<path d="M12 4L4 20H20L12 4Z" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round"/>
<circle cx="12" cy="13" r="1" fill="currentColor" className="opacity-0 group-hover:opacity-100 transition-opacity duration-300" />
</svg>
</div>
<span className="hidden sm:block text-white/90 font-light tracking-[0.15em] text-xs uppercase select-none group-hover:text-white transition-colors">
Team Triangle
</span>
</div>
<div className="absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2 hidden md:block">
<div className={`flex items-center justify-center transition-all duration-500 ease-out ${activeSection ? 'opacity-0 pointer-events-none translate-y-[-10px]' : hideClass}`}>
<div className="flex gap-1 bg-white/5 backdrop-blur-md px-1.5 py-1.5 rounded-full border border-white/5 shadow-2xl shadow-black/20">
{menuItems.map((item) => (
<button
key={item}
onClick={() => onSectionClick(item)}
className={`px-6 py-2 rounded-full text-[10px] font-mono tracking-widest uppercase transition-all duration-300 select-none
${activeSection === item ? 'bg-white text-black' : 'text-white/50 hover:text-white hover:bg-white/10'}
`}
>
{item}
</button>
))}
</div>
</div>
</div>
<div className={`flex items-center gap-6 transition-all duration-500 z-50 ${activeSection ? 'opacity-0 pointer-events-none' : ''}`}>
<button disabled={isTtsPlaying} onClick={toggleShape} className={`hidden md:block transition-colors duration-300 group ${isTtsPlaying ? 'text-white/20 cursor-not-allowed' : 'text-white/50 hover:text-red-400'} ${isTriangle ? 'text-red-400' : ''}`}>
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" className="group-hover:animate-pulse">
<polygon points="11 5 6 9 2 9 2 15 6 15 11 19 11 5"></polygon>
<path d="M15.54 8.46a5 5 0 0 1 0 7.07" className={`transition-opacity duration-300 ${isTriangle ? 'opacity-100' : 'opacity-50 group-hover:opacity-100'}`} />
<path d="M19.07 4.93a10 10 0 0 1 0 14.14" className={`transition-all duration-500 ${isTriangle ? 'opacity-100 translate-x-0' : 'opacity-0 -translate-x-1'}`} />
</svg>
</button>
<button
onClick={() => setMobileMenuOpen(!mobileMenuOpen)}
className={`md:hidden flex items-center justify-center w-10 h-10 border border-white/10 rounded-full bg-black/20 backdrop-blur-md transition-all duration-300 ${mobileMenuOpen ? 'border-red-500/50 text-red-500' : 'text-white/70'}`}
>
{mobileMenuOpen ? (
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5">
<line x1="18" y1="6" x2="6" y2="18"></line>
<line x1="6" y1="6" x2="18" y2="18"></line>
</svg>
) : (
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5">
<rect x="3" y="3" width="7" height="7"></rect>
<rect x="14" y="3" width="7" height="7"></rect>
<rect x="14" y="14" width="7" height="7"></rect>
<rect x="3" y="14" width="7" height="7"></rect>
</svg>
)}
</button>
</div>
</div>
</nav>
<div className={`fixed inset-0 z-30 bg-black/95 backdrop-blur-xl flex flex-col items-center justify-center transition-all duration-500 md:hidden ${mobileMenuOpen ? 'opacity-100 visible' : 'opacity-0 invisible pointer-events-none'}`}>
<div className="flex flex-col gap-6 w-full px-12">
{menuItems.map((item, i) => (
<button
key={item}
onClick={() => { onSectionClick(item); setMobileMenuOpen(false); }}
className={`text-3xl font-light text-white uppercase tracking-widest text-left py-4 border-b border-white/10 transition-all duration-500
${mobileMenuOpen ? 'translate-x-0 opacity-100' : '-translate-x-8 opacity-0'}
`}
style={{ transitionDelay: `${i * 100}ms` }}
>
<span className="text-xs text-red-500 font-mono mr-4">0{i + 1}</span>
{item}
</button>
))}
<div className={`mt-8 pt-8 flex justify-between items-center transition-all duration-700 delay-300 ${mobileMenuOpen ? 'opacity-100' : 'opacity-0'}`}>
<span className="text-[10px] font-mono text-white/30 uppercase">System Status: Online</span>
<button onClick={toggleShape} className="text-white/50 text-xs font-mono uppercase border border-white/10 px-3 py-1 rounded-full">
Toggle Geo
</button>
</div>
</div>
</div>
</>
);
};
const TypingConsole = ({ visible }: { visible: boolean }) => {
const [text, setText] = useState('');
const [isDeleting, setIsDeleting] = useState(false);
const [loopNum, setLoopNum] = useState(0);
const [typingSpeed, setTypingSpeed] = useState(150);
const phrases = ["training model...", "deploying gen-ai pipeline...", "optimizing inference...", "allocating neural buffers..."];
useEffect(() => {
let timer: any;
const handleTyping = () => {
const i = loopNum % phrases.length;
const fullText = phrases[i];
setText(isDeleting ? fullText.substring(0, text.length - 1) : fullText.substring(0, text.length + 1));
let typeSpeed = 50 + Math.random() * 60;
if (isDeleting) typeSpeed /= 2.5;
if (!isDeleting && text === fullText) { typeSpeed = 2500; setIsDeleting(true); }
else if (isDeleting && text === '') { setIsDeleting(false); setLoopNum(loopNum + 1); typeSpeed = 500; }
setTypingSpeed(typeSpeed);
};
if (visible) timer = setTimeout(handleTyping, typingSpeed);
return () => clearTimeout(timer);
}, [text, isDeleting, loopNum, phrases, typingSpeed, visible]);
return (
<div
className={`fixed z-40 p-4 rounded-sm bg-black/20 backdrop-blur-sm border border-white/5 font-mono text-xs text-white/70 shadow-lg select-none transition-all duration-1000 delay-700 ease-out hidden md:block
${visible ? 'opacity-100 translate-y-0' : 'opacity-0 translate-y-8'}
md:bottom-28 md:right-12 md:w-64
`}
>
<div className="flex flex-col gap-2">
<div className="flex justify-between items-center text-[8px] uppercase tracking-widest text-white/20 border-b border-white/5 pb-2 mb-1"><span>System Output</span><div className="flex gap-1"><span className="w-1 h-1 rounded-full bg-white/20"></span><span className="w-1 h-1 rounded-full bg-white/20"></span></div></div>
<div className="min-h-[2em] flex items-center"><span className="text-red-500/60 mr-2 text-[10px]"></span><span className="text-white/80">{text}</span><span className="animate-pulse ml-0.5 inline-block w-1.5 h-3 bg-red-500/80 align-middle"></span></div>
</div>
</div>
);
};
// --- Reusable Scroll Sections ---
const ContentTeaser = ({ title, sub, desc, btnText, onClick }: any) => {
return (
<div className="border border-white/10 bg-black/40 backdrop-blur-sm p-8 md:p-12 hover:bg-white/5 transition-all duration-500 group flex flex-col justify-between h-full">
<div>
<div className="flex items-center gap-3 mb-6">
<span className="text-xs font-mono uppercase tracking-widest text-white/40 border border-white/10 px-2 py-1 rounded-full">{sub}</span>
</div>
<h3 className="text-3xl md:text-4xl font-light text-white mb-4 group-hover:text-red-500 transition-colors">{title}</h3>
<p className="text-white/60 text-sm md:text-base leading-relaxed mb-8">{desc}</p>
</div>
<button onClick={onClick} className="self-start flex items-center gap-3 text-xs font-mono uppercase tracking-widest text-white hover:text-red-400 transition-colors pb-1 border-b border-white/20 hover:border-red-400">
{btnText} <span className="group-hover:translate-x-1 transition-transform"></span>
</button>
</div>
)
}
// --- Main App ---
const App: React.FC = () => {
const [showLoader, setShowLoader] = useState(true);
const [loadProgress, setLoadProgress] = useState(0);
const [showInterface, setShowInterface] = useState(false);
const [isNavDimmed, setIsNavDimmed] = useState(false);
const [isAudioMuted, setIsAudioMuted] = useState(true);
const audioRef = useRef<HTMLAudioElement | null>(null);
const audioInitializedRef = useRef(false);
const [isTtsPlaying, setIsTtsPlaying] = useState(false);
const ttsAudioRef = useRef<HTMLAudioElement | null>(null);
const [activeSection, setActiveSection] = useState<string | null>(null);
const [isTriangle, setIsTriangle] = useState(false);
const scrollRef = useRef(0);
const smoothScrollRef = useRef(0);
const shiftRef = useRef(0);
const lastScrollTop = useRef(0);
const isMobile = useIsMobile();
const shapeMode = activeSection ? 'explode' : ((isTriangle || isTtsPlaying) ? 'triangle' : 'sphere');
useEffect(() => {
try {
const saved = window.localStorage.getItem('tt_audio_muted');
if (saved === '0') setIsAudioMuted(false);
if (saved === '1') setIsAudioMuted(true);
} catch {
// ignore
}
}, []);
useEffect(() => {
try {
window.localStorage.setItem('tt_audio_muted', isAudioMuted ? '1' : '0');
} catch {
// ignore
}
}, [isAudioMuted]);
useEffect(() => {
return () => {
if (audioRef.current) {
audioRef.current.pause();
audioRef.current.src = '';
audioRef.current.load();
audioRef.current = null;
}
};
}, []);
const ensureAudioInitialized = () => {
if (audioInitializedRef.current) return;
audioInitializedRef.current = true;
const a = new Audio('/audio/theme.mp3');
a.loop = true;
a.volume = 0.35;
a.preload = 'auto';
audioRef.current = a;
};
const applyAudioState = async (nextMuted: boolean) => {
ensureAudioInitialized();
const a = audioRef.current;
if (!a) return;
if (nextMuted) {
a.pause();
a.muted = true;
return;
}
a.muted = false;
try {
await a.play();
} catch {
// Autoplay can be blocked; user can click again.
}
};
const toggleAudioMuted = async () => {
const next = !isAudioMuted;
setIsAudioMuted(next);
await applyAudioState(next);
};
const stopTts = () => {
setIsTtsPlaying(false);
if (ttsAudioRef.current) {
ttsAudioRef.current.pause();
ttsAudioRef.current.src = '';
ttsAudioRef.current.load();
ttsAudioRef.current = null;
}
};
const playHomeTts = async () => {
if (activeSection) return;
if (isTtsPlaying) return;
const a = new Audio('/audio/tts.wav');
a.preload = 'auto';
a.volume = 1.0;
ttsAudioRef.current = a;
a.addEventListener('ended', stopTts, { once: true });
a.addEventListener('error', stopTts, { once: true });
setIsTtsPlaying(true);
try {
await a.play();
} catch {
stopTts();
}
};
const handleSectionClick = (section: string) => {
setActiveSection(section === activeSection ? null : section);
};
// --- FIXED LOADER LOGIC ---
useEffect(() => {
let interval: any;
interval = setInterval(() => {
setLoadProgress(prev => {
// When close to 100, ensure we reach it
if (prev >= 98) {
clearInterval(interval);
return 100;
}
// Random increment for organic feel
const increment = Math.random() * 3;
const next = prev + increment;
// Cap at 100
return Math.min(next, 100);
});
}, 25);
return () => clearInterval(interval);
}, []);
// Handle Transition from Loader to Interface
useEffect(() => {
if (loadProgress >= 100) {
// Small delay at 100% before fading out
const timeout = setTimeout(() => {
setShowLoader(false);
// Delay interface appearance slightly to ensure smooth fade
setTimeout(() => setShowInterface(true), 500);
}, 500);
return () => clearTimeout(timeout);
}
}, [loadProgress]);
// Scroll Loop
useEffect(() => {
let frameId: number;
const loop = () => {
smoothScrollRef.current = THREE.MathUtils.lerp(smoothScrollRef.current, scrollRef.current, 0.08);
frameId = requestAnimationFrame(loop);
};
loop();
return () => cancelAnimationFrame(frameId);
}, []);
const handleScroll = (e: React.UIEvent<HTMLDivElement>) => {
const s = e.currentTarget.scrollTop;
scrollRef.current = s;
if (s > lastScrollTop.current && s > 50) setIsNavDimmed(true);
else if (s < lastScrollTop.current || s <= 50) setIsNavDimmed(false);
lastScrollTop.current = s;
};
return (
<div className="relative w-full h-full bg-[#050000] text-white overflow-hidden selection:bg-red-500/30">
<div className="absolute inset-0 z-0 pointer-events-none">
<Canvas dpr={[1, 2]} camera={{ position: [0, 0, 18], fov: 35, near: 0.1, far: 100 }} gl={{ antialias: true, alpha: false, powerPreference: "high-performance" }}>
<Suspense fallback={null}>
<AmbientBackground scrollRef={scrollRef} beatActive={isTtsPlaying} />
{/* Sphere is visible behind the loader */}
<GenerativeSphere
scrollRef={scrollRef}
shiftRef={shiftRef}
mode={shapeMode}
isMobile={isMobile}
/>
</Suspense>
</Canvas>
</div>
{/*
Loader Overlay: Transparent BG, shows Sphere behind.
Mobile: Bottom aligned (pb-32)
Desktop: Center aligned (pb-0)
*/}
<LoaderOverlay visible={showLoader} progress={Math.floor(loadProgress)} />
{/*
Main Interface Container:
Remains strictly hidden (opacity-0) until showInterface is true.
*/}
<div className={`transition-opacity duration-1000 ${showInterface ? 'opacity-100' : 'opacity-0'}`}>
<Navbar
visible={true}
isDimmed={isNavDimmed}
shapeMode={shapeMode}
toggleShape={playHomeTts}
isTtsPlaying={isTtsPlaying}
onSectionClick={handleSectionClick}
activeSection={activeSection}
/>
<HeroOverlay
visible={!activeSection}
smoothScrollRef={smoothScrollRef}
shiftRef={shiftRef}
shapeMode={shapeMode}
isMobile={isMobile}
/>
<TypingConsole visible={!activeSection} />
</div>
{/* Active Section Modals */}
{activeSection === 'work' && (isMobile ? <MobileWorkSection onClose={() => setActiveSection(null)} /> : <WorkSection onClose={() => setActiveSection(null)} />)}
{activeSection === 'research' && (isMobile ? <MobileResearchSection onClose={() => setActiveSection(null)} /> : <ResearchSection onClose={() => setActiveSection(null)} />)}
{activeSection === 'services' && (isMobile ? <MobileServicesSection onClose={() => setActiveSection(null)} /> : <ServicesSection onClose={() => setActiveSection(null)} />)}
{activeSection === 'contact' && (
isMobile ? (
<MobileContactSection onClose={() => setActiveSection(null)} />
) : (
<ContactSection onClose={() => setActiveSection(null)} isAudioMuted={isAudioMuted} onToggleAudioMuted={toggleAudioMuted} />
)
)}
{/* Main Scroll Container */}
<div
className={`absolute inset-0 z-10 overflow-y-auto scroll-smooth overflow-x-hidden no-scrollbar
${activeSection || !showInterface ? 'pointer-events-none' : ''}
${showInterface ? 'opacity-100' : 'opacity-0'} transition-opacity duration-1000
`}
onScroll={handleScroll}
>
{/* Spacer for Hero to be visible */}
<div className="h-[100vh] w-full pointer-events-none"></div>
{/* 1. Core Capabilities List */}
<section className="relative z-10 w-full max-w-[1400px] mx-auto px-6 md:px-12 py-32 md:py-48">
<div className="grid grid-cols-1 md:grid-cols-3 gap-8 md:gap-12 border-t border-white/20 pt-12">
<div className="group">
<span className="text-xs font-mono text-red-500 uppercase tracking-widest mb-4 block">[01]</span>
<h2 className="text-4xl md:text-5xl font-light leading-tight text-white/90 group-hover:text-white transition-colors">
Machine Learning Systems
</h2>
</div>
<div className="group">
<span className="text-xs font-mono text-blue-400 uppercase tracking-widest mb-4 block">[02]</span>
<h2 className="text-4xl md:text-5xl font-light leading-tight text-white/90 group-hover:text-white transition-colors">
Generative AI Platforms
</h2>
</div>
<div className="group">
<span className="text-xs font-mono text-emerald-500 uppercase tracking-widest mb-4 block">[03]</span>
<h2 className="text-4xl md:text-5xl font-light leading-tight text-white/90 group-hover:text-white transition-colors">
Fullstack Product Delivery
</h2>
</div>
</div>
</section>
{/* 2. Content Teasers */}
<section className="relative z-10 w-full max-w-[1400px] mx-auto px-6 md:px-12 pb-32">
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
<ContentTeaser
title="Selected Works"
sub="Portfolio"
desc="A curated index of deployed neural architectures and generative interfaces."
btnText="Access Index"
onClick={() => handleSectionClick('work')}
/>
<ContentTeaser
title="R&D Lab"
sub="Experiments"
desc="Deep tech research. Turning whitepapers into executable code."
btnText="View Protocols"
onClick={() => handleSectionClick('research')}
/>
<ContentTeaser
title="System Services"
sub="Capabilities"
desc="High-performance engineering for the next generation of the web."
btnText="Initialize"
onClick={() => handleSectionClick('services')}
/>
<ContentTeaser
title="Direct Uplink"
sub="Contact"
desc="Open a secure channel to the founders. Transmit a packet, or tap a profile for analysis."
btnText="Initiate"
onClick={() => handleSectionClick('contact')}
/>
</div>
</section>
{/* 3. Massive Footer */}
<footer className="relative z-10 w-full pt-32 pb-12 overflow-hidden flex flex-col items-center justify-center">
<div className="w-full border-t border-white/10 mb-12"></div>
<div className="w-full px-2 md:px-6 text-center">
{/* Fixed: Reduced mobile size from 15vw to 10vw so "TEAM TRIANGLE" fits without clipping */}
<h1 className="text-[10vw] md:text-[12vw] leading-none font-black text-white/10 tracking-tighter select-none whitespace-nowrap">
TEAM TRIANGLE
</h1>
</div>
<div className="mt-12 flex flex-col md:flex-row gap-8 items-center justify-between w-full max-w-[1400px] px-12 text-[10px] font-mono uppercase text-white/30 tracking-widest">
<span>© 2025 Team Triangle</span>
<button onClick={() => handleSectionClick('contact')} className="hover:text-white transition-colors border-b border-white/10 hover:border-white pb-0.5">
Establish Uplink
</button>
</div>
</footer>
</div>
</div>
);
};
export default App;