/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/Header.tsx (30479B)
'use client' import Link from 'next/link' import { useRouter, usePathname } from 'next/navigation' import { useSession, signOut } from '@/lib/auth' import { useLocale } from '@/context/locale' import { formatShortName } from '@/lib/formatName' import { LanguageSwitcher } from './LanguageSwitcher' import { useState, useRef, useEffect } from 'react' import { getUnreadCount, getMyDashboard, getPublicSettings } from '@/lib/api' import type { PlanTier } from '@/lib/api' import { io } from 'socket.io-client' import { useEffectiveRole } from '@/hooks/useEffectiveRole' import dynamic from 'next/dynamic' const SupportModal = dynamic(() => import('./SupportModal'), { ssr: false }) const API_URL = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:4000' export function Header() { const router = useRouter() const pathname = usePathname() const { data: session } = useSession() const { t, locale } = useLocale() const [menuOpen, setMenuOpen] = useState(false) const [profileOpen, setProfileOpen] = useState(false) const [supportOpen, setSupportOpen] = useState(false) const [unreadNotif, setUnreadNotif] = useState(0) const [unreadChat, setUnreadChat] = useState(0) const [msgToast, setMsgToast] = useState<{ roomId: string; body: string } | null>(null) const [planTier, setPlanTier] = useState(null) const [showPricing, setShowPricing] = useState(true) const profileRef = useRef(null) const headerRef = useRef(null) const toastTimer = useRef | null>(null) const pathnameRef = useRef(pathname) useEffect(() => { pathnameRef.current = pathname }, [pathname]) useEffect(() => { getPublicSettings().then((settings) => setShowPricing(settings.showPricing)).catch(() => {}) }, []) // Close dropdowns on outside click useEffect(() => { function handleClick(e: MouseEvent) { if (profileRef.current && !profileRef.current.contains(e.target as Node)) { setProfileOpen(false) } } document.addEventListener('mousedown', handleClick) return () => document.removeEventListener('mousedown', handleClick) }, []) const isAdmin = (session?.user as any)?.role === 'admin' const effectiveRole = useEffectiveRole() const isSpecialist = effectiveRole === 'specialist' const isActive = (href: string, exact = false) => exact ? pathname === href : pathname === href || pathname.startsWith(href + '/') const navCls = (href: string, exact = false) => `font-medium text-sm transition ${isActive(href, exact) ? 'text-green-600' : 'text-gray-600 hover:text-green-600'}` const mobileNavCls = (href: string, exact = false) => `flex items-center gap-2 font-medium py-1 ${isActive(href, exact) ? 'text-green-600' : 'text-gray-700'}` // Reset chat badge when entering chat useEffect(() => { if (pathname === '/chat') setUnreadChat(0) }, [pathname]) useEffect(() => { if (!session) return const socket = io(API_URL, { withCredentials: true, transports: ['websocket', 'polling'] }) socket.on('new_message', (msg: { roomId: string; body: string }) => { if (pathnameRef.current === '/chat') return setUnreadChat((c) => c + 1) if (toastTimer.current) clearTimeout(toastTimer.current) setMsgToast(msg) toastTimer.current = setTimeout(() => setMsgToast(null), 5000) }) return () => { socket.disconnect() if (toastTimer.current) clearTimeout(toastTimer.current) } }, [session]) useEffect(() => { if (!session) { setUnreadNotif(0); return } const fetch = () => getUnreadCount().then((r) => setUnreadNotif(r.count)).catch(() => {}) fetch() const timer = setInterval(fetch, 30_000) return () => clearInterval(timer) }, [session]) useEffect(() => { if (!session) { setPlanTier(null); return } getMyDashboard().then((d) => setPlanTier((d.plan?.tier ?? null) as PlanTier | null)).catch(() => {}) }, [session]) useEffect(() => { const onScroll = () => { if (!headerRef.current) return if (window.scrollY > 40) { headerRef.current.classList.add('header-shrink') } else { headerRef.current.classList.remove('header-shrink') } } window.addEventListener('scroll', onScroll) return () => window.removeEventListener('scroll', onScroll) }, []) return ( <>
{/* Logo */} {/* eslint-disable-next-line @next/next/no-img-element */} CanHelp CanHelp {/* Desktop nav */} {/* Mobile: language + burger */}
{/* Mobile menu */} {menuOpen && (
{isSpecialist ? ( <> setMenuOpen(false)}> {t('nav.find_tasks')} setMenuOpen(false)}> {t('nav.dashboard')} ) : ( <> setMenuOpen(false)}> {t('nav.new_task')} setMenuOpen(false)}> {t('nav.tasks')} setMenuOpen(false)}> {t('nav.specialists')} )} {session ? ( <> setMenuOpen(false)}> 0 ? 'text-green-500' : ''}`} fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.8}> {t('nav.chat')}{unreadChat > 0 && {unreadChat}} {isSpecialist ? ( <> setMenuOpen(false)}> {t('nav.new_task')} setMenuOpen(false)}> {t('nav.specialists')} ) : ( <> setMenuOpen(false)}> 0 ? 'text-green-600' : ''}`} fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.8}> {t('nav.notifications')}{unreadNotif > 0 && {unreadNotif > 99 ? '99+' : unreadNotif}} setMenuOpen(false)}> {t('nav.dashboard')} )} setMenuOpen(false)}> {t('nav.favorites')} {(isSpecialist || isAdmin) && ( setMenuOpen(false)}> {t('nav.schedule')} )} {isAdmin && ( setMenuOpen(false)}> {t('nav.admin')} )} setMenuOpen(false)}> {t('nav.profile')} ) : ( <> setMenuOpen(false)}>{t('nav.login')} setMenuOpen(false)}>{t('nav.register')} )}
)}
{/* New message toast */} {msgToast && (
💬

{t('chat.toast_title')}

{msgToast.body}

setMsgToast(null)} className="text-xs text-green-600 hover:underline font-medium" >{t('chat.toast_open')}
)} {supportOpen && setSupportOpen(false)} />} ) }