claudish/landingpage/components/HeroSection.tsx

343 lines
17 KiB
TypeScript
Raw Normal View History

import React, { useState, useRef, useEffect } from 'react';
import { TerminalWindow } from './TerminalWindow';
import { HERO_SEQUENCE } from '../constants';
import { TypingAnimation } from './TypingAnimation';
import { BlockLogo } from './BlockLogo';
// Text-based Ghost Logo from CLI
const AsciiGhost = () => {
return (
<pre
className="text-[#d97757] font-bold select-none"
style={{
fontFamily: "'JetBrains Mono', monospace",
fontSize: '18px',
lineHeight: 0.95,
}}
>
{` ▐▛███▜▌
`}
</pre>
);
};
const HeroSection: React.FC = () => {
const [rotation, setRotation] = useState({ x: 0, y: 0 });
const [visibleLines, setVisibleLines] = useState<number>(0);
// State for status bar
const [status, setStatus] = useState({
model: 'google/gemini-3-pro-preview',
cost: '$0.000',
context: '0%'
});
const containerRef = useRef<HTMLDivElement>(null);
const scrollRef = useRef<HTMLDivElement>(null);
// Mouse movement for 3D effect
const handleMouseMove = (e: React.MouseEvent<HTMLDivElement>) => {
if (!containerRef.current) return;
const rect = containerRef.current.getBoundingClientRect();
const x = e.clientX - rect.left;
const y = e.clientY - rect.top;
// Calculate percentage from center (-1 to 1)
const xPct = (x / rect.width - 0.5) * 2;
const yPct = (y / rect.height - 0.5) * 2;
// Limit rotation to 15 degrees
setRotation({
x: yPct * -8,
y: xPct * 8
});
};
const handleMouseLeave = () => {
setRotation({ x: 0, y: 0 });
};
// Sequence Controller
useEffect(() => {
const timeouts: ReturnType<typeof setTimeout>[] = [];
const runSequence = () => {
setVisibleLines(0);
let cumulativeDelay = 0;
HERO_SEQUENCE.forEach((line, index) => {
const t = setTimeout(() => {
setVisibleLines(prev => Math.max(prev, index + 1));
}, line.delay);
timeouts.push(t);
if (line.delay && line.delay > cumulativeDelay) {
cumulativeDelay = line.delay;
}
});
const restart = setTimeout(() => {
runSequence();
}, cumulativeDelay + 4000);
timeouts.push(restart);
};
runSequence();
return () => timeouts.forEach(clearTimeout);
}, []);
// Update Status Bar based on visible lines
useEffect(() => {
let newStatus = { ...status };
let hasUpdates = false;
// Scan visible lines to find the latest state
for (let i = 0; i < visibleLines && i < HERO_SEQUENCE.length; i++) {
const line = HERO_SEQUENCE[i];
if (line.data) {
if (line.data.model) { newStatus.model = line.data.model; hasUpdates = true; }
if (line.data.cost) { newStatus.cost = line.data.cost; hasUpdates = true; }
if (line.data.context) { newStatus.context = line.data.context; hasUpdates = true; }
}
}
if (hasUpdates) {
setStatus(newStatus);
}
}, [visibleLines]);
// Auto-scroll effect
useEffect(() => {
if (scrollRef.current) {
scrollRef.current.scrollTo({
top: scrollRef.current.scrollHeight,
behavior: 'smooth'
});
}
}, [visibleLines]);
return (
<section className="relative min-h-screen flex flex-col items-center justify-center pt-24 pb-12 px-4 overflow-hidden">
{/* Background Gradients */}
<div className="absolute top-0 left-0 w-full h-full overflow-hidden -z-10 pointer-events-none">
<div className="absolute top-[-10%] left-[20%] w-[600px] h-[600px] bg-claude-accent/5 rounded-full blur-[120px]" />
<div className="absolute bottom-[-10%] right-[10%] w-[500px] h-[500px] bg-claude-ish/5 rounded-full blur-[100px]" />
</div>
<div className="text-center mb-12 max-w-5xl mx-auto z-10 flex flex-col items-center">
<div className="flex gap-3 mb-8 animate-fadeIn">
<div className="inline-flex items-center gap-2 px-3 py-1 rounded-full bg-white/5 border border-white/10 text-xs font-mono text-claude-ish">
<span className="w-2 h-2 rounded-full bg-claude-ish animate-pulse"></span>
v2.4.0 Public Beta
</div>
<div className="inline-flex items-center gap-2 px-3 py-1 rounded-full bg-green-900/20 border border-green-500/20 text-xs font-mono text-green-400">
<span className="text-[10px]">🎁</span>
Top models free on OpenRouter Grok, Gemini, DeepSeek, Llama
</div>
</div>
{/* BlockLogo */}
<div className="mb-6 scale-90 md:scale-110 origin-center">
<BlockLogo />
</div>
<h1 className="text-3xl md:text-5xl font-sans font-bold tracking-tight text-white mb-2">
Claude Code. <span className="text-gray-500">Any Model.</span>
</h1>
<p className="text-lg md:text-xl text-gray-400 max-w-3xl mx-auto leading-relaxed font-sans mb-10">
The most powerful AI coding agent now speaks every language.<br/>
<span className="text-white">Gemini</span>, <span className="text-white">GPT</span>, <span className="text-white">Grok</span>, <span className="text-white">DeepSeek</span>. <span className="text-white">580+ models via OpenRouter.</span><br/>
<span className="text-claude-ish">Works with your Claude subscription. Or start completely free.</span>
</p>
<div className="mt-6 flex flex-col items-center animate-float">
<div className="bg-[#1a1a1a] border border-white/10 rounded-xl p-5 md:p-6 shadow-2xl relative group">
<div className="absolute -top-3 left-1/2 -translate-x-1/2 bg-[#d97757] text-[#0f0f0f] text-[10px] font-bold px-2 py-0.5 rounded shadow-lg">
GET STARTED
</div>
<div className="flex flex-col gap-3 font-mono text-sm md:text-base text-left">
<div className="flex items-center gap-3 text-gray-300 group-hover:text-white transition-colors">
<span className="text-claude-ish select-none font-bold">$</span>
<span>npm install -g claudish</span>
</div>
<div className="w-full h-[1px] bg-white/5"></div>
<div className="flex items-center gap-3 text-white font-bold">
<span className="text-claude-ish select-none font-bold">$</span>
<span>claudish --free</span>
</div>
</div>
</div>
</div>
</div>
{/* 3D Container */}
<div
ref={containerRef}
className="perspective-container w-full max-w-4xl relative h-[550px] mt-4"
onMouseMove={handleMouseMove}
onMouseLeave={handleMouseLeave}
>
<div
className="w-full h-full transition-transform duration-100 ease-out preserve-3d"
style={{
transform: `rotateX(${rotation.x}deg) rotateY(${rotation.y}deg)`
}}
>
<TerminalWindow
className="h-full w-full bg-[#0d1117] shadow-[0_0_50px_rgba(0,0,0,0.6)] border-[#30363d]"
title="claudish — -zsh — 140×45"
noPadding={true}
>
<div className="flex flex-col h-full font-mono text-[13px] md:text-sm">
{/* Terminal Flow - Scrollable Area */}
<div ref={scrollRef} className="flex-1 overflow-y-auto scrollbar-hide scroll-smooth p-4 md:p-6 pb-2">
{HERO_SEQUENCE.map((line, idx) => {
if (idx >= visibleLines) return null;
return (
<div key={line.id} className="leading-normal mb-2">
{/* System / Boot Output */}
{line.type === 'system' && (
<div className="text-gray-400 font-semibold px-2">
<span className="text-[#3fb950]"></span> {line.content}
</div>
)}
{/* Rich Welcome Screen */}
{line.type === 'welcome' && (
<div className="my-4 border border-[#d97757] rounded p-1 mx-2 relative">
<div className="absolute top-[-10px] left-4 bg-[#0d1117] px-2 text-[#d97757] text-xs font-bold uppercase tracking-wider">
Claudish
</div>
<div className="flex gap-2 md:gap-6 p-4">
{/* Left Side: Logo & Info */}
<div className="flex-1 border-r border-[#30363d] pr-4 md:pr-6 flex items-center justify-center">
<div className="flex items-center gap-4 md:gap-6">
<AsciiGhost />
<div className="flex flex-col text-left space-y-0.5 md:space-y-1">
<div className="font-bold text-gray-200">Claude Code {line.data.version}</div>
<div className="text-xs text-gray-400">{line.data.model} Claude Max</div>
<div className="text-xs text-gray-600">~/dev/claudish-landing</div>
</div>
</div>
</div>
{/* Right Side: Activity */}
<div className="hidden md:block flex-1 text-xs space-y-3 pl-2">
<div className="text-[#d97757] font-bold">Recent activity</div>
<div className="flex gap-2 text-gray-400">
<span className="text-gray-600">1m ago</span>
<span>Tracking Real OpenRouter Cost</span>
</div>
<div className="flex gap-2 text-gray-400">
<span className="text-gray-600">39m ago</span>
<span>Refactoring Auth Middleware</span>
</div>
<div className="w-full h-[1px] bg-[#30363d] my-2"></div>
<div className="text-[#d97757] font-bold">What's new</div>
<div className="text-gray-400">
Fixed duplicate message display when using Gemini.
</div>
</div>
</div>
</div>
)}
{/* Rich Input (Updated to be cleaner, status moved to bottom) */}
{line.type === 'rich-input' && (
<div className="mt-4 mb-2 px-2">
<div className="flex items-start text-white group">
<span className="text-[#ff5f56] mr-3 font-bold select-none text-base">{'>>'}</span>
<TypingAnimation text={line.content} speed={15} className="text-gray-100 font-medium" />
</div>
</div>
)}
{/* Thinking Block */}
{line.type === 'thinking' && (
<div className="text-gray-500 px-2 flex items-center gap-2 text-xs my-2">
<span className="animate-pulse"></span>
{line.content}
</div>
)}
{/* Tool Execution */}
{line.type === 'tool' && (
<div className="my-2 px-2">
<div className="flex items-center gap-2">
<div className="w-2 h-2 rounded-full bg-blue-500"></div>
<span className="bg-[#1f2937] text-blue-400 px-1 rounded text-xs font-bold">
{line.content.split('(')[0]}
</span>
<span className="text-gray-400 text-xs">
({line.content.split('(')[1]}
</span>
</div>
{line.data?.details && (
<div className="border-l border-gray-700 ml-3 pl-3 mt-1 text-gray-500 text-xs py-1">
{line.data.details}
</div>
)}
</div>
)}
{/* Standard Output/Success/Info */}
{line.type === 'info' && (
<div className="text-gray-500 px-2 py-1">
{line.content}
</div>
)}
{line.type === 'progress' && (
<div className="text-claude-accent animate-pulse px-2">
{line.content}
</div>
)}
{line.type === 'success' && (
<div className="text-[#3fb950] px-2">
{line.content}
</div>
)}
</div>
);
})}
{/* Interactive Cursor line if active */}
<div className="flex items-center text-white mt-1 px-2 pb-4">
<span className="text-[#ff5f56] mr-3 font-bold text-base opacity-0">{'>'}</span>
<div className="h-4 w-2.5 bg-gray-500/50 animate-cursor-blink" />
</div>
</div>
{/* Persistent Footer Status Bar */}
<div className="bg-[#161b22] border-t border-[#30363d] px-3 py-1.5 flex justify-between items-center text-[10px] md:text-[11px] font-mono leading-none shrink-0 select-none z-20">
<div className="flex items-center gap-2 md:gap-3">
<span className="font-bold text-claude-ish">claudish</span>
<span className="text-[#484f58]"></span>
<span className="text-[#e2b340]">{status.model}</span>
<span className="text-[#484f58]"></span>
<span className="text-[#3fb950]">{status.cost}</span>
<span className="text-[#484f58]"></span>
<span className="text-[#a371f7]">{status.context}</span>
</div>
<div className="flex items-center gap-2 text-gray-500">
<span className="hidden sm:inline">bypass permissions <span className="text-[#ff5f56]">on</span></span>
<span className="text-[#484f58] hidden sm:inline">|</span>
<span className="hidden sm:inline">(shift+tab to cycle)</span>
</div>
</div>
</div>
</TerminalWindow>
</div>
</div>
</section>
);
};
export default HeroSection;