eletrotupi / tcc/ commit / 409c375

frontend: introduce toast components/context

Pedro Lucas Porcellis porcellis@eletrotupi.com · 4 months ago 409c3751b50fc119fcd4ef0ef0b88531565cfd95 Browse files
Parents f74f10c
Changed 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 +
}