mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-10-24 16:03:02 +00:00
a7b53988ff
The travel tab is four tabs now, figures, maps, countries and marks, and offers only the newer looks, each dressed in the book's theme and linked to it. The looks books already hold still draw on their pages exactly as before; they are just no longer offered. - Figures: a summary as a poster, a ticket, tiles, a road sign, an odometer, a handwritten note, a pencil sketch or a band, and each figure on its own as a medal, on odometer drums or ringed by hand. - Maps: the route with its photographs, its stops numbered, its two ends marked or on a dark ground, over satellite or relief imagery, a map per trip, and a map of each country with its stops alone, cut to its shape. - Countries: each country as a passport stamp, a visa, an airmail postcard, its flag in cloth, a map pin, a luggage label or a sketch. - Marks: the date, the day, the distance, the coordinates and the flag as stamps, tags, pills, ribbons, die-cut stickers, embroidered patches and sketches. The schema gains a summary's and a country's look, seven mark styles and the numbered and ends stops of a map, all additive, as revision 11. Also in Studio: the save state is a tile beside who is here, a book a newer Studio saved is said by an overlay that holds the whole Studio until it reloads, and the page rail draws the first and the last page beside the blank inside of their cover. The notifications page and the bell's panel take the dialogs' look: a head with a tile, cards with an unread dot, and tools that come forward on hover.
124 lines
5.8 KiB
TypeScript
124 lines
5.8 KiB
TypeScript
import React from 'react'
|
|
import { Bell, CheckCheck, Trash2 } from 'lucide-react'
|
|
import { useTranslation } from '../i18n'
|
|
import PageShell from '../components/Layout/PageShell'
|
|
import HelpAnchor from '../components/Help/HelpAnchor'
|
|
import EmptyState from '../components/shared/EmptyState'
|
|
import ConfirmDialog from '../components/shared/ConfirmDialog'
|
|
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, confirmingDeleteAll, requestDeleteAll, cancelDeleteAll, confirmDeleteAll,
|
|
} = useInAppNotifications()
|
|
|
|
return (
|
|
<PageShell background="var(--bg-primary)">
|
|
<HelpAnchor id="notifications" />
|
|
<div className="max-w-2xl mx-auto px-4 py-8">
|
|
{/* Header: a tile, the name with how many are unread, the count under it, the bulk tools at the end. */}
|
|
<div className="flex items-center gap-4 mb-6">
|
|
<span className="grid place-items-center w-12 h-12 rounded-2xl bg-accent-subtle text-accent flex-shrink-0">
|
|
<Bell className="w-5 h-5" />
|
|
</span>
|
|
<div className="flex-1 min-w-0">
|
|
<h1 className="flex items-center gap-2 text-title font-semibold text-content">
|
|
{t('notifications.title')}
|
|
{unreadCount > 0 && (
|
|
<span className="px-2 h-6 inline-flex items-center rounded-full text-caption font-semibold tabular-nums bg-accent text-accent-text">
|
|
{unreadCount}
|
|
</span>
|
|
)}
|
|
</h1>
|
|
<p className="text-body 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 h-9 px-3 rounded-xl text-body font-medium transition-colors text-content bg-surface-card border border-edge hover:bg-surface-hover focus-visible:bg-surface-hover"
|
|
>
|
|
<CheckCheck className="w-4 h-4" />
|
|
<span className="hidden sm:inline">{t('notifications.markAllRead')}</span>
|
|
</button>
|
|
)}
|
|
{notifications.length > 0 && (
|
|
<button type="button"
|
|
onClick={requestDeleteAll}
|
|
className="flex items-center gap-1.5 h-9 px-3 rounded-xl text-body font-medium transition-colors text-danger bg-surface-card border border-edge hover:bg-danger-soft focus-visible:bg-danger-soft"
|
|
>
|
|
<Trash2 className="w-4 h-4" />
|
|
<span className="hidden sm:inline">{t('notifications.deleteAll')}</span>
|
|
</button>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
{/* Filter: one segmented control rather than two loose buttons. */}
|
|
<div className="inline-flex gap-1 p-1 mb-4 rounded-xl bg-surface-secondary" role="group" aria-label={t('notifications.title')}>
|
|
<button type="button"
|
|
onClick={() => setUnreadOnly(false)}
|
|
aria-pressed={!unreadOnly}
|
|
className={`h-8 px-3.5 rounded-lg text-body font-medium transition-colors ${!unreadOnly ? 'bg-surface-card text-content shadow-sm' : 'text-content-muted hover:text-content'}`}
|
|
>
|
|
{t('notifications.all')}
|
|
</button>
|
|
<button type="button"
|
|
onClick={() => setUnreadOnly(true)}
|
|
aria-pressed={unreadOnly}
|
|
className={`h-8 px-3.5 rounded-lg text-body font-medium transition-colors ${unreadOnly ? 'bg-surface-card text-content shadow-sm' : 'text-content-muted hover:text-content'}`}
|
|
>
|
|
{t('notifications.unreadOnly')}
|
|
</button>
|
|
</div>
|
|
|
|
{/* Notification list */}
|
|
<div className="rounded-2xl border overflow-hidden border-edge bg-surface-card py-1">
|
|
{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>
|
|
<ConfirmDialog
|
|
isOpen={confirmingDeleteAll}
|
|
onClose={cancelDeleteAll}
|
|
onConfirm={confirmDeleteAll}
|
|
title={t('notifications.deleteAllConfirmTitle')}
|
|
message={t('notifications.deleteAllConfirmText')}
|
|
confirmLabel={t('notifications.deleteAll')}
|
|
/>
|
|
</PageShell>
|
|
)
|
|
}
|