mirror of
https://github.com/ajnart/homarr.git
synced 2025-11-10 07:25:48 +01:00
✨ Add autocomplete to Search Module
Suggestions when searching with the search bar Fixes #12
This commit is contained in:
@@ -1,10 +1,4 @@
|
|||||||
import {
|
import { Kbd, createStyles, Text, Popover, Autocomplete } from '@mantine/core';
|
||||||
Kbd,
|
|
||||||
createStyles,
|
|
||||||
Text,
|
|
||||||
Popover,
|
|
||||||
TextInput,
|
|
||||||
} from '@mantine/core';
|
|
||||||
import { useDebouncedValue, useForm, useHotkeys } from '@mantine/hooks';
|
import { useDebouncedValue, useForm, useHotkeys } from '@mantine/hooks';
|
||||||
import { useEffect, useRef, useState } from 'react';
|
import { useEffect, useRef, useState } from 'react';
|
||||||
import {
|
import {
|
||||||
@@ -36,13 +30,10 @@ export const SearchModule: IModule = {
|
|||||||
export default function SearchBar(props: any) {
|
export default function SearchBar(props: any) {
|
||||||
const { config, setConfig } = useConfig();
|
const { config, setConfig } = useConfig();
|
||||||
const [opened, setOpened] = useState(false);
|
const [opened, setOpened] = useState(false);
|
||||||
const [results, setOpenedResults] = useState(false);
|
|
||||||
const [icon, setIcon] = useState(<Search />);
|
const [icon, setIcon] = useState(<Search />);
|
||||||
const queryUrl = config.settings.searchUrl ?? 'https://www.google.com/search?q=';
|
const queryUrl = config.settings.searchUrl ?? 'https://www.google.com/search?q=';
|
||||||
const textInput = useRef<HTMLInputElement>();
|
const textInput = useRef<HTMLInputElement>();
|
||||||
// Find a service with the type of 'Overseerr'
|
// Find a service with the type of 'Overseerr'
|
||||||
const service = config.services.find((s) => s.type === 'Overseerr');
|
|
||||||
|
|
||||||
const form = useForm({
|
const form = useForm({
|
||||||
initialValues: {
|
initialValues: {
|
||||||
query: '',
|
query: '',
|
||||||
@@ -50,19 +41,12 @@ export default function SearchBar(props: any) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const [debounced, cancel] = useDebouncedValue(form.values.query, 250);
|
const [debounced, cancel] = useDebouncedValue(form.values.query, 250);
|
||||||
const [data, setData] = useState([]);
|
const [results, setResults] = useState<any[]>([]);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (form.values.query !== debounced || form.values.query === '') return;
|
if (form.values.query !== debounced || form.values.query === '') return;
|
||||||
setOpened(false);
|
|
||||||
setOpenedResults(true);
|
|
||||||
if (service) {
|
|
||||||
const serviceUrl = new URL(service.url);
|
|
||||||
axios
|
axios
|
||||||
.post(`/api/modules/overseerr?query=${form.values.query}`, {
|
.get(`/api/modules/search?q=${form.values.query}`)
|
||||||
service,
|
.then((res) => setResults(res.data ?? []));
|
||||||
})
|
|
||||||
.then((res) => setData(res.data.results ?? []));
|
|
||||||
}
|
|
||||||
}, [debounced]);
|
}, [debounced]);
|
||||||
useHotkeys([['ctrl+K', () => textInput.current && textInput.current.focus()]]);
|
useHotkeys([['ctrl+K', () => textInput.current && textInput.current.focus()]]);
|
||||||
const { classes, cx } = useStyles();
|
const { classes, cx } = useStyles();
|
||||||
@@ -82,7 +66,10 @@ export default function SearchBar(props: any) {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Data with label as item.name
|
const autocompleteData = results.map((result) => ({
|
||||||
|
label: result.phrase,
|
||||||
|
value: result.phrase,
|
||||||
|
}));
|
||||||
return (
|
return (
|
||||||
<form
|
<form
|
||||||
onChange={() => {
|
onChange={() => {
|
||||||
@@ -114,9 +101,6 @@ export default function SearchBar(props: any) {
|
|||||||
}, 20);
|
}, 20);
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
<Popover
|
|
||||||
opened={results}
|
|
||||||
target={
|
|
||||||
<Popover
|
<Popover
|
||||||
opened={opened}
|
opened={opened}
|
||||||
position="bottom"
|
position="bottom"
|
||||||
@@ -129,8 +113,9 @@ export default function SearchBar(props: any) {
|
|||||||
onFocusCapture={() => setOpened(true)}
|
onFocusCapture={() => setOpened(true)}
|
||||||
onBlurCapture={() => setOpened(false)}
|
onBlurCapture={() => setOpened(false)}
|
||||||
target={
|
target={
|
||||||
<TextInput
|
<Autocomplete
|
||||||
variant="filled"
|
variant="filled"
|
||||||
|
data={autocompleteData}
|
||||||
icon={icon}
|
icon={icon}
|
||||||
ref={textInput}
|
ref={textInput}
|
||||||
rightSectionWidth={90}
|
rightSectionWidth={90}
|
||||||
@@ -143,13 +128,6 @@ export default function SearchBar(props: any) {
|
|||||||
{...form.getInputProps('query')}
|
{...form.getInputProps('query')}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
>
|
|
||||||
<Text>
|
|
||||||
tip: Use the prefixes <b>!yt</b> and <b>!t</b> in front of your query to search on
|
|
||||||
YouTube or for a Torrent respectively.
|
|
||||||
</Text>
|
|
||||||
</Popover>
|
|
||||||
}
|
|
||||||
>
|
>
|
||||||
<Text>
|
<Text>
|
||||||
Tip: Use the prefixes <b>!yt</b> and <b>!t</b> in front of your query to search on YouTube
|
Tip: Use the prefixes <b>!yt</b> and <b>!t</b> in front of your query to search on YouTube
|
||||||
|
|||||||
19
src/pages/api/modules/search.ts
Normal file
19
src/pages/api/modules/search.ts
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
import axios from 'axios';
|
||||||
|
import { NextApiRequest, NextApiResponse } from 'next';
|
||||||
|
|
||||||
|
async function Get(req: NextApiRequest, res: NextApiResponse) {
|
||||||
|
const { q } = req.query;
|
||||||
|
const response = await axios.get(`https://duckduckgo.com/ac/?q=${q}`);
|
||||||
|
res.status(200).json(response.data);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default async (req: NextApiRequest, res: NextApiResponse) => {
|
||||||
|
// Filter out if the reuqest is a POST or a GET
|
||||||
|
if (req.method === 'GET') {
|
||||||
|
return Get(req, res);
|
||||||
|
}
|
||||||
|
return res.status(405).json({
|
||||||
|
statusCode: 405,
|
||||||
|
message: 'Method not allowed',
|
||||||
|
});
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user