web3.news/app/components/SearchBar.tsx

224 lines
6.8 KiB
TypeScript
Raw Normal View History

import React, {
2023-12-18 03:18:17 +00:00
type FormEvent,
useCallback,
useEffect,
useRef,
2023-12-18 03:18:17 +00:00
useState,
} from "react";
2023-12-23 11:42:46 +00:00
import { ChevronRightIcon } from "@heroicons/react/24/outline"; // Assuming usage of Heroicons for icons
2023-12-18 03:18:17 +00:00
import { flushSync } from "react-dom";
2023-12-23 11:42:46 +00:00
import { Link, useFetcher, useSearchParams } from "@remix-run/react";
import { FILTER_TIMES, formatDate } from "@/utils";
2023-12-08 18:29:17 +00:00
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
2023-12-18 03:18:17 +00:00
SelectValue,
} from "./ui/select";
import { SitesCombobox } from "./SitesCombobox";
2023-12-22 09:11:03 +00:00
import { SearchResult, SiteList } from "@/types.js";
2023-11-10 17:17:30 +00:00
2023-12-18 03:18:17 +00:00
type Props = {};
2023-11-10 17:17:30 +00:00
2023-12-22 09:11:03 +00:00
const SearchBar = ({ sites }: { sites: SiteList }) => {
2023-12-18 03:18:17 +00:00
let [searchParams] = useSearchParams();
const [query, setQuery] = useState(searchParams.get("q") ?? "");
const inputRef = useRef<HTMLInputElement>();
const [isLoading, setIsLoading] = useState(false);
const [activeInput, setActiveInput] = useState(true);
const [dirtyInput, setDirtyInput] = useState(false);
const [results, setResults] = useState<SearchResult[]>([]);
2023-12-23 11:42:46 +00:00
const [selectedSite, setSelectedSite] = useState(null);
2023-12-23 12:10:18 +00:00
const [selectedTime, setSelectedTime] = useState<string | null>(null);
2023-11-10 17:17:30 +00:00
2023-12-23 11:42:46 +00:00
const fetcher = useFetcher({ key: "seach" });
2023-12-23 10:51:12 +00:00
const handleSearch = useCallback(
2023-12-23 11:42:46 +00:00
(event: FormEvent<HTMLFormElement>) => {
2023-12-18 03:18:17 +00:00
event.preventDefault();
2023-12-23 11:42:46 +00:00
doSearch();
},
[query]
2023-12-23 11:42:46 +00:00
);
2023-11-14 15:52:42 +00:00
2023-12-23 11:42:46 +00:00
function doSearch() {
setIsLoading(true);
let newSearchParams = new URLSearchParams(searchParams.toString());
2023-11-14 15:52:42 +00:00
2023-12-23 11:42:46 +00:00
if (query) {
newSearchParams.set("q", query);
} else {
newSearchParams.delete("q");
return;
}
2023-11-14 15:52:42 +00:00
2023-12-23 11:42:46 +00:00
if (selectedSite) {
newSearchParams.set("site", selectedSite);
}
2023-12-23 12:10:18 +00:00
if (selectedTime) {
const timestampInMilliseconds = Date.parse(selectedTime); // Date.parse returns the timestamp in milliseconds
const timestamp = timestampInMilliseconds / 1000;
newSearchParams.set("time", timestamp.toString());
}
2023-12-23 11:42:46 +00:00
fetcher.load(`/api/search?${newSearchParams}`);
}
2023-11-14 15:52:42 +00:00
2023-12-23 10:51:12 +00:00
useEffect(() => {
if (fetcher.data) {
setResults(fetcher.data as SearchResult[]);
setIsLoading(false);
setActiveInput(false);
}
}, [fetcher.data]);
2023-12-23 11:42:46 +00:00
useEffect(() => {
doSearch();
2023-12-23 12:10:18 +00:00
}, [selectedSite, selectedTime]);
2023-12-23 11:42:46 +00:00
2023-12-18 03:18:17 +00:00
const isActive = results.length > 0 || dirtyInput;
2023-11-10 17:17:30 +00:00
return (
<div
2023-11-15 09:35:32 +00:00
className={`w-full mt-8 p-4 border-2 ${
isActive ? "border-sky-300 bg-gray-950" : "border-primary"
2023-11-10 17:17:30 +00:00
}`}
>
<form className={`flex items-center text-lg`} onSubmit={handleSearch}>
{isLoading || isActive ? (
2023-11-10 17:17:30 +00:00
<span className="text-white mr-2">Searching for</span>
) : null}
{activeInput ? (
<fieldset
className={`block w-full p-0 h-auto flex-1 overflow-hidden`}
>
{isActive ? (
2023-11-10 17:17:30 +00:00
<span className="text-blue-300 underline-offset-4 underline mr-[-0.5px]">
{'"'}
</span>
) : (
""
)}
<input
ref={(element) => {
if (element) {
2023-12-18 03:18:17 +00:00
inputRef.current = element;
2023-11-10 17:17:30 +00:00
}
}}
className={`flex-grow inline bg-transparent text-white placeholder-gray-400 outline-none ring-none ${
isActive
2023-11-10 17:17:30 +00:00
? `text-blue-300 p-0 underline underline-offset-4`
: "w-full p-2"
}`}
placeholder={
2023-11-22 11:59:45 +00:00
!isActive
2023-11-10 17:17:30 +00:00
? "Search for web3 news from the community"
: undefined
}
value={query}
size={query ? query.length : 1}
style={
query
? {
2023-12-18 03:18:17 +00:00
width: `calc(${query.length}ch+2px)`,
2023-11-10 17:17:30 +00:00
}
: undefined
}
onChange={(e) => {
2023-12-08 18:29:17 +00:00
if (!dirtyInput) {
2023-12-18 03:18:17 +00:00
setDirtyInput(true);
}
2023-12-18 03:18:17 +00:00
setQuery(e.target.value);
}}
2023-11-10 17:17:30 +00:00
/>
{isActive ? (
2023-11-10 17:17:30 +00:00
<span className="text-blue-300 underline-offset-4 underline ml-[-5.5px]">
{'"'}
</span>
) : (
""
)}
</fieldset>
) : (
<span
className="block w-full flex-1 text-blue-300"
onClick={() => {
flushSync(() => {
2023-12-18 03:18:17 +00:00
setActiveInput(true);
});
inputRef.current?.focus();
2023-11-10 17:17:30 +00:00
}}
>
{'"'}
{query}
{'"'}
</span>
)}
{isLoading ? (
// Shadcn Loading component placeholder
<LoadingComponent />
) : (
2023-12-08 18:29:17 +00:00
<div className="justify-self-end min-w-[220px] flex justify-end gap-2">
2023-11-10 17:17:30 +00:00
{/* Dropdown component should be here */}
2023-12-23 11:42:46 +00:00
<SitesCombobox siteList={sites} onSiteSelect={setSelectedSite} />
2023-11-10 17:17:30 +00:00
{/* Dropdown component should be here */}
2023-12-23 12:10:18 +00:00
<Select
defaultValue={"0"}
onValueChange={(v) => setSelectedTime(v)}
>
2023-12-08 18:29:17 +00:00
<SelectTrigger className="hover:bg-muted w-auto">
2023-12-18 03:18:17 +00:00
<SelectValue placeholder="Time ago" />
2023-12-08 18:29:17 +00:00
</SelectTrigger>
<SelectContent>
{FILTER_TIMES.map((v) => (
2023-12-18 03:18:17 +00:00
<SelectItem
value={String(v.value)}
key={`FilteTimeSelectItem_${v.value}`}
>
{v.label}
</SelectItem>
2023-12-08 18:29:17 +00:00
))}
</SelectContent>
</Select>
2023-11-10 17:17:30 +00:00
</div>
)}
</form>
{results.length > 0 && (
<>
<hr className="my-4 border-1" />
{results.map((item) => (
<Link
2023-12-18 03:18:17 +00:00
to={`/article/${item.slug}`}
2023-11-10 17:17:30 +00:00
key={item.id}
className="flex cursor-pointer flex-row items-center space-x-5 my-2 py-2 px-4 hover:bg-gray-800 rounded-md"
>
<span className="text-sm text-gray-400">
{formatDate(item.timestamp)}
</span>
<h3 className="text-md font-semibold text-white">{item.title}</h3>
</Link>
2023-11-10 17:17:30 +00:00
))}
2023-12-18 03:18:17 +00:00
<Link to={`/search?q=${encodeURIComponent(query)}`}>
2023-11-14 15:52:42 +00:00
<button className="mt-4 flex justify-center items-center bg-secondary w-full py-7 text-white hover:bg-teal-800 transition-colors">
{results.length}+ search results for{" "}
<span className="text-blue-300 ml-1">{query}</span>
2023-11-14 15:52:42 +00:00
<ChevronRightIcon className="w-5 h-5 inline ml-2 mt-[1px]" />
2023-11-10 17:17:30 +00:00
</button>
2023-11-14 15:52:42 +00:00
</Link>
2023-11-10 17:17:30 +00:00
</>
)}
</div>
2023-12-18 03:18:17 +00:00
);
};
2023-11-10 17:17:30 +00:00
// Placeholder components for Shadcn
const LoadingComponent = () => {
// Replace with actual Shadcn Loading component
2023-12-18 03:18:17 +00:00
return <div>Loading...</div>;
};
2023-11-10 17:17:30 +00:00
2023-12-18 03:18:17 +00:00
export default SearchBar;