Skip to content

Commit de9d75a

Browse files
committed
feat(#114): Complete HeroPlayground with UX polish
- Fix Supabase type imports (Session, User → type-only) - Fix ReactFlow type imports (Node, Edge → type-only) - Fix search input disabled bug in demo mode - Add loading spinner with helpful message - Add auto-scroll to results after search - Add 'New Search' back button in results header - Add fade-in animation for results section - Add hover scale effect on result cards - Format search time (3.6s vs 3606ms) - Show query in results header Closes #114
1 parent a13e9af commit de9d75a

5 files changed

Lines changed: 134 additions & 64 deletions

File tree

frontend/package-lock.json

Lines changed: 0 additions & 13 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

frontend/src/components/DependencyGraph.tsx

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,13 @@
11
import { useEffect, useState, useCallback } from 'react'
22
import ReactFlow, {
3-
Node,
4-
Edge,
53
Controls,
64
Background,
75
useNodesState,
86
useEdgesState,
97
MarkerType,
108
MiniMap,
119
} from 'reactflow'
10+
import type { Node, Edge } from 'reactflow'
1211
import dagre from 'dagre'
1312
import 'reactflow/dist/style.css'
1413
import { useDependencyGraph } from '../hooks/useCachedQuery'

frontend/src/components/playground/HeroPlayground.tsx

Lines changed: 40 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -106,7 +106,33 @@ export function HeroPlayground({
106106
const showUrlInput = mode === 'custom' && !['indexing', 'ready'].includes(state.status);
107107
const showValidation = mode === 'custom' && ['validating', 'valid', 'invalid'].includes(state.status);
108108
const showIndexing = mode === 'custom' && state.status === 'indexing';
109-
const showSearch = mode === 'demo' || state.status === 'ready';
109+
// Always show search - in custom mode it's disabled until repo is indexed
110+
const showSearch = true;
111+
const isSearchDisabled = mode === 'custom' && state.status !== 'ready';
112+
113+
// Get contextual placeholder text
114+
const getPlaceholder = () => {
115+
if (mode === 'demo') {
116+
return "Search for authentication, error handling...";
117+
}
118+
// Custom mode placeholders based on state
119+
switch (state.status) {
120+
case 'idle':
121+
return "Enter a GitHub URL above to start...";
122+
case 'validating':
123+
return "Validating repository...";
124+
case 'valid':
125+
return "Click 'Index Repository' to continue...";
126+
case 'invalid':
127+
return "Fix the URL above to continue...";
128+
case 'indexing':
129+
return "Indexing in progress...";
130+
case 'ready':
131+
return `Search in ${state.repoName}...`;
132+
default:
133+
return "Enter a GitHub URL to search...";
134+
}
135+
};
110136

111137
return (
112138
<div className="w-full max-w-2xl mx-auto">
@@ -205,26 +231,31 @@ export function HeroPlayground({
205231
<>
206232
<div className="relative mb-4">
207233
<div className="absolute inset-0 bg-gradient-to-r from-indigo-500/20 to-cyan-500/20 rounded-2xl blur-xl opacity-50" />
208-
<div className="relative bg-zinc-900/80 rounded-2xl border border-zinc-800 p-3">
234+
<div className={cn(
235+
"relative bg-zinc-900/80 rounded-2xl border border-zinc-800 p-3",
236+
isSearchDisabled && "opacity-60"
237+
)}>
209238
<form onSubmit={handleSearch} className="flex items-center gap-3">
210239
<div className="flex-1 flex items-center gap-3">
211240
<div className="text-zinc-500 ml-2"><SearchIcon /></div>
212241
<input
213242
type="text"
214243
value={query}
215244
onChange={(e) => setQuery(e.target.value)}
216-
placeholder={mode === 'demo'
217-
? "Search for authentication, error handling..."
218-
: `Search in ${state.status === 'ready' ? state.repoName : 'your repo'}...`
219-
}
245+
placeholder={getPlaceholder()}
220246
className="flex-1 bg-transparent text-white placeholder:text-zinc-500 focus:outline-none text-base py-3"
221-
disabled={!canSearch && query.length === 0}
247+
disabled={isSearchDisabled}
222248
/>
223249
</div>
224250
<Button
225251
type="submit"
226-
disabled={!canSearch || loading}
227-
className="px-6 py-3 h-auto bg-indigo-600 hover:bg-indigo-500 rounded-xl disabled:opacity-50"
252+
disabled={!canSearch || loading || isSearchDisabled}
253+
className={cn(
254+
"px-6 py-3 h-auto rounded-xl transition-all",
255+
canSearch && !loading && !isSearchDisabled
256+
? "bg-indigo-600 hover:bg-indigo-500 text-white"
257+
: "bg-zinc-700 text-zinc-400 cursor-not-allowed"
258+
)}
228259
>
229260
{loading ? (
230261
<div className="w-4 h-4 border-2 border-white/30 border-t-white rounded-full animate-spin" />

frontend/src/contexts/AuthContext.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { createContext, useContext, useState, useEffect, ReactNode } from 'react';
2-
import { createClient, SupabaseClient, User, Session } from '@supabase/supabase-js';
2+
import { createClient } from '@supabase/supabase-js';
3+
import type { SupabaseClient, User, Session } from '@supabase/supabase-js';
34

45
interface AuthContextType {
56
user: User | null;

frontend/src/pages/LandingPage.tsx

Lines changed: 91 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@ function AnimatedSection({ children, className = '' }: { children: React.ReactNo
6767

6868
export function LandingPage() {
6969
const navigate = useNavigate()
70+
const resultsRef = useRef<HTMLElement>(null)
7071
const [results, setResults] = useState<SearchResult[]>([])
7172
const [loading, setLoading] = useState(false)
7273
const [searchTime, setSearchTime] = useState<number | null>(null)
@@ -77,6 +78,22 @@ export function LandingPage() {
7778
const [rateLimitError, setRateLimitError] = useState<string | null>(null)
7879
const [lastQuery, setLastQuery] = useState('')
7980

81+
// Scroll to results when they appear
82+
const scrollToResults = () => {
83+
setTimeout(() => {
84+
resultsRef.current?.scrollIntoView({ behavior: 'smooth', block: 'start' })
85+
}, 100)
86+
}
87+
88+
// Reset to search state
89+
const handleNewSearch = () => {
90+
setHasSearched(false)
91+
setResults([])
92+
setSearchTime(null)
93+
setLastQuery('')
94+
window.scrollTo({ top: 0, behavior: 'smooth' })
95+
}
96+
8097
// Fetch rate limit status on mount
8198
useEffect(() => {
8299
fetch(`${API_URL}/playground/limits`, { credentials: 'include' })
@@ -126,6 +143,8 @@ export function LandingPage() {
126143
if (typeof data.remaining_searches === 'number') {
127144
setRemaining(data.remaining_searches)
128145
}
146+
// Scroll to results after they load
147+
scrollToResults()
129148
} else if (data.status === 429) {
130149
setRateLimitError('Daily limit reached. Sign up for unlimited searches!')
131150
setRemaining(0)
@@ -199,57 +218,90 @@ export function LandingPage() {
199218

200219
{/* ============ RESULTS SECTION (if searched) ============ */}
201220
{hasSearched && (
202-
<section className="pb-20 px-6">
221+
<section ref={resultsRef} className="pb-20 px-6 pt-8 animate-in fade-in slide-in-from-bottom-4 duration-500">
203222
<div className="max-w-4xl mx-auto">
223+
{/* Results Header */}
204224
<div className="flex items-center justify-between mb-6">
205-
<div className="flex items-center gap-4 text-sm">
206-
<span className="text-gray-400"><span className="text-white font-semibold">{results.length}</span> results</span>
207-
{searchTime && <><span className="text-gray-700"></span><span className="font-mono text-green-400">{searchTime}ms</span></>}
225+
<div className="flex items-center gap-4">
226+
<button
227+
onClick={handleNewSearch}
228+
className="flex items-center gap-2 px-3 py-1.5 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"
229+
>
230+
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
231+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M10 19l-7-7m0 0l7-7m-7 7h18" />
232+
</svg>
233+
New Search
234+
</button>
235+
<span className="text-gray-500">|</span>
236+
<span className="text-gray-400 text-sm">
237+
<span className="text-white font-semibold">{results.length}</span> results for "<span className="text-blue-400">{lastQuery}</span>"
238+
</span>
239+
{searchTime && (
240+
<span className="font-mono text-sm text-green-400">
241+
{searchTime > 1000 ? `${(searchTime/1000).toFixed(1)}s` : `${searchTime}ms`}
242+
</span>
243+
)}
208244
</div>
209245
{remaining > 0 && remaining < limit && (
210246
<div className="text-sm text-gray-500">{remaining} remaining</div>
211247
)}
212248
</div>
213249

214-
{(remaining <= 0 || rateLimitError) && (
215-
<Card className="bg-gradient-to-r from-blue-600/20 to-purple-600/20 border-blue-500/30 p-6 mb-6">
216-
<h3 className="text-lg font-semibold mb-2">You've reached today's limit</h3>
217-
<p className="text-gray-300 mb-4">
218-
{rateLimitError || 'Sign up to get unlimited searches and index your own repos.'}
219-
</p>
220-
<Button onClick={() => navigate('/signup')} className="bg-white text-black hover:bg-gray-100">Get started — it's free</Button>
221-
</Card>
250+
{/* Loading State */}
251+
{loading && (
252+
<div className="flex flex-col items-center justify-center py-20">
253+
<div className="relative">
254+
<div className="w-12 h-12 border-4 border-zinc-700 border-t-blue-500 rounded-full animate-spin" />
255+
</div>
256+
<p className="mt-4 text-zinc-400 text-sm">Searching codebase...</p>
257+
<p className="text-zinc-600 text-xs mt-1">This may take a few seconds for first search</p>
258+
</div>
222259
)}
223260

224-
<div className="space-y-4">
225-
{results.map((result, idx) => (
226-
<Card key={idx} className="bg-[#111113] border-white/5 overflow-hidden hover:border-white/10 transition-all">
227-
<div className="px-5 py-4 border-b border-white/5 flex items-start justify-between">
228-
<div>
229-
<div className="flex items-center gap-3">
230-
<h3 className="font-mono font-semibold">{result.name}</h3>
231-
<Badge variant="outline" className="text-[10px] text-gray-400 border-gray-700">{result.type.replace('_', ' ')}</Badge>
261+
{/* Results Content (only when not loading) */}
262+
{!loading && (
263+
<>
264+
{(remaining <= 0 || rateLimitError) && (
265+
<Card className="bg-gradient-to-r from-blue-600/20 to-purple-600/20 border-blue-500/30 p-6 mb-6">
266+
<h3 className="text-lg font-semibold mb-2">You've reached today's limit</h3>
267+
<p className="text-gray-300 mb-4">
268+
{rateLimitError || 'Sign up to get unlimited searches and index your own repos.'}
269+
</p>
270+
<Button onClick={() => navigate('/signup')} className="bg-white text-black hover:bg-gray-100">Get started — it's free</Button>
271+
</Card>
272+
)}
273+
274+
<div className="space-y-4">
275+
{results.map((result, idx) => (
276+
<Card key={idx} className="bg-[#111113] border-white/5 overflow-hidden hover:border-white/10 transition-all hover:scale-[1.01] duration-200">
277+
<div className="px-5 py-4 border-b border-white/5 flex items-start justify-between">
278+
<div>
279+
<div className="flex items-center gap-3">
280+
<h3 className="font-mono font-semibold">{result.name}</h3>
281+
<Badge variant="outline" className="text-[10px] text-gray-400 border-gray-700">{result.type.replace('_', ' ')}</Badge>
282+
</div>
283+
<p className="text-sm text-gray-500 font-mono mt-1">{result.file_path.split('/').slice(-2).join('/')}</p>
284+
</div>
285+
<div className="text-right">
286+
<div className="text-2xl font-bold text-blue-400">{(result.score * 100).toFixed(0)}%</div>
287+
<div className="text-[10px] text-gray-500 uppercase tracking-wider">match</div>
288+
</div>
232289
</div>
233-
<p className="text-sm text-gray-500 font-mono mt-1">{result.file_path.split('/').slice(-2).join('/')}</p>
234-
</div>
235-
<div className="text-right">
236-
<div className="text-2xl font-bold text-blue-400">{(result.score * 100).toFixed(0)}%</div>
237-
<div className="text-[10px] text-gray-500 uppercase tracking-wider">match</div>
238-
</div>
239-
</div>
240-
<SyntaxHighlighter language={result.language || 'python'} style={oneDark} customStyle={{ margin: 0, borderRadius: 0, fontSize: '0.8rem', background: '#0d0d0f' }} showLineNumbers startingLineNumber={result.line_start || 1}>
241-
{result.code}
242-
</SyntaxHighlighter>
243-
</Card>
244-
))}
245-
</div>
290+
<SyntaxHighlighter language={result.language || 'python'} style={oneDark} customStyle={{ margin: 0, borderRadius: 0, fontSize: '0.8rem', background: '#0d0d0f' }} showLineNumbers startingLineNumber={result.line_start || 1}>
291+
{result.code}
292+
</SyntaxHighlighter>
293+
</Card>
294+
))}
295+
</div>
246296

247-
{results.length === 0 && !loading && (
248-
<div className="text-center py-16">
249-
<div className="text-5xl mb-4">🔍</div>
250-
<h3 className="text-lg font-semibold mb-2">No results found</h3>
251-
<p className="text-gray-500">Try a different query</p>
252-
</div>
297+
{results.length === 0 && (
298+
<div className="text-center py-16">
299+
<div className="text-5xl mb-4">🔍</div>
300+
<h3 className="text-lg font-semibold mb-2">No results found</h3>
301+
<p className="text-gray-500">Try a different query</p>
302+
</div>
303+
)}
304+
</>
253305
)}
254306
</div>
255307
</section>

0 commit comments

Comments
 (0)