Files
TREK/client/src/pages/InAppNotificationsPage.tsx
T
Maurice de128cc4ae wip(i18n): plural forms through Intl.PluralRules
Unfinished, continued in the next session: t() resolves key.one/.few/...
by the language's own plural rule, the parity check knows which forms a
language needs, 16 singular/plural key pairs are merged in every locale
and every count === 1 branch in the client uses the plural form. Still
open: the forms each locale needs for the remaining count strings, the
two new labels in every locale, and 35 tests that expect the old English
wording.
2026-10-07 22:57:51 +02:00

110 lines
4.8 KiB
TypeScript

import React from 'react'
import { CheckCheck, Trash2 } from 'lucide-react'
import { useTranslation } from '../i18n'
import PageShell from '../components/Layout/PageShell'
import EmptyState from '../components/shared/EmptyState'
import { Spinner } from '../components/shared/Spinner'
import InAppNotificationItem from '../components/Notifications/InAppNotificationItem.tsx'
import { useInAppNotifications } from './inAppNotifications/useInAppNotifications'
export default function InAppNotificationsPage(): React.ReactElement {
// ViewportRoute in App.tsx picks the branch now, so the phone screen is a
// chunk of its own instead of a dead limb in this one.
return <InAppNotificationsPageDesktop />
}
function InAppNotificationsPageDesktop(): React.ReactElement {
const { t } = useTranslation()
// Page = wiring container: store, filter, fetch + infinite scroll live in the hook.
const {
notifications, unreadCount, total, isLoading, hasMore,
unreadOnly, setUnreadOnly, loaderRef, displayed,
markAllRead, deleteAll,
} = useInAppNotifications()
return (
<PageShell background="var(--bg-primary)">
<div className="max-w-2xl mx-auto px-4 py-8">
{/* Header */}
<div className="flex items-center justify-between mb-6">
<div>
<h1 className="text-xl font-semibold text-content">
{t('notifications.title')}
{unreadCount > 0 && (
<span className="ml-2 px-2 py-0.5 rounded-full text-xs font-medium align-middle inline-flex items-center justify-center bg-content text-surface">
{unreadCount}
</span>
)}
</h1>
<p className="text-sm mt-0.5 text-content-muted">
{total} {t('notifications.countLabel', { count: total })}
</p>
</div>
{/* Bulk actions */}
<div className="flex items-center gap-2">
{unreadCount > 0 && (
<button type="button"
onClick={markAllRead}
className="flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-sm transition-colors text-content-secondary bg-surface-hover"
onMouseEnter={e => e.currentTarget.style.background = 'var(--bg-tertiary)'}
onMouseLeave={e => e.currentTarget.style.background = 'var(--bg-hover)'}
>
<CheckCheck className="w-4 h-4" />
<span className="hidden sm:inline">{t('notifications.markAllRead')}</span>
</button>
)}
{notifications.length > 0 && (
<button type="button"
onClick={deleteAll}
className="flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-sm transition-colors text-red-500 hover:bg-red-500/10"
>
<Trash2 className="w-4 h-4" />
<span className="hidden sm:inline">{t('notifications.deleteAll')}</span>
</button>
)}
</div>
</div>
{/* Filter toggle */}
<div className="flex gap-2 mb-4">
<button type="button"
onClick={() => setUnreadOnly(false)}
className={`px-3 py-1.5 rounded-lg text-sm font-medium transition-colors ${!unreadOnly ? 'bg-content text-surface' : 'bg-surface-hover text-content-secondary'}`}
>
{t('notifications.all')}
</button>
<button type="button"
onClick={() => setUnreadOnly(true)}
className={`px-3 py-1.5 rounded-lg text-sm font-medium transition-colors ${unreadOnly ? 'bg-content text-surface' : 'bg-surface-hover text-content-secondary'}`}
>
{t('notifications.unreadOnly')}
</button>
</div>
{/* Notification list */}
<div className="rounded-xl border overflow-hidden border-edge bg-surface-card">
{isLoading && displayed.length === 0 ? (
<div className="flex items-center justify-center py-16">
<Spinner className="w-6 h-6 border-2 border-slate-200 border-t-current" />
</div>
) : displayed.length === 0 ? (
<EmptyState scene="notifications" title={t('notifications.empty')} />
) : (
displayed.map(n => (
<InAppNotificationItem key={n.id} notification={n} />
))
)}
{/* Infinite scroll trigger */}
{hasMore && (
<div ref={loaderRef} className="flex items-center justify-center py-4">
{isLoading && <Spinner className="w-5 h-5 border-2 border-slate-200 border-t-current" />}
</div>
)}
</div>
</div>
</PageShell>
)
}