Day06 要做的是動態搜尋功能,是個可以偵測使用者輸入字串的篩選器
資料可以由 fetch 遠端取得
searchQuery 用來儲存搜尋字串
suggestions 是建議
const URL =
"https://gist.githubusercontent.com/Miserlou/c5cd8364bf9b2420bb29/raw/2bf258763cdddd704f8ffd3ea9a3e81d25e2c6f6/cities.json";
type City = {
city: string;
growth_from_2000_to_2013: string;
latitude: number;
longitude: number;
population: string;
rank: string;
state: string;
};
const [cities, setCities] = useState<City[]>(() => {
fetch(URL)
.then((res) => res.json())
.then((data: City[]) => setCities(data))
.catch((error) => console.log("Failed to fetch cities:", error));
return [];
});
const [searchQuery, setSearchQuery] = useState<string>("");
const [suggestions, setSuggestions] = useState<City[]>([]);
if (cities.length === 0) {
return <div>Loading...</div>;
}
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>): void => {
const value = e.target.value;
setSearchQuery(value);
if (!value) {
setSuggestions([]);
return;
}
const matches = cities.filter((place) => {
const regex = new RegExp(value, "gi");
return place.city.match(regex) || place.state.match(regex);
});
setSuggestions(matches);
};
const evenItemStyle: React.CSSProperties = {
background: "linear-gradient(to bottom, #ffffff 0%, #EFEFEF 100%)",
transform: "perspective(100px) rotateX(3deg) translateY(2px) scale(1.001)",
};
const oddItemStyle: React.CSSProperties = {
background: "linear-gradient(to top, #ffffff 0%, #EFEFEF 100%)",
transform: "perspective(100px) rotateX(-3deg) translateY(3px)",
};
return (
<div className="box-border bg-gradient-to-r from-yellow-300 via-yellow-400 to-orange-400 text-xl font-light min-h-screen py-10">
<form className="max-w-[320px] mx-auto">
<input
type="text"
className="w-[120%] -left-[10%] relative top-2 z-10 p-5 m-0 text-center outline-none border-[10px] border-gray-100 rounded-[5px] text-4xl shadow-[0_0_5px_rgba(0,0,0,0.12),inset_0_0_2px_rgba(0,0,0,0.19)]"
placeholder="City or State"
value={searchQuery}
onChange={handleInputChange}
/>
<ul className="m-0 p-0 relative">
{!suggestions.length && !searchQuery && (
<>
<li
className="bg-white list-none border-b border-gray-300 shadow-[0_0_10px_rgba(0,0,0,0.14)] m-0 p-5 transition-colors duration-200 flex justify-between capitalize"
style={evenItemStyle}
>
Filter for a city
</li>
<li
className="bg-white list-none border-b border-gray-300 shadow-[0_0_10px_rgba(0,0,0,0.14)] m-0 p-5 transition-colors duration-200 flex justify-between capitalize"
style={oddItemStyle}
>
or a state
</li>
</>
)}
{suggestions.map((place, index) => (
<li
key={place.city}
className="bg-white list-none border-b border-gray-300 shadow-[0_0_10px_rgba(0,0,0,0.14)] m-0 p-5 transition-colors duration-200 flex justify-between capitalize"
style={index % 2 === 0 ? evenItemStyle : oddItemStyle}
>
<span>
{highlightMatch(`${place.city}, ${place.state}`, searchQuery)}
</span>
<span className="text-sm">
{numberWithCommas(place.population)}
</span>
</li>
))}
</ul>
</form>
</div>
);
const numberWithCommas = (x: string): string =>
String(x).replace(/\B(?=(\d{3})+(?!\d))/g, ",");
const highlightMatch = (
text: string,
match: string
): (string | JSX.Element)[] => {
const regex = new RegExp(`(${match})`, "gi");
return text.split(regex).map((part) =>
regex.test(part) ? (
<span key={part} className="bg-yellow-400">
{part}
</span>
) : (
part
)
);
};
const regex = new RegExp(value, "gi");
const numberWithCommas = (x: string): string =>
String(x).replace(/\B(?=(\d{3})+(?!\d))/g, ",");
transform: "perspective(100px) rotateX(-3deg) translateY(3px)",