feat: enhance ActionSearchBar and TopControl with improved cursor handling and add scroll suggestions
This commit is contained in:
parent
d807942688
commit
1b5fc21731
|
@ -6,6 +6,7 @@ import { motion, AnimatePresence } from "framer-motion"
|
|||
import { Search, Send, BarChart2, Globe, Video, PlaneTakeoff, AudioLines, X } from "lucide-react"
|
||||
import { cn } from "../_lib/utils"
|
||||
import useDebounce from "../_hooks/use-debounce"
|
||||
import { toast } from "sonner"
|
||||
|
||||
export interface Action {
|
||||
id: string
|
||||
|
@ -132,8 +133,14 @@ const ActionSearchBar = forwardRef<HTMLInputElement, ActionSearchBarProps>(
|
|||
useEffect(() => {
|
||||
if (autoFocus && inputRef.current) {
|
||||
inputRef.current.focus()
|
||||
|
||||
// Position cursor at the end of any prefix
|
||||
if (activeAction?.prefix) {
|
||||
inputRef.current.selectionStart = activeAction.prefix.length;
|
||||
inputRef.current.selectionEnd = activeAction.prefix.length;
|
||||
}
|
||||
}
|
||||
}, [autoFocus])
|
||||
}, [autoFocus, activeAction])
|
||||
|
||||
useEffect(() => {
|
||||
if (!debouncedQuery) {
|
||||
|
@ -177,6 +184,17 @@ const ActionSearchBar = forwardRef<HTMLInputElement, ActionSearchBarProps>(
|
|||
} else {
|
||||
setQuery(valueWithPrefix);
|
||||
}
|
||||
|
||||
// Restore cursor position after prefix
|
||||
setTimeout(() => {
|
||||
if (!inputRef.current || !activeAction.prefix) {
|
||||
return toast.error("Error: Input ref is null or prefix is undefined");
|
||||
}
|
||||
|
||||
inputRef.current.selectionStart = activeAction.prefix.length;
|
||||
inputRef.current.selectionEnd = activeAction.prefix.length;
|
||||
}, 0);
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -32,16 +32,51 @@ import { IconAnalyze, IconMessage } from "@tabler/icons-react"
|
|||
import { FloatingActionSearchBar } from "../../floating-action-search-bar"
|
||||
import { format } from 'date-fns'
|
||||
import { Card } from "@/app/_components/ui/card"
|
||||
import { ICrimes } from "@/app/_utils/types/crimes"
|
||||
|
||||
// Sample crime data for suggestions
|
||||
// Expanded sample crime data with more entries for testing
|
||||
const SAMPLE_CRIME_DATA = [
|
||||
{ id: "CR-12345-2023", description: "Robbery at Main Street" },
|
||||
{ id: "CR-23456-2023", description: "Assault in Central Park" },
|
||||
{ id: "CI-7890-2023", description: "Burglary report at Downtown" },
|
||||
{ id: "CI-4567-2024", description: "Vandalism at City Hall" },
|
||||
{ id: "CI-4167-2024", description: "Graffiti at Public Library" },
|
||||
{ id: "CI-4067-2024", description: "Property damage at School" },
|
||||
{ id: "CR-34567-2024", description: "Car theft on 5th Avenue" },
|
||||
{ id: "CR-34517-2024", description: "Mugging at Central Station" },
|
||||
{ id: "CR-14517-2024", description: "Shoplifting at Mall" },
|
||||
{ id: "CR-24517-2024", description: "Break-in at Office Building" },
|
||||
// Add more entries for testing (up to 100)
|
||||
// ...more sample entries...
|
||||
];
|
||||
|
||||
// Generate additional sample data for testing scrolling
|
||||
const generateSampleData = () => {
|
||||
const additionalData = [];
|
||||
for (let i = 1; i <= 90; i++) {
|
||||
// Mix of crime and incident IDs
|
||||
const prefix = i % 2 === 0 ? "CR-" : "CI-";
|
||||
const id = `${prefix}${10000 + i}-${2022 + i % 3}`;
|
||||
const descriptions = [
|
||||
"Theft at residence",
|
||||
"Traffic violation",
|
||||
"Noise complaint",
|
||||
"Suspicious activity",
|
||||
"Drug related incident",
|
||||
"Vandalism of public property",
|
||||
"Illegal parking",
|
||||
"Public disturbance",
|
||||
"Domestic dispute",
|
||||
"Assault case"
|
||||
];
|
||||
const description = descriptions[i % descriptions.length];
|
||||
additionalData.push({ id, description: `${description} #${i}` });
|
||||
}
|
||||
return [...SAMPLE_CRIME_DATA, ...additionalData];
|
||||
}
|
||||
|
||||
const EXPANDED_SAMPLE_DATA = generateSampleData();
|
||||
|
||||
const ACTIONS = [
|
||||
{
|
||||
id: "crime_id",
|
||||
|
@ -136,6 +171,7 @@ export default function TopControl({
|
|||
setSelectedCategory,
|
||||
availableYears = [2022, 2023, 2024],
|
||||
categories = [],
|
||||
|
||||
}: TopControlProps) {
|
||||
const [showSelectors, setShowSelectors] = useState(false)
|
||||
const [showSearch, setShowSearch] = useState(false)
|
||||
|
@ -177,23 +213,33 @@ export default function TopControl({
|
|||
const selectedAction = ACTIONS.find(action => action.id === actionId);
|
||||
if (selectedAction) {
|
||||
setSelectedSearchType(actionId);
|
||||
setSearchValue(selectedAction.prefix || "");
|
||||
|
||||
const prefix = selectedAction.prefix || "";
|
||||
setSearchValue(prefix);
|
||||
setIsInputValid(true);
|
||||
if (selectedAction.prefix) {
|
||||
const initialSuggestions = SAMPLE_CRIME_DATA.filter(item => {
|
||||
if (actionId === 'crime_id' && item.id.startsWith('CR-')) {
|
||||
return true;
|
||||
} else if (actionId === 'incident_id' && item.id.startsWith('CI-')) {
|
||||
return true;
|
||||
} else if (actionId === 'description') {
|
||||
return true;
|
||||
} else if (actionId === 'address') {
|
||||
return true;
|
||||
}
|
||||
|
||||
const initialSuggestions = EXPANDED_SAMPLE_DATA.filter(item => {
|
||||
if (actionId === 'crime_id' && item.id.startsWith('CR-')) {
|
||||
return true;
|
||||
} else if (actionId === 'incident_id' && item.id.startsWith('CI-')) {
|
||||
return true;
|
||||
} else if (actionId === 'description' || actionId === 'address') {
|
||||
return true;
|
||||
} else if (actionId === 'coordinates') {
|
||||
return false;
|
||||
});
|
||||
setSuggestions(initialSuggestions.slice(0, 5));
|
||||
}
|
||||
}
|
||||
return false;
|
||||
});
|
||||
|
||||
setSuggestions(initialSuggestions);
|
||||
|
||||
setTimeout(() => {
|
||||
if (searchInputRef.current) {
|
||||
searchInputRef.current.focus();
|
||||
searchInputRef.current.selectionStart = prefix.length;
|
||||
searchInputRef.current.selectionEnd = prefix.length;
|
||||
}
|
||||
}, 50);
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -248,7 +294,7 @@ export default function TopControl({
|
|||
const currentSearchType = selectedSearchType ?
|
||||
ACTIONS.find(action => action.id === selectedSearchType) : null;
|
||||
|
||||
if (currentSearchType?.prefix && value) {
|
||||
if (currentSearchType?.prefix && currentSearchType.prefix.length > 0) {
|
||||
if (!value.startsWith(currentSearchType.prefix)) {
|
||||
value = currentSearchType.prefix;
|
||||
}
|
||||
|
@ -266,39 +312,55 @@ export default function TopControl({
|
|||
setIsInputValid(true);
|
||||
}
|
||||
|
||||
if (currentSearchType) {
|
||||
if (!value || value === currentSearchType.prefix) {
|
||||
const initialSuggestions = SAMPLE_CRIME_DATA.filter(item => {
|
||||
if (currentSearchType.id === 'crime_id' && item.id.startsWith('CR-')) {
|
||||
return true;
|
||||
} else if (currentSearchType.id === 'incident_id' && item.id.startsWith('CI-')) {
|
||||
return true;
|
||||
} else if (currentSearchType.id === 'description') {
|
||||
return true;
|
||||
} else if (currentSearchType.id === 'address') {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
setSuggestions(initialSuggestions.slice(0, 5));
|
||||
} else {
|
||||
const filteredSuggestions = SAMPLE_CRIME_DATA.filter(item => {
|
||||
if (currentSearchType.id === 'crime_id' && item.id.startsWith('CR-')) {
|
||||
return item.id.toLowerCase().includes(value.toLowerCase());
|
||||
} else if (currentSearchType.id === 'incident_id' && item.id.startsWith('CI-')) {
|
||||
return item.id.toLowerCase().includes(value.toLowerCase());
|
||||
} else if (currentSearchType.id === 'description') {
|
||||
return item.description.toLowerCase().includes(value.toLowerCase());
|
||||
} else if (currentSearchType.id === 'address') {
|
||||
return item.description.toLowerCase().includes(value.toLowerCase());
|
||||
}
|
||||
return false;
|
||||
});
|
||||
setSuggestions(filteredSuggestions);
|
||||
}
|
||||
} else {
|
||||
if (!selectedSearchType) {
|
||||
setSuggestions([]);
|
||||
return;
|
||||
}
|
||||
|
||||
let filteredSuggestions: Array<{ id: string, description: string }> = [];
|
||||
const searchText = value.toLowerCase();
|
||||
|
||||
if (currentSearchType?.id === 'crime_id') {
|
||||
filteredSuggestions = EXPANDED_SAMPLE_DATA.filter(item =>
|
||||
item.id.startsWith('CR-') &&
|
||||
(item.id.toLowerCase().includes(searchText) ||
|
||||
item.description.toLowerCase().includes(searchText))
|
||||
);
|
||||
}
|
||||
else if (currentSearchType?.id === 'incident_id') {
|
||||
filteredSuggestions = EXPANDED_SAMPLE_DATA.filter(item =>
|
||||
item.id.startsWith('CI-') &&
|
||||
(item.id.toLowerCase().includes(searchText) ||
|
||||
item.description.toLowerCase().includes(searchText))
|
||||
);
|
||||
}
|
||||
else if (currentSearchType?.id === 'description') {
|
||||
filteredSuggestions = EXPANDED_SAMPLE_DATA.filter(item =>
|
||||
item.description.toLowerCase().includes(searchText)
|
||||
);
|
||||
}
|
||||
else if (currentSearchType?.id === 'address') {
|
||||
filteredSuggestions = EXPANDED_SAMPLE_DATA.filter(item =>
|
||||
item.description.toLowerCase().includes(searchText)
|
||||
);
|
||||
}
|
||||
else if (currentSearchType?.id === 'coordinates') {
|
||||
filteredSuggestions = [];
|
||||
}
|
||||
|
||||
if (!value || (currentSearchType?.prefix && value === currentSearchType.prefix)) {
|
||||
if (currentSearchType?.id === 'crime_id') {
|
||||
filteredSuggestions = EXPANDED_SAMPLE_DATA.filter(item => item.id.startsWith('CR-'));
|
||||
}
|
||||
else if (currentSearchType?.id === 'incident_id') {
|
||||
filteredSuggestions = EXPANDED_SAMPLE_DATA.filter(item => item.id.startsWith('CI-'));
|
||||
}
|
||||
else if (currentSearchType?.id === 'description' || currentSearchType?.id === 'address') {
|
||||
filteredSuggestions = EXPANDED_SAMPLE_DATA;
|
||||
}
|
||||
}
|
||||
|
||||
setSuggestions(filteredSuggestions);
|
||||
};
|
||||
|
||||
const toggleSelectors = () => {
|
||||
|
@ -517,18 +579,35 @@ export default function TopControl({
|
|||
)}
|
||||
|
||||
{suggestions.length > 0 && (
|
||||
<div className="mt-2 max-h-60 overflow-y-auto border border-border rounded-md bg-background/80">
|
||||
<div className="mt-2 max-h-[300px] overflow-y-auto border border-border rounded-md bg-background/80 scrollbar-thin scrollbar-thumb-gray-300 scrollbar-track-transparent">
|
||||
<div className="sticky top-0 bg-muted/70 backdrop-blur-sm px-3 py-2 border-b border-border">
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{suggestions.length} results found
|
||||
</p>
|
||||
</div>
|
||||
<ul className="py-1">
|
||||
{suggestions.map((item, index) => (
|
||||
<li
|
||||
key={index}
|
||||
className="px-3 py-2 hover:bg-muted cursor-pointer flex justify-between items-center"
|
||||
className="px-3 py-2 hover:bg-muted cursor-pointer flex items-center justify-between"
|
||||
onClick={() => handleSuggestionSelect(item)}
|
||||
>
|
||||
<span className="font-medium">{item.id}</span>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-muted-foreground text-sm">{item.description}</span>
|
||||
<Info className="h-4 w-4 text-muted-foreground" />
|
||||
<span className="text-muted-foreground text-sm truncate max-w-[300px]">
|
||||
{item.description}
|
||||
</span>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="h-6 w-6 p-0 rounded-full hover:bg-muted"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handleSuggestionSelect(item);
|
||||
}}
|
||||
>
|
||||
<Info className="h-3.5 w-3.5 text-muted-foreground" />
|
||||
</Button>
|
||||
</div>
|
||||
</li>
|
||||
))}
|
||||
|
@ -536,9 +615,7 @@ export default function TopControl({
|
|||
</div>
|
||||
)}
|
||||
|
||||
{searchValue.length > 0 &&
|
||||
searchValue !== (ACTIONS.find(a => a.id === selectedSearchType)?.prefix || '') &&
|
||||
selectedSearchType &&
|
||||
{searchValue.length > (selectedSearchType && ACTIONS.find(a => a.id === selectedSearchType)?.prefix?.length || 0) &&
|
||||
suggestions.length === 0 && (
|
||||
<div className="mt-2 p-3 border border-border rounded-md bg-background/80 text-center">
|
||||
<p className="text-sm text-muted-foreground">No matching incidents found</p>
|
||||
|
@ -566,14 +643,14 @@ export default function TopControl({
|
|||
<Card className="p-4 border border-border">
|
||||
<div className="flex justify-between items-start mb-4">
|
||||
<h3 className="text-lg font-semibold">{selectedSuggestion?.id}</h3>
|
||||
<Button
|
||||
{/* <Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={handleCloseInfoBox}
|
||||
className="h-6 w-6 p-0 rounded-full"
|
||||
>
|
||||
<XCircle size={16} />
|
||||
</Button>
|
||||
</Button> */}
|
||||
</div>
|
||||
|
||||
{selectedSuggestion && (
|
||||
|
|
Loading…
Reference in New Issue