/opt/canhelp/apps/web/src/app/notifications
NameSizeModeActions
page.tsx121150644editdlrm
Edit: /opt/canhelp/apps/web/src/app/notifications/page.tsx (12115B)
'use client' import { useEffect, useState } from 'react' import { useRouter } from 'next/navigation' import { useSession } from '@/lib/auth' import { useLocale } from '@/context/locale' import { getNotifications, markAllRead, markRead, dismissNotification } from '@/lib/api' import type { Notification } from '@canhelp/shared' function NotifIcon({ type }: { type: string }) { const cls = 'w-5 h-5' switch (type) { case 'new_offer': return ( ) case 'offer_accepted': return ( ) case 'offer_declined': return ( ) case 'task_completed': return ( ) case 'task_cancelled': return ( ) case 'new_message': return ( ) case 'new_review': return ( ) case 'task_invite': return ( ) default: return ( ) } } const typeColor: Record = { new_offer: 'bg-green-100 text-green-600', offer_accepted: 'bg-green-100 text-green-600', offer_declined: 'bg-red-100 text-red-600', task_completed: 'bg-emerald-100 text-emerald-600', task_cancelled: 'bg-amber-100 text-amber-600', new_message: 'bg-purple-100 text-purple-600', new_review: 'bg-yellow-100 text-yellow-600', task_invite: 'bg-indigo-100 text-indigo-600', referral_reward: 'bg-cyan-100 text-cyan-700', specialist_card_submitted: 'bg-amber-100 text-amber-700', specialist_card_approved: 'bg-green-100 text-green-700', specialist_card_rejected: 'bg-rose-100 text-rose-700', } /** Extract the quoted task title from a stored body string (any locale) */ function extractParam(body?: string | null): string | null { if (!body) return null const m = body.match(/[«""](.+?)[»""]/) return m?.[1] ?? null } /** Extract rating number from a stored review body */ function extractRating(body?: string | null): string | null { if (!body) return null const m = body.match(/(\d+)\s*[★\*]/) return m?.[1] ?? null } /** Extract first integer from text (for referral days, etc.) */ function extractNumber(body?: string | null): string | null { if (!body) return null const m = body.match(/(\d+)/) return m?.[1] ?? null } export default function NotificationsPage() { const { data: session, isPending } = useSession() const router = useRouter() const { t, locale } = useLocale() const [notifications, setNotifications] = useState([]) const [loading, setLoading] = useState(true) useEffect(() => { if (!isPending && !session) router.push('/login') }, [session, isPending]) useEffect(() => { if (!session) return getNotifications() .then(setNotifications) .catch(() => {}) .finally(() => setLoading(false)) }, [session]) async function handleMarkAll() { await markAllRead() setNotifications((prev) => prev.map((n) => ({ ...n, isRead: true }))) } async function handleMark(id: string) { await markRead(id) setNotifications((prev) => prev.map((n) => n.id === id ? { ...n, isRead: true } : n)) } async function handleDismiss(e: React.MouseEvent, id: string) { e.stopPropagation() await dismissNotification(id).catch(() => {}) setNotifications((prev) => prev.filter((n) => n.id !== id)) } if (isPending || loading) return
{t('common.loading')}
function relativeTime(dateStr: string): string { const diff = Date.now() - new Date(dateStr).getTime() const sec = Math.floor(diff / 1000) const min = Math.floor(sec / 60) const hrs = Math.floor(min / 60) const days = Math.floor(hrs / 24) if (locale === 'ru') { if (sec < 60) return 'только что' if (min < 60) return `${min} мин. назад` if (hrs < 24) return `${hrs} ч. назад` if (days === 1) return 'вчера' if (days < 7) return `${days} дн. назад` } else if (locale === 'uk') { if (sec < 60) return 'щойно' if (min < 60) return `${min} хв. тому` if (hrs < 24) return `${hrs} год. тому` if (days === 1) return 'вчора' if (days < 7) return `${days} дн. тому` } else if (locale === 'en') { if (sec < 60) return 'just now' if (min < 60) return `${min}m ago` if (hrs < 24) return `${hrs}h ago` if (days === 1) return 'yesterday' if (days < 7) return `${days}d ago` } else { if (sec < 60) return 'μόλις τώρα' if (min < 60) return `${min} λ. πριν` if (hrs < 24) return `${hrs} ω. πριν` if (days === 1) return 'χθες' if (days < 7) return `${days} μ. πριν` } return new Date(dateStr).toLocaleDateString(dateLocale, { day: 'numeric', month: 'short', year: days > 365 ? 'numeric' : undefined }) } const unreadCount = notifications.filter((n) => !n.isRead).length const dateLocale = locale === 'ru' ? 'ru-RU' : locale === 'uk' ? 'uk-UA' : locale === 'en' ? 'en-US' : 'el-GR' return (

{t('notif.title')} {unreadCount > 0 && ({unreadCount})}

{unreadCount > 0 && ( )}
{/* Notification settings */} {notifications.length === 0 ? (
{t('notif.empty')}
) : (
{notifications.map((n) => { const link = n.type === 'referral_reward' ? '/profile?tab=referral' : n.referenceId ? n.type === 'new_message' ? `/chat?room=${n.referenceId}` : `/tasks/${n.referenceId}` : null const handleClick = async () => { if (!n.isRead) await handleMark(n.id) if (link) router.push(link) } return (

{t(`notif.type.${n.type}`, t('notif.type.generic', 'Notification'))}

{n.body && (

{(() => { if (n.type === 'new_message') return n.body if (n.type === 'new_review') { const rating = extractRating(n.body) const tpl = t(`notif.body.new_review`, '') return rating && tpl ? tpl.replace('{rating}', rating) : n.body } if (n.type === 'referral_reward') { const days = extractNumber(n.body) const plan = extractParam(n.body) const tpl = t('notif.body.referral_reward', '') if (tpl && days && plan) return tpl.replace('{days}', days).replace('{plan}', plan) return tpl || n.body } const title = extractParam(n.body) const tpl = t(`notif.body.${n.type}`, '') if (tpl && title) return tpl.replace('{title}', title) return tpl || n.body })()}

)}

{relativeTime(n.createdAt)}

e.stopPropagation()}>
{!n.isRead && }
) })}
)}
) }