pmtool / src /components /custom /multi-team-selector.tsx
devarshia5's picture
Upload 487 files
d97b8f9 verified
Raw
History Blame Contribute Delete
6.34 kB
import React, { useState, useRef, useEffect } from "react";
import { ChevronsUpDown, Plus, Search, X } from "lucide-react";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Avatar, AvatarFallback } from "@/components/ui/avatar";
import { Badge } from "@/components/ui/badge";
import { cn } from "@/lib/utils";
import { Team } from "@/types";
interface MultiTeamSelectorProps {
teams: Team[];
selectedTeams: Team[];
onSelect: (team: Team) => void;
onRemove: (teamId: number) => void;
placeholder?: string;
disabled?: boolean;
className?: string;
}
const MultiTeamSelector = ({
teams,
selectedTeams,
onSelect,
onRemove,
placeholder = "Select teams to add",
disabled = false,
className
}: MultiTeamSelectorProps) => {
const [isOpen, setIsOpen] = useState(false);
const [searchQuery, setSearchQuery] = useState("");
const dropdownRef = useRef<HTMLDivElement>(null);
// Filter out already selected teams
const availableTeams = teams.filter(team =>
!selectedTeams.some(selected => selected.id === team.id)
);
// Filter teams based on search query
const filteredTeams = availableTeams.filter(team =>
team.name.toLowerCase().includes(searchQuery.toLowerCase()) ||
(team.description && team.description.toLowerCase().includes(searchQuery.toLowerCase()))
);
// Handle click outside to close dropdown
useEffect(() => {
function handleClickOutside(event: MouseEvent) {
if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) {
setIsOpen(false);
}
}
document.addEventListener("mousedown", handleClickOutside);
return () => {
document.removeEventListener("mousedown", handleClickOutside);
};
}, []);
// Handle selecting a team
const handleSelectTeam = (team: Team) => {
onSelect(team);
setSearchQuery("");
if (availableTeams.length <= 1) {
setIsOpen(false);
}
};
return (
<div className={cn("relative w-full", className)} ref={dropdownRef}>
<Button
type="button"
variant="outline"
role="combobox"
aria-expanded={isOpen}
className={cn(
"w-full justify-between font-normal",
availableTeams.length === 0 && "opacity-50"
)}
onClick={() => setIsOpen(!isOpen)}
disabled={disabled || availableTeams.length === 0}
>
<span>{availableTeams.length === 0 ? "No more teams available" : placeholder}</span>
<div className="flex items-center">
<Plus className="mr-1 h-4 w-4 opacity-50" />
<ChevronsUpDown className="h-4 w-4 opacity-50" />
</div>
</Button>
{selectedTeams.length > 0 && (
<div className="flex flex-wrap gap-2 mt-2">
{selectedTeams.map(team => (
<Badge
key={team.id}
variant="secondary"
className="flex items-center gap-1 px-2 py-1"
>
<Avatar className="h-4 w-4">
<AvatarFallback className="text-[10px]">
{team.name.slice(0, 2).toUpperCase()}
</AvatarFallback>
</Avatar>
<span>{team.name}</span>
<Button
variant="ghost"
size="sm"
className="h-4 w-4 p-0 ml-1 opacity-70 hover:opacity-100 hover:bg-transparent"
onClick={() => onRemove(team.id)}
>
<X className="h-3 w-3" />
<span className="sr-only">Remove {team.name}</span>
</Button>
</Badge>
))}
</div>
)}
{isOpen && (
<div className="absolute top-full z-[9999] mt-1 w-full rounded-md border border-input bg-popover shadow-md animate-in fade-in-0 zoom-in-95">
<div className="flex items-center border-b px-3 py-2">
<Search className="mr-2 h-4 w-4 shrink-0 opacity-50" />
<Input
placeholder="Search teams..."
className="flex h-8 w-full rounded-md bg-transparent py-2 text-sm outline-none placeholder:text-muted-foreground disabled:cursor-not-allowed disabled:opacity-50 border-0 focus-visible:ring-0 focus-visible:ring-offset-0"
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
onClick={(e) => e.stopPropagation()}
/>
</div>
<div className="max-h-[15rem] overflow-y-auto py-1">
{filteredTeams.length === 0 ? (
<div className="px-2 py-3 text-sm text-center text-muted-foreground">
{searchQuery
? "No matching teams found"
: "No more teams available to add"}
</div>
) : (
filteredTeams.map(team => (
<div
key={team.id}
className="flex items-center justify-between px-3 py-2 text-sm cursor-pointer hover:bg-accent"
onClick={() => handleSelectTeam(team)}
>
<div className="flex items-center gap-2">
<Avatar className="h-6 w-6">
<AvatarFallback>
{team.name.slice(0, 2).toUpperCase()}
</AvatarFallback>
</Avatar>
<div>
<p className="font-medium">{team.name}</p>
{team.description && (
<p className="text-xs text-muted-foreground truncate max-w-[200px]">
{team.description}
</p>
)}
</div>
</div>
<Button
variant="ghost"
size="icon"
className="h-6 w-6"
>
<Plus className="h-4 w-4" />
</Button>
</div>
))
)}
</div>
</div>
)}
</div>
);
};
export default MultiTeamSelector;