/opt/canhelp/apps/web/src/components
NameSizeModeActions
AdminRolePreview.tsx49280644editdlrm
BecomeSpecialistButton.tsx7020644editdlrm
CategoryIcon.tsx15760644editdlrm
FaqAccordion.tsx15000644editdlrm
FavoriteSpecialistButton.tsx14640644editdlrm
Footer.tsx30190644editdlrm
Header.tsx304790644editdlrm
HeroFloatingIcons.tsx43780644editdlrm
HeroSearch.tsx16110644editdlrm
HomeCategoryGrid.tsx46250644editdlrm
HomeCityCloud.tsx17900644editdlrm
HomeCityPicker.tsx54900644editdlrm
HomeSectionArt.tsx59120644editdlrm
LanguageSwitcher.tsx24380644editdlrm
PlanBadge.tsx31000644editdlrm
PromoCountdown.tsx34830644editdlrm
ScrollToTopButton.tsx8180644editdlrm
Section.tsx2520644editdlrm
ShareButton.tsx10410644editdlrm
SocialIcons.tsx68710644editdlrm
SpecialistsFiltersClient.tsx64610644editdlrm
SpecialistTaskCard.tsx28370644editdlrm
SupportModal.tsx61950644editdlrm
TaskCategoryPicker.tsx111170644editdlrm
TaskLocationPicker.tsx110380644editdlrm
Turnstile.tsx6510644editdlrm
UpgradePrompt.tsx34270644editdlrm
UsageMeter.tsx16020644editdlrm
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 */}
setSearch(e.target.value)} className="w-full pl-8 pr-7 py-1.5 text-sm border border-gray-200 rounded-md focus:outline-none focus:border-green-400" placeholder="Поиск..." onClick={(e) => e.stopPropagation()} /> {search && ( )}
{/* "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 ( ) })}
)}
) })}
)}
) }