Добавлены уведомления
This commit is contained in:
@@ -0,0 +1,121 @@
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import React, { useEffect, useMemo, useRef, useState } from "react";
|
||||
import { FlatList, StyleSheet, Text, View } from "react-native";
|
||||
import { notificationApi, notificationKeys } from "../notification-api";
|
||||
import { useNotificationAction } from "../notification-actions";
|
||||
import { typeMap } from "../notification-presenter";
|
||||
import { colors, spacing } from "../theme";
|
||||
import type { NotificationItem } from "../types";
|
||||
import { ErrorNotice, Loading, styles } from "../ui";
|
||||
import { NotificationCard } from "./NotificationCard";
|
||||
|
||||
export function NotificationCarousel({
|
||||
authenticated,
|
||||
autoplay = false,
|
||||
autoplayIntervalMs = 5000,
|
||||
requireAuth,
|
||||
}: {
|
||||
authenticated: boolean;
|
||||
autoplay?: boolean;
|
||||
autoplayIntervalMs?: number;
|
||||
requireAuth: (afterAuth?: () => Promise<void>) => void;
|
||||
}) {
|
||||
const client = useQueryClient();
|
||||
const list = useRef<FlatList<NotificationItem>>(null);
|
||||
const [activeIndex, setActiveIndex] = useState(0);
|
||||
const [actionError, setActionError] = useState<unknown>();
|
||||
const catalog = useQuery({
|
||||
queryKey: notificationKeys.catalog,
|
||||
queryFn: notificationApi.catalog,
|
||||
staleTime: Infinity,
|
||||
});
|
||||
const notifications = useQuery({
|
||||
queryKey: notificationKeys.home(authenticated),
|
||||
queryFn: authenticated ? () => notificationApi.list("home") : notificationApi.guestHome,
|
||||
});
|
||||
const byCode = useMemo(() => typeMap(catalog.data ?? []), [catalog.data]);
|
||||
const action = useNotificationAction({ authenticated, requireAuth, onError: setActionError });
|
||||
const hide = useMutation({
|
||||
mutationFn: notificationApi.hide,
|
||||
onSuccess: async () => {
|
||||
await Promise.all([
|
||||
client.invalidateQueries({ queryKey: notificationKeys.home(true) }),
|
||||
client.invalidateQueries({ queryKey: notificationKeys.center }),
|
||||
]);
|
||||
},
|
||||
onError: setActionError,
|
||||
});
|
||||
const data = notifications.data ?? [];
|
||||
|
||||
useEffect(() => {
|
||||
if (!autoplay || data.length < 2) return;
|
||||
const timer = setInterval(() => {
|
||||
setActiveIndex((current) => {
|
||||
const next = (current + 1) % data.length;
|
||||
list.current?.scrollToIndex({ index: next, animated: true });
|
||||
return next;
|
||||
});
|
||||
}, Math.max(1000, autoplayIntervalMs));
|
||||
return () => clearInterval(timer);
|
||||
}, [autoplay, autoplayIntervalMs, data.length]);
|
||||
|
||||
if (notifications.isLoading || catalog.isLoading) {
|
||||
return <View style={local.state}><Loading /></View>;
|
||||
}
|
||||
if (notifications.error || catalog.error) {
|
||||
return (
|
||||
<View style={local.state}>
|
||||
<ErrorNotice
|
||||
error={notifications.error ?? catalog.error}
|
||||
retry={() => { void notifications.refetch(); void catalog.refetch(); }}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
if (!data.length) return null;
|
||||
|
||||
return (
|
||||
<View style={local.section}>
|
||||
<Text accessibilityRole="header" style={[styles.heading, local.heading]}>Важное для вас</Text>
|
||||
<FlatList
|
||||
ref={list}
|
||||
horizontal
|
||||
data={data}
|
||||
keyExtractor={(item) => item.id}
|
||||
contentContainerStyle={local.content}
|
||||
ItemSeparatorComponent={() => <View style={{ width: spacing.md }} />}
|
||||
onMomentumScrollEnd={(event) => {
|
||||
const width = event.nativeEvent.layoutMeasurement.width;
|
||||
if (width > 0) setActiveIndex(Math.round(event.nativeEvent.contentOffset.x / width));
|
||||
}}
|
||||
renderItem={({ item }) => (
|
||||
<NotificationCard
|
||||
disabled={hide.isPending}
|
||||
item={item}
|
||||
type={byCode.get(item.notification_type)}
|
||||
onCta={() => void action(item, byCode.get(item.notification_type))}
|
||||
{...(authenticated ? { onHide: () => hide.mutate(item.id) } : {})}
|
||||
/>
|
||||
)}
|
||||
showsHorizontalScrollIndicator={false}
|
||||
/>
|
||||
{data.length > 1 && (
|
||||
<View style={local.dots} accessibilityLabel={`${activeIndex + 1} из ${data.length}`}>
|
||||
{data.map((item, index) => <View key={item.id} style={[local.dot, index === activeIndex && local.dotActive]} />)}
|
||||
</View>
|
||||
)}
|
||||
{Boolean(actionError) && <View style={local.error}><ErrorNotice error={actionError} /></View>}
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
const local = StyleSheet.create({
|
||||
section: { paddingVertical: spacing.md },
|
||||
heading: { paddingHorizontal: spacing.lg, marginBottom: spacing.sm },
|
||||
content: { paddingHorizontal: spacing.lg },
|
||||
state: { paddingHorizontal: spacing.lg, paddingVertical: spacing.md },
|
||||
dots: { flexDirection: "row", justifyContent: "center", gap: 6, marginTop: spacing.sm },
|
||||
dot: { width: 6, height: 6, borderRadius: 3, backgroundColor: colors.muted },
|
||||
dotActive: { width: 16, backgroundColor: colors.primary },
|
||||
error: { paddingHorizontal: spacing.lg, paddingTop: spacing.sm },
|
||||
});
|
||||
Reference in New Issue
Block a user