Skip to content

Commit 4ddc123

Browse files
Added providers folder & created AuthProvider + NotificationProvider.
1 parent eff7e6a commit 4ddc123

File tree

2 files changed

+159
-0
lines changed

2 files changed

+159
-0
lines changed

providers/AuthProvider.tsx

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
// providers/AuthProvider.tsx
2+
import { createContext, useState, type ReactNode } from 'react';
3+
import type { AuthUser, AuthContextType } from '../hooks/useAuth';
4+
5+
export const AuthContext = createContext<AuthContextType | undefined>(undefined);
6+
7+
export function AuthProvider({ children }: { children: ReactNode }) {
8+
const [user, setUser] = useState<AuthUser | null>(null);
9+
10+
return (
11+
<AuthContext.Provider value={{ user, setUser }}>
12+
{children}
13+
</AuthContext.Provider>
14+
);
15+
}

providers/NotificationProvider.tsx

Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,144 @@
1+
import React, { useEffect, useMemo, useRef, useState } from 'react';
2+
import { View, Text, StyleSheet, Pressable, Animated } from 'react-native';
3+
import { subscribe } from '@/services/notifier.service';
4+
5+
export type BannerItem = {
6+
id: string;
7+
type: 'info' | 'success' | 'warning' | 'error';
8+
title: string;
9+
message: string;
10+
duration: number;
11+
};
12+
13+
const maxStack = 4;
14+
15+
function useAnimatedEntrance() {
16+
const translateY = useRef(new Animated.Value(-40)).current;
17+
const opacity = useRef(new Animated.Value(0)).current;
18+
useEffect(() => {
19+
Animated.parallel([
20+
Animated.timing(translateY, { toValue: 0, duration: 200, useNativeDriver: true }),
21+
Animated.timing(opacity, { toValue: 1, duration: 200, useNativeDriver: true }),
22+
]).start();
23+
}, [translateY, opacity]);
24+
return { translateY, opacity };
25+
}
26+
27+
function Banner({ item, onClose }: { item: BannerItem; onClose: (id: string) => void }) {
28+
const { translateY, opacity } = useAnimatedEntrance();
29+
const color = item.type === 'success' ? '#1b5e20' : item.type === 'warning' ? '#9a6b00' : item.type === 'info' ? '#0d47a1' : '#7f0000';
30+
31+
return (
32+
<Animated.View style={[styles.banner, { backgroundColor: color, transform: [{ translateY }], opacity }]}>
33+
<View style={{ flex: 1 }}>
34+
<Text style={styles.bannerTitle}>{item.title}</Text>
35+
<Text style={styles.bannerText} numberOfLines={3}>
36+
{item.message}
37+
</Text>
38+
</View>
39+
<Pressable hitSlop={10} onPress={() => onClose(item.id)}>
40+
<Text style={styles.close}>×</Text>
41+
</Pressable>
42+
</Animated.View>
43+
);
44+
}
45+
46+
export function NotificationProvider({ children }: { children: React.ReactNode }) {
47+
const [items, setItems] = useState<BannerItem[]>([]);
48+
const timers = useRef(new Map<string, ReturnType<typeof setTimeout>>());
49+
50+
useEffect(() => {
51+
const unsub = subscribe((n) => {
52+
setItems((prev) => {
53+
const next = [...prev, n].slice(-maxStack);
54+
return next;
55+
});
56+
// auto-dismiss
57+
if (timers.current.has(n.id)) clearTimeout(timers.current.get(n.id)!);
58+
timers.current.set(
59+
n.id,
60+
setTimeout(() => {
61+
setItems((curr) => curr.filter((x) => x.id !== n.id));
62+
timers.current.delete(n.id);
63+
}, n.duration)
64+
);
65+
});
66+
return () => {
67+
unsub();
68+
// clear timers
69+
timers.current.forEach((t) => clearTimeout(t));
70+
timers.current.clear();
71+
};
72+
}, []);
73+
74+
const onClose = (id: string) => {
75+
if (timers.current.has(id)) {
76+
clearTimeout(timers.current.get(id)!);
77+
timers.current.delete(id);
78+
}
79+
setItems((curr) => curr.filter((x) => x.id !== id));
80+
};
81+
82+
const stack = useMemo(
83+
() => (
84+
<View pointerEvents="box-none" style={styles.container}>
85+
<View pointerEvents="box-none" style={styles.stack}>
86+
{items.map((it) => (
87+
<Banner key={it.id} item={it} onClose={onClose} />
88+
))}
89+
</View>
90+
</View>
91+
),
92+
[items]
93+
);
94+
95+
return (
96+
<View style={{ flex: 1 }}>
97+
{children}
98+
{stack}
99+
</View>
100+
);
101+
}
102+
103+
const styles = StyleSheet.create({
104+
container: {
105+
position: 'absolute',
106+
top: 0,
107+
left: 0,
108+
right: 0,
109+
zIndex: 9999,
110+
// allow touches to pass through except on banners
111+
pointerEvents: 'box-none',
112+
},
113+
stack: {
114+
marginTop: 40,
115+
paddingHorizontal: 12,
116+
gap: 8,
117+
},
118+
banner: {
119+
flexDirection: 'row',
120+
alignItems: 'center',
121+
paddingHorizontal: 12,
122+
paddingVertical: 10,
123+
borderRadius: 8,
124+
shadowColor: '#000',
125+
shadowOpacity: 0.2,
126+
shadowRadius: 6,
127+
shadowOffset: { width: 0, height: 2 },
128+
elevation: 2,
129+
},
130+
bannerTitle: {
131+
fontWeight: '700',
132+
color: 'white',
133+
marginBottom: 2,
134+
},
135+
bannerText: {
136+
color: 'white',
137+
},
138+
close: {
139+
color: 'white',
140+
fontSize: 22,
141+
marginLeft: 10,
142+
lineHeight: 22,
143+
},
144+
});

0 commit comments

Comments
 (0)