Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
87 changes: 54 additions & 33 deletions components/autocomplete/language-autocomplete.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import * as React from 'react'
import { Check, ChevronsUpDown, X } from "lucide-react"
import { cn } from "@/lib/utils"
import { Button } from "@/components/ui/button"
import { Badge } from "@/components/ui/badge"
import {
Command,
CommandEmpty,
Expand Down Expand Up @@ -35,14 +36,16 @@ interface LanguageAutocompleteProps {
mobilePlaceholder?: string
}

const MAX_VISIBLE_BADGES = 6

export const LanguageAutocomplete = React.forwardRef<
{ reset: () => void },
LanguageAutocompleteProps
>(function LanguageAutocomplete({
languages,
onSelect,
>(function LanguageAutocomplete({
languages,
onSelect,
selectedLanguages,
maxSelections = 8,
maxSelections = 6,
'aria-label': ariaLabel = "Select languages",
className,
placeholder = "Filter by languages",
Expand All @@ -60,18 +63,14 @@ export const LanguageAutocomplete = React.forwardRef<
}))

const formattedLanguages = useMemo(() => {
// Create a Map to ensure unique language entries by code
const languageMap = new Map()

// Filter out any 'All' entries from the input languages
const filteredLanguages = languages.filter(lang =>

const filteredLanguages = languages.filter(lang =>
typeof lang === 'string' ? lang !== 'All' : lang.code !== 'All'
)

for (const lang of filteredLanguages) {
// If it's a string, assume it's a language name (not code)
if (typeof lang === 'string') {
// Find the language code by name
const entry = Object.entries(countryLanguages).find(([_, data]) => data.name === lang)
if (entry) {
const [code, data] = entry
Expand All @@ -81,15 +80,13 @@ export const LanguageAutocomplete = React.forwardRef<
display: `${data.name} (${code})`
})
} else {
// If not found in countryLanguages, use the string as is
languageMap.set(lang, {
code: lang,
name: lang,
display: lang
})
}
} else if (typeof lang === 'object' && 'code' in lang) {
// If it's already an object with a code, format it properly
const code = lang.code
if (!languageMap.has(code)) {
languageMap.set(code, {
Expand All @@ -104,8 +101,8 @@ export const LanguageAutocomplete = React.forwardRef<
return Array.from(languageMap.values())
}, [languages])

const filteredLanguages = React.useMemo(() =>
formattedLanguages.filter(language =>
const filteredLanguages = React.useMemo(() =>
formattedLanguages.filter(language =>
language.display.toLowerCase().includes(value.toLowerCase()) ||
language.code.toLowerCase().includes(value.toLowerCase())
),
Expand All @@ -114,22 +111,25 @@ export const LanguageAutocomplete = React.forwardRef<

const handleToggleLanguage = (languageCode: string) => {
const isSelected = selectedLanguages.includes(languageCode)

if (isSelected) {
// Remove language
onSelect(selectedLanguages.filter(l => l !== languageCode))
} else if (selectedLanguages.length < maxSelections) {
// Add language (max reached check)
onSelect([...selectedLanguages, languageCode])
}
// If max reached and not selected, do nothing
}

// Get language name for badge display
const getLanguageName = (code: string) => {
const lang = formattedLanguages.find(l => l.code === code)
return lang?.name ?? code
}

const visibleBadges = selectedLanguages.slice(0, MAX_VISIBLE_BADGES)
const extraCount = selectedLanguages.length - MAX_VISIBLE_BADGES

return (
<Popover
open={open}
onOpenChange={setOpen}
>
<Popover open={open} onOpenChange={setOpen}>
<PopoverTrigger asChild>
<Button
type="button"
Expand All @@ -138,26 +138,48 @@ export const LanguageAutocomplete = React.forwardRef<
aria-controls="language-search"
aria-haspopup="listbox"
aria-label={ariaLabel}
className={cn("w-[200px] justify-between", className)}
className={cn("w-full min-h-10 justify-between", className)}
>
{selectedLanguages.length === 0 ? (
<span className="fade-in text-left truncate">All Languages</span>
<span className="text-left truncate text-muted-foreground">{placeholder}</span>
) : (
<span className="text-left truncate">
{selectedLanguages.length} language{selectedLanguages.length > 1 ? 's' : ''}
</span>
<div className="flex items-center gap-1 flex-wrap flex-1 min-w-0">
{visibleBadges.map(code => (
<Badge
key={code}
variant="secondary"
className="flex items-center gap-0.5 px-1.5 py-0 text-xs font-medium"
>
<span className="truncate max-w-[80px]">{getLanguageName(code)}</span>
<button
type="button"
onClick={(e) => {
e.stopPropagation()
onSelect(selectedLanguages.filter(l => l !== code))
}}
className="ml-0.5 rounded-full hover:bg-black/10 dark:hover:bg-white/10 p-0.5 transition-colors"
aria-label={`Remove ${getLanguageName(code)}`}
>
<X className="h-2.5 w-2.5" />
</button>
</Badge>
))}
{extraCount > 0 && (
<span className="text-xs text-muted-foreground">+{extraCount} more</span>
)}
</div>
)}
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
</Button>
</PopoverTrigger>
<PopoverContent className="w-[180px] p-0">
<PopoverContent className="w-[220px] p-0">
<Command>
<CommandInput
placeholder="Search languages..."
<CommandInput
placeholder="Search languages..."
value={value}
onValueChange={setValue}
/>
<CommandList className="max-h-[200px] overflow-y-auto">
<CommandList className="max-h-[240px] overflow-y-auto">
{filteredLanguages.length === 0 ? (
<CommandEmpty>No languages found.</CommandEmpty>
) : (
Expand All @@ -177,7 +199,7 @@ export const LanguageAutocomplete = React.forwardRef<
{filteredLanguages.map((language) => {
const isSelected = selectedLanguages.includes(language.code)
const canSelect = selectedLanguages.length < maxSelections || isSelected

return (
<CommandItem
key={language.code}
Expand Down Expand Up @@ -209,4 +231,3 @@ export const LanguageAutocomplete = React.forwardRef<
})

LanguageAutocomplete.displayName = 'LanguageAutocomplete'

4 changes: 3 additions & 1 deletion components/autocomplete/location-autocomplete.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,8 @@ export const LocationAutocomplete = React.memo(function LocationAutocomplete({
return 'bg-pink-100 text-pink-800 border-pink-200 dark:bg-pink-900/30 dark:text-pink-300 dark:border-pink-700'
case 2:
return 'bg-purple-100 text-purple-800 border-purple-200 dark:bg-purple-900/30 dark:text-purple-300 dark:border-purple-700'
case 3:
return 'bg-emerald-100 text-emerald-800 border-emerald-200 dark:bg-emerald-900/30 dark:text-emerald-300 dark:border-emerald-700'
default:
return 'bg-secondary text-secondary-foreground'
}
Expand Down Expand Up @@ -245,7 +247,7 @@ export const LocationAutocomplete = React.memo(function LocationAutocomplete({
value={value}
onValueChange={setValue}
/>
<CommandList className="max-h-[300px] overflow-y-auto">
<CommandList className="max-h-[256px] md:max-h-[300px] overflow-y-auto">
{filteredLocations.length === 0 ? (
<CommandEmpty>No locations found.</CommandEmpty>
) : (
Expand Down
106 changes: 59 additions & 47 deletions components/autocomplete/proximity-autocomplete.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import * as React from 'react'
import { Check, ChevronsUpDown } from "lucide-react"
import { Check, ChevronsUpDown, X } from "lucide-react"
import { cn } from "@/lib/utils"
import { Button } from "@/components/ui/button"
import {
Expand All @@ -15,12 +15,12 @@ import {
PopoverContent,
PopoverTrigger,
} from "@/components/ui/popover"
import { forwardRef, useImperativeHandle, useState, useMemo } from 'react'
import { useState, useMemo } from 'react'
import type { TimeType } from '@/components/same-time/types'

interface ProximityAutocompleteProps {
value: TimeType
onSelect: (value: TimeType) => void
values: TimeType[]
onSelect: (values: TimeType[]) => void
'aria-label'?: string
className?: string
placeholder?: string
Expand All @@ -33,7 +33,6 @@ interface ProximityOption {
}

const PROXIMITY_OPTIONS: ProximityOption[] = [
{ value: 'All', label: 'All Proximities', display: 'All Proximities' },
{ value: 'Same Time', label: 'Same Time', display: '✅ Same Time' },
{ value: 'Close Time', label: 'Close Time', display: '☑️ Close Time' },
{ value: 'Reverse Time', label: 'Reverse Time', display: '😵‍💫 Reverse Time' },
Expand All @@ -42,48 +41,49 @@ const PROXIMITY_OPTIONS: ProximityOption[] = [
export const ProximityAutocomplete = React.forwardRef<
{ reset: () => void },
ProximityAutocompleteProps
>(function ProximityAutocomplete({
value,
>(function ProximityAutocomplete({
values,
onSelect,
'aria-label': ariaLabel = "Filter by proximity",
className,
placeholder = "Filter by Proximity"
placeholder = "All Proximities",
}, ref) {
const [open, setOpen] = useState(false)
const [searchValue, setSearchValue] = useState('')

useImperativeHandle(ref, () => ({
React.useImperativeHandle(ref, () => ({
reset: () => {
setSearchValue('')
onSelect('All')
onSelect([])
setOpen(false)
}
}))

const filteredOptions = useMemo(() => {
if (!searchValue) {
return PROXIMITY_OPTIONS
}
return PROXIMITY_OPTIONS.filter(option =>
if (!searchValue) return PROXIMITY_OPTIONS
return PROXIMITY_OPTIONS.filter(option =>
option.label.toLowerCase().includes(searchValue.toLowerCase()) ||
option.display.toLowerCase().includes(searchValue.toLowerCase())
)
}, [searchValue])

const selectedOption = PROXIMITY_OPTIONS.find(opt => opt.value === value)
const displayText = selectedOption?.display || placeholder

const handleSelect = (selectedValue: TimeType) => {
onSelect(selectedValue)
setOpen(false)
setSearchValue('')
const handleToggle = (value: TimeType) => {
const isSelected = values.includes(value)
if (isSelected) {
onSelect(values.filter(v => v !== value))
} else {
onSelect([...values, value])
}
}

const triggerLabel = values.length === 0
? placeholder
: values.length === 1
? PROXIMITY_OPTIONS.find(o => o.value === values[0])?.display ?? values[0]
: `${values.length} proximities`

return (
<Popover
open={open}
onOpenChange={setOpen}
>
<Popover open={open} onOpenChange={setOpen}>
<PopoverTrigger asChild>
<Button
type="button"
Expand All @@ -94,42 +94,54 @@ export const ProximityAutocomplete = React.forwardRef<
aria-label={ariaLabel}
className={cn("w-full justify-between", className)}
>
<span className="text-left truncate">{displayText}</span>
<span className="text-left truncate">{triggerLabel}</span>
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
</Button>
</PopoverTrigger>
<PopoverContent className="w-[200px] p-0">
<PopoverContent className="w-[210px] p-0">
<Command>
<CommandInput
placeholder="Search proximity..."
<CommandInput
placeholder="Search proximity..."
value={searchValue}
onValueChange={setSearchValue}
/>
<CommandList className="max-h-[200px] overflow-y-auto">
{filteredOptions.length === 0 ? (
<CommandEmpty>No proximity types found.</CommandEmpty>
) : (
<CommandGroup>
{filteredOptions.map((option) => {
const isSelected = value === option.value

return (
<>
{values.length > 0 && (
<CommandGroup>
<CommandItem
key={option.value}
value={option.label}
onSelect={() => handleSelect(option.value)}
onSelect={() => onSelect([])}
className="text-destructive"
>
<Check
className={cn(
"mr-2 h-4 w-4",
isSelected ? "opacity-100" : "opacity-0"
)}
/>
<span className="flex-1">{option.display}</span>
<X className="mr-2 h-4 w-4" />
Clear all proximities
</CommandItem>
)
})}
</CommandGroup>
</CommandGroup>
)}
<CommandGroup>
{filteredOptions.map((option) => {
const isSelected = values.includes(option.value)
return (
<CommandItem
key={option.value}
value={option.label}
onSelect={() => handleToggle(option.value)}
>
<Check
className={cn(
"mr-2 h-4 w-4",
isSelected ? "opacity-100" : "opacity-0"
)}
/>
<span className="flex-1">{option.display}</span>
</CommandItem>
)
})}
</CommandGroup>
</>
)}
</CommandList>
</Command>
Expand Down
Loading