FarmerHelp / frontend /src /components /apmc /CommoditySearch.jsx
Gankit12's picture
Res
87e0b5a
Raw
History Blame Contribute Delete
9.98 kB
/**
* CommoditySearch - Commodity selection with search, recent history, and quick picks.
*
* Features:
* - Searchable dropdown for commodity selection
* - Recent searches stored in localStorage (up to 5)
* - Popular commodities quick-select chips with emojis
*/
import { useState, useCallback, useRef, useEffect } from "react";
import PropTypes from "prop-types";
import {
MagnifyingGlassIcon,
ClockIcon,
} from "@heroicons/react/24/outline";
import storage from "@utils/storage";
const RECENT_KEY = "farmhelp_recent_commodities";
const MAX_RECENT = 5;
// Commodity emojis mapping
const COMMODITY_EMOJIS = {
Wheat: "\uD83C\uDF3E",
Rice: "\uD83C\uDF5A",
Paddy: "\uD83C\uDF3E",
Cotton: "\uD83E\uDDF6",
Onion: "\uD83E\uDDC5",
Potato: "\uD83E\uDD54",
Tomato: "\uD83C\uDF45",
Soyabean: "\uD83C\uDF31",
Maize: "\uD83C\uDF3D",
Corn: "\uD83C\uDF3D",
Groundnut: "\uD83E\uDD5C",
Chilli: "\uD83C\uDF36\uFE0F",
Banana: "\uD83C\uDF4C",
Apple: "\uD83C\uDF4E",
Mango: "\uD83E\uDD6D",
Grapes: "\uD83C\uDF47",
Orange: "\uD83C\uDF4A",
Cabbage: "\uD83E\uDD6C",
Carrot: "\uD83E\uDD55",
Cauliflower: "\uD83E\uDD66",
Garlic: "\uD83E\uDDC4",
Ginger: "\uD83E\uDED0",
Lemon: "\uD83C\uDF4B",
Peas: "\uD83E\uDED1",
Brinjal: "\uD83C\uDF46",
Cucumber: "\uD83E\uDD52",
Spinach: "\uD83E\uDD6C",
Mustard: "\uD83C\uDF3B",
Bajra: "\uD83C\uDF3E",
Jowar: "\uD83C\uDF3E",
Barley: "\uD83C\uDF3E",
Pulses: "\uD83E\uDED8",
Oilseeds: "\uD83C\uDF3B",
Millets: "\uD83C\uDF3E",
Cumin: "\uD83E\uDED2",
Coriander: "\uD83C\uDF3F",
Turmeric: "\uD83D\uDFE1",
Coconut: "\uD83E\uDD65",
Arecanut: "\uD83C\uDF30",
Cashew: "\uD83C\uDF30",
Coffee: "\u2615",
Tea: "\uD83C\uDF75",
Pepper: "\uD83C\uDF36\uFE0F",
Cardamom: "\uD83C\uDF3F",
Jute: "\uD83E\uDDF6",
Tobacco: "\uD83C\uDF43",
};
// Get emoji for a commodity (with fallback)
function getCommodityEmoji(commodity) {
return COMMODITY_EMOJIS[commodity] || "\uD83C\uDF3F";
}
const POPULAR_COMMODITIES = [
"Wheat",
"Rice",
"Cotton",
"Onion",
"Potato",
"Tomato",
"Soyabean",
"Groundnut",
];
function getRecentCommodities() {
return storage.get(RECENT_KEY, []);
}
function saveRecentCommodity(commodity) {
const recent = getRecentCommodities().filter((c) => c !== commodity);
recent.unshift(commodity);
storage.set(RECENT_KEY, recent.slice(0, MAX_RECENT));
}
function CommoditySearch({
commodities = [],
value = "",
onSelect,
loading = false,
}) {
const [query, setQuery] = useState("");
const [isOpen, setIsOpen] = useState(false);
const containerRef = useRef(null);
const inputRef = useRef(null);
const recentCommodities = getRecentCommodities();
// Close on outside click
useEffect(() => {
function handleClickOutside(e) {
if (containerRef.current && !containerRef.current.contains(e.target)) {
setIsOpen(false);
}
}
document.addEventListener("mousedown", handleClickOutside);
return () => document.removeEventListener("mousedown", handleClickOutside);
}, []);
// Build commodity names from the list or use popular as fallback
const commodityNames = commodities.length > 0
? commodities.map((c) => (typeof c === "string" ? c : c.commodity || c.name))
: POPULAR_COMMODITIES;
const filtered = query
? commodityNames.filter((name) =>
name.toLowerCase().includes(query.toLowerCase()),
)
: commodityNames;
const handleSelect = useCallback(
(commodity) => {
saveRecentCommodity(commodity);
setQuery("");
setIsOpen(false);
onSelect(commodity);
},
[onSelect],
);
const handleInputChange = useCallback((e) => {
setQuery(e.target.value);
setIsOpen(true);
}, []);
const handleFocus = useCallback(() => {
setIsOpen(true);
}, []);
const handleKeyDown = useCallback(
(e) => {
if (e.key === "Enter" && filtered.length > 0) {
handleSelect(filtered[0]);
}
if (e.key === "Escape") {
setIsOpen(false);
}
},
[filtered, handleSelect],
);
return (
<div className="space-y-3">
{/* Search input */}
<div ref={containerRef} className="relative">
<label
htmlFor="commodity-search"
className="block text-sm font-medium text-neutral-700 mb-1"
>
Select Commodity
</label>
<div className="relative">
<div className="pointer-events-none absolute inset-y-0 left-0 flex items-center pl-3 text-neutral-400">
<MagnifyingGlassIcon className="h-4 w-4" />
</div>
<input
ref={inputRef}
id="commodity-search"
type="text"
value={query}
onChange={handleInputChange}
onFocus={handleFocus}
onKeyDown={handleKeyDown}
placeholder={value || "Search commodity (e.g. Wheat, Rice)"}
disabled={loading}
autoComplete="off"
className={[
"block w-full rounded-lg border px-3 py-2 pl-10 text-sm text-neutral-900",
"placeholder:text-neutral-400",
"transition-colors duration-150",
"focus:outline-none focus:ring-2 focus:ring-primary-500 focus:border-primary-500",
"border-neutral-300",
"disabled:bg-neutral-100 disabled:cursor-not-allowed",
].join(" ")}
/>
{value && (
<div className="absolute inset-y-0 right-0 flex items-center pr-3">
<span className="text-xs font-medium text-primary-600 bg-primary-50 px-2 py-0.5 rounded flex items-center gap-1">
<span role="img" aria-hidden="true">{getCommodityEmoji(value)}</span>
{value}
</span>
</div>
)}
</div>
{/* Dropdown */}
{isOpen && (
<div className="absolute z-20 mt-1 w-full rounded-lg border border-neutral-200 bg-white shadow-lg max-h-64 overflow-hidden">
{/* Recent searches */}
{!query && recentCommodities.length > 0 && (
<div className="border-b border-neutral-100">
<div className="flex items-center gap-1.5 px-3 py-2 text-xs font-medium text-neutral-500">
<ClockIcon className="h-3.5 w-3.5" />
Recent
</div>
<ul>
{recentCommodities.map((c) => (
<li key={`recent-${c}`}>
<button
type="button"
onClick={() => handleSelect(c)}
className={[
"flex w-full items-center gap-2 px-3 py-2 text-sm",
"hover:bg-primary-50 hover:text-primary-700 transition-colors",
c === value
? "bg-primary-50 text-primary-700 font-medium"
: "text-neutral-700",
].join(" ")}
>
<span className="text-base" role="img" aria-hidden="true">
{getCommodityEmoji(c)}
</span>
{c}
</button>
</li>
))}
</ul>
</div>
)}
{/* Filtered list */}
<ul className="max-h-48 overflow-y-auto py-1">
{filtered.length === 0 && (
<li className="px-3 py-3 text-sm text-neutral-500 text-center">
No commodities found
</li>
)}
{filtered.map((name) => (
<li key={name}>
<button
type="button"
onClick={() => handleSelect(name)}
className={[
"flex w-full items-center gap-2 px-3 py-2 text-sm",
"hover:bg-primary-50 hover:text-primary-700 transition-colors",
name === value
? "bg-primary-50 text-primary-700 font-medium"
: "text-neutral-700",
].join(" ")}
>
<span className="text-base" role="img" aria-hidden="true">
{getCommodityEmoji(name)}
</span>
{name}
</button>
</li>
))}
</ul>
</div>
)}
</div>
{/* Popular commodities quick-select with emojis */}
<div>
<p className="text-xs font-medium text-neutral-500 mb-1.5">
Popular Commodities
</p>
<div className="flex flex-wrap gap-2">
{POPULAR_COMMODITIES.map((commodity) => (
<button
key={commodity}
type="button"
onClick={() => handleSelect(commodity)}
disabled={loading}
className={[
"px-3 py-1.5 rounded-full text-xs font-medium transition-colors",
"border flex items-center gap-1.5",
"disabled:opacity-50 disabled:cursor-not-allowed",
commodity === value
? "bg-secondary-100 border-secondary-300 text-secondary-800"
: "bg-white border-neutral-200 text-neutral-600 hover:bg-secondary-50 hover:border-secondary-300 hover:text-secondary-700",
].join(" ")}
>
<span className="text-sm" role="img" aria-hidden="true">
{getCommodityEmoji(commodity)}
</span>
{commodity}
</button>
))}
</div>
</div>
</div>
);
}
CommoditySearch.propTypes = {
commodities: PropTypes.array,
value: PropTypes.string,
onSelect: PropTypes.func.isRequired,
loading: PropTypes.bool,
};
export default CommoditySearch;