/opt/canhelp/apps/web/src/components
Edit: /opt/canhelp/apps/web/src/components/TaskLocationPicker.tsx (11038B)
'use client'
import { useRef, useState, useEffect } from 'react'
import { useLocale } from '@/context/locale'
interface LocationNode {
id: number
slug: string
names: Record
children?: LocationNode[]
}
interface Props {
locations: LocationNode[]
value: string
onChange: (slug: string) => void
anyLabel: string
allLabel: string
}
export function TaskLocationPicker({ locations, value, onChange, anyLabel, allLabel }: Props) {
const [open, setOpen] = useState(false)
const [expandedCity, setExpandedCity] = useState(null)
const [search, setSearch] = useState('')
const ref = useRef(null)
const { locale } = useLocale()
useEffect(() => {
function handleClick(e: MouseEvent) {
if (ref.current && !ref.current.contains(e.target as Node)) {
setOpen(false)
setExpandedCity(null)
}
}
document.addEventListener('mousedown', handleClick)
return () => document.removeEventListener('mousedown', handleClick)
}, [])
// Auto-expand city of currently selected district
useEffect(() => {
if (!value) { setExpandedCity(null); return }
for (const city of locations) {
if (city.slug === value) return // city itself selected — no need to expand
for (const d of city.children ?? []) {
if (d.slug === value) { setExpandedCity(city.slug); return }
}
}
}, [value, locations])
let selectedLabel = anyLabel
outer: for (const city of locations) {
if (city.slug === value) { selectedLabel = city.names[locale] ?? city.names.el; break }
for (const d of city.children ?? []) {
if (d.slug === value) { selectedLabel = d.names[locale] ?? d.names.el; break outer }
}
}
function handleClose() {
setOpen(false)
setExpandedCity(null)
setSearch('')
}
const q = search.toLowerCase()
const filteredLocations = q
? locations.filter(
(city) =>
(city.names[locale] ?? city.names.el).toLowerCase().includes(q) ||
(city.children ?? []).some((d) => (d.names[locale] ?? d.names.el).toLowerCase().includes(q))
)
: locations
return (
setOpen(!open)}
className={`w-full flex items-center gap-1.5 px-3 py-2 text-sm transition-colors ${
open
? 'rounded-t-lg border border-green-400 bg-white text-gray-900'
: 'rounded-lg border border-gray-200 bg-gray-50 hover:bg-gray-100 text-gray-600 hover:text-gray-900'
}`}
>
{selectedLabel}
{open && (
{/* Search input */}
{/* "Any location" row */}
{ onChange(''); handleClose() }}
>
{anyLabel}
{!value && (
)}
{filteredLocations.map((city) => {
const cityName = city.names[locale] ?? city.names.el
const districts = city.children ?? []
const matchingDistricts = q
? districts.filter((d) => (d.names[locale] ?? d.names.el).toLowerCase().includes(q))
: districts
const hasDistricts = districts.length > 0
const isCitySelected = value === city.slug
const isExpanded = expandedCity === city.slug || (!!q && matchingDistricts.length > 0 && !cityName.toLowerCase().includes(q))
return (
{/* City row */}
{
if (hasDistricts) {
setExpandedCity(isExpanded ? null : city.slug)
} else {
onChange(city.slug)
handleClose()
}
}}
>
{cityName}
{hasDistricts ? (
) : isCitySelected ? (
) : null}
{/* Districts (expanded) */}
{hasDistricts && isExpanded && (
{/* City-wide option — only show when city name matches or no search */}
{(!q || cityName.toLowerCase().includes(q)) && (
{ onChange(city.slug); handleClose() }}
>
{cityName} — {allLabel}
{isCitySelected && (
)}
)}
{matchingDistricts.map((d) => {
const dName = d.names[locale] ?? d.names.el
const isSelected = value === d.slug
return (
{ onChange(d.slug); handleClose() }}
>
{dName}
{isSelected && (
)}
)
})}
)}
)
})}
)}
)
}