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 { Search, Send, BarChart2, Globe, Video, PlaneTakeoff, AudioLines, X } from "lucide-react"
|
||||||
import { cn } from "../_lib/utils"
|
import { cn } from "../_lib/utils"
|
||||||
import useDebounce from "../_hooks/use-debounce"
|
import useDebounce from "../_hooks/use-debounce"
|
||||||
|
import { toast } from "sonner"
|
||||||
|
|
||||||
export interface Action {
|
export interface Action {
|
||||||
id: string
|
id: string
|
||||||
|
@ -132,8 +133,14 @@ const ActionSearchBar = forwardRef<HTMLInputElement, ActionSearchBarProps>(
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (autoFocus && inputRef.current) {
|
if (autoFocus && inputRef.current) {
|
||||||
inputRef.current.focus()
|
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(() => {
|
useEffect(() => {
|
||||||
if (!debouncedQuery) {
|
if (!debouncedQuery) {
|
||||||
|
@ -177,6 +184,17 @@ const ActionSearchBar = forwardRef<HTMLInputElement, ActionSearchBarProps>(
|
||||||
} else {
|
} else {
|
||||||
setQuery(valueWithPrefix);
|
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;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -32,16 +32,51 @@ import { IconAnalyze, IconMessage } from "@tabler/icons-react"
|
||||||
import { FloatingActionSearchBar } from "../../floating-action-search-bar"
|
import { FloatingActionSearchBar } from "../../floating-action-search-bar"
|
||||||
import { format } from 'date-fns'
|
import { format } from 'date-fns'
|
||||||
import { Card } from "@/app/_components/ui/card"
|
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 = [
|
const SAMPLE_CRIME_DATA = [
|
||||||
{ id: "CR-12345-2023", description: "Robbery at Main Street" },
|
{ id: "CR-12345-2023", description: "Robbery at Main Street" },
|
||||||
{ id: "CR-23456-2023", description: "Assault in Central Park" },
|
{ id: "CR-23456-2023", description: "Assault in Central Park" },
|
||||||
{ id: "CI-7890-2023", description: "Burglary report at Downtown" },
|
{ id: "CI-7890-2023", description: "Burglary report at Downtown" },
|
||||||
{ id: "CI-4567-2024", description: "Vandalism at City Hall" },
|
{ 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-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 = [
|
const ACTIONS = [
|
||||||
{
|
{
|
||||||
id: "crime_id",
|
id: "crime_id",
|
||||||
|
@ -136,6 +171,7 @@ export default function TopControl({
|
||||||
setSelectedCategory,
|
setSelectedCategory,
|
||||||
availableYears = [2022, 2023, 2024],
|
availableYears = [2022, 2023, 2024],
|
||||||
categories = [],
|
categories = [],
|
||||||
|
|
||||||
}: TopControlProps) {
|
}: TopControlProps) {
|
||||||
const [showSelectors, setShowSelectors] = useState(false)
|
const [showSelectors, setShowSelectors] = useState(false)
|
||||||
const [showSearch, setShowSearch] = useState(false)
|
const [showSearch, setShowSearch] = useState(false)
|
||||||
|
@ -177,23 +213,33 @@ export default function TopControl({
|
||||||
const selectedAction = ACTIONS.find(action => action.id === actionId);
|
const selectedAction = ACTIONS.find(action => action.id === actionId);
|
||||||
if (selectedAction) {
|
if (selectedAction) {
|
||||||
setSelectedSearchType(actionId);
|
setSelectedSearchType(actionId);
|
||||||
setSearchValue(selectedAction.prefix || "");
|
|
||||||
|
const prefix = selectedAction.prefix || "";
|
||||||
|
setSearchValue(prefix);
|
||||||
setIsInputValid(true);
|
setIsInputValid(true);
|
||||||
if (selectedAction.prefix) {
|
|
||||||
const initialSuggestions = SAMPLE_CRIME_DATA.filter(item => {
|
const initialSuggestions = EXPANDED_SAMPLE_DATA.filter(item => {
|
||||||
if (actionId === 'crime_id' && item.id.startsWith('CR-')) {
|
if (actionId === 'crime_id' && item.id.startsWith('CR-')) {
|
||||||
return true;
|
return true;
|
||||||
} else if (actionId === 'incident_id' && item.id.startsWith('CI-')) {
|
} else if (actionId === 'incident_id' && item.id.startsWith('CI-')) {
|
||||||
return true;
|
return true;
|
||||||
} else if (actionId === 'description') {
|
} else if (actionId === 'description' || actionId === 'address') {
|
||||||
return true;
|
return true;
|
||||||
} else if (actionId === 'address') {
|
} else if (actionId === 'coordinates') {
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return false;
|
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 ?
|
const currentSearchType = selectedSearchType ?
|
||||||
ACTIONS.find(action => action.id === selectedSearchType) : null;
|
ACTIONS.find(action => action.id === selectedSearchType) : null;
|
||||||
|
|
||||||
if (currentSearchType?.prefix && value) {
|
if (currentSearchType?.prefix && currentSearchType.prefix.length > 0) {
|
||||||
if (!value.startsWith(currentSearchType.prefix)) {
|
if (!value.startsWith(currentSearchType.prefix)) {
|
||||||
value = currentSearchType.prefix;
|
value = currentSearchType.prefix;
|
||||||
}
|
}
|
||||||
|
@ -266,39 +312,55 @@ export default function TopControl({
|
||||||
setIsInputValid(true);
|
setIsInputValid(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (currentSearchType) {
|
if (!selectedSearchType) {
|
||||||
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 {
|
|
||||||
setSuggestions([]);
|
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 = () => {
|
const toggleSelectors = () => {
|
||||||
|
@ -517,18 +579,35 @@ export default function TopControl({
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{suggestions.length > 0 && (
|
{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">
|
<ul className="py-1">
|
||||||
{suggestions.map((item, index) => (
|
{suggestions.map((item, index) => (
|
||||||
<li
|
<li
|
||||||
key={index}
|
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)}
|
onClick={() => handleSuggestionSelect(item)}
|
||||||
>
|
>
|
||||||
<span className="font-medium">{item.id}</span>
|
<span className="font-medium">{item.id}</span>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<span className="text-muted-foreground text-sm">{item.description}</span>
|
<span className="text-muted-foreground text-sm truncate max-w-[300px]">
|
||||||
<Info className="h-4 w-4 text-muted-foreground" />
|
{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>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
))}
|
))}
|
||||||
|
@ -536,9 +615,7 @@ export default function TopControl({
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{searchValue.length > 0 &&
|
{searchValue.length > (selectedSearchType && ACTIONS.find(a => a.id === selectedSearchType)?.prefix?.length || 0) &&
|
||||||
searchValue !== (ACTIONS.find(a => a.id === selectedSearchType)?.prefix || '') &&
|
|
||||||
selectedSearchType &&
|
|
||||||
suggestions.length === 0 && (
|
suggestions.length === 0 && (
|
||||||
<div className="mt-2 p-3 border border-border rounded-md bg-background/80 text-center">
|
<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>
|
<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">
|
<Card className="p-4 border border-border">
|
||||||
<div className="flex justify-between items-start mb-4">
|
<div className="flex justify-between items-start mb-4">
|
||||||
<h3 className="text-lg font-semibold">{selectedSuggestion?.id}</h3>
|
<h3 className="text-lg font-semibold">{selectedSuggestion?.id}</h3>
|
||||||
<Button
|
{/* <Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="sm"
|
size="sm"
|
||||||
onClick={handleCloseInfoBox}
|
onClick={handleCloseInfoBox}
|
||||||
className="h-6 w-6 p-0 rounded-full"
|
className="h-6 w-6 p-0 rounded-full"
|
||||||
>
|
>
|
||||||
<XCircle size={16} />
|
<XCircle size={16} />
|
||||||
</Button>
|
</Button> */}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{selectedSuggestion && (
|
{selectedSuggestion && (
|
||||||
|
|
Loading…
Reference in New Issue