-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #553 from bounswe/frontend/feature/502/QuestionCreate
- Loading branch information
Showing
14 changed files
with
1,037 additions
and
35 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,272 @@ | ||
import { cva, type VariantProps } from "class-variance-authority"; | ||
import { CheckIcon, ChevronDown, XCircle, XIcon } from "lucide-react"; | ||
import * as React from "react"; | ||
|
||
import { Badge } from "@/components/ui/badge"; | ||
import { Button } from "@/components/ui/button"; | ||
import { | ||
Command, | ||
CommandEmpty, | ||
CommandGroup, | ||
CommandInput, | ||
CommandItem, | ||
CommandList, | ||
} from "@/components/ui/command"; | ||
import { | ||
Popover, | ||
PopoverContent, | ||
PopoverTrigger, | ||
} from "@/components/ui/popover"; | ||
import { Separator } from "@/components/ui/separator"; | ||
import { cn } from "@/lib/utils"; | ||
|
||
const multiSelectVariants = cva( | ||
"m-1 transition ease-in-out delay-150 duration-300", | ||
{ | ||
variants: { | ||
variant: { | ||
default: | ||
"border-foreground/10 text-foreground bg-card hover:bg-card/80", | ||
secondary: | ||
"border-foreground/10 bg-secondary text-secondary-foreground hover:bg-secondary/80", | ||
destructive: | ||
"border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80", | ||
inverted: "inverted", | ||
}, | ||
}, | ||
defaultVariants: { | ||
variant: "default", | ||
}, | ||
}, | ||
); | ||
|
||
interface MultiSelectProps | ||
extends React.ButtonHTMLAttributes<HTMLButtonElement>, | ||
VariantProps<typeof multiSelectVariants> { | ||
options: { | ||
label: string; | ||
value: string; | ||
icon?: React.ComponentType<{ className?: string }>; | ||
}[]; | ||
onValueChange: (value: string[]) => void; | ||
defaultValue?: string[]; | ||
placeholder?: string; | ||
animation?: number; | ||
maxCount?: number; | ||
modalPopover?: boolean; | ||
asChild?: boolean; | ||
className?: string; | ||
} | ||
|
||
export const MultiSelect = React.forwardRef< | ||
HTMLButtonElement, | ||
MultiSelectProps | ||
>( | ||
( | ||
{ | ||
options, | ||
onValueChange, | ||
variant, | ||
defaultValue = [], | ||
placeholder = "Select options", | ||
maxCount = 3, | ||
modalPopover = false, | ||
className, | ||
...props | ||
}, | ||
ref, | ||
) => { | ||
const [selectedValues, setSelectedValues] = | ||
React.useState<string[]>(defaultValue); | ||
const [isPopoverOpen, setIsPopoverOpen] = React.useState(false); | ||
|
||
const toggleOption = (optionValue: string | undefined) => { | ||
if (!optionValue) { | ||
console.warn(`Attempted to toggle non-existent value: ${optionValue}`); | ||
return; | ||
} | ||
|
||
const isSelected = selectedValues.includes(optionValue); | ||
const newSelectedValues = isSelected | ||
? selectedValues.filter((value) => value !== optionValue) // Deselect | ||
: [...selectedValues, optionValue]; // Select | ||
|
||
setSelectedValues(newSelectedValues); | ||
onValueChange(newSelectedValues); | ||
}; | ||
|
||
const handleClear = () => { | ||
setSelectedValues([]); | ||
onValueChange([]); | ||
}; | ||
|
||
const handleTogglePopover = () => { | ||
setIsPopoverOpen((prev) => !prev); | ||
}; | ||
|
||
const clearExtraOptions = () => { | ||
const newSelectedValues = selectedValues.slice(0, maxCount); | ||
setSelectedValues(newSelectedValues); | ||
onValueChange(newSelectedValues); | ||
}; | ||
|
||
const toggleAll = () => { | ||
if (selectedValues.length === options.length) { | ||
handleClear(); | ||
} else { | ||
const allValues = options.map((option) => option.value); | ||
setSelectedValues(allValues); | ||
onValueChange(allValues); | ||
} | ||
}; | ||
|
||
return ( | ||
<Popover | ||
open={isPopoverOpen} | ||
onOpenChange={setIsPopoverOpen} | ||
modal={modalPopover} | ||
> | ||
<PopoverTrigger asChild> | ||
<Button | ||
ref={ref} | ||
{...props} | ||
onClick={handleTogglePopover} | ||
className={cn( | ||
"flex h-auto min-h-10 w-full items-center justify-between rounded-md border bg-inherit p-1 hover:bg-inherit [&_svg]:pointer-events-auto", | ||
className, | ||
)} | ||
> | ||
{selectedValues.length > 0 ? ( | ||
<div className="flex w-full items-center justify-between"> | ||
<div className="flex flex-wrap items-center"> | ||
{selectedValues.slice(0, maxCount).map((value) => { | ||
const option = options.find((o) => o.value === value); | ||
const IconComponent = option?.icon; | ||
return ( | ||
<Badge | ||
key={`badge-${value}`} | ||
className={cn(multiSelectVariants({ variant }))} | ||
> | ||
{IconComponent && ( | ||
<IconComponent className="mr-2 h-4 w-4" /> | ||
)} | ||
{option?.label || "Unknown"} | ||
<XCircle | ||
className="ml-2 h-4 w-4 cursor-pointer" | ||
onClick={(event) => { | ||
event.stopPropagation(); | ||
toggleOption(value); | ||
}} | ||
/> | ||
</Badge> | ||
); | ||
})} | ||
{selectedValues.length > maxCount && ( | ||
<Badge | ||
className={cn( | ||
"border-foreground/1 bg-transparent text-foreground hover:bg-transparent", | ||
)} | ||
> | ||
{`+ ${selectedValues.length - maxCount} more`} | ||
<XCircle | ||
className="ml-2 h-4 w-4 cursor-pointer" | ||
onClick={(event) => { | ||
event.stopPropagation(); | ||
clearExtraOptions(); | ||
}} | ||
/> | ||
</Badge> | ||
)} | ||
</div> | ||
<div className="flex items-center justify-between"> | ||
<XIcon | ||
className="mx-2 h-4 cursor-pointer text-muted-foreground" | ||
onClick={(event) => { | ||
event.stopPropagation(); | ||
handleClear(); | ||
}} | ||
/> | ||
<Separator | ||
orientation="vertical" | ||
className="flex h-full min-h-6" | ||
/> | ||
<ChevronDown className="mx-2 h-4 cursor-pointer text-muted-foreground" /> | ||
</div> | ||
</div> | ||
) : ( | ||
<div className="mx-auto flex w-full items-center justify-between"> | ||
<span className="mx-3 text-sm text-muted-foreground"> | ||
{placeholder} | ||
</span> | ||
<ChevronDown className="mx-2 h-4 cursor-pointer text-muted-foreground" /> | ||
</div> | ||
)} | ||
</Button> | ||
</PopoverTrigger> | ||
<PopoverContent className="w-auto p-0" align="start"> | ||
<Command> | ||
<CommandInput placeholder="Search..." /> | ||
|
||
<CommandList> | ||
<CommandEmpty>No results found.</CommandEmpty> | ||
<CommandGroup> | ||
<CommandItem | ||
key="select-all" | ||
onSelect={toggleAll} | ||
className="cursor-pointer" | ||
> | ||
<div | ||
className={cn( | ||
"mr-2 flex h-4 w-4 items-center justify-center rounded-sm border border-primary", | ||
selectedValues.length === options.length | ||
? "bg-primary text-primary-foreground" | ||
: "opacity-50 [&_svg]:invisible", | ||
)} | ||
> | ||
<CheckIcon className="h-4 w-4" /> | ||
</div> | ||
<span>(Select All)</span> | ||
</CommandItem> | ||
{options.map((option, index) => { | ||
const uniqueKey = option.value || `option-${index}`; // Generate a unique key | ||
const isSelected = selectedValues.includes(option.value); // Check if this option is selected | ||
|
||
return ( | ||
<CommandItem | ||
key={`command-item-${uniqueKey}`} // Unique key for each CommandItem | ||
onSelect={() => toggleOption(option.value)} // Toggle this option | ||
className="cursor-pointer" | ||
> | ||
<div | ||
className={cn( | ||
"mr-2 flex h-4 w-4 items-center justify-center rounded-sm border border-primary", | ||
isSelected | ||
? "bg-primary text-primary-foreground" | ||
: "opacity-50 [&_svg]:invisible", | ||
)} | ||
> | ||
<CheckIcon className="h-4 w-4" /> | ||
</div> | ||
{option.icon && ( | ||
<option.icon | ||
key={`icon-${uniqueKey}`} // Unique key for the icon | ||
className="mr-2 h-4 w-4 text-muted-foreground" | ||
/> | ||
)} | ||
<span key={`label-${uniqueKey}`}> | ||
{option.label || "Unknown"} | ||
</span>{" "} | ||
{/* Display label */} | ||
</CommandItem> | ||
); | ||
})} | ||
</CommandGroup> | ||
</CommandList> | ||
</Command> | ||
</PopoverContent> | ||
</Popover> | ||
); | ||
}, | ||
); | ||
|
||
MultiSelect.displayName = "MultiSelect"; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.