Skip to content

Commit b02c34e

Browse files
committed
feat(landing): redesign hero with auto-typing demo
- new headline: 'Stop feeling lost in unfamiliar codebases' - add 'Now in beta' badge with sparkle icon - auto-typing animation cycles through demo queries - auto-triggers search to show live results - dual CTA: primary 'Index your first repo' + secondary 'View on GitHub' - improved subheadline copy - light/dark mode color fixes
1 parent 3b8d8f1 commit b02c34e

1 file changed

Lines changed: 126 additions & 46 deletions

File tree

frontend/src/components/landing/Hero.tsx

Lines changed: 126 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1-
import { useRef, useEffect, useState } from 'react'
1+
import { useRef, useEffect, useState, useCallback } from 'react'
22
import { motion, AnimatePresence } from 'framer-motion'
3-
import { Search, Loader2 } from 'lucide-react'
3+
import { Loader2, Sparkles } from 'lucide-react'
44
import { HeroSearch, type HeroSearchHandle } from './HeroSearch'
55
import { useDemoSearch, DEMO_REPOS, type DemoRepo } from '@/hooks/useDemoSearch'
66
import type { SearchResult } from '@/types'
@@ -11,21 +11,73 @@ interface Props {
1111

1212
const PYTHON_REPOS = DEMO_REPOS.filter(r => ['flask', 'fastapi'].includes(r.id))
1313

14+
const TYPING_QUERIES = [
15+
'authentication middleware',
16+
'database connection pool',
17+
'error handling patterns',
18+
'caching implementation',
19+
'request validation',
20+
]
21+
1422
export function Hero({ onResultsReady }: Props) {
1523
const searchRef = useRef<HeroSearchHandle>(null)
1624
const cardRef = useRef<HTMLDivElement>(null)
1725
const { query, repo, results, loading, searchTime, setQuery, setRepo, search } = useDemoSearch(false)
1826
const [mousePos, setMousePos] = useState({ x: 0, y: 0 })
27+
const [isTyping, setIsTyping] = useState(true)
28+
const [typingIndex, setTypingIndex] = useState(0)
29+
const [displayText, setDisplayText] = useState('')
30+
const typingTimeoutRef = useRef<ReturnType<typeof setTimeout>>()
1931

2032
useEffect(() => {
2133
if (results.length) onResultsReady?.(results, query, repo.id, searchTime)
2234
}, [results, query, repo.id, searchTime, onResultsReady])
2335

36+
// Typing animation
37+
useEffect(() => {
38+
if (!isTyping) return
39+
40+
const currentQuery = TYPING_QUERIES[typingIndex]
41+
let charIndex = 0
42+
43+
const typeChar = () => {
44+
if (charIndex <= currentQuery.length) {
45+
setDisplayText(currentQuery.slice(0, charIndex))
46+
setQuery(currentQuery.slice(0, charIndex))
47+
charIndex++
48+
typingTimeoutRef.current = setTimeout(typeChar, 60 + Math.random() * 40)
49+
} else {
50+
// Done typing, trigger search after a pause
51+
typingTimeoutRef.current = setTimeout(() => {
52+
search()
53+
// Wait then move to next query
54+
typingTimeoutRef.current = setTimeout(() => {
55+
setTypingIndex((i) => (i + 1) % TYPING_QUERIES.length)
56+
setDisplayText('')
57+
}, 4000)
58+
}, 500)
59+
}
60+
}
61+
62+
typingTimeoutRef.current = setTimeout(typeChar, 1500)
63+
return () => {
64+
if (typingTimeoutRef.current) clearTimeout(typingTimeoutRef.current)
65+
}
66+
}, [isTyping, typingIndex, setQuery, search])
67+
68+
// Stop auto-typing when user interacts
69+
const handleUserInput = useCallback((val: string) => {
70+
setIsTyping(false)
71+
if (typingTimeoutRef.current) clearTimeout(typingTimeoutRef.current)
72+
setQuery(val)
73+
}, [setQuery])
74+
2475
useEffect(() => {
2576
const onKey = (e: KeyboardEvent) => {
2677
const tag = (e.target as HTMLElement).tagName
2778
if (e.key === '/' && tag !== 'INPUT' && tag !== 'TEXTAREA') {
2879
e.preventDefault()
80+
setIsTyping(false)
2981
searchRef.current?.focus()
3082
}
3183
}
@@ -47,11 +99,11 @@ export function Hero({ onResultsReady }: Props) {
4799

48100
return (
49101
<section className="relative min-h-screen flex flex-col justify-center pt-20 pb-12 px-6 overflow-hidden">
50-
{/* Animated gradient orbs - Linear style */}
102+
{/* Animated gradient orbs */}
51103
<div className="absolute inset-0 overflow-hidden pointer-events-none">
52104
<motion.div
53105
className="absolute top-1/4 left-1/4 w-[500px] h-[500px] rounded-full"
54-
style={{ background: 'radial-gradient(circle, rgba(59,130,246,0.15) 0%, transparent 70%)' }}
106+
style={{ background: 'radial-gradient(circle, rgba(79,70,229,0.15) 0%, transparent 70%)' }}
55107
animate={{ x: [0, 30, 0], y: [0, -20, 0], scale: [1, 1.1, 1] }}
56108
transition={{ duration: 8, repeat: Infinity, ease: 'easeInOut' }}
57109
/>
@@ -70,38 +122,52 @@ export function Hero({ onResultsReady }: Props) {
70122
</div>
71123

72124
<div className="relative max-w-3xl mx-auto w-full">
125+
{/* Badge */}
126+
<motion.div
127+
className="flex justify-center mb-6"
128+
initial={{ opacity: 0, y: 20 }}
129+
animate={{ opacity: 1, y: 0 }}
130+
transition={{ duration: 0.5 }}
131+
>
132+
<div className="inline-flex items-center gap-2 px-3 py-1.5 rounded-full border border-accent/30 bg-accent/5 text-sm">
133+
<Sparkles className="w-3.5 h-3.5 text-accent" />
134+
<span className="text-accent font-medium">Now in beta</span>
135+
<span className="text-muted-foreground">• Free for open source</span>
136+
</div>
137+
</motion.div>
138+
73139
{/* Headline */}
74140
<motion.div
75141
className="text-center mb-10"
76142
initial={{ opacity: 0, y: 20 }}
77143
animate={{ opacity: 1, y: 0 }}
78-
transition={{ duration: 0.5 }}
144+
transition={{ duration: 0.5, delay: 0.1 }}
79145
>
80-
<h1 className="text-4xl md:text-5xl lg:text-6xl font-bold text-white leading-[1.1] tracking-tight">
81-
Find code by meaning,
146+
<h1 className="text-4xl md:text-5xl lg:text-6xl font-bold text-foreground leading-[1.1] tracking-tight">
147+
Stop feeling lost in
82148
<br />
83-
<span className="bg-gradient-to-r from-blue-400 via-violet-400 to-cyan-400 bg-clip-text text-transparent">
84-
not by keywords.
149+
<span className="bg-gradient-to-r from-accent via-violet-400 to-cyan-400 bg-clip-text text-transparent">
150+
unfamiliar codebases.
85151
</span>
86152
</h1>
87-
<p className="mt-5 text-lg text-zinc-400 max-w-lg mx-auto">
88-
Stop grep-ing through thousands of files.
89-
<br />
90-
Describe what you need and get the exact function.
153+
<p className="mt-6 text-lg text-muted-foreground max-w-xl mx-auto leading-relaxed">
154+
Describe what you're looking for in plain English.
155+
<br className="hidden sm:block" />
156+
Get the exact function, class, or pattern—instantly.
91157
</p>
92158
</motion.div>
93159

94160
{/* Search */}
95161
<motion.div
96162
initial={{ opacity: 0, y: 20 }}
97163
animate={{ opacity: 1, y: 0 }}
98-
transition={{ duration: 0.5, delay: 0.1 }}
164+
transition={{ duration: 0.5, delay: 0.2 }}
99165
>
100166
<HeroSearch
101167
ref={searchRef}
102168
value={query}
103-
onChange={setQuery}
104-
onSubmit={() => search()}
169+
onChange={handleUserInput}
170+
onSubmit={() => { setIsTyping(false); search() }}
105171
searching={loading}
106172
repoName={repo.name}
107173
/>
@@ -112,9 +178,9 @@ export function Hero({ onResultsReady }: Props) {
112178
className="mt-4 flex items-center justify-center gap-2"
113179
initial={{ opacity: 0 }}
114180
animate={{ opacity: 1 }}
115-
transition={{ duration: 0.4, delay: 0.2 }}
181+
transition={{ duration: 0.4, delay: 0.3 }}
116182
>
117-
<span className="text-xs text-zinc-600">Try on:</span>
183+
<span className="text-xs text-muted-foreground/60">Try on:</span>
118184
{PYTHON_REPOS.map(r => (
119185
<button
120186
key={r.id}
@@ -123,8 +189,8 @@ export function Hero({ onResultsReady }: Props) {
123189
className={`
124190
px-3 py-1.5 text-xs rounded-lg transition-all font-medium
125191
${repo.id === r.id
126-
? 'bg-white/10 text-white border border-white/10'
127-
: 'text-zinc-500 hover:text-zinc-300 hover:bg-white/5'
192+
? 'bg-accent/10 text-accent border border-accent/20'
193+
: 'text-muted-foreground hover:text-foreground hover:bg-white/5 dark:hover:bg-white/5'
128194
}
129195
`}
130196
>
@@ -133,7 +199,7 @@ export function Hero({ onResultsReady }: Props) {
133199
))}
134200
</motion.div>
135201

136-
{/* Result card - only shows when loading or has results */}
202+
{/* Result card */}
137203
<AnimatePresence>
138204
{(loading || topResult) && (
139205
<motion.div
@@ -152,12 +218,12 @@ export function Hero({ onResultsReady }: Props) {
152218
<div
153219
className="absolute -inset-px rounded-xl opacity-0 group-hover:opacity-100 transition-opacity duration-300 pointer-events-none"
154220
style={{
155-
background: `radial-gradient(400px circle at ${mousePos.x}px ${mousePos.y}px, rgba(59,130,246,0.15), transparent 40%)`
221+
background: `radial-gradient(400px circle at ${mousePos.x}px ${mousePos.y}px, rgba(79,70,229,0.15), transparent 40%)`
156222
}}
157223
/>
158224

159225
{/* Card */}
160-
<div className="relative rounded-xl border border-white/[0.08] bg-zinc-900/50 backdrop-blur-sm overflow-hidden">
226+
<div className="relative rounded-xl border border-white/[0.08] dark:border-white/[0.08] light:border-black/[0.08] bg-card/50 backdrop-blur-sm overflow-hidden">
161227
<AnimatePresence mode="wait">
162228
{loading ? (
163229
<motion.div
@@ -168,13 +234,13 @@ export function Hero({ onResultsReady }: Props) {
168234
className="p-6"
169235
>
170236
<div className="flex items-center gap-3 mb-4">
171-
<Loader2 className="w-4 h-4 animate-spin text-blue-400" />
172-
<span className="text-sm text-zinc-500">Searching {repo.name}...</span>
237+
<Loader2 className="w-4 h-4 animate-spin text-accent" />
238+
<span className="text-sm text-muted-foreground">Searching {repo.name}...</span>
173239
</div>
174240
<div className="space-y-3 animate-pulse">
175-
<div className="h-5 w-48 bg-white/5 rounded" />
176-
<div className="h-3 w-32 bg-white/5 rounded" />
177-
<div className="h-24 bg-white/[0.02] rounded-lg mt-3" />
241+
<div className="h-5 w-48 bg-white/5 dark:bg-white/5 light:bg-black/5 rounded" />
242+
<div className="h-3 w-32 bg-white/5 dark:bg-white/5 light:bg-black/5 rounded" />
243+
<div className="h-24 bg-white/[0.02] dark:bg-white/[0.02] light:bg-black/[0.02] rounded-lg mt-3" />
178244
</div>
179245
</motion.div>
180246
) : topResult ? (
@@ -185,36 +251,36 @@ export function Hero({ onResultsReady }: Props) {
185251
exit={{ opacity: 0 }}
186252
>
187253
{/* Header */}
188-
<div className="px-5 py-3 border-b border-white/[0.06] flex items-center justify-between bg-white/[0.02]">
254+
<div className="px-5 py-3 border-b border-white/[0.06] dark:border-white/[0.06] light:border-black/[0.06] flex items-center justify-between bg-white/[0.02] dark:bg-white/[0.02] light:bg-black/[0.02]">
189255
<div className="flex items-center gap-3">
190256
<div className="flex items-center gap-2">
191257
<div className="w-2 h-2 rounded-full bg-emerald-400 animate-pulse" />
192-
<span className="text-xs text-zinc-500">Found in {searchTime}ms</span>
258+
<span className="text-xs text-muted-foreground">Found in {searchTime}ms</span>
193259
</div>
194260
<span className="text-xs px-2 py-0.5 rounded-full bg-emerald-500/10 text-emerald-400 font-medium">
195261
{Math.round(topResult.score * 100)}% match
196262
</span>
197263
</div>
198-
<span className="text-xs text-zinc-600">{repo.name}</span>
264+
<span className="text-xs text-muted-foreground/60">{repo.name}</span>
199265
</div>
200266

201267
{/* Content */}
202268
<div className="p-5">
203269
<div className="flex items-start gap-3 mb-4">
204270
<div className="flex-1">
205271
<div className="flex items-center gap-2">
206-
<span className="font-mono text-sm font-semibold text-white">{topResult.name}</span>
272+
<span className="font-mono text-sm font-semibold text-foreground">{topResult.name}</span>
207273
<span className="text-[10px] px-1.5 py-0.5 rounded bg-violet-500/10 text-violet-400 uppercase font-medium">
208274
{topResult.type}
209275
</span>
210276
</div>
211-
<div className="text-xs text-zinc-600 font-mono mt-1">{topResult.file_path}</div>
277+
<div className="text-xs text-muted-foreground/60 font-mono mt-1">{topResult.file_path}</div>
212278
</div>
213279
</div>
214280

215281
{/* Code preview */}
216282
<div className="relative rounded-lg overflow-hidden">
217-
<div className="absolute inset-0 bg-gradient-to-br from-blue-500/5 to-violet-500/5" />
283+
<div className="absolute inset-0 bg-gradient-to-br from-accent/5 to-violet-500/5" />
218284
<pre className="relative text-xs text-zinc-300 bg-black/40 p-4 overflow-x-auto font-mono leading-relaxed">
219285
<code>{topResult.content?.slice(0, 250)}...</code>
220286
</pre>
@@ -223,8 +289,8 @@ export function Hero({ onResultsReady }: Props) {
223289

224290
{/* Footer */}
225291
{results.length > 1 && (
226-
<div className="px-5 py-3 border-t border-white/[0.06] bg-white/[0.01]">
227-
<span className="text-xs text-zinc-600">+{results.length - 1} more results</span>
292+
<div className="px-5 py-3 border-t border-white/[0.06] dark:border-white/[0.06] light:border-black/[0.06] bg-white/[0.01] dark:bg-white/[0.01] light:bg-black/[0.01]">
293+
<span className="text-xs text-muted-foreground/60">+{results.length - 1} more results</span>
228294
</div>
229295
)}
230296
</motion.div>
@@ -238,19 +304,33 @@ export function Hero({ onResultsReady }: Props) {
238304

239305
{/* CTA */}
240306
<motion.div
241-
className="mt-10 text-center"
307+
className="mt-10 flex flex-col items-center gap-4"
242308
initial={{ opacity: 0 }}
243309
animate={{ opacity: 1 }}
244-
transition={{ delay: 0.4 }}
310+
transition={{ delay: 0.5 }}
245311
>
246-
<a
247-
href="/signup"
248-
className="inline-flex items-center gap-2 px-6 py-3 text-sm font-medium text-zinc-300 rounded-lg border border-zinc-700 hover:border-zinc-500 hover:text-white hover:bg-white/5 transition-all"
249-
>
250-
Index your first repo free →
251-
</a>
252-
<p className="text-xs text-zinc-500 mt-4">
253-
Works with any Python repository • Now in beta
312+
<div className="flex flex-col sm:flex-row items-center gap-3">
313+
<a
314+
href="/signup"
315+
className="inline-flex items-center gap-2 px-6 py-3 text-sm font-medium text-white rounded-lg bg-accent hover:bg-accent/90 transition-all shadow-lg shadow-accent/20"
316+
>
317+
Index your first repo free
318+
<span className="text-white/60"></span>
319+
</a>
320+
<a
321+
href="https://github.com/OpenCodeIntel/opencodeintel"
322+
target="_blank"
323+
rel="noopener noreferrer"
324+
className="inline-flex items-center gap-2 px-6 py-3 text-sm font-medium text-muted-foreground rounded-lg border border-white/10 dark:border-white/10 light:border-black/10 hover:border-white/20 dark:hover:border-white/20 hover:text-foreground hover:bg-white/5 dark:hover:bg-white/5 light:hover:bg-black/5 transition-all"
325+
>
326+
<svg viewBox="0 0 24 24" className="w-4 h-4 fill-current" aria-hidden="true">
327+
<path d="M12 0C5.37 0 0 5.37 0 12c0 5.31 3.435 9.795 8.205 11.385.6.105.825-.255.825-.57 0-.285-.015-1.23-.015-2.235-3.015.555-3.795-.735-4.035-1.41-.135-.345-.72-1.41-1.23-1.695-.42-.225-1.02-.78-.015-.795.945-.015 1.62.87 1.845 1.23 1.08 1.815 2.805 1.305 3.495.99.105-.78.42-1.305.765-1.605-2.67-.3-5.46-1.335-5.46-5.925 0-1.305.465-2.385 1.23-3.225-.12-.3-.54-1.53.12-3.18 0 0 1.005-.315 3.3 1.23.96-.27 1.98-.405 3-.405s2.04.135 3 .405c2.295-1.56 3.3-1.23 3.3-1.23.66 1.65.24 2.88.12 3.18.765.84 1.23 1.905 1.23 3.225 0 4.605-2.805 5.625-5.475 5.925.435.375.81 1.095.81 2.22 0 1.605-.015 2.895-.015 3.3 0 .315.225.69.825.57A12.02 12.02 0 0024 12c0-6.63-5.37-12-12-12z" />
328+
</svg>
329+
View on GitHub
330+
</a>
331+
</div>
332+
<p className="text-xs text-muted-foreground/60">
333+
Works with any Python repository • Self-host or cloud
254334
</p>
255335
</motion.div>
256336
</div>

0 commit comments

Comments
 (0)