Files
fotospiel-app/resources/js/guest-v2/components/NotificationSheet.tsx
Codex Agent 298a8375b6
Some checks failed
linter / quality (push) Has been cancelled
tests / ci (push) Has been cancelled
tests / ui (push) Has been cancelled
Update guest v2 branding and theming
2026-02-03 15:18:44 +01:00

199 lines
7.5 KiB
TypeScript

import React from 'react';
import { YStack, XStack } from '@tamagui/stacks';
import { SizableText as Text } from '@tamagui/text';
import { Button } from '@tamagui/button';
import { ScrollView } from '@tamagui/scroll-view';
import { X } from 'lucide-react';
import { useOptionalNotificationCenter } from '@/guest/context/NotificationCenterContext';
import { useTranslation } from '@/guest/i18n/useTranslation';
import { useGuestThemeVariant } from '../lib/guestTheme';
type NotificationSheetProps = {
open: boolean;
onOpenChange: (open: boolean) => void;
};
export default function NotificationSheet({ open, onOpenChange }: NotificationSheetProps) {
const { t } = useTranslation();
const center = useOptionalNotificationCenter();
const { isDark } = useGuestThemeVariant();
const mutedButton = isDark ? 'rgba(248, 250, 255, 0.08)' : 'rgba(15, 23, 42, 0.06)';
const mutedButtonBorder = isDark ? 'rgba(248, 250, 255, 0.2)' : 'rgba(15, 23, 42, 0.12)';
const notifications = center?.notifications ?? [];
const unreadCount = center?.unreadCount ?? 0;
const uploadCount = (center?.queueCount ?? 0) + (center?.pendingCount ?? 0);
return (
<>
<YStack
position="fixed"
top={0}
right={0}
bottom={0}
left={0}
zIndex={1200}
pointerEvents={open ? 'auto' : 'none'}
style={{
backgroundColor: isDark ? 'rgba(15, 23, 42, 0.45)' : 'rgba(15, 23, 42, 0.2)',
opacity: open ? 1 : 0,
transition: 'opacity 240ms ease',
}}
onPress={() => onOpenChange(false)}
onClick={() => onOpenChange(false)}
onMouseDown={() => onOpenChange(false)}
onTouchStart={() => onOpenChange(false)}
/>
<YStack
position="fixed"
left={0}
right={0}
bottom={0}
zIndex={1300}
padding="$4"
backgroundColor={isDark ? '#0B101E' : '#FFFFFF'}
borderTopLeftRadius="$6"
borderTopRightRadius="$6"
pointerEvents={open ? 'auto' : 'none'}
style={{
transform: open ? 'translateY(0)' : 'translateY(100%)',
opacity: open ? 1 : 0,
transition: 'transform 320ms cubic-bezier(0.22, 0.61, 0.36, 1), opacity 220ms ease',
maxHeight: '82vh',
paddingBottom: 'calc(16px + env(safe-area-inset-bottom))',
}}
>
<YStack
width={52}
height={5}
borderRadius={999}
marginBottom="$3"
alignSelf="center"
style={{ backgroundColor: isDark ? 'rgba(148, 163, 184, 0.6)' : '#CBD5E1' }}
/>
<XStack alignItems="center" justifyContent="space-between" marginBottom="$3">
<YStack gap="$1">
<Text fontSize="$6" fontFamily="$display" fontWeight="$8" color={isDark ? '#F8FAFF' : '#0F172A'}>
{t('header.notifications.title', 'Updates')}
</Text>
<Text color={isDark ? 'rgba(226, 232, 240, 0.7)' : 'rgba(15, 23, 42, 0.6)'}>
{unreadCount > 0
? t('header.notifications.unread', { count: unreadCount }, '{count} neu')
: t('header.notifications.allRead', 'Alles gelesen')}
</Text>
</YStack>
<Button
size="$3"
circular
backgroundColor={mutedButton}
borderColor={mutedButtonBorder}
borderWidth={1}
onPress={() => onOpenChange(false)}
aria-label="Close notifications"
>
<X size={18} color={isDark ? '#F8FAFF' : '#0F172A'} />
</Button>
</XStack>
<ScrollView flex={1} showsVerticalScrollIndicator={false}>
<YStack gap="$4" paddingBottom="$2">
{center ? (
<XStack gap="$3" flexWrap="wrap">
<InfoBadge label={t('header.notifications.tabUploads', 'Uploads')} value={uploadCount} />
<InfoBadge label={t('header.notifications.tabUnread', 'Nachrichten')} value={unreadCount} />
</XStack>
) : null}
{center?.loading ? (
<Text color={isDark ? 'rgba(226, 232, 240, 0.7)' : 'rgba(15, 23, 42, 0.6)'}>
{t('common.actions.loading', 'Loading...')}
</Text>
) : notifications.length === 0 ? (
<YStack gap="$1">
<Text color={isDark ? '#F8FAFF' : '#0F172A'} fontSize="$5" fontWeight="$7">
{t('header.notifications.emptyUnread', 'Du bist auf dem neuesten Stand!')}
</Text>
<Text color={isDark ? 'rgba(226, 232, 240, 0.7)' : 'rgba(15, 23, 42, 0.6)'}>
{t('header.notifications.emptyStatus', 'Keine Upload-Hinweise oder Wartungen aktiv.')}
</Text>
</YStack>
) : (
<YStack gap="$3">
{notifications.map((item) => (
<YStack
key={item.id}
padding="$3"
borderRadius="$4"
backgroundColor={
item.status === 'new'
? isDark
? 'rgba(148, 163, 184, 0.18)'
: 'rgba(15, 23, 42, 0.06)'
: isDark
? 'rgba(15, 23, 42, 0.7)'
: 'rgba(255, 255, 255, 0.8)'
}
borderWidth={1}
borderColor={isDark ? 'rgba(148, 163, 184, 0.2)' : 'rgba(15, 23, 42, 0.12)'}
gap="$2"
>
<Text fontSize="$4" fontWeight="$7" color={isDark ? '#F8FAFF' : '#0F172A'}>
{item.title}
</Text>
{item.body ? (
<Text color={isDark ? 'rgba(226, 232, 240, 0.7)' : 'rgba(15, 23, 42, 0.6)'}>
{item.body}
</Text>
) : null}
<XStack gap="$2" flexWrap="wrap">
<Button
size="$2"
backgroundColor="$primary"
color="#FFFFFF"
onPress={() => center?.markAsRead(item.id)}
>
{t('header.notifications.markRead', 'Als gelesen markieren')}
</Button>
<Button
size="$2"
backgroundColor={mutedButton}
borderColor={mutedButtonBorder}
borderWidth={1}
onPress={() => center?.dismiss(item.id)}
>
{t('header.notifications.dismiss', 'Ausblenden')}
</Button>
</XStack>
</YStack>
))}
</YStack>
)}
</YStack>
</ScrollView>
</YStack>
</>
);
}
function InfoBadge({ label, value }: { label: string; value: number }) {
const { isDark } = useGuestThemeVariant();
return (
<YStack
padding="$3"
borderRadius="$4"
backgroundColor={isDark ? 'rgba(15, 23, 42, 0.7)' : 'rgba(255, 255, 255, 0.8)'}
borderWidth={1}
borderColor={isDark ? 'rgba(148, 163, 184, 0.2)' : 'rgba(15, 23, 42, 0.12)'}
gap="$1"
>
<Text fontSize="$2" color={isDark ? 'rgba(226, 232, 240, 0.7)' : 'rgba(15, 23, 42, 0.6)'}>
{label}
</Text>
<Text fontSize="$5" fontWeight="$7" color={isDark ? '#F8FAFF' : '#0F172A'}>
{value}
</Text>
</YStack>
);
}