1+ import React , { useState , FormEvent } from "react" ;
2+ import VideoCard from "./VideoCard" ;
3+
4+ export interface VideoSummary {
5+ videoId : string ;
6+ title : string ;
7+ thumbnailUrl ?: string | null ;
8+ userId : string ;
9+ submittedAt : string ;
10+ content_rating ?: string | null ;
11+ category ?: string | null ;
12+ views : number ;
13+ averageRating ?: number | null ;
14+ }
15+
16+ interface Pagination {
17+ currentPage : number ;
18+ pageSize : number ;
19+ totalItems : number ;
20+ totalPages : number ;
21+ }
22+
23+ interface PaginatedResponse {
24+ data : VideoSummary [ ] ;
25+ pagination : Pagination ;
26+ }
27+
28+ interface SemanticSearchBarProps {
29+ /**
30+ * Optional base URL for the API. Defaults to relative path
31+ * so that the component works when the front-end and backend
32+ * are served from the same origin (e.g. behind a reverse proxy).
33+ */
34+ apiBaseUrl ?: string ;
35+ /**
36+ * Number of results to request per page (default: 10)
37+ */
38+ pageSize ?: number ;
39+ }
40+
41+ const SemanticSearchBar : React . FC < SemanticSearchBarProps > = ( {
42+ apiBaseUrl = "" ,
43+ pageSize = 10 ,
44+ } ) => {
45+ const [ query , setQuery ] = useState < string > ( "" ) ;
46+ const [ results , setResults ] = useState < VideoSummary [ ] > ( [ ] ) ;
47+ const [ loading , setLoading ] = useState < boolean > ( false ) ;
48+ const [ error , setError ] = useState < string | null > ( null ) ;
49+
50+ const handleSubmit = async ( e : FormEvent ) => {
51+ e . preventDefault ( ) ;
52+ if ( ! query . trim ( ) ) {
53+ return ;
54+ }
55+ await performSearch ( query . trim ( ) ) ;
56+ } ;
57+
58+ const performSearch = async ( q : string ) => {
59+ try {
60+ setLoading ( true ) ;
61+ setError ( null ) ;
62+ const params = new URLSearchParams ( ) ;
63+ params . set ( "query" , q ) ;
64+ params . set ( "mode" , "semantic" ) ;
65+ params . set ( "page" , "1" ) ;
66+ params . set ( "pageSize" , pageSize . toString ( ) ) ;
67+
68+ const response = await fetch (
69+ `${ apiBaseUrl } /api/v1/search/videos?${ params . toString ( ) } `
70+ ) ;
71+
72+ if ( ! response . ok ) {
73+ throw new Error ( `Backend search failed: ${ response . status } ` ) ;
74+ }
75+
76+ const payload : PaginatedResponse = await response . json ( ) ;
77+ setResults ( payload . data ) ;
78+ } catch ( err : any ) {
79+ console . error ( err ) ;
80+ setError ( err . message ?? "Unknown error" ) ;
81+ } finally {
82+ setLoading ( false ) ;
83+ }
84+ } ;
85+
86+ return (
87+ < div className = "semantic-search-bar" >
88+ < form onSubmit = { handleSubmit } className = "search-form" >
89+ < input
90+ type = "text"
91+ placeholder = "Search videos…"
92+ aria-label = "Search videos"
93+ value = { query }
94+ onChange = { ( e ) => setQuery ( e . target . value ) }
95+ disabled = { loading }
96+ />
97+ < button type = "submit" disabled = { loading || ! query . trim ( ) } >
98+ { loading ? "Searching…" : "Search" }
99+ </ button >
100+ </ form >
101+
102+ { error && < p className = "error" > { error } </ p > }
103+
104+ < div className = "results" >
105+ { results . map ( ( video ) => (
106+ < VideoCard key = { video . videoId } video = { video } />
107+ ) ) }
108+ </ div >
109+ </ div >
110+ ) ;
111+ } ;
112+
113+ export default SemanticSearchBar ;
0 commit comments