1- import { useContext , useRef } from 'react' ;
1+ import { useContext , useRef , useState , useEffect } from 'react' ;
22import { useTranslation } from 'react-i18next' ;
33import PageViewsContext from './Context' ;
44import pageNameDecoder from '../../helpers/pageNameDecoder' ;
55import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' ;
66import { faAdd , faXmark } from '@fortawesome/free-solid-svg-icons' ;
77
88export default function Pages ( ) {
9- const { pages, setPages } = useContext ( PageViewsContext ) ;
9+ const { pages, setPages, project } = useContext ( PageViewsContext ) ;
1010 const { t } = useTranslation ( ) ;
11- const inputRef = useRef ( '' ) ;
12-
11+ const inputRef = useRef ( null ) ;
12+ const [ query , setQuery ] = useState ( '' ) ;
13+ const [ suggestions , setSuggestions ] = useState ( [ ] ) ;
14+ const [ isLoading , setIsLoading ] = useState ( false ) ;
15+
1316 const handleSubmit = ( e ) => {
1417 e . preventDefault ( ) ;
15- setPages ( [ ...pages , inputRef . current . value ] ) ;
16- inputRef . current . value = '' ;
18+ const value = query . trim ( ) ;
19+ if ( ! value ) return ;
20+ if ( ! pages . includes ( value ) ) setPages ( [ ...pages , value ] ) ;
21+ setQuery ( '' ) ;
22+ setSuggestions ( [ ] ) ;
1723 } ;
1824
1925 function deleteItemAtIndex ( i ) {
@@ -27,19 +33,80 @@ export default function Pages() {
2733 setPages ( newPages ) ;
2834 }
2935
36+ // Debounced fetch for Wikipedia OpenSearch
37+ // Replace the useEffect hook with this version that includes 'project' in the dependency array
38+ useEffect ( ( ) => {
39+ if ( ! query ) {
40+ setSuggestions ( [ ] ) ;
41+ return ;
42+ }
43+
44+ const controller = new AbortController ( ) ;
45+ const timeout = setTimeout ( ( ) => {
46+ setIsLoading ( true ) ;
47+ const encoded = encodeURIComponent ( query ) ;
48+ const host =
49+ project && ( project . startsWith ( 'http://' ) || project . startsWith ( 'https://' ) ) ? project : `https://${ project || 'en.wikipedia.org' } ` ;
50+ const url = `${ host . replace ( / \/ $ / , '' ) } /w/api.php?action=opensearch&format=json&formatversion=2&search=${ encoded } &namespace=0&limit=10&origin=*` ;
51+
52+ fetch ( url , { signal : controller . signal } )
53+ . then ( ( res ) => res . json ( ) )
54+ . then ( ( data ) => {
55+ // data[1] = titles, data[3] = urls
56+ const titles = data [ 1 ] || [ ] ;
57+ const urls = data [ 3 ] || [ ] ;
58+ const merged = titles . map ( ( title , i ) => ( { title, url : urls [ i ] || '' } ) ) ;
59+ setSuggestions ( merged ) ;
60+ } )
61+ . catch ( ( err ) => {
62+ if ( err . name !== 'AbortError' ) {
63+ // You might want to handle the error here
64+ }
65+ } )
66+ . finally ( ( ) => setIsLoading ( false ) ) ;
67+ } , 300 ) ;
68+
69+ return ( ) => {
70+ controller . abort ( ) ;
71+ clearTimeout ( timeout ) ;
72+ } ;
73+ } , [ query , project ] ) ;
74+
75+ const handleSelectSuggestion = ( title ) => {
76+ if ( ! pages . includes ( title ) ) setPages ( [ ...pages , title ] ) ;
77+ setQuery ( '' ) ;
78+ setSuggestions ( [ ] ) ;
79+ inputRef . current ?. focus ( ) ;
80+ } ;
81+
3082 return (
3183 < div className = 'flex my-3 gap-4 flex-wrap' >
32- < form onSubmit = { handleSubmit } method = 'POST' className = 'flex gap-3 flex-wrap' >
33- < input
34- type = 'text'
35- className = 'page-input'
36- name = 'page'
37- ref = { inputRef }
84+ < form onSubmit = { handleSubmit } method = 'POST' className = 'flex gap-3 flex-wrap relative w-full max-w-lg' >
85+ < input
86+ type = 'text'
87+ className = 'page-input w-[320px]'
88+ name = 'page'
89+ ref = { inputRef }
90+ value = { query }
91+ onChange = { ( e ) => setQuery ( e . target . value ) }
3892 placeholder = { t ( 'pageViews.selectArticle' ) }
93+ autoComplete = 'off'
3994 />
4095 < button type = 'submit' className = 'bg-blue-500 text-white px-4 py-2 rounded' >
4196 < FontAwesomeIcon icon = { faAdd } />
4297 </ button >
98+
99+ { ( suggestions . length > 0 || isLoading ) && (
100+ < ul className = 'absolute left-0 mt-12 w-[320px] bg-white border border-gray-200 rounded shadow max-h-56 overflow-auto z-50' >
101+ { isLoading && < li className = 'p-2 text-sm text-gray-500' > { t ( 'common.loading' ) || 'Loading...' } </ li > }
102+ { suggestions . map ( ( s , i ) => (
103+ < li key = { i } className = 'p-2 hover:bg-gray-100 cursor-pointer text-sm' onClick = { ( ) => handleSelectSuggestion ( s . title ) } >
104+ < div className = 'font-medium' > { s . title } </ div >
105+ { s . url && < div className = 'text-xs text-gray-500' > { s . url } </ div > }
106+ </ li >
107+ ) ) }
108+ </ ul >
109+ ) }
43110 </ form >
44111 < div className = 'list-none flex flex-wrap gap-1' >
45112 { pages . map ( ( page , index ) => (
@@ -54,9 +121,7 @@ export default function Pages() {
54121 </ span >
55122 ) ) }
56123 </ div >
57- { pages . length === 0 && (
58- < div className = "text-gray-500" > { t ( 'common.noData' ) } </ div >
59- ) }
124+ { pages . length === 0 && < div className = 'text-gray-500' > { t ( 'common.noData' ) } </ div > }
60125 </ div >
61126 ) ;
62127}
0 commit comments