frontend: introduce toast components/context
Pedro Lucas Porcellis porcellis@eletrotupi.com · 4 months ago
409c3751b50fc119fcd4ef0ef0b88531565cfd95 Browse filesParents
f74f10cChanged files
4
Additions
+138
Deletions
-18
frontend/app/_layout.tsx
1 1
import {
2 2
DarkTheme,
3 3
DefaultTheme,
4 4
ThemeProvider,
5 5
} from "@react-navigation/native";
7 7
import { useColorScheme } from "react-native";
8 8
import { QueryClientProvider } from "@tanstack/react-query";
9 9
import { queryClient } from "@/lib/queryClient";
10 10
import { useNotificationObserver } from "@/hooks/useNotificationObserver";
11 11
import { AuthProvider } from "@/context/AuthContext";
12 +
import { ToastProvider } from "@/context/ToastContext";
12 13
13 14
export const unstable_settings = {
14 15
initialRouteName: "(tabs)",
15 16
};
16 17
23 24
const colorScheme = useColorScheme();
24 25
25 26
return (
26 27
<QueryClientProvider client={queryClient}>
27 28
<ThemeProvider value={DefaultTheme}>
29 +
<ToastProvider>
28 30
<AuthProvider>
29 31
<AppBootstrap />
30 32
<Stack>
31 33
<Stack.Screen name="index" options={{ headerShown: false }} />
32 34
<Stack.Screen name="(tabs)" options={{ headerShown: false }} />
117 119
name="modal"
118 120
options={{ presentation: "modal", title: "Modal" }}
119 121
/>
120 122
</Stack>
121 123
</AuthProvider>
124 +
</ToastProvider>
122 125
</ThemeProvider>
123 126
</QueryClientProvider>
124 127
);
125 128
}
/dev/null → frontend/components/ui/Toast.tsx
1 +
import React, { useEffect, useRef } from 'react';
2 +
import { Animated, StyleSheet, Text } from 'react-native';
3 +
import { useSafeAreaInsets } from 'react-native-safe-area-context';
4 +
5 +
export type ToastVariant = 'error' | 'success' | 'warning';
6 +
7 +
interface ToastProps {
8 +
message: string;
9 +
variant: ToastVariant;
10 +
visible: boolean;
11 +
}
12 +
13 +
const VARIANT_COLORS: Record<ToastVariant, string> = {
14 +
error: '#EF4444',
15 +
success: '#22c55e',
16 +
warning: '#F97316',
17 +
};
18 +
19 +
export function Toast({ message, variant, visible }: ToastProps) {
20 +
const insets = useSafeAreaInsets();
21 +
const translateY = useRef(new Animated.Value(-120)).current;
22 +
const opacity = useRef(new Animated.Value(0)).current;
23 +
24 +
useEffect(() => {
25 +
if (visible) {
26 +
Animated.parallel([
27 +
Animated.spring(translateY, {
28 +
toValue: 0,
29 +
useNativeDriver: true,
30 +
tension: 80,
31 +
friction: 10,
32 +
}),
33 +
Animated.timing(opacity, {
34 +
toValue: 1,
35 +
duration: 200,
36 +
useNativeDriver: true,
37 +
}),
38 +
]).start();
39 +
} else {
40 +
Animated.parallel([
41 +
Animated.timing(translateY, {
42 +
toValue: -120,
43 +
duration: 250,
44 +
useNativeDriver: true,
45 +
}),
46 +
Animated.timing(opacity, {
47 +
toValue: 0,
48 +
duration: 200,
49 +
useNativeDriver: true,
50 +
}),
51 +
]).start();
52 +
}
53 +
}, [visible]);
54 +
55 +
return (
56 +
<Animated.View
57 +
style={[
58 +
styles.container,
59 +
{ top: insets.top + 12, backgroundColor: VARIANT_COLORS[variant] },
60 +
{ transform: [{ translateY }], opacity },
61 +
]}
62 +
pointerEvents="none"
63 +
>
64 +
<Text style={styles.message} numberOfLines={3}>
65 +
{message}
66 +
</Text>
67 +
</Animated.View>
68 +
);
69 +
}
70 +
71 +
const styles = StyleSheet.create({
72 +
container: {
73 +
position: 'absolute',
74 +
left: 16,
75 +
right: 16,
76 +
zIndex: 9999,
77 +
borderRadius: 10,
78 +
paddingHorizontal: 16,
79 +
paddingVertical: 12,
80 +
shadowColor: '#000',
81 +
shadowOffset: { width: 0, height: 4 },
82 +
shadowOpacity: 0.15,
83 +
shadowRadius: 8,
84 +
elevation: 8,
85 +
},
86 +
message: {
87 +
color: '#fff',
88 +
fontSize: 14,
89 +
fontWeight: '500',
90 +
lineHeight: 20,
91 +
},
92 +
});
frontend/context/AuthContext.tsx
1 1
import React, { createContext, useContext, useEffect, useState } from "react";
2 2
import { apiClient, User } from "@/lib/api";
3 3
import { queryClient, storage } from "@/lib/queryClient";
4 4
5 5
interface AuthProviderProps {
94 94
95 95
const login = async (
96 96
email: string,
97 97
password: string,
98 98
): Promise<{ user: User } | null> => {
99 -
setIsLoading(true);
99 +
const response = await apiClient.login(email, password);
100 100
101 -
try {
102 -
const response = await apiClient.login(email, password);
101 +
if (response.user) {
102 +
setUser(response.user);
103 +
}
103 104
104 -
if (response.user) {
105 -
setUser(response.user);
106 -
}
107 -
108 -
return response;
109 -
} finally {
110 -
setIsLoading(false);
111 -
}
105 +
return response;
112 106
};
113 107
114 108
const signup = async (userData: {
115 109
firstName: string;
116 110
lastName?: string;
117 111
email: string;
118 112
password: string;
119 113
}) => {
120 -
setIsLoading(true);
121 -
try {
122 -
const response = await apiClient.signup(userData);
123 -
setUser(response.user);
124 -
} finally {
125 -
setIsLoading(false);
126 -
}
114 +
const response = await apiClient.signup(userData);
115 +
setUser(response.user);
127 116
};
128 117
129 118
const logout = async () => {
130 119
setIsLoading(true);
131 120
try {
175 164
requestActivateCode,
176 165
};
177 166
178 167
return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;
179 168
};
/dev/null → frontend/context/ToastContext.tsx
1 +
import React, { createContext, useCallback, useContext, useRef, useState } from 'react';
2 +
import { Toast, ToastVariant } from '@/components/ui/Toast';
3 +
4 +
interface ToastContextValue {
5 +
showToast: (message: string, variant?: ToastVariant) => void;
6 +
}
7 +
8 +
const ToastContext = createContext<ToastContextValue>({
9 +
showToast: () => {},
10 +
});
11 +
12 +
export function useToast() {
13 +
return useContext(ToastContext);
14 +
}
15 +
16 +
export function ToastProvider({ children }: { children: React.ReactNode }) {
17 +
const [message, setMessage] = useState('');
18 +
const [variant, setVariant] = useState<ToastVariant>('error');
19 +
const [visible, setVisible] = useState(false);
20 +
const timerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
21 +
22 +
const showToast = useCallback((msg: string, v: ToastVariant = 'error') => {
23 +
if (timerRef.current) clearTimeout(timerRef.current);
24 +
setMessage(msg);
25 +
setVariant(v);
26 +
setVisible(true);
27 +
timerRef.current = setTimeout(() => setVisible(false), 3500);
28 +
}, []);
29 +
30 +
return (
31 +
<ToastContext.Provider value={{ showToast }}>
32 +
{children}
33 +
<Toast message={message} variant={variant} visible={visible} />
34 +
</ToastContext.Provider>
35 +
);
36 +
}