/opt/canhelp/apps/web/src/components
Edit: /opt/canhelp/apps/web/src/components/TaskCategoryPicker.tsx (11117B)
'use client'
import { useRef, useState, useEffect } from 'react'
import type { Category } from '@canhelp/shared'
import { CategoryIcon } from '@/components/CategoryIcon'
interface Props {
categories: Category[]
locale: string
value: string
onChange: (slug: string) => void
placeholder: string
}
function isPathLikeLabel(value: string): boolean {
return value.startsWith('/api/') || value.startsWith('/uploads/') || value.startsWith('http://') || value.startsWith('https://')
}
function slugToLabel(slug: string): string {
return slug
.split('-')
.filter(Boolean)
.map((part) => part.charAt(0).toUpperCase() + part.slice(1))
.join(' ')
}
function safeCategoryLabel(value: unknown, slug: string): string {
const raw = typeof value === 'string' ? value.trim() : ''
if (!raw || isPathLikeLabel(raw)) return slugToLabel(slug)
return raw
}
export function TaskCategoryPicker({ categories, locale, value, onChange, placeholder }: Props) {
const [open, setOpen] = useState(false)
const [expandedParent, setExpandedParent] = useState
(null)
const [search, setSearch] = useState('')
const ref = useRef(null)
const searchRef = useRef(null)
useEffect(() => {
function handleClick(e: MouseEvent) {
if (ref.current && !ref.current.contains(e.target as Node)) {
setOpen(false)
setExpandedParent(null)
}
}
document.addEventListener('mousedown', handleClick)
return () => document.removeEventListener('mousedown', handleClick)
}, [])
// Auto-expand the parent of the currently selected subcategory
useEffect(() => {
if (!value) { setExpandedParent(null); return }
for (const cat of categories) {
for (const sub of (cat as any).children ?? []) {
if (sub.slug === value) { setExpandedParent(cat.slug); return }
}
}
}, [value, categories])
let selectedLabel = ''
let selectedIcon = ''
outer: for (const cat of categories) {
if (cat.slug === value) {
selectedLabel = safeCategoryLabel((cat.names as any)[locale] ?? cat.names.el, cat.slug)
selectedIcon = (cat as any).icon ?? ''
break
}
for (const sub of (cat as any).children ?? []) {
if (sub.slug === value) {
selectedLabel = safeCategoryLabel((sub.names as any)[locale] ?? sub.names.el, sub.slug)
selectedIcon = (cat as any).icon ?? ''
break outer
}
}
}
function handleClose() {
setOpen(false)
setExpandedParent(null)
setSearch('')
}
const q = search.toLowerCase()
const filteredCategories = q
? categories.filter(
(cat) =>
(((cat.names as any)[locale] ?? cat.names.el) as string).toLowerCase().includes(q) ||
((cat as any).children ?? []).some((sub: any) =>
((sub.names as any)[locale] ?? sub.names.el as string).toLowerCase().includes(q)
)
)
: categories
return (
{open && (
{/* Search input */}
{/* "All categories" row */}
{filteredCategories.map((cat) => {
const name = safeCategoryLabel((cat.names as any)[locale] ?? cat.names.el, cat.slug)
const icon = (cat as any).icon ?? ''
const children: any[] = (cat as any).children ?? []
const matchingChildren = q
? children.filter((sub) =>
((sub.names as any)[locale] ?? sub.names.el as string).toLowerCase().includes(q) ||
name.toLowerCase().includes(q)
)
: children
const isExpanded = expandedParent === cat.slug || (!!q && matchingChildren.length > 0 && !name.toLowerCase().includes(q))
const hasChildren = children.length > 0
return (
{/* Parent row */}
{/* Subcategories (expanded) */}
{hasChildren && isExpanded && (
{matchingChildren.map((sub) => {
const subName = safeCategoryLabel((sub.names as any)[locale] ?? sub.names.el, sub.slug)
const isSelected = value === sub.slug
return (
)
})}
)}
)
})}
)}
)
}