Spaces:
Sleeping
Sleeping
| import React, { useState, useRef, useEffect } from "react"; | |
| import { Check, ChevronsUpDown, 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 { cn } from "@/lib/utils"; | |
| import { Team } from "@/types"; | |
| interface TeamSelectorProps { | |
| teams: Team[]; | |
| selectedTeam: string; | |
| onSelect: (teamId: string) => void; | |
| placeholder?: string; | |
| disabled?: boolean; | |
| className?: string; | |
| } | |
| const TeamSelector = ({ | |
| teams, | |
| selectedTeam, | |
| onSelect, | |
| placeholder = "Select a team", | |
| disabled = false, | |
| className | |
| }: TeamSelectorProps) => { | |
| const [isOpen, setIsOpen] = useState(false); | |
| const [searchQuery, setSearchQuery] = useState(""); | |
| const dropdownRef = useRef<HTMLDivElement>(null); | |
| // Find the currently selected team | |
| const selected = teams.find(team => team.id.toString() === selectedTeam); | |
| // Filter teams based on search query | |
| const filteredTeams = teams.filter(team => | |
| team.name.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 = (teamId: string) => { | |
| onSelect(teamId); | |
| setIsOpen(false); | |
| setSearchQuery(""); | |
| }; | |
| // Handle clearing selection | |
| const handleClearSelection = (e: React.MouseEvent) => { | |
| e.stopPropagation(); | |
| onSelect(""); | |
| }; | |
| 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", | |
| !selectedTeam && "text-muted-foreground" | |
| )} | |
| onClick={() => setIsOpen(!isOpen)} | |
| disabled={disabled} | |
| > | |
| {selected ? ( | |
| <div className="flex items-center gap-2"> | |
| <Avatar className="h-6 w-6"> | |
| <AvatarFallback> | |
| {selected.name.slice(0, 2).toUpperCase()} | |
| </AvatarFallback> | |
| </Avatar> | |
| <span>{selected.name}</span> | |
| </div> | |
| ) : ( | |
| <span>{placeholder}</span> | |
| )} | |
| <div className="flex items-center"> | |
| {selectedTeam && ( | |
| <Button | |
| variant="ghost" | |
| size="sm" | |
| className="h-4 w-4 p-0 mr-1 opacity-70 hover:opacity-100" | |
| onClick={handleClearSelection} | |
| > | |
| <X className="h-3 w-3" /> | |
| <span className="sr-only">Clear selection</span> | |
| </Button> | |
| )} | |
| <ChevronsUpDown className="h-4 w-4 opacity-50" /> | |
| </div> | |
| </Button> | |
| {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-2 text-sm text-center text-muted-foreground"> | |
| No teams found | |
| </div> | |
| ) : ( | |
| filteredTeams.map(team => ( | |
| <div | |
| key={team.id} | |
| className={cn( | |
| "flex items-center justify-between px-3 py-2 text-sm cursor-pointer hover:bg-accent", | |
| team.id.toString() === selectedTeam && "bg-accent/50" | |
| )} | |
| onClick={() => handleSelectTeam(team.id.toString())} | |
| > | |
| <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> | |
| {team.id.toString() === selectedTeam && ( | |
| <Check className="h-4 w-4 text-primary" /> | |
| )} | |
| </div> | |
| )) | |
| )} | |
| </div> | |
| </div> | |
| )} | |
| </div> | |
| ); | |
| }; | |
| export default TeamSelector; |