Skip to content

Commit 3bb6b35

Browse files
authored
Merge pull request #178 from DevanshuNEU/feature/168-hero-section
feat: Premium hero section redesign with semantic search demo
2 parents e6f06e4 + 46de495 commit 3bb6b35

15 files changed

Lines changed: 1110 additions & 821 deletions
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
import { motion } from 'framer-motion'
2+
import { Search, ArrowLeft, Loader2 } from 'lucide-react'
3+
import { Button } from '@/components/ui/button'
4+
import { cn } from '@/lib/utils'
5+
6+
interface Props {
7+
query: string
8+
onQueryChange: (q: string) => void
9+
onSearch: () => void
10+
onBack: () => void
11+
loading: boolean
12+
remaining: number
13+
}
14+
15+
export function CompactSearchBar({ query, onQueryChange, onSearch, onBack, loading, remaining }: Props) {
16+
const canSearch = query.trim() && !loading && remaining > 0
17+
18+
const submit = (e: React.FormEvent) => {
19+
e.preventDefault()
20+
if (canSearch) onSearch()
21+
}
22+
23+
return (
24+
<motion.div
25+
className="bg-[#09090b]/95 backdrop-blur-xl border-b border-white/5 sticky top-16 z-40"
26+
animate={loading ? { boxShadow: ['0 0 0 rgba(99,102,241,0)', '0 0 30px rgba(99,102,241,0.3)', '0 0 0 rgba(99,102,241,0)'] } : {}}
27+
transition={{ duration: 1.5, repeat: Infinity }}
28+
>
29+
<div className="max-w-4xl mx-auto px-6 py-4">
30+
<div className="flex items-center gap-4">
31+
<button
32+
onClick={onBack}
33+
className="flex items-center gap-2 px-3 py-2 rounded-lg bg-zinc-800/50 hover:bg-zinc-700/50 border border-zinc-700 text-sm text-zinc-300 hover:text-white transition-all shrink-0"
34+
>
35+
<ArrowLeft className="w-4 h-4" />
36+
<span className="hidden sm:inline">New Search</span>
37+
</button>
38+
39+
<form onSubmit={submit} className="flex-1 flex items-center gap-3">
40+
<div className="flex-1 relative">
41+
<div className="absolute left-4 top-1/2 -translate-y-1/2 text-zinc-500">
42+
{loading ? <Loader2 className="w-5 h-5 animate-spin" /> : <Search className="w-5 h-5" />}
43+
</div>
44+
<input
45+
type="text"
46+
value={query}
47+
onChange={(e) => onQueryChange(e.target.value)}
48+
placeholder="Search again..."
49+
className={cn(
50+
"w-full bg-zinc-900/80 border rounded-xl pl-12 pr-4 py-3 text-white placeholder:text-zinc-500 focus:outline-none transition-all",
51+
loading ? "border-indigo-500/50 shadow-lg shadow-indigo-500/20" : "border-zinc-800 focus:border-zinc-700"
52+
)}
53+
/>
54+
</div>
55+
<Button
56+
type="submit"
57+
disabled={!canSearch}
58+
className={cn(
59+
"px-6 py-3 h-auto rounded-xl shrink-0",
60+
canSearch ? "bg-indigo-600 hover:bg-indigo-500 text-white" : "bg-zinc-700 text-zinc-400 cursor-not-allowed"
61+
)}
62+
>
63+
{loading ? <Loader2 className="w-4 h-4 animate-spin" /> : 'Search'}
64+
</Button>
65+
</form>
66+
</div>
67+
</div>
68+
</motion.div>
69+
)
70+
}
Lines changed: 259 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,259 @@
1+
import { useRef, useEffect, useState } from 'react'
2+
import { motion, AnimatePresence } from 'framer-motion'
3+
import { Search, Loader2 } from 'lucide-react'
4+
import { HeroSearch, type HeroSearchHandle } from './HeroSearch'
5+
import { useDemoSearch, DEMO_REPOS, type DemoRepo } from '@/hooks/useDemoSearch'
6+
import type { SearchResult } from '@/types'
7+
8+
interface Props {
9+
onResultsReady?: (results: SearchResult[], query: string, repoId: string, time: number | null) => void
10+
}
11+
12+
const PYTHON_REPOS = DEMO_REPOS.filter(r => ['flask', 'fastapi'].includes(r.id))
13+
14+
export function Hero({ onResultsReady }: Props) {
15+
const searchRef = useRef<HeroSearchHandle>(null)
16+
const cardRef = useRef<HTMLDivElement>(null)
17+
const { query, repo, results, loading, searchTime, setQuery, setRepo, search } = useDemoSearch(false)
18+
const [mousePos, setMousePos] = useState({ x: 0, y: 0 })
19+
20+
useEffect(() => {
21+
if (results.length) onResultsReady?.(results, query, repo.id, searchTime)
22+
}, [results, query, repo.id, searchTime, onResultsReady])
23+
24+
useEffect(() => {
25+
const onKey = (e: KeyboardEvent) => {
26+
const tag = (e.target as HTMLElement).tagName
27+
if (e.key === '/' && tag !== 'INPUT' && tag !== 'TEXTAREA') {
28+
e.preventDefault()
29+
searchRef.current?.focus()
30+
}
31+
}
32+
window.addEventListener('keydown', onKey)
33+
return () => window.removeEventListener('keydown', onKey)
34+
}, [])
35+
36+
const handleMouseMove = (e: React.MouseEvent) => {
37+
if (!cardRef.current) return
38+
const rect = cardRef.current.getBoundingClientRect()
39+
setMousePos({ x: e.clientX - rect.left, y: e.clientY - rect.top })
40+
}
41+
42+
const switchRepo = (r: DemoRepo) => {
43+
setRepo(r)
44+
}
45+
46+
const topResult = results[0]
47+
48+
return (
49+
<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 */}
51+
<div className="absolute inset-0 overflow-hidden pointer-events-none">
52+
<motion.div
53+
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%)' }}
55+
animate={{ x: [0, 30, 0], y: [0, -20, 0], scale: [1, 1.1, 1] }}
56+
transition={{ duration: 8, repeat: Infinity, ease: 'easeInOut' }}
57+
/>
58+
<motion.div
59+
className="absolute top-1/3 right-1/4 w-[400px] h-[400px] rounded-full"
60+
style={{ background: 'radial-gradient(circle, rgba(139,92,246,0.1) 0%, transparent 70%)' }}
61+
animate={{ x: [0, -25, 0], y: [0, 25, 0], scale: [1, 0.9, 1] }}
62+
transition={{ duration: 10, repeat: Infinity, ease: 'easeInOut' }}
63+
/>
64+
<motion.div
65+
className="absolute bottom-1/4 left-1/3 w-[350px] h-[350px] rounded-full"
66+
style={{ background: 'radial-gradient(circle, rgba(34,211,238,0.08) 0%, transparent 70%)' }}
67+
animate={{ x: [0, 20, 0], y: [0, -15, 0] }}
68+
transition={{ duration: 12, repeat: Infinity, ease: 'easeInOut' }}
69+
/>
70+
</div>
71+
72+
<div className="relative max-w-3xl mx-auto w-full">
73+
{/* Headline */}
74+
<motion.div
75+
className="text-center mb-10"
76+
initial={{ opacity: 0, y: 20 }}
77+
animate={{ opacity: 1, y: 0 }}
78+
transition={{ duration: 0.5 }}
79+
>
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,
82+
<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.
85+
</span>
86+
</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.
91+
</p>
92+
</motion.div>
93+
94+
{/* Search */}
95+
<motion.div
96+
initial={{ opacity: 0, y: 20 }}
97+
animate={{ opacity: 1, y: 0 }}
98+
transition={{ duration: 0.5, delay: 0.1 }}
99+
>
100+
<HeroSearch
101+
ref={searchRef}
102+
value={query}
103+
onChange={setQuery}
104+
onSubmit={() => search()}
105+
searching={loading}
106+
repoName={repo.name}
107+
/>
108+
</motion.div>
109+
110+
{/* Repo switcher */}
111+
<motion.div
112+
className="mt-4 flex items-center justify-center gap-2"
113+
initial={{ opacity: 0 }}
114+
animate={{ opacity: 1 }}
115+
transition={{ duration: 0.4, delay: 0.2 }}
116+
>
117+
<span className="text-xs text-zinc-600">Try on:</span>
118+
{PYTHON_REPOS.map(r => (
119+
<button
120+
key={r.id}
121+
onClick={() => switchRepo(r)}
122+
disabled={loading}
123+
className={`
124+
px-3 py-1.5 text-xs rounded-lg transition-all font-medium
125+
${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'
128+
}
129+
`}
130+
>
131+
{r.name}
132+
</button>
133+
))}
134+
</motion.div>
135+
136+
{/* Result card - only shows when loading or has results */}
137+
<AnimatePresence>
138+
{(loading || topResult) && (
139+
<motion.div
140+
className="mt-8"
141+
initial={{ opacity: 0, y: 20, height: 0 }}
142+
animate={{ opacity: 1, y: 0, height: 'auto' }}
143+
exit={{ opacity: 0, y: -10, height: 0 }}
144+
transition={{ duration: 0.3 }}
145+
>
146+
<div
147+
ref={cardRef}
148+
onMouseMove={handleMouseMove}
149+
className="relative group"
150+
>
151+
{/* Mouse glow effect */}
152+
<div
153+
className="absolute -inset-px rounded-xl opacity-0 group-hover:opacity-100 transition-opacity duration-300 pointer-events-none"
154+
style={{
155+
background: `radial-gradient(400px circle at ${mousePos.x}px ${mousePos.y}px, rgba(59,130,246,0.15), transparent 40%)`
156+
}}
157+
/>
158+
159+
{/* Card */}
160+
<div className="relative rounded-xl border border-white/[0.08] bg-zinc-900/50 backdrop-blur-sm overflow-hidden">
161+
<AnimatePresence mode="wait">
162+
{loading ? (
163+
<motion.div
164+
key="loading"
165+
initial={{ opacity: 0 }}
166+
animate={{ opacity: 1 }}
167+
exit={{ opacity: 0 }}
168+
className="p-6"
169+
>
170+
<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>
173+
</div>
174+
<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" />
178+
</div>
179+
</motion.div>
180+
) : topResult ? (
181+
<motion.div
182+
key="result"
183+
initial={{ opacity: 0 }}
184+
animate={{ opacity: 1 }}
185+
exit={{ opacity: 0 }}
186+
>
187+
{/* Header */}
188+
<div className="px-5 py-3 border-b border-white/[0.06] flex items-center justify-between bg-white/[0.02]">
189+
<div className="flex items-center gap-3">
190+
<div className="flex items-center gap-2">
191+
<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>
193+
</div>
194+
<span className="text-xs px-2 py-0.5 rounded-full bg-emerald-500/10 text-emerald-400 font-medium">
195+
{Math.round(topResult.score * 100)}% match
196+
</span>
197+
</div>
198+
<span className="text-xs text-zinc-600">{repo.name}</span>
199+
</div>
200+
201+
{/* Content */}
202+
<div className="p-5">
203+
<div className="flex items-start gap-3 mb-4">
204+
<div className="flex-1">
205+
<div className="flex items-center gap-2">
206+
<span className="font-mono text-sm font-semibold text-white">{topResult.name}</span>
207+
<span className="text-[10px] px-1.5 py-0.5 rounded bg-violet-500/10 text-violet-400 uppercase font-medium">
208+
{topResult.type}
209+
</span>
210+
</div>
211+
<div className="text-xs text-zinc-600 font-mono mt-1">{topResult.file_path}</div>
212+
</div>
213+
</div>
214+
215+
{/* Code preview */}
216+
<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" />
218+
<pre className="relative text-xs text-zinc-300 bg-black/40 p-4 overflow-x-auto font-mono leading-relaxed">
219+
<code>{topResult.content?.slice(0, 250)}...</code>
220+
</pre>
221+
</div>
222+
</div>
223+
224+
{/* Footer */}
225+
{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>
228+
</div>
229+
)}
230+
</motion.div>
231+
) : null}
232+
</AnimatePresence>
233+
</div>
234+
</div>
235+
</motion.div>
236+
)}
237+
</AnimatePresence>
238+
239+
{/* CTA */}
240+
<motion.div
241+
className="mt-10 text-center"
242+
initial={{ opacity: 0 }}
243+
animate={{ opacity: 1 }}
244+
transition={{ delay: 0.4 }}
245+
>
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
254+
</p>
255+
</motion.div>
256+
</div>
257+
</section>
258+
)
259+
}

0 commit comments

Comments
 (0)