matiru
Blockchain-Based Supply Chain Transparency for Agricultural Producegit clone https://github.com/maydayv7/matiru
frontend/screens/Notifications.js (5252B)
1import { useState, useEffect, useContext, useCallback } from "react";
2import {
3 ActivityIndicator,
4 Alert,
5 FlatList,
6 RefreshControl,
7 StyleSheet,
8 Text,
9 View,
10} from "react-native";
11import { SafeAreaView } from "react-native-safe-area-context";
12import AsyncStorage from "@react-native-async-storage/async-storage";
13import { MaterialCommunityIcons } from "@expo/vector-icons";
14
15import ScreenHeader from "../components/ScreenHeader";
16import { api } from "../services/api";
17import { AuthContext } from "../AuthContext";
18import styles, { colors } from "../styles";
19
20export default function NotificationsScreen({ navigation }) {
21 const { user } = useContext(AuthContext);
22 const [notifications, setNotifications] = useState([]);
23 const [loading, setLoading] = useState(true);
24 const [refreshing, setRefreshing] = useState(false);
25
26 const CACHE_KEY = `@notifications_${user?.id}`;
27
28 const loadNotifications = useCallback(
29 async (isRefreshing = false) => {
30 if (!user?.token) return;
31 if (!isRefreshing) setLoading(true);
32
33 try {
34 const cachedData = await AsyncStorage.getItem(CACHE_KEY);
35 if (cachedData && !isRefreshing) {
36 const parsedData = JSON.parse(cachedData);
37 parsedData.sort(sortByLastAction);
38 setProduces(parsedData);
39 }
40 } catch (e) {
41 console.error("Failed to load cache:", e);
42 }
43
44 try {
45 const data = await api.getNotifications(user.token);
46 const sorted = (data.notifications || []).sort(
47 (a, b) => new Date(b.date) - new Date(a.date)
48 );
49 setNotifications(sorted);
50 await AsyncStorage.setItem(CACHE_KEY, JSON.stringify(sorted));
51 } catch (err) {
52 if (notifications.length === 0)
53 Alert.alert("Error", "Failed to fetch new notifications");
54 console.error("Network fetch failed:", err.message);
55 } finally {
56 if (!isRefreshing) setLoading(false);
57 }
58 },
59 [user?.token, notifications.length, CACHE_KEY]
60 );
61
62 useEffect(() => {
63 setLoading(true);
64 loadNotifications().finally(() => setLoading(false));
65 }, [loadNotifications]);
66
67 const onRefresh = useCallback(async () => {
68 setRefreshing(true);
69 await loadNotifications(true);
70 setRefreshing(false);
71 }, [loadNotifications]);
72
73 const getIconForTitle = (title) => {
74 if (title.toLowerCase().includes("registered")) return "sprout";
75 if (title.toLowerCase().includes("location")) return "map-marker-check";
76 if (title.toLowerCase().includes("inspected")) return "shield-search";
77 if (title.toLowerCase().includes("received")) return "package-down";
78 if (title.toLowerCase().includes("transferred")) return "package-up";
79 if (title.toLowerCase().includes("updated")) return "pencil";
80 if (title.toLowerCase().includes("split")) return "call-split";
81 return "bell";
82 };
83
84 const renderItem = ({ item }) => (
85 <View style={localStyles.card}>
86 <MaterialCommunityIcons
87 name={getIconForTitle(item.title)}
88 size={24}
89 color={colors.darkGreen}
90 style={localStyles.icon}
91 />
92 <View style={localStyles.content}>
93 <Text style={localStyles.title}>{item.title}</Text>
94 <Text style={localStyles.message}>{item.message}</Text>
95 <Text style={localStyles.date}>
96 {new Date(item.date).toLocaleString()}
97 </Text>
98 </View>
99 </View>
100 );
101
102 return (
103 <SafeAreaView style={styles.container}>
104 <ScreenHeader
105 title="Notifications"
106 navigation={navigation}
107 showBack={true}
108 hideSearchButton={true}
109 hideNotificationsButton={true}
110 />
111 {loading && notifications.length === 0 ? (
112 <ActivityIndicator
113 size="large"
114 color={colors.darkGreen}
115 style={{ marginTop: 50 }}
116 />
117 ) : (
118 <FlatList
119 data={notifications}
120 renderItem={renderItem}
121 keyExtractor={(item) => item.id}
122 ListEmptyComponent={
123 <Text style={localStyles.emptyText}>No notifications found</Text>
124 }
125 contentContainerStyle={{ paddingHorizontal: 16 }}
126 refreshControl={
127 <RefreshControl
128 refreshing={refreshing}
129 onRefresh={onRefresh}
130 colors={[colors.darkGreen]}
131 tintColor={colors.darkGreen}
132 />
133 }
134 />
135 )}
136 </SafeAreaView>
137 );
138}
139
140const localStyles = StyleSheet.create({
141 card: {
142 backgroundColor: "white",
143 borderRadius: 12,
144 padding: 16,
145 marginBottom: 12,
146 flexDirection: "row",
147 alignItems: "flex-start",
148 shadowColor: "#000",
149 shadowOpacity: 0.08,
150 shadowOffset: { width: 0, height: 2 },
151 shadowRadius: 5,
152 elevation: 3,
153 },
154 icon: {
155 marginRight: 16,
156 marginTop: 2,
157 },
158 content: {
159 flex: 1,
160 },
161 title: {
162 fontSize: 16,
163 fontWeight: "bold",
164 color: colors.darkGreen,
165 },
166 message: {
167 fontSize: 14,
168 color: colors.gray,
169 marginTop: 4,
170 lineHeight: 20,
171 },
172 date: {
173 fontSize: 12,
174 color: "#999",
175 marginTop: 8,
176 textAlign: "right",
177 },
178 emptyText: {
179 textAlign: "center",
180 marginTop: 50,
181 fontSize: 16,
182 color: colors.gray,
183 },
184});