claudish/landingpage/components/HeroSection.tsx

343 lines
17 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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;