feat: enhance ActionSearchBar and TopControl with improved cursor handling and add scroll suggestions

This commit is contained in:
vergiLgood1 2025-05-04 09:21:46 +07:00
parent d807942688
commit 1b5fc21731
2 changed files with 153 additions and 58 deletions

View File

@ -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;
}
}

View File

@ -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 && (