notification store and update in local storage

This commit is contained in:
Jaanu423
2025-10-07 15:33:24 +05:30
parent d197e31acb
commit 80fdb77590
12 changed files with 375 additions and 47 deletions
+4 -1
View File
@@ -6,12 +6,15 @@ import { AppRegistry } from 'react-native';
import App from './App'; import App from './App';
import { name as appName } from './app.json'; import { name as appName } from './app.json';
import { Provider } from 'react-redux'; import { Provider } from 'react-redux';
import { store } from './src/redux/store'; import { persistor, store } from './src/redux/store';
import { PersistGate } from 'redux-persist/integration/react';
// ✅ Wrapper with Provider // ✅ Wrapper with Provider
const ReduxWrapper = () => ( const ReduxWrapper = () => (
<Provider store={store}> <Provider store={store}>
<PersistGate loading={null} persistor={persistor}>
<App /> <App />
</PersistGate>
</Provider> </Provider>
); );
+45 -1
View File
@@ -9,6 +9,7 @@
"version": "0.0.1", "version": "0.0.1",
"dependencies": { "dependencies": {
"@notifee/react-native": "^9.1.8", "@notifee/react-native": "^9.1.8",
"@react-native-async-storage/async-storage": "^1.24.0",
"@react-native-clipboard/clipboard": "^1.16.3", "@react-native-clipboard/clipboard": "^1.16.3",
"@react-native-community/blur": "^4.4.1", "@react-native-community/blur": "^4.4.1",
"@react-native-community/slider": "^5.0.1", "@react-native-community/slider": "^5.0.1",
@@ -45,7 +46,8 @@
"react-native-vector-icons": "^10.3.0", "react-native-vector-icons": "^10.3.0",
"react-native-walkthrough-tooltip": "^1.6.0", "react-native-walkthrough-tooltip": "^1.6.0",
"react-native-worklets": "^0.6.0", "react-native-worklets": "^0.6.0",
"react-redux": "^9.2.0" "react-redux": "^9.2.0",
"redux-persist": "^6.0.0"
}, },
"devDependencies": { "devDependencies": {
"@babel/core": "^7.25.2", "@babel/core": "^7.25.2",
@@ -3288,6 +3290,18 @@
"integrity": "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==", "integrity": "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==",
"license": "BSD-3-Clause" "license": "BSD-3-Clause"
}, },
"node_modules/@react-native-async-storage/async-storage": {
"version": "1.24.0",
"resolved": "https://registry.npmjs.org/@react-native-async-storage/async-storage/-/async-storage-1.24.0.tgz",
"integrity": "sha512-W4/vbwUOYOjco0x3toB8QCr7EjIP6nE9G7o8PMguvvjYT5Awg09lyV4enACRx4s++PPulBiBSjL0KTFx2u0Z/g==",
"license": "MIT",
"dependencies": {
"merge-options": "^3.0.4"
},
"peerDependencies": {
"react-native": "^0.0.0-0 || >=0.60 <1.0"
}
},
"node_modules/@react-native-clipboard/clipboard": { "node_modules/@react-native-clipboard/clipboard": {
"version": "1.16.3", "version": "1.16.3",
"license": "MIT", "license": "MIT",
@@ -7892,6 +7906,15 @@
"node": ">=8" "node": ">=8"
} }
}, },
"node_modules/is-plain-obj": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-2.1.0.tgz",
"integrity": "sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA==",
"license": "MIT",
"engines": {
"node": ">=8"
}
},
"node_modules/is-regex": { "node_modules/is-regex": {
"version": "1.2.1", "version": "1.2.1",
"dev": true, "dev": true,
@@ -9094,6 +9117,18 @@
"version": "5.2.1", "version": "5.2.1",
"license": "MIT" "license": "MIT"
}, },
"node_modules/merge-options": {
"version": "3.0.4",
"resolved": "https://registry.npmjs.org/merge-options/-/merge-options-3.0.4.tgz",
"integrity": "sha512-2Sug1+knBjkaMsMgf1ctR1Ujx+Ayku4EdJN4Z+C2+JzoeF7A3OZ9KM2GY0CpQS51NR61LTurMJrRKPhSs3ZRTQ==",
"license": "MIT",
"dependencies": {
"is-plain-obj": "^2.1.0"
},
"engines": {
"node": ">=10"
}
},
"node_modules/merge-stream": { "node_modules/merge-stream": {
"version": "2.0.0", "version": "2.0.0",
"license": "MIT" "license": "MIT"
@@ -13213,6 +13248,15 @@
"version": "5.0.1", "version": "5.0.1",
"license": "MIT" "license": "MIT"
}, },
"node_modules/redux-persist": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/redux-persist/-/redux-persist-6.0.0.tgz",
"integrity": "sha512-71LLMbUq2r02ng2We9S215LtPu3fY0KgaGE0k8WRgl6RkqxtGfl7HUozz1Dftwsb0D/5mZ8dwAaPbtnzfvbEwQ==",
"license": "MIT",
"peerDependencies": {
"redux": ">4.0.0"
}
},
"node_modules/redux-thunk": { "node_modules/redux-thunk": {
"version": "3.1.0", "version": "3.1.0",
"license": "MIT", "license": "MIT",
+3 -1
View File
@@ -11,6 +11,7 @@
}, },
"dependencies": { "dependencies": {
"@notifee/react-native": "^9.1.8", "@notifee/react-native": "^9.1.8",
"@react-native-async-storage/async-storage": "^1.24.0",
"@react-native-clipboard/clipboard": "^1.16.3", "@react-native-clipboard/clipboard": "^1.16.3",
"@react-native-community/blur": "^4.4.1", "@react-native-community/blur": "^4.4.1",
"@react-native-community/slider": "^5.0.1", "@react-native-community/slider": "^5.0.1",
@@ -47,7 +48,8 @@
"react-native-vector-icons": "^10.3.0", "react-native-vector-icons": "^10.3.0",
"react-native-walkthrough-tooltip": "^1.6.0", "react-native-walkthrough-tooltip": "^1.6.0",
"react-native-worklets": "^0.6.0", "react-native-worklets": "^0.6.0",
"react-redux": "^9.2.0" "react-redux": "^9.2.0",
"redux-persist": "^6.0.0"
}, },
"devDependencies": { "devDependencies": {
"@babel/core": "^7.25.2", "@babel/core": "^7.25.2",
+9 -6
View File
@@ -9,13 +9,13 @@ import {
StyleProp, StyleProp,
} from "react-native"; } from "react-native";
import LinearGradient from "react-native-linear-gradient"; import LinearGradient from "react-native-linear-gradient";
import { Ionicons } from "@react-native-vector-icons/ionicons"; import { Ionicons, IoniconsIconName } from "@react-native-vector-icons/ionicons";
import colors from "../colors"; // Assuming your pastel colors are defined here import colors from "../colors"; // Assuming your pastel colors are defined here
type CardProps = { type CardProps = {
title: string; title: string;
subtitle?: string; subtitle?: string;
iconName?: string; iconName?: IoniconsIconName;
onPress?: () => void; onPress?: () => void;
backOne?: string; backOne?: string;
backTwo?: string; backTwo?: string;
@@ -89,7 +89,7 @@ const AnimatedCard: React.FC<CardProps> = ({
const styles = StyleSheet.create({ const styles = StyleSheet.create({
card: { card: {
minWidth: 170, // minWidth: 170,
flex: 1, flex: 1,
height: 170, height: 170,
marginHorizontal: 8, marginHorizontal: 8,
@@ -101,13 +101,16 @@ const styles = StyleSheet.create({
shadowOpacity: 0.15, shadowOpacity: 0.15,
shadowRadius: 12, shadowRadius: 12,
elevation: 5, elevation: 5,
backgroundColor: colors.white,
}, },
gradient: { gradient: {
position: "relative", position: "relative",
flex: 1, flex: 1,
justifyContent: "center", // justifyContent: "center",
alignItems: "center", // alignItems: "center",
justifyContent: "space-between",
borderRadius: 24,
overflow: "hidden",
gap: 10,
}, },
graphicCircleLarge: { graphicCircleLarge: {
position: "absolute", position: "absolute",
+8 -2
View File
@@ -2,6 +2,7 @@ import React from "react";
import { View, Text, TouchableOpacity, StyleSheet } from "react-native"; import { View, Text, TouchableOpacity, StyleSheet } from "react-native";
import { useNavigation } from "@react-navigation/native"; import { useNavigation } from "@react-navigation/native";
import {Ionicons} from "@react-native-vector-icons/ionicons"; import {Ionicons} from "@react-native-vector-icons/ionicons";
import colors from "../colors";
type HeaderProps = { type HeaderProps = {
title: string; title: string;
@@ -17,8 +18,8 @@ const Header: React.FC<HeaderProps> = ({
showBack = true, showBack = true,
onBackPress, onBackPress,
rightAction, rightAction,
titleColor = "", titleColor = colors.primary,
arrowColor='pink' arrowColor = colors.primary
}) => { }) => {
const navigation = useNavigation(); const navigation = useNavigation();
@@ -32,6 +33,11 @@ const Header: React.FC<HeaderProps> = ({
return ( return (
<View style={styles.header}> <View style={styles.header}>
{showBack && (
<TouchableOpacity onPress={handleBack} style={{padding:6}}>
<Ionicons name="arrow-back" size={24} color={arrowColor} />
</TouchableOpacity>
)}
<Text style={[styles.headerTitle, {color:titleColor}]}>{title}</Text> <Text style={[styles.headerTitle, {color:titleColor}]}>{title}</Text>
<View>{rightAction}</View> <View>{rightAction}</View>
</View> </View>
+3
View File
@@ -20,6 +20,7 @@ import InviteScreen from "../screens/InviteScreen";
import ManagementTabNavigation from "../screens/management/ManagementTabNavigation"; import ManagementTabNavigation from "../screens/management/ManagementTabNavigation";
import TransactionsList from "../screens/transaction/TransactionList"; import TransactionsList from "../screens/transaction/TransactionList";
import RandomSelectUserScreen from "../screens/management/RandomSelectUserScreen"; import RandomSelectUserScreen from "../screens/management/RandomSelectUserScreen";
import Notification from "../screens/notification/Notification";
export type RootStackParamList = { export type RootStackParamList = {
Splash: undefined; Splash: undefined;
@@ -42,6 +43,7 @@ export type RootStackParamList = {
ManagementTabs:{ groupId: string | number;} ManagementTabs:{ groupId: string | number;}
TransactionList:undefined TransactionList:undefined
RandomSelectUserScreen: { groupId: string | number, cycleNumber: number }; RandomSelectUserScreen: { groupId: string | number, cycleNumber: number };
Notification:undefined
}; };
const Stack = createNativeStackNavigator<RootStackParamList>(); const Stack = createNativeStackNavigator<RootStackParamList>();
@@ -95,6 +97,7 @@ const linking = {
<Stack.Screen name="ManagementTabs" component={ManagementTabNavigation} options={{ headerShown: false }} /> <Stack.Screen name="ManagementTabs" component={ManagementTabNavigation} options={{ headerShown: false }} />
<Stack.Screen name="TransactionList" component={TransactionsList} options={{ headerShown: false }} /> <Stack.Screen name="TransactionList" component={TransactionsList} options={{ headerShown: false }} />
<Stack.Screen name="RandomSelectUserScreen" component={RandomSelectUserScreen} options={{ headerShown: false }} /> <Stack.Screen name="RandomSelectUserScreen" component={RandomSelectUserScreen} options={{ headerShown: false }} />
<Stack.Screen name="Notification" component={Notification} options={{ headerShown: false }} />
</Stack.Navigator> </Stack.Navigator>
</NavigationContainer> </NavigationContainer>
); );
+58
View File
@@ -0,0 +1,58 @@
import { createSlice, PayloadAction } from '@reduxjs/toolkit';
// Define the shape of a notification object
export interface Notification {
id: string;
title: string;
message: string;
time: string;
read: boolean;
}
// Define the slice state type
interface NotificationState {
notifications: Notification[];
}
// Initial state
const initialState: NotificationState = {
notifications: [],
};
const notificationSlice = createSlice({
name: 'notifications',
initialState,
reducers: {
addNotification: (state, action: PayloadAction<Notification>) => {
// Adds the new notification to the top
state.notifications.unshift(action.payload);
},
removeNotification: (state, action: PayloadAction<string>) => {
// Remove notification by id
state.notifications = state.notifications.filter(
(item) => item.id !== action.payload
);
},
clearAllNotifications: (state) => {
state.notifications = [];
},
markAsRead: (state, action: PayloadAction<string>) => {
// Mark one notification as read
const notification = state.notifications.find(
(item) => item.id === action.payload
);
if (notification) {
notification.read = true;
}
},
},
});
export const {
addNotification,
removeNotification,
clearAllNotifications,
markAsRead,
} = notificationSlice.actions;
export default notificationSlice.reducer;
+47 -10
View File
@@ -1,13 +1,50 @@
// store.ts // // store.ts
import { configureStore } from "@reduxjs/toolkit"; // import { configureStore } from "@reduxjs/toolkit";
import userReducer from "./userSlice"; // adjust the path // import userReducer from "./userSlice"; // adjust the path
export const store = configureStore({ // export const store = configureStore({
reducer: { // reducer: {
user: userReducer, // register user slice here // user: userReducer, // register user slice here
}, // },
// });
// // ✅ Types for TypeScript
// export type RootState = ReturnType<typeof store.getState>;
// export type AppDispatch = typeof store.dispatch;
import { configureStore, combineReducers } from '@reduxjs/toolkit';
import AsyncStorage from '@react-native-async-storage/async-storage';
import { persistReducer, persistStore } from 'redux-persist';
// import slices
import userReducer from './userSlice';
import notificationReducer from './notificationSlice';
// combine reducers
const rootReducer = combineReducers({
user: userReducer,
notifications: notificationReducer,
}); });
// ✅ Types for TypeScript // persist configuration
export type RootState = ReturnType<typeof store.getState>; const persistConfig = {
export type AppDispatch = typeof store.dispatch; key: 'root',
storage: AsyncStorage,
whitelist: ['user', 'notifications'], // persist both slices
};
// wrap root reducer with persistReducer
const persistedReducer = persistReducer(persistConfig, rootReducer);
// configure store
export const store = configureStore({
reducer: persistedReducer,
middleware: (getDefaultMiddleware) =>
getDefaultMiddleware({
serializableCheck: false,
}),
});
export const persistor = persistStore(store);
+6 -4
View File
@@ -20,7 +20,7 @@ import { setUserData } from "../../redux/userSlice";
import { TabNavigationProp } from "../../navigation/BottomTabs"; import { TabNavigationProp } from "../../navigation/BottomTabs";
import UserProfileStatus from "./UserProfileStatus"; import UserProfileStatus from "./UserProfileStatus";
import AnimatedCard from "../../components/common/AnimatedCard"; import AnimatedCard from "../../components/common/AnimatedCard";
import { Ionicons } from "@react-native-vector-icons/ionicons"; import { Ionicons, IoniconsIconName } from "@react-native-vector-icons/ionicons";
import { apiCall } from "../../utils/ApiHelper"; import { apiCall } from "../../utils/ApiHelper";
if (Platform.OS === "android" && UIManager.setLayoutAnimationEnabledExperimental) { if (Platform.OS === "android" && UIManager.setLayoutAnimationEnabledExperimental) {
@@ -124,8 +124,9 @@ const Home: React.FC = () => {
<Text style={styles.greeting}>Hello,</Text> <Text style={styles.greeting}>Hello,</Text>
<Text style={styles.userName}>{user.firstName || apiUser?.firstName || 'User'}</Text> <Text style={styles.userName}>{user.firstName || apiUser?.firstName || 'User'}</Text>
</View> </View>
<TouchableOpacity onPress={() => navigation.navigate("Profile" as never)}> <TouchableOpacity onPress={() => navigation.navigate("Notification" as never)}>
<Image source={getProfileImageUri()} style={styles.profileImage} /> {/* <Image source={getProfileImageUri()} style={styles.profileImage} /> */}
<Ionicons name="notifications-outline" size={30} color={colors.textDark} />
</TouchableOpacity> </TouchableOpacity>
</View> </View>
@@ -184,6 +185,7 @@ const styles = StyleSheet.create({
}, },
cardWrapper: { cardWrapper: {
flex: 1, flex: 1,
marginHorizontal: 8, // marginHorizontal: 8,
// backgroundColor:'red'
}, },
}); });
+170
View File
@@ -0,0 +1,170 @@
import React from 'react';
import {
View,
Text,
TouchableOpacity,
FlatList,
StyleSheet,
} from 'react-native';
import { useDispatch, useSelector } from 'react-redux';
import {
addNotification,
removeNotification,
clearAllNotifications,
} from '../../redux/notificationSlice';
import LinearGradient from 'react-native-linear-gradient';
import { Ionicons } from '@react-native-vector-icons/ionicons';
import AppView from '../../components/AppView';
import Header from '../../components/common/Header';
const Notification = () => {
const dispatch = useDispatch();
const notifications = useSelector(
(state: any) => state.notifications.notifications
);
const handleAdd = () => {
const newNotification = {
id: Date.now().toString(),
title: 'New Message',
message: 'You have a new message in your inbox.',
time: new Date().toLocaleString(),
read: false,
};
dispatch(addNotification(newNotification));
};
const handleDelete = (id: string) => {
dispatch(removeNotification(id));
};
const handleClearAll = () => {
dispatch(clearAllNotifications());
};
return (
<AppView>
{/* Header with Add and Clear All */}
<Header title='Notifications' rightAction={<LinearGradient
colors={['#FF6B6B', '#FF8E53']}
style={styles.clearButtonGradient}>
<TouchableOpacity onPress={handleClearAll}>
<Text style={styles.clearButtonText}>Delete All</Text>
</TouchableOpacity>
</LinearGradient>}
/>
{/* Add Notification Button */}
{/* <LinearGradient
colors={['#4facfe', '#00f2fe']}
style={styles.addButtonGradient}>
<TouchableOpacity onPress={handleAdd}>
<Text style={styles.addButtonText}>Add Notification</Text>
</TouchableOpacity>
</LinearGradient> */}
{/* Notification List */}
<FlatList
data={notifications}
keyExtractor={(item) => item.id}
showsVerticalScrollIndicator={false}
contentContainerStyle={styles.listContainer}
renderItem={({ item }) => (
<LinearGradient
colors={['#f5f7fa', '#c3cfe2']}
style={styles.notificationCard}>
<View style={styles.notificationContent}>
<View style={{ flex: 1 }}>
<Text style={styles.title}>{item.title}</Text>
<Text style={styles.message}>{item.message}</Text>
<Text style={styles.time}>{item.time}</Text>
</View>
<TouchableOpacity onPress={() => handleDelete(item.id)}>
<Ionicons name="trash-outline" size={22} color="#e63946" />
</TouchableOpacity>
</View>
</LinearGradient>
)}
ListEmptyComponent={
<Text style={styles.emptyText}>No notifications found.</Text>
}
/>
</AppView>
);
};
export default Notification;
const styles = StyleSheet.create({
headerContainer: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
paddingHorizontal: 16,
paddingTop: 12,
},
headerTitle: {
fontSize: 20,
fontWeight: '700',
color: '#222',
},
clearButtonGradient: {
borderRadius: 8,
paddingVertical: 6,
paddingHorizontal: 14,
},
clearButtonText: {
color: '#fff',
fontWeight: '600',
},
addButtonGradient: {
margin: 16,
borderRadius: 8,
paddingVertical: 10,
alignItems: 'center',
},
addButtonText: {
color: '#fff',
fontWeight: '600',
fontSize: 16,
},
listContainer: {
paddingHorizontal: 12,
paddingBottom: 30,
},
notificationCard: {
borderRadius: 10,
padding: 12,
marginBottom: 10,
shadowColor: '#000',
shadowOpacity: 0.1,
shadowRadius: 5,
elevation: 2,
},
notificationContent: {
flexDirection: 'row',
alignItems: 'center',
},
title: {
fontWeight: '700',
fontSize: 16,
color: '#333',
},
message: {
fontSize: 14,
color: '#555',
marginTop: 2,
},
time: {
fontSize: 12,
color: '#888',
marginTop: 4,
},
emptyText: {
textAlign: 'center',
marginTop: 60,
fontSize: 16,
color: '#999',
},
});
+2 -2
View File
@@ -377,7 +377,7 @@ const Profile = () => {
<View style={styles.header}> <View style={styles.header}>
<Text style={styles.headerTitle}>User Profile</Text> <Text style={styles.headerTitle}>User Profile</Text>
<TouchableOpacity onPress={() => confirmLogout(navigation)} style={styles.refreshButton}> <TouchableOpacity onPress={() => confirmLogout(navigation)} style={styles.refreshButton}>
<Ionicons name="log-out-outline" size={24} color={colors.Profile} /> <Ionicons name="log-out-outline" size={24} color={colors.primary} />
</TouchableOpacity> </TouchableOpacity>
</View> </View>
{/* if (fetching || !user) { {/* if (fetching || !user) {
@@ -701,7 +701,7 @@ const styles = StyleSheet.create({
borderBottomRightRadius: 20, borderBottomRightRadius: 20,
}, },
headerTitle: { headerTitle: {
fontSize: 22, fontWeight: "600", color: colors.Profile fontSize: 22, fontWeight: "600", color: colors.primary
}, },
refreshButton: { refreshButton: {
padding: 4, padding: 4,
+1 -1
View File
@@ -1,6 +1,6 @@
export const BASE_URL = "https://api.godemo.in/FundCollect/api/"
// export const BASE_URL = "https://api.godemo.in/FundCollect/api/" // export const BASE_URL = "https://api.godemo.in/FundCollect/api/"
export const BASE_URL = "http://192.168.1.5:9060/api/"
export const LOGIN_URL = `${BASE_URL}user/generate/otp?mobiles=` export const LOGIN_URL = `${BASE_URL}user/generate/otp?mobiles=`
export const VARIFY_OTP_URL = `${BASE_URL}user/otp/verify` export const VARIFY_OTP_URL = `${BASE_URL}user/otp/verify`
export const ORGRAGISTRATION = `${BASE_URL}organizer/registration/apply` export const ORGRAGISTRATION = `${BASE_URL}organizer/registration/apply`