Skip to content

Commit 35eb0e1

Browse files
Merge pull request #263 from gopavasanth/T389421
fix: enhanced article selection with project-aware search and suggestions
2 parents e04a44d + 902e979 commit 35eb0e1

File tree

1 file changed

+80
-15
lines changed

1 file changed

+80
-15
lines changed

src/components/PageViews/Pages.jsx

Lines changed: 80 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,25 @@
1-
import { useContext, useRef } from 'react';
1+
import { useContext, useRef, useState, useEffect } from 'react';
22
import { useTranslation } from 'react-i18next';
33
import PageViewsContext from './Context';
44
import pageNameDecoder from '../../helpers/pageNameDecoder';
55
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
66
import { faAdd, faXmark } from '@fortawesome/free-solid-svg-icons';
77

88
export 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

Comments
 (0)