eletrotupi / tcc/ commit / 409c375

frontend: introduce toast components/context

Pedro Lucas Porcellis porcellis@eletrotupi.com 29 days ago 409c3751b50fc119fcd4ef0ef0b88531565cfd95
Parents: f74f10c
4 file(s) changed
  • frontend/app/_layout.tsx +3 -0
  • frontend/components/ui/Toast.tsx +92 -0
  • frontend/context/AuthContext.tsx +7 -18
  • frontend/context/ToastContext.tsx +36 -0
frontend/app/_layout.tsx
1 1
import {
2 2
  DarkTheme,
3 3
  DefaultTheme,
4 4
  ThemeProvider,
5 5
} from "@react-navigation/native";
▸ 1 unchanged lines
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

▸ 6 unchanged lines
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 }} />
▸ 84 unchanged lines
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
}
frontend/components/ui/Toast.tsx
@@ -0,0 +1,92 @@
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 {
▸ 88 unchanged lines
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 {
▸ 43 unchanged lines
175 164
    requestActivateCode,
176 165
  };
177 166

178 167
  return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;
179 168
};
frontend/context/ToastContext.tsx
@@ -0,0 +1,36 @@
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 + }