mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-10-23 07:26:30 +00:00
de128cc4ae
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.
110 lines
4.8 KiB
TypeScript
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>
|
|
)
|
|
}
|