From 44b026fe044bb03e5e868ce4f7c1d726c57fefde Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Puchyr?= Date: Sat, 6 Dec 2025 21:07:39 +0100 Subject: [PATCH 01/11] ZPI-173 fix: notification flag is now properly passed on creation --- .../OutageLocations/LocationForm/EditLocation.tsx | 1 + .../OutageLocations/LocationForm/LocationForm.tsx | 6 +++--- Client/watchout/features/settings/AccountSettings.tsx | 11 +++++++++++ Client/watchout/utils/types.ts | 2 +- 4 files changed, 16 insertions(+), 4 deletions(-) create mode 100644 Client/watchout/features/settings/AccountSettings.tsx diff --git a/Client/watchout/features/outages/OutageLocations/LocationForm/EditLocation.tsx b/Client/watchout/features/outages/OutageLocations/LocationForm/EditLocation.tsx index 837595e..0b6f6d9 100644 --- a/Client/watchout/features/outages/OutageLocations/LocationForm/EditLocation.tsx +++ b/Client/watchout/features/outages/OutageLocations/LocationForm/EditLocation.tsx @@ -22,6 +22,7 @@ export const EditLocation = () => { services: { ...location.services, }, + notificationsEnable: location.notificationsEnable, }, }; diff --git a/Client/watchout/features/outages/OutageLocations/LocationForm/LocationForm.tsx b/Client/watchout/features/outages/OutageLocations/LocationForm/LocationForm.tsx index e176a72..165c220 100644 --- a/Client/watchout/features/outages/OutageLocations/LocationForm/LocationForm.tsx +++ b/Client/watchout/features/outages/OutageLocations/LocationForm/LocationForm.tsx @@ -38,8 +38,8 @@ const defaultLocation: AddLocationRequest = { eventTypes: [], }, radius: DEFAULT_LOCATION_RADIUS_KM, // in km, converted to meters when submitting + notificationsEnable: false, }, - notificationsEnable: false, }; export const LocationForm = ({ initialLocation, submit, isPending }: LocationFormProps) => { @@ -199,9 +199,9 @@ export const LocationForm = ({ initialLocation, submit, isPending }: LocationFor Otrzymuj powiadomienia push o przerwach w dostawie usług dla tej lokalizacji - setLocation((prev) => ({ ...prev, notificationsEnable: value })) + setLocation((prev) => ({ ...prev, settings: { ...prev.settings, notificationsEnable: value } })) } /> diff --git a/Client/watchout/features/settings/AccountSettings.tsx b/Client/watchout/features/settings/AccountSettings.tsx new file mode 100644 index 0000000..3b0e394 --- /dev/null +++ b/Client/watchout/features/settings/AccountSettings.tsx @@ -0,0 +1,11 @@ +import React from 'react' +import { View } from 'react-native' +import { Text } from 'components/Base/Text'; + +export const AccountSettings = () => { + return ( + + Konto + + ) +} diff --git a/Client/watchout/utils/types.ts b/Client/watchout/utils/types.ts index e1fd18f..e77f8de 100644 --- a/Client/watchout/utils/types.ts +++ b/Client/watchout/utils/types.ts @@ -109,8 +109,8 @@ export type AddLocationRequest = { weather: boolean; eventTypes?: number[]; }; - }; notificationsEnable: boolean; + }; }; export type UpdateLocationRequest = AddLocationRequest; From d5a5e506a3141463a336b817620852dd1f9bbd46 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Puchyr?= Date: Sun, 7 Dec 2025 11:15:19 +0100 Subject: [PATCH 02/11] ZPI-173 refactor: export loading screen to separate compoent --- .../watchout/components/Common/LoadingScreen.tsx | 15 +++++++++++++++ .../watchout/components/Layout/AppNavigator.tsx | 4 +++- 2 files changed, 18 insertions(+), 1 deletion(-) create mode 100644 Client/watchout/components/Common/LoadingScreen.tsx diff --git a/Client/watchout/components/Common/LoadingScreen.tsx b/Client/watchout/components/Common/LoadingScreen.tsx new file mode 100644 index 0000000..31de351 --- /dev/null +++ b/Client/watchout/components/Common/LoadingScreen.tsx @@ -0,0 +1,15 @@ +import { ActivityIndicator, View } from 'react-native'; +import { Icon } from 'react-native-paper'; +import { Text } from 'components/Base/Text'; + +export const LoadingScreen = () => { + return ( + + + + Ładowanie... + + + + ); +}; diff --git a/Client/watchout/components/Layout/AppNavigator.tsx b/Client/watchout/components/Layout/AppNavigator.tsx index a1d6189..03d5c43 100644 --- a/Client/watchout/components/Layout/AppNavigator.tsx +++ b/Client/watchout/components/Layout/AppNavigator.tsx @@ -55,7 +55,9 @@ export const AppNavigator = () => { const { user, loading } = useAuth(); const { dismissAll } = useBottomSheetModal(); - if (loading) return null; + if (loading) { + return ; + } if (!user) { return ( From 9845f2ed37c38dc27cd1ce47371ce357cacb36e5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Puchyr?= Date: Sun, 7 Dec 2025 11:20:48 +0100 Subject: [PATCH 03/11] ZPI-173 fix: improve login errors messages --- Client/watchout/App.tsx | 14 ++--- .../components/Layout/AppNavigator.tsx | 18 ++++--- Client/watchout/features/auth/LoginScreen.tsx | 36 ++++++++++--- .../watchout/features/auth/SignUpScreen.tsx | 19 +++++-- Client/watchout/features/auth/auth.ts | 53 +++++++++++-------- Client/watchout/features/auth/authContext.tsx | 2 + Client/watchout/utils/AuthError.ts | 48 +++++++++++++++++ Client/watchout/utils/apiDefinition.ts | 3 ++ 8 files changed, 139 insertions(+), 54 deletions(-) create mode 100644 Client/watchout/utils/AuthError.ts diff --git a/Client/watchout/App.tsx b/Client/watchout/App.tsx index cc06b17..8021c18 100644 --- a/Client/watchout/App.tsx +++ b/Client/watchout/App.tsx @@ -28,6 +28,7 @@ import { SnackbarProvider } from 'utils/useSnackbar'; import { View } from 'react-native'; import Reactotron, { openInEditor } from 'reactotron-react-native'; import AsyncStorage from '@react-native-async-storage/async-storage'; +import { LoadingScreen } from 'components/Common/LoadingScreen'; Reactotron.setAsyncStorageHandler(AsyncStorage) .configure({ name: 'WatchOut' }) @@ -54,6 +55,7 @@ export default function App() { const { loaded } = useCustomFonts(); useEffect(() => { + console.info('Initializing Geocoding with API Key:', GEOCODING_API_KEY.substring(0, 5) + '****' + ' (truncated for security)'); Geocoding.init(GEOCODING_API_KEY); }, []); @@ -63,17 +65,7 @@ export default function App() { }; if (!loaded) { - return ( - <> - - - - Ładowanie... - - - - - ); + return ; } return ( diff --git a/Client/watchout/components/Layout/AppNavigator.tsx b/Client/watchout/components/Layout/AppNavigator.tsx index 03d5c43..765cdcb 100644 --- a/Client/watchout/components/Layout/AppNavigator.tsx +++ b/Client/watchout/components/Layout/AppNavigator.tsx @@ -13,12 +13,14 @@ import { getFocusedRouteNameFromRoute, RouteProp } from '@react-navigation/nativ import { navigationTheme } from 'components/Base/navigationTheme'; import { AlertsNavigator } from 'features/outages/AlertsNavigator'; import { useBottomSheetModal } from '@gorhom/bottom-sheet'; +import { LoadingScreen } from 'components/Common/LoadingScreen'; const NavDrawer = createDrawerNavigator(); +const LoginStack = createDrawerNavigator(); const routes = [ { - name: 'WatchOut', + name: 'Map', component: Map, label: 'Mapa', icon: 'map', @@ -58,15 +60,15 @@ export const AppNavigator = () => { if (loading) { return ; } + const isUserLoggedIn = user != null && user.isEmailVerified; - if (!user) { + if (!isUserLoggedIn) { return ( - }> - - - + + + + ); } diff --git a/Client/watchout/features/auth/LoginScreen.tsx b/Client/watchout/features/auth/LoginScreen.tsx index e6f46ae..26e0e84 100644 --- a/Client/watchout/features/auth/LoginScreen.tsx +++ b/Client/watchout/features/auth/LoginScreen.tsx @@ -7,9 +7,10 @@ import { Button, Icon } from 'react-native-paper'; import { GoogleSignInButton } from 'features/auth/GoogleSignInButton'; import { signInWithEmail, resetPassword } from './auth'; import { AuthLayout } from 'components/Layout/AuthLayout'; +import { AuthError, firebaseAuthErrorMessages } from 'utils/AuthError'; export const LoginScreen = () => { - const navigation = useNavigation(); + const { navigate } = useNavigation(); const [email, setEmail] = useState(''); const [password, setPassword] = useState(''); @@ -19,8 +20,16 @@ export const LoginScreen = () => { const handleLogin = async () => { try { await signInWithEmail(email, password); + navigate('Map' as never); } catch (err: any) { - Alert.alert('Login failed', err.message || 'Nie udało się zalogować'); + if (err.code && err.code in firebaseAuthErrorMessages) { + Alert.alert('Ups! Coś poszło nie tak', firebaseAuthErrorMessages[err.code]); + return; + } + Alert.alert( + 'Ups! Coś poszło nie tak', + 'Nie udało się zalogować. Spróbuj ponownie później. Przepraszamy za utrudnienia.' + ); } }; @@ -35,13 +44,22 @@ export const LoginScreen = () => { setResetVisible(false); setResetEmail(''); } catch (err: any) { - Alert.alert('Błąd', err.message || 'Nie udało się wysłać maila'); + if (err.code && err.code in firebaseAuthErrorMessages) { + Alert.alert('Ups! Coś poszło nie tak', firebaseAuthErrorMessages[err.code]); + return; + } + if (err instanceof AuthError) { + Alert.alert('Ups! Coś poszło nie tak', err.message); + return; + } + Alert.alert( + 'Ups! Coś poszło nie tak', + 'Nie udało się zarejestrować. Spróbuj ponownie później. Przepraszamy za utrudnienia.' + ); } }; - const header = ( - - ); + const header = ; const footer = ( <> @@ -87,7 +105,7 @@ export const LoginScreen = () => { Zaloguj się - navigation.navigate('SignUp' as never)}> + navigate('SignUp' as never)}> Nie masz konta? Zarejestruj się @@ -110,7 +128,9 @@ export const LoginScreen = () => { setResetVisible(false)} style={{ marginTop: 12 }}> - Zamknij + + Zamknij + diff --git a/Client/watchout/features/auth/SignUpScreen.tsx b/Client/watchout/features/auth/SignUpScreen.tsx index 4955f78..2ca1788 100644 --- a/Client/watchout/features/auth/SignUpScreen.tsx +++ b/Client/watchout/features/auth/SignUpScreen.tsx @@ -7,9 +7,10 @@ import { Button, Icon } from 'react-native-paper'; import { GoogleSignInButton } from 'features/auth/GoogleSignInButton'; import { signUpEmail } from './auth'; import { AuthLayout } from 'components//Layout/AuthLayout'; +import { AuthError, firebaseAuthErrorMessages } from 'utils/AuthError'; export const SignUpScreen = () => { - const navigation = useNavigation(); + const { navigate } = useNavigation(); const [email, setEmail] = useState(''); const [password, setPassword] = useState(''); const [displayName, setDisplayName] = useState(''); @@ -17,10 +18,18 @@ export const SignUpScreen = () => { const handleSignup = async () => { try { await signUpEmail(email, password, displayName); - Alert.alert('Sukces', 'Konto zostało utworzone'); - navigation.navigate('Login' as never); + Alert.alert('Udało się!', 'Konto zostało utworzone. Zapraszamy do korzystania z aplikacji!'); + navigate('Map' as never); } catch (err: any) { - Alert.alert('Rejestracja nie powiodła się', err.message || 'Błąd'); + let message = err.message || ''; + if (err.code && err.code in firebaseAuthErrorMessages) { + message = firebaseAuthErrorMessages[err.code]; + } else if (err instanceof AuthError) { + message = err.message; + } else { + message = 'Nie udało się zarejestrować. Spróbuj ponownie później. Przepraszamy za utrudnienia.'; + } + Alert.alert('Ups! Coś poszło nie tak', message); } }; @@ -71,7 +80,7 @@ export const SignUpScreen = () => { Zarejestruj się - navigation.navigate('Login' as never)}> + navigate('Login' as never)}> Masz już konto? Zaloguj się diff --git a/Client/watchout/features/auth/auth.ts b/Client/watchout/features/auth/auth.ts index 88c71fc..6fa231e 100644 --- a/Client/watchout/features/auth/auth.ts +++ b/Client/watchout/features/auth/auth.ts @@ -1,16 +1,20 @@ -import { - getAuth, - createUserWithEmailAndPassword, - signInWithEmailAndPassword, - sendEmailVerification, - sendPasswordResetEmail, - updateProfile, - signOut, - GoogleAuthProvider, +import { + getAuth, + createUserWithEmailAndPassword, + signInWithEmailAndPassword, + sendEmailVerification, + sendPasswordResetEmail, + updateProfile, + signOut, + GoogleAuthProvider, signInWithCredential, - getIdToken -} from "@react-native-firebase/auth"; -import { apiClient } from "utils/apiClient"; + getIdToken, + firebase, + FirebaseAuthTypes, +} from '@react-native-firebase/auth'; +import { apiClient } from 'utils/apiClient'; +import { API_ENDPOINTS } from 'utils/apiDefinition'; +import { AuthError } from 'utils/AuthError'; export async function signUpEmail(email: string, password: string, displayName?: string) { try { @@ -26,12 +30,15 @@ export async function signUpEmail(email: string, password: string, displayName?: return user; } catch (err: any) { - console.error("signUpEmail error", err); + console.error('signUpEmail error', err); throw err; } } -export async function signInWithEmail(email: string, password: string) { +export async function signInWithEmail( + email: string, + password: string +): Promise { try { const auth = getAuth(); const cred = await signInWithEmailAndPassword(auth, email, password); @@ -39,13 +46,13 @@ export async function signInWithEmail(email: string, password: string) { if (!user.emailVerified) { await sendEmailVerification(user); - throw new Error("Email nie został jeszcze zweryfikowany. Sprawdź skrzynkę email."); + throw new AuthError('Email nie został jeszcze zweryfikowany. Sprawdź skrzynkę email.'); } try { - const idToken = await getIdToken(user) + const idToken = await getIdToken(user); await apiClient.post( - "/users/create", + API_ENDPOINTS.users.create, { firebaseUid: user.uid, email: user.email, @@ -55,13 +62,15 @@ export async function signInWithEmail(email: string, password: string) { headers: { Authorization: `Bearer ${idToken}` }, } ); + return user; } catch (syncErr) { - console.error("Backend sync failed:", syncErr); + console.error('Backend sync failed:', syncErr); + throw new AuthError( + 'Nie udało się zsynchronizować konta z serwerem. Spróbuj ponownie później.' + ); } - - return user; } catch (err: any) { - console.error("signInWithEmail error", err); + console.error('signInWithEmail error', err); throw err; } } @@ -84,7 +93,7 @@ export async function signInWithGoogleIdToken(idToken: string) { const firebaseIdToken = await getIdToken(user); await apiClient.post( - "/users/create", + '/users/create', { firebaseUid: user.uid, email: user.email, diff --git a/Client/watchout/features/auth/authContext.tsx b/Client/watchout/features/auth/authContext.tsx index 6e37940..bf834ab 100644 --- a/Client/watchout/features/auth/authContext.tsx +++ b/Client/watchout/features/auth/authContext.tsx @@ -10,6 +10,7 @@ interface AuthUser { uid: string; email: string | null; displayName: string | null; + isEmailVerified: boolean; } interface AuthContextType { @@ -37,6 +38,7 @@ export const AuthProvider = ({ children }: { children: React.ReactNode }) => { uid: firebaseUser.uid, email: firebaseUser.email, displayName: firebaseUser.displayName, + isEmailVerified: firebaseUser.emailVerified, }); } else { setUser(null); diff --git a/Client/watchout/utils/AuthError.ts b/Client/watchout/utils/AuthError.ts new file mode 100644 index 0000000..236b10f --- /dev/null +++ b/Client/watchout/utils/AuthError.ts @@ -0,0 +1,48 @@ +export class AuthError extends Error { + constructor(message: string) { + super(message); + this.name = 'AuthError'; + } +} + +export const firebaseAuthErrorMessages: { [code: string]: string } = { + // Klient ma już konto + 'auth/email-already-exists': 'Podany adres e-mail jest już zajęty przez istniejące konto.', + 'auth/email-already-in-use': 'Podany adres e-mail jest już zajęty przez istniejące konto.', // Duplikat/Alternatywa + 'auth/phone-number-already-exists': 'Podany numer telefonu jest już używany przez innego użytkownika.', + + // Problemy z danymi wejściowymi + 'auth/invalid-email': 'Wprowadzono nieprawidłowy format adresu e-mail.', + 'auth/invalid-credential': 'Adres e-mail lub hasło są nieprawidłowe.', + 'auth/invalid-password': 'Hasło jest nieprawidłowe. Musi zawierać co najmniej 6 znaków.', + 'auth/weak-password': 'Hasło jest zbyt słabe. Użyj co najmniej 6 znaków.', // Dodano z Twojego przykładu + + // Problemy z kontem + 'auth/user-not-found': 'Nie znaleziono użytkownika dla podanych danych.', + 'auth/user-disabled': 'To konto użytkownika zostało zablokowane przez administratora.', + 'auth/wrong-password': 'Nieprawidłowe hasło.', // Dodano z Twojego przykładu + + // === Błędy Tokenów i Sesji === + 'auth/id-token-expired': 'Sesja wygasła. Proszę zalogować się ponownie.', + 'auth/session-cookie-expired': 'Sesja wygasła. Proszę zalogować się ponownie.', + 'auth/id-token-revoked': 'Token sesji został unieważniony. Proszę zalogować się ponownie.', + 'auth/session-cookie-revoked': 'Token sesji został unieważniony. Proszę zalogować się ponownie.', + + // === Błędy Serwera i Limity === + 'auth/too-many-requests': 'Wykryto zbyt wiele nieudanych prób. Proszę spróbować ponownie później.', + 'auth/internal-error': 'Wystąpił nieoczekiwany błąd serwera. Jeśli problem się powtarza, skontaktuj się ze wsparciem.', + + // === Błędy Konfiguracji i Argumentów === + 'auth/invalid-argument': 'Wystąpił błąd danych. Proszę zweryfikować wprowadzone informacje.', + 'auth/operation-not-allowed': 'Metoda logowania jest wyłączona w ustawieniach projektu Firebase.', + 'auth/invalid-phone-number': 'Wprowadzony numer telefonu jest nieprawidłowy (wymagany format E.164).', + 'auth/invalid-display-name': 'Nazwa użytkownika jest nieprawidłowa lub pusta.', + 'auth/unauthorized-continue-uri': 'Domena przekierowania nie jest autoryzowana. Skontaktuj się z administratorem.', + + // === Błędy Admin SDK (rzadziej na kliencie) === + 'auth/claims-too-large': 'Nie można ustawić atrybutów: przekroczono limit rozmiaru (1000 bajtów).', + 'auth/uid-already-exists': 'Wewnętrzny identyfikator UID jest już używany przez innego użytkownika.', + + // === Domyślne (catch-all) === + 'default': 'Wystąpił nieznany błąd autoryzacji. Spróbuj ponownie lub skontaktuj się ze wsparciem.' +}; \ No newline at end of file diff --git a/Client/watchout/utils/apiDefinition.ts b/Client/watchout/utils/apiDefinition.ts index 8e11e52..3da6a4f 100644 --- a/Client/watchout/utils/apiDefinition.ts +++ b/Client/watchout/utils/apiDefinition.ts @@ -42,6 +42,9 @@ export const API_ENDPOINTS = { add: 'fcm-tokens', get: 'fcm-tokens', }, + users: { + create: 'users/create', + } }; // utility functions From 02e5152f430ad88fa83607b01b519259910e332a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Puchyr?= Date: Sun, 7 Dec 2025 12:56:47 +0100 Subject: [PATCH 04/11] ZPI-173 refactor: rewrite event creation form to complete lag removal --- Client/watchout/App.tsx | 6 +- .../events/create/CreateEventBottomSheet.tsx | 82 ++++++++---- .../events/create/useEventCreateForm.ts | 61 ++++----- Client/watchout/package-lock.json | 124 +++++++++++++++++- Client/watchout/package.json | 3 +- 5 files changed, 204 insertions(+), 72 deletions(-) diff --git a/Client/watchout/App.tsx b/Client/watchout/App.tsx index 8021c18..6495204 100644 --- a/Client/watchout/App.tsx +++ b/Client/watchout/App.tsx @@ -2,8 +2,7 @@ import '@expo/metro-runtime'; import { StatusBar } from 'expo-status-bar'; import { GEOCODING_API_KEY } from '@env'; -import { ActivityIndicator, Icon, PaperProvider } from 'react-native-paper'; -import { Text } from 'components/Base/Text'; +import { PaperProvider } from 'react-native-paper'; import { DefaultTheme, NavigationContainer } from '@react-navigation/native'; import { SafeAreaProvider } from 'react-native-safe-area-context'; import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; @@ -23,11 +22,10 @@ import { useEffect } from 'react'; import { useCustomFonts } from 'utils/useCustomFonts'; import { theme } from 'utils/theme'; import { AppNavigator } from 'components/Layout/AppNavigator'; -import { DevToolsBubble } from 'react-native-react-query-devtools'; import { SnackbarProvider } from 'utils/useSnackbar'; -import { View } from 'react-native'; import Reactotron, { openInEditor } from 'reactotron-react-native'; import AsyncStorage from '@react-native-async-storage/async-storage'; +import { DevToolsBubble } from "react-native-react-query-devtools"; import { LoadingScreen } from 'components/Common/LoadingScreen'; Reactotron.setAsyncStorageHandler(AsyncStorage) diff --git a/Client/watchout/features/events/create/CreateEventBottomSheet.tsx b/Client/watchout/features/events/create/CreateEventBottomSheet.tsx index 36c57a9..4bd86fa 100644 --- a/Client/watchout/features/events/create/CreateEventBottomSheet.tsx +++ b/Client/watchout/features/events/create/CreateEventBottomSheet.tsx @@ -12,6 +12,7 @@ import { useEffect } from 'react'; import { useActionAvailability } from './useActionAvailability'; import { Row } from 'components/Base/Row'; import { theme } from 'utils/theme'; +import { Controller } from 'react-hook-form'; type CreateEventProps = { location: Coordinates; @@ -21,13 +22,12 @@ type CreateEventProps = { export const CreateEventBottomSheet = ({ location, onSuccess, onClose }: CreateEventProps) => { const { - formData, + control, handleSubmit, handleSetSelectedEventType, eventTypeModalVisible, setEventTypeModalVisible, selectedEventType, - updateField, geocodedAddress, eventBottomSheetRef, isLoading, @@ -53,35 +53,58 @@ export const CreateEventBottomSheet = ({ location, onSuccess, onClose }: CreateE - updateField('name', value)} + ( + + )} /> - updateField('description', value)} - multiline - numberOfLines={3} + ( + + )} + /> + ( + setEventTypeModalVisible(true)}> + + + )} /> - - setEventTypeModalVisible(true)}> - - - updateField('images', images)} + ( + + )} /> { return ( - + Akcja niedostępna - Nie możesz zgłosić nowego zdarzenia, ponieważ twoja reputacja jest zbyt niska. Spróbuj ponownie później. + Nie możesz zgłosić nowego zdarzenia, ponieważ twoja reputacja jest zbyt niska. Spróbuj + ponownie później. ); diff --git a/Client/watchout/features/events/create/useEventCreateForm.ts b/Client/watchout/features/events/create/useEventCreateForm.ts index 9ff9cda..1a81c66 100644 --- a/Client/watchout/features/events/create/useEventCreateForm.ts +++ b/Client/watchout/features/events/create/useEventCreateForm.ts @@ -6,6 +6,7 @@ import { Coordinates, CreateEventRequest } from 'utils/types'; import { useCreateEvent } from './useEventCreate'; import { BottomSheetModal } from '@gorhom/bottom-sheet'; import { useUserLocation } from 'features/map/useLocation'; +import { useForm } from 'react-hook-form'; type FormData = { name: string; @@ -26,11 +27,13 @@ export const useEventCreateForm = (location: Coordinates, onSuccess: () => void) const [selectedEventType, setSelectedEventType] = useState(null); const [eventTypeModalVisible, setEventTypeModalVisible] = useState(false); const [geocodedAddress, setGeocodedAddress] = useState('Ładowanie lokalizacji...'); - const [formData, setFormData] = useState({ - name: '', - description: '', - eventTypeId: null, - images: [], + const { control, handleSubmit, setValue, reset } = useForm({ + defaultValues: { + name: '', + description: '', + eventTypeId: null, + images: [], + }, }); useEffect(() => { @@ -41,73 +44,63 @@ export const useEventCreateForm = (location: Coordinates, onSuccess: () => void) const createEventMutation = useCreateEvent(); - const updateField = useCallback((field: K, value: FormData[K]) => { - setFormData((prev) => ({ ...prev, [field]: value })); - }, []); - const handleSetSelectedEventType = useCallback((eventType: SelectedEventType) => { setSelectedEventType(eventType); - setFormData((prev) => ({ ...prev, eventTypeId: eventType?.id || null })); - }, []); - - const validateForm = useCallback(() => { - const { name, description, eventTypeId } = formData; - return name && description && eventTypeId !== null; - }, [formData]); - - const resetForm = useCallback(() => { - setFormData({ name: '', description: '', eventTypeId: null, images: [] }); - }, []); + setValue('eventTypeId', eventType?.id || null); + }, [setValue]); const userLocation = useUserLocation(); - const handleSubmit = useCallback(() => { - if (!validateForm()) { + const onSubmit = (values: FormData) => { + const isValid = values.name && values.description && values.eventTypeId !== null; + if (!isValid) { Alert.alert('Błąd', 'Wszystkie pola muszą być wypełnione.'); return; } if (!userLocation.location) { - Alert.alert('Błąd', 'Aby móc utworzyć zdarzenie, upewnij się, że usługi lokalizacyjne są włączone.'); + Alert.alert( + 'Błąd', + 'Aby móc utworzyć zdarzenie, upewnij się, że usługi lokalizacyjne są włączone.' + ); return; } const dateInOneHour = dayjs().add(3, 'hours'); const requestData: CreateEventRequest = { - name: formData.name, - description: formData.description, + name: values.name, + description: values.description, latitude: location.latitude, longitude: location.longitude, - userLatitude: userLocation?.location.latitude, - userLongitude: userLocation?.location.longitude, + userLatitude: location.latitude, + userLongitude: location.longitude, endDate: dateInOneHour.toISOString(), - images: formData.images.map((image) => image.base64), - eventTypeId: formData.eventTypeId!, + images: values.images.map((image) => image.base64), + eventTypeId: values.eventTypeId!, }; createEventMutation.mutate(requestData, { onSuccess: () => { Alert.alert('Sukces!', 'Nowe zdarzenie zostało zgłoszone.'); - resetForm(); + reset(); onSuccess(); }, onError: (error) => { Alert.alert('Błąd', `Nie udało się zgłosić zdarzenia. Spróbuj ponownie. ${error.message}`); }, }); - }, [validateForm, formData, location, createEventMutation, resetForm, onSuccess, userLocation.location]); + }; return { - formData, + control, eventBottomSheetRef, selectedEventType, eventTypeModalVisible, geocodedAddress, isLoading: createEventMutation.isPending, - updateField, handleSetSelectedEventType, setEventTypeModalVisible, - handleSubmit, + handleSubmit: handleSubmit(onSubmit), }; }; diff --git a/Client/watchout/package-lock.json b/Client/watchout/package-lock.json index 4d957d9..de402b3 100644 --- a/Client/watchout/package-lock.json +++ b/Client/watchout/package-lock.json @@ -41,9 +41,10 @@ "expo-image-picker": "^17.0.8", "expo-notifications": "~0.31.4", "expo-status-bar": "~3.0.8", + "firebase": "^12.6.0", "react": "19.1.0", "react-dom": "19.1.0", - "react-hook-form": "^7.64.0", + "react-hook-form": "^7.68.0", "react-native": "0.81.5", "react-native-geocoding": "^0.5.0", "react-native-gesture-handler": "~2.28.0", @@ -2384,6 +2385,26 @@ "node": ">=8" } }, + "node_modules/@firebase/ai": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/@firebase/ai/-/ai-2.6.0.tgz", + "integrity": "sha512-NGyE7NQDFznOv683Xk4+WoUv39iipa9lEfrwvvPz33ChzVbCCiB69FJQTK2BI/11pRtzYGbHo1/xMz7gxWWhJw==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/app-check-interop-types": "0.3.3", + "@firebase/component": "0.7.0", + "@firebase/logger": "0.5.0", + "@firebase/util": "1.13.0", + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "@firebase/app": "0.x", + "@firebase/app-types": "0.x" + } + }, "node_modules/@firebase/analytics": { "version": "0.10.19", "resolved": "https://registry.npmjs.org/@firebase/analytics/-/analytics-0.10.19.tgz", @@ -2513,6 +2534,49 @@ "license": "Apache-2.0", "peer": true }, + "node_modules/@firebase/auth": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@firebase/auth/-/auth-1.11.1.tgz", + "integrity": "sha512-Mea0G/BwC1D0voSG+60Ylu3KZchXAFilXQ/hJXWCw3gebAu+RDINZA0dJMNeym7HFxBaBaByX8jSa7ys5+F2VA==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/component": "0.7.0", + "@firebase/logger": "0.5.0", + "@firebase/util": "1.13.0", + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "@firebase/app": "0.x", + "@react-native-async-storage/async-storage": "^1.18.1" + }, + "peerDependenciesMeta": { + "@react-native-async-storage/async-storage": { + "optional": true + } + } + }, + "node_modules/@firebase/auth-compat": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/@firebase/auth-compat/-/auth-compat-0.6.1.tgz", + "integrity": "sha512-I0o2ZiZMnMTOQfqT22ur+zcGDVSAfdNZBHo26/Tfi8EllfR1BO7aTVo2rt/ts8o/FWsK8pOALLeVBGhZt8w/vg==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/auth": "1.11.1", + "@firebase/auth-types": "0.13.0", + "@firebase/component": "0.7.0", + "@firebase/util": "1.13.0", + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "@firebase/app-compat": "0.x" + } + }, "node_modules/@firebase/auth-interop-types": { "version": "0.2.4", "resolved": "https://registry.npmjs.org/@firebase/auth-interop-types/-/auth-interop-types-0.2.4.tgz", @@ -2542,6 +2606,22 @@ "node": ">=20.0.0" } }, + "node_modules/@firebase/data-connect": { + "version": "0.3.12", + "resolved": "https://registry.npmjs.org/@firebase/data-connect/-/data-connect-0.3.12.tgz", + "integrity": "sha512-baPddcoNLj/+vYo+HSJidJUdr5W4OkhT109c5qhR8T1dJoZcyJpkv/dFpYlw/VJ3dV66vI8GHQFrmAZw/xUS4g==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/auth-interop-types": "0.2.4", + "@firebase/component": "0.7.0", + "@firebase/logger": "0.5.0", + "@firebase/util": "1.13.0", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app": "0.x" + } + }, "node_modules/@firebase/database": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@firebase/database/-/database-1.1.0.tgz", @@ -8722,6 +8802,42 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/firebase": { + "version": "12.6.0", + "resolved": "https://registry.npmjs.org/firebase/-/firebase-12.6.0.tgz", + "integrity": "sha512-8ZD1Gcv916Qp8/nsFH2+QMIrfX/76ti6cJwxQUENLXXnKlOX/IJZaU2Y3bdYf5r1mbownrQKfnWtrt+MVgdwLA==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/ai": "2.6.0", + "@firebase/analytics": "0.10.19", + "@firebase/analytics-compat": "0.2.25", + "@firebase/app": "0.14.6", + "@firebase/app-check": "0.11.0", + "@firebase/app-check-compat": "0.4.0", + "@firebase/app-compat": "0.5.6", + "@firebase/app-types": "0.9.3", + "@firebase/auth": "1.11.1", + "@firebase/auth-compat": "0.6.1", + "@firebase/data-connect": "0.3.12", + "@firebase/database": "1.1.0", + "@firebase/database-compat": "2.1.0", + "@firebase/firestore": "4.9.2", + "@firebase/firestore-compat": "0.4.2", + "@firebase/functions": "0.13.1", + "@firebase/functions-compat": "0.4.1", + "@firebase/installations": "0.6.19", + "@firebase/installations-compat": "0.2.19", + "@firebase/messaging": "0.12.23", + "@firebase/messaging-compat": "0.2.23", + "@firebase/performance": "0.7.9", + "@firebase/performance-compat": "0.2.22", + "@firebase/remote-config": "0.7.0", + "@firebase/remote-config-compat": "0.2.20", + "@firebase/storage": "0.14.0", + "@firebase/storage-compat": "0.4.0", + "@firebase/util": "1.13.0" + } + }, "node_modules/flat-cache": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", @@ -12359,9 +12475,9 @@ } }, "node_modules/react-hook-form": { - "version": "7.66.0", - "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.66.0.tgz", - "integrity": "sha512-xXBqsWGKrY46ZqaHDo+ZUYiMUgi8suYu5kdrS20EG8KiL7VRQitEbNjm+UcrDYrNi1YLyfpmAeGjCZYXLT9YBw==", + "version": "7.68.0", + "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.68.0.tgz", + "integrity": "sha512-oNN3fjrZ/Xo40SWlHf1yCjlMK417JxoSJVUXQjGdvdRCU07NTFei1i1f8ApUAts+IVh14e4EdakeLEA+BEAs/Q==", "license": "MIT", "engines": { "node": ">=18.0.0" diff --git a/Client/watchout/package.json b/Client/watchout/package.json index b43848b..9399f1e 100644 --- a/Client/watchout/package.json +++ b/Client/watchout/package.json @@ -44,9 +44,10 @@ "expo-image-picker": "^17.0.8", "expo-notifications": "~0.31.4", "expo-status-bar": "~3.0.8", + "firebase": "^12.6.0", "react": "19.1.0", "react-dom": "19.1.0", - "react-hook-form": "^7.64.0", + "react-hook-form": "^7.68.0", "react-native": "0.81.5", "react-native-geocoding": "^0.5.0", "react-native-gesture-handler": "~2.28.0", From 3f7886c62aa3129822de95e44d8f90baa6c39df3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Puchyr?= Date: Sun, 7 Dec 2025 14:59:41 +0100 Subject: [PATCH 05/11] ZPI-173 feat: user can't rate his own events --- .../features/events/details/EventBottomSheet.tsx | 9 ++++++++- Client/watchout/utils/types.ts | 1 + 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/Client/watchout/features/events/details/EventBottomSheet.tsx b/Client/watchout/features/events/details/EventBottomSheet.tsx index 8f80a1b..9c959df 100644 --- a/Client/watchout/features/events/details/EventBottomSheet.tsx +++ b/Client/watchout/features/events/details/EventBottomSheet.tsx @@ -1,4 +1,5 @@ import { Event } from 'utils/types'; +import { Text } from 'components/Base/Text'; import { StyleSheet, View } from 'react-native'; import { useEffect, useRef, useState } from 'react'; import { CommentList } from 'features/events/comments/CommentList'; @@ -50,7 +51,13 @@ export const EventBottomSheet = ({ event, onClose }: EventBottomSheetProps) => { - + {event.isAuthor ? ( + + Jako autor zdarzenia nie możesz ocenić własnego zgłoszenia. + + ) : ( + + )} diff --git a/Client/watchout/utils/types.ts b/Client/watchout/utils/types.ts index e77f8de..57dfaaf 100644 --- a/Client/watchout/utils/types.ts +++ b/Client/watchout/utils/types.ts @@ -30,6 +30,7 @@ export type Event = { active: boolean; rating: number; ratingForCurrentUser: number; + isAuthor: boolean; }; export type Outage = { From cd98a49ad01a779ac95c91a5a0512cb3842e8283 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Puchyr?= Date: Sun, 7 Dec 2025 15:00:29 +0100 Subject: [PATCH 06/11] ZPI-173 fix: dates are now properly displayed --- Client/watchout/features/events/comments/CommentList.tsx | 5 ++--- Client/watchout/features/events/details/EventDetails.tsx | 2 +- Client/watchout/utils/types.ts | 6 +++--- 3 files changed, 6 insertions(+), 7 deletions(-) diff --git a/Client/watchout/features/events/comments/CommentList.tsx b/Client/watchout/features/events/comments/CommentList.tsx index 0acbad7..6b6c7d9 100644 --- a/Client/watchout/features/events/comments/CommentList.tsx +++ b/Client/watchout/features/events/comments/CommentList.tsx @@ -6,7 +6,7 @@ import { AddCommentModal } from './AddCommentModal'; import dayjs from 'dayjs'; import { theme } from 'utils/theme'; import { ConfirmationModal } from 'components/Common/ConfirmationModal'; -import { generateAnonName } from 'utils/helpers'; +import { formatDate, generateAnonName } from 'utils/helpers'; import { useCommentsList } from './useCommentsList'; import { useActionAvailability } from 'features/events/create/useActionAvailability'; @@ -93,8 +93,7 @@ export const CommentList = ({ eventId }: CommentListProps) => { {item.content} - {dayjs.utc(item.createdAt).local().format('YYYY-MM-DD HH:mm')} ( - {dayjs.utc(item.createdAt).local().fromNow()}) + {formatDate(item.createdAt)} {item.isAuthor && ( diff --git a/Client/watchout/features/events/details/EventDetails.tsx b/Client/watchout/features/events/details/EventDetails.tsx index 456da95..19d1d1a 100644 --- a/Client/watchout/features/events/details/EventDetails.tsx +++ b/Client/watchout/features/events/details/EventDetails.tsx @@ -17,7 +17,7 @@ export const EventDetails = ({ event }: EventDetailsProps) => { const [modalVisible, setModalVisible] = useState(false); const [selectedImage, setSelectedImage] = useState(null); - const reportedDateText = formatDate(new Date(event.reportedDate)); + const reportedDateText = formatDate(event.reportedDate); const openImage = (uri: string) => { setSelectedImage(uri); diff --git a/Client/watchout/utils/types.ts b/Client/watchout/utils/types.ts index 57dfaaf..c8b798f 100644 --- a/Client/watchout/utils/types.ts +++ b/Client/watchout/utils/types.ts @@ -24,8 +24,8 @@ export type Event = { images: string[]; latitude: number; longitude: number; - reportedDate: string; - endDate: string; + reportedDate: Date; + endDate: Date; eventType: EventType; active: boolean; rating: number; @@ -110,7 +110,7 @@ export type AddLocationRequest = { weather: boolean; eventTypes?: number[]; }; - notificationsEnable: boolean; + notificationsEnable: boolean; }; }; From 97e78cdbb09568f9ef6dfad6b196e93d32f595e1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Puchyr?= Date: Sun, 7 Dec 2025 15:57:31 +0100 Subject: [PATCH 07/11] ZPI-99 feat: display reputation badges --- .../watchout/components/Base/CustomChip.tsx | 31 +++++++++ .../features/events/details/EventDetails.tsx | 8 +++ .../events/details/ReportStatusIcons.tsx | 66 +++++++++++++++++++ Client/watchout/utils/constants.ts | 5 +- Client/watchout/utils/types.ts | 4 ++ 5 files changed, 113 insertions(+), 1 deletion(-) create mode 100644 Client/watchout/components/Base/CustomChip.tsx create mode 100644 Client/watchout/features/events/details/ReportStatusIcons.tsx diff --git a/Client/watchout/components/Base/CustomChip.tsx b/Client/watchout/components/Base/CustomChip.tsx new file mode 100644 index 0000000..16716e7 --- /dev/null +++ b/Client/watchout/components/Base/CustomChip.tsx @@ -0,0 +1,31 @@ +import { CustomSurface } from 'components/Layout/CustomSurface'; +import { ViewStyle } from 'react-native'; +import { Icon } from 'react-native-paper'; +import { View } from 'react-native-reanimated/lib/typescript/Animated'; + +type CustomChipProps = { + icon: string; + iconColor: string; + style: ViewStyle; + children?: React.ReactNode; +}; + +export const CustomChip = ({ icon, iconColor, style, children }: CustomChipProps) => { + return ( + + + {children} + + ); +}; diff --git a/Client/watchout/features/events/details/EventDetails.tsx b/Client/watchout/features/events/details/EventDetails.tsx index 19d1d1a..8a1e1ce 100644 --- a/Client/watchout/features/events/details/EventDetails.tsx +++ b/Client/watchout/features/events/details/EventDetails.tsx @@ -6,6 +6,10 @@ import { icons } from 'components/Base/icons'; import { Row } from 'components/Base/Row'; import { useState } from 'react'; import { formatDate } from 'utils/helpers'; +import { HIGH_REPUTATION_THRESHOLD, LOW_REPUTATION_THRESHOLD } from 'utils/constants'; +import { Chip, Icon } from 'react-native-paper'; +import { CustomChip } from 'components/Base/CustomChip'; +import { ReportStatusIcons } from './ReportStatusIcons'; type EventDetailsProps = { event: Event; @@ -41,6 +45,9 @@ export const EventDetails = ({ event }: EventDetailsProps) => { Zgłoszono: {reportedDateText} + + + {event.description} {event.images.length > 0 && ( @@ -86,6 +93,7 @@ export const EventDetails = ({ event }: EventDetailsProps) => { ); }; + const styles = StyleSheet.create({ header: { flexDirection: 'row', diff --git a/Client/watchout/features/events/details/ReportStatusIcons.tsx b/Client/watchout/features/events/details/ReportStatusIcons.tsx new file mode 100644 index 0000000..558d733 --- /dev/null +++ b/Client/watchout/features/events/details/ReportStatusIcons.tsx @@ -0,0 +1,66 @@ +import { CustomChip } from "components/Base/CustomChip"; +import dayjs from "dayjs"; +import { View } from "react-native"; +import { HIGH_REPUTATION_THRESHOLD, LOW_REPUTATION_THRESHOLD } from "utils/constants"; +import { Text } from "components/Base/Text"; +import { Event } from "utils/types"; + +const reportIcons = (event: Event) => [ + { + icon: 'account-alert', + color: '#f44336', + text: 'Niska reputacja autora', + predicate: event.author.reputation < LOW_REPUTATION_THRESHOLD, + }, + { + icon: 'account-check', + color: '#4caf50', + text: 'Zaufany autor', + predicate: event.author.reputation >= HIGH_REPUTATION_THRESHOLD, + }, + { + icon: 'thumb-down', + color: '#f44336', + text: 'Nieautentyczne', + predicate: event.rating < LOW_REPUTATION_THRESHOLD, + }, + { + icon: 'check-decagram', + color: '#4caf50', + text: 'Potwierdzane', + predicate: event.rating >= HIGH_REPUTATION_THRESHOLD, + }, +]; + +export const ReportStatusIcons = ({ event }: { event: Event }) => { + const icons = reportIcons(event).filter(({ predicate }) => predicate); + const dayjsNow = dayjs.utc(); + console.log({ + reportedDate: event.reportedDate, + dayjsNow: dayjsNow, + diffMinutes: dayjsNow.diff(dayjs.utc(event.reportedDate), 'minute'), + }) + + return ( + + {icons.map(({ icon, color, text }) => ( + + {text} + + ))} + + {dayjs.utc().diff(dayjs.utc(event.reportedDate), 'minute') < 30 && ( + + Nowe zgłoszenie + + )} + + ); +}; \ No newline at end of file diff --git a/Client/watchout/utils/constants.ts b/Client/watchout/utils/constants.ts index 3cdd867..4ac6c12 100644 --- a/Client/watchout/utils/constants.ts +++ b/Client/watchout/utils/constants.ts @@ -7,4 +7,7 @@ export const DEFAULT_REPORT_HOURS_FILTER = 6; export const MIN_LOCATION_RADIUS_KM = 0; export const MAX_LOCATION_RADIUS_KM = 50; export const DEFAULT_LOCATION_RADIUS_KM = 5; -export const METERS_IN_KM = 1000; \ No newline at end of file +export const METERS_IN_KM = 1000; + +export const LOW_REPUTATION_THRESHOLD = 0.25; +export const HIGH_REPUTATION_THRESHOLD = 0.75; \ No newline at end of file diff --git a/Client/watchout/utils/types.ts b/Client/watchout/utils/types.ts index c8b798f..79f6501 100644 --- a/Client/watchout/utils/types.ts +++ b/Client/watchout/utils/types.ts @@ -27,6 +27,10 @@ export type Event = { reportedDate: Date; endDate: Date; eventType: EventType; + author: { + id: number; + reputation: number; + } active: boolean; rating: number; ratingForCurrentUser: number; From e22f9aae0119364265f9be58d591b3ac91ecd9b7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Puchyr?= Date: Sun, 7 Dec 2025 16:01:05 +0100 Subject: [PATCH 08/11] ZPI-173 fix: suppress navigation warnings in settings --- Client/watchout/features/outages/Alerts.tsx | 9 +++------ Client/watchout/features/settings/Settings.tsx | 5 +++-- .../features/settings/SettingsNavigator.tsx | 17 ++++++----------- 3 files changed, 12 insertions(+), 19 deletions(-) diff --git a/Client/watchout/features/outages/Alerts.tsx b/Client/watchout/features/outages/Alerts.tsx index 9962b0e..defb27e 100644 --- a/Client/watchout/features/outages/Alerts.tsx +++ b/Client/watchout/features/outages/Alerts.tsx @@ -43,17 +43,14 @@ export const Alerts = () => { iconName="cog" /> + {isLoading && } {alerts?.length === 0 && ( - + - + Brak alertów o awariach. diff --git a/Client/watchout/features/settings/Settings.tsx b/Client/watchout/features/settings/Settings.tsx index 3a227f3..cf7b5a5 100644 --- a/Client/watchout/features/settings/Settings.tsx +++ b/Client/watchout/features/settings/Settings.tsx @@ -3,18 +3,19 @@ import { Text } from 'components/Base/Text'; import { PageWrapper } from 'components/Common/PageWrapper'; import { TouchableOpacity, View } from 'react-native'; import { Icon } from 'react-native-paper'; +import { settingsRoutes } from './SettingsNavigator'; type SettingsProps = { options: { icon: string; label: string; link: string }[]; }; -export const Settings = ({ options }: SettingsProps) => { +export const Settings = () => { const navigation = useNavigation(); return ( - {options.map((option) => ( + {settingsRoutes.map((option) => ( navigation.navigate(option.link as never)} diff --git a/Client/watchout/features/settings/SettingsNavigator.tsx b/Client/watchout/features/settings/SettingsNavigator.tsx index d54c064..4bb6eea 100644 --- a/Client/watchout/features/settings/SettingsNavigator.tsx +++ b/Client/watchout/features/settings/SettingsNavigator.tsx @@ -3,6 +3,7 @@ import { createStackNavigator } from '@react-navigation/stack'; import { Settings } from './Settings'; import { navigationTheme } from 'components/Base/navigationTheme'; import { NotificationSettings } from './notifications/NotificationSettings'; +import { AccountSettings } from './AccountSettings'; const Stack = createStackNavigator(); @@ -10,32 +11,26 @@ export const settingsRoutes = [ { icon: 'account-circle', label: 'Konto', - component: () => Konto, + component: AccountSettings, link: 'AccountSettings', }, { icon: 'bell', label: 'Powiadomienia', - component: () => , + component: NotificationSettings, link: 'NotificationSettings', }, - { - icon: 'lock', - label: 'Prywatność', - component: () => Prywatność, - link: 'PrivacySettings', - }, ]; export const SettingsNavigator = () => { return ( + ...navigationTheme, + }}> } + component={Settings} options={{ headerShown: false }} /> {settingsRoutes.map((option) => ( From dd25c676dc4fa9bbc1294ff579a6432e298a3cf0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Puchyr?= Date: Sun, 7 Dec 2025 20:01:02 +0100 Subject: [PATCH 09/11] ZPI-173 feat: implement messaging if user is not able to create new event --- .../events/create/CreateEventBottomSheet.tsx | 54 +++++++++++++----- .../events/create/useActionAvailability.ts | 14 +++-- .../features/events/details/EventDetails.tsx | 4 -- .../events/details/ReportStatusIcons.tsx | 8 +-- Client/watchout/features/map/useLocation.ts | 4 +- Client/watchout/utils/AuthError.ts | 44 +------------- Client/watchout/utils/apiDefinition.ts | 6 +- Client/watchout/utils/dictionaries.ts | 57 +++++++++++++++++++ Client/watchout/utils/types.ts | 16 +++++- 9 files changed, 129 insertions(+), 78 deletions(-) create mode 100644 Client/watchout/utils/dictionaries.ts diff --git a/Client/watchout/features/events/create/CreateEventBottomSheet.tsx b/Client/watchout/features/events/create/CreateEventBottomSheet.tsx index 4bd86fa..7d620b4 100644 --- a/Client/watchout/features/events/create/CreateEventBottomSheet.tsx +++ b/Client/watchout/features/events/create/CreateEventBottomSheet.tsx @@ -1,7 +1,7 @@ import { StyleSheet, TouchableOpacity, View } from 'react-native'; import { Text } from 'components/Base/Text'; import { CustomTextInput } from 'components/Base/CustomTextInput'; -import { Coordinates } from 'utils/types'; +import { Coordinates, PostUnabilityReason } from 'utils/types'; import { EventTypeSelectionModal } from './EventTypeSelectionModal'; import { BottomSheetModal, BottomSheetView } from '@gorhom/bottom-sheet'; import { ActivityIndicator, Button, Icon } from 'react-native-paper'; @@ -13,6 +13,8 @@ import { useActionAvailability } from './useActionAvailability'; import { Row } from 'components/Base/Row'; import { theme } from 'utils/theme'; import { Controller } from 'react-hook-form'; +import { useUserLocation } from 'features/map/useLocation'; +import { unavailabilityDictionary } from 'utils/dictionaries'; type CreateEventProps = { location: Coordinates; @@ -33,18 +35,46 @@ export const CreateEventBottomSheet = ({ location, onSuccess, onClose }: CreateE isLoading, } = useEventCreateForm(location, onSuccess); + const { location: userLocation, loading: isUserLocationLoading } = useUserLocation(); + const isUserLocationFetched = !isUserLocationLoading && userLocation != null; + const { data: actionAvailability, isLoading: isActionAvailabilityLoading } = - useActionAvailability(); + useActionAvailability( + { + lat: userLocation?.latitude ?? 0, + long: userLocation?.longitude ?? 0, + eventLat: location.latitude, + eventLong: location.longitude, + }, + { isEnabled: isUserLocationFetched } + ); useEffect(() => { eventBottomSheetRef.current?.present(); }, [eventBottomSheetRef]); + console.log({ + isUserLocationFetched + }) return ( - {isActionAvailabilityLoading ? ( + {!isUserLocationFetched ? ( + + + + + + Lokalizacja niedostępna + + + Aby móc zgłosić nowe zdarzenie, potrzebujemy wiedzieć, że znajdujesz się w pobliżu + miejsca zdarzenia. Upewnij się, że usługi lokalizacyjne są włączone i spróbuj + ponownie. + + + ) : isActionAvailabilityLoading ? ( ) : actionAvailability?.canPost ? ( @@ -100,10 +130,7 @@ export const CreateEventBottomSheet = ({ location, onSuccess, onClose }: CreateE control={control} name="images" render={({ field: { value, onChange } }) => ( - + )} /> @@ -125,7 +152,7 @@ export const CreateEventBottomSheet = ({ location, onSuccess, onClose }: CreateE ) : ( - + )} @@ -133,7 +160,11 @@ export const CreateEventBottomSheet = ({ location, onSuccess, onClose }: CreateE ); }; -const ActionNotAvailableMessage = () => { +const ActionNotAvailableMessage = ({ reason }: { reason?: PostUnabilityReason }) => { + const errorDescription = reason + ? unavailabilityDictionary[reason] + : 'Chilowo nie jest możliwe zgłoszenie zdarzenia. Spróbuj ponownie później.'; + return ( @@ -142,10 +173,7 @@ const ActionNotAvailableMessage = () => { Akcja niedostępna - - Nie możesz zgłosić nowego zdarzenia, ponieważ twoja reputacja jest zbyt niska. Spróbuj - ponownie później. - + {errorDescription} ); }; diff --git a/Client/watchout/features/events/create/useActionAvailability.ts b/Client/watchout/features/events/create/useActionAvailability.ts index 8ae71b4..848a677 100644 --- a/Client/watchout/features/events/create/useActionAvailability.ts +++ b/Client/watchout/features/events/create/useActionAvailability.ts @@ -1,11 +1,17 @@ import { useQuery } from '@tanstack/react-query'; import { apiClient } from 'utils/apiClient'; import { API_ENDPOINTS } from 'utils/apiDefinition'; -import { ActionAvailability } from 'utils/types'; +import { ActionAvailabilityRequest, ActionAvailabilityResponse } from 'utils/types'; -export const useActionAvailability = () => +export const useActionAvailability = ( + request: ActionAvailabilityRequest, + { isEnabled }: { isEnabled: boolean } +) => useQuery({ - queryKey: ['actionAvailability'], + queryKey: ['actionAvailability', request], queryFn: () => - apiClient.get(API_ENDPOINTS.events.availability).then((res) => res.data), + apiClient + .get(API_ENDPOINTS.events.availability(request)) + .then((res) => res.data), + enabled: isEnabled, }); diff --git a/Client/watchout/features/events/details/EventDetails.tsx b/Client/watchout/features/events/details/EventDetails.tsx index 8a1e1ce..eaa4a95 100644 --- a/Client/watchout/features/events/details/EventDetails.tsx +++ b/Client/watchout/features/events/details/EventDetails.tsx @@ -1,14 +1,10 @@ import { StyleSheet, View, Image, Modal, Pressable } from 'react-native'; import { Text } from 'components/Base/Text'; import { Event } from 'utils/types'; -import dayjs from 'dayjs'; import { icons } from 'components/Base/icons'; import { Row } from 'components/Base/Row'; import { useState } from 'react'; import { formatDate } from 'utils/helpers'; -import { HIGH_REPUTATION_THRESHOLD, LOW_REPUTATION_THRESHOLD } from 'utils/constants'; -import { Chip, Icon } from 'react-native-paper'; -import { CustomChip } from 'components/Base/CustomChip'; import { ReportStatusIcons } from './ReportStatusIcons'; type EventDetailsProps = { diff --git a/Client/watchout/features/events/details/ReportStatusIcons.tsx b/Client/watchout/features/events/details/ReportStatusIcons.tsx index 558d733..7de884d 100644 --- a/Client/watchout/features/events/details/ReportStatusIcons.tsx +++ b/Client/watchout/features/events/details/ReportStatusIcons.tsx @@ -34,13 +34,7 @@ const reportIcons = (event: Event) => [ export const ReportStatusIcons = ({ event }: { event: Event }) => { const icons = reportIcons(event).filter(({ predicate }) => predicate); - const dayjsNow = dayjs.utc(); - console.log({ - reportedDate: event.reportedDate, - dayjsNow: dayjsNow, - diffMinutes: dayjsNow.diff(dayjs.utc(event.reportedDate), 'minute'), - }) - + return ( {icons.map(({ icon, color, text }) => ( diff --git a/Client/watchout/features/map/useLocation.ts b/Client/watchout/features/map/useLocation.ts index 78d4fc1..49f7da8 100644 --- a/Client/watchout/features/map/useLocation.ts +++ b/Client/watchout/features/map/useLocation.ts @@ -25,6 +25,7 @@ export const useUserLocation = () => { const [hasPermission, setHasPermission] = useState(false); const [location, setLocation] = useState<{ latitude: number; longitude: number } | null>(null); const [error, setError] = useState(null); + const [loading, setLoading] = useState(true); useEffect(() => { const checkAndRequest = async () => { @@ -51,9 +52,10 @@ export const useUserLocation = () => { } ); } + setLoading(false); }; checkAndRequest(); }, []); - return { hasPermission, location, error }; + return { hasPermission, location, error, loading }; }; \ No newline at end of file diff --git a/Client/watchout/utils/AuthError.ts b/Client/watchout/utils/AuthError.ts index 236b10f..352c789 100644 --- a/Client/watchout/utils/AuthError.ts +++ b/Client/watchout/utils/AuthError.ts @@ -3,46 +3,4 @@ export class AuthError extends Error { super(message); this.name = 'AuthError'; } -} - -export const firebaseAuthErrorMessages: { [code: string]: string } = { - // Klient ma już konto - 'auth/email-already-exists': 'Podany adres e-mail jest już zajęty przez istniejące konto.', - 'auth/email-already-in-use': 'Podany adres e-mail jest już zajęty przez istniejące konto.', // Duplikat/Alternatywa - 'auth/phone-number-already-exists': 'Podany numer telefonu jest już używany przez innego użytkownika.', - - // Problemy z danymi wejściowymi - 'auth/invalid-email': 'Wprowadzono nieprawidłowy format adresu e-mail.', - 'auth/invalid-credential': 'Adres e-mail lub hasło są nieprawidłowe.', - 'auth/invalid-password': 'Hasło jest nieprawidłowe. Musi zawierać co najmniej 6 znaków.', - 'auth/weak-password': 'Hasło jest zbyt słabe. Użyj co najmniej 6 znaków.', // Dodano z Twojego przykładu - - // Problemy z kontem - 'auth/user-not-found': 'Nie znaleziono użytkownika dla podanych danych.', - 'auth/user-disabled': 'To konto użytkownika zostało zablokowane przez administratora.', - 'auth/wrong-password': 'Nieprawidłowe hasło.', // Dodano z Twojego przykładu - - // === Błędy Tokenów i Sesji === - 'auth/id-token-expired': 'Sesja wygasła. Proszę zalogować się ponownie.', - 'auth/session-cookie-expired': 'Sesja wygasła. Proszę zalogować się ponownie.', - 'auth/id-token-revoked': 'Token sesji został unieważniony. Proszę zalogować się ponownie.', - 'auth/session-cookie-revoked': 'Token sesji został unieważniony. Proszę zalogować się ponownie.', - - // === Błędy Serwera i Limity === - 'auth/too-many-requests': 'Wykryto zbyt wiele nieudanych prób. Proszę spróbować ponownie później.', - 'auth/internal-error': 'Wystąpił nieoczekiwany błąd serwera. Jeśli problem się powtarza, skontaktuj się ze wsparciem.', - - // === Błędy Konfiguracji i Argumentów === - 'auth/invalid-argument': 'Wystąpił błąd danych. Proszę zweryfikować wprowadzone informacje.', - 'auth/operation-not-allowed': 'Metoda logowania jest wyłączona w ustawieniach projektu Firebase.', - 'auth/invalid-phone-number': 'Wprowadzony numer telefonu jest nieprawidłowy (wymagany format E.164).', - 'auth/invalid-display-name': 'Nazwa użytkownika jest nieprawidłowa lub pusta.', - 'auth/unauthorized-continue-uri': 'Domena przekierowania nie jest autoryzowana. Skontaktuj się z administratorem.', - - // === Błędy Admin SDK (rzadziej na kliencie) === - 'auth/claims-too-large': 'Nie można ustawić atrybutów: przekroczono limit rozmiaru (1000 bajtów).', - 'auth/uid-already-exists': 'Wewnętrzny identyfikator UID jest już używany przez innego użytkownika.', - - // === Domyślne (catch-all) === - 'default': 'Wystąpił nieznany błąd autoryzacji. Spróbuj ponownie lub skontaktuj się ze wsparciem.' -}; \ No newline at end of file +} \ No newline at end of file diff --git a/Client/watchout/utils/apiDefinition.ts b/Client/watchout/utils/apiDefinition.ts index 3da6a4f..7362d45 100644 --- a/Client/watchout/utils/apiDefinition.ts +++ b/Client/watchout/utils/apiDefinition.ts @@ -1,4 +1,4 @@ -import type { GetEventsRequest, PaginationRequest } from './types'; +import type { ActionAvailabilityRequest, GetEventsRequest, PaginationRequest } from './types'; export type ApiDefinition = { key: string[]; @@ -11,7 +11,7 @@ export const API_ENDPOINTS = { getClusters: (request: GetEventsRequest, minPoints: number, eps: number) => `events/clusters${queryParams(request)}&minPoints=${minPoints}&eps=${eps}`, create: 'events', - availability: 'events/ability', + availability: (request: ActionAvailabilityRequest) => 'events/ability' + queryParams(request), }, comments: { getByEventId: (eventId: number, pagination: PaginationRequest) => @@ -28,7 +28,7 @@ export const API_ENDPOINTS = { locations: { getAll: 'users/favourite-places', add: 'users/favourite-places', - edit: (placeId: string) => `users/favourite-places/${placeId}`, + edit: (placeId: string) => `users/favourite-places/${placeId}/preferences`, delete: (placeId: string) => `users/favourite-places/${placeId}`, }, externalWarnings: { diff --git a/Client/watchout/utils/dictionaries.ts b/Client/watchout/utils/dictionaries.ts new file mode 100644 index 0000000..a2dface --- /dev/null +++ b/Client/watchout/utils/dictionaries.ts @@ -0,0 +1,57 @@ +import { PostUnabilityReason } from './types'; + +export const firebaseAuthErrorMessages: { [code: string]: string } = { + // Klient ma już konto + 'auth/email-already-exists': 'Podany adres e-mail jest już zajęty przez istniejące konto.', + 'auth/email-already-in-use': 'Podany adres e-mail jest już zajęty przez istniejące konto.', // Duplikat/Alternatywa + 'auth/phone-number-already-exists': + 'Podany numer telefonu jest już używany przez innego użytkownika.', + + // Problemy z danymi wejściowymi + 'auth/invalid-email': 'Wprowadzono nieprawidłowy format adresu e-mail.', + 'auth/invalid-credential': 'Adres e-mail lub hasło są nieprawidłowe.', + 'auth/invalid-password': 'Hasło jest nieprawidłowe. Musi zawierać co najmniej 6 znaków.', + 'auth/weak-password': 'Hasło jest zbyt słabe. Użyj co najmniej 6 znaków.', // Dodano z Twojego przykładu + + // Problemy z kontem + 'auth/user-not-found': 'Nie znaleziono użytkownika dla podanych danych.', + 'auth/user-disabled': 'To konto użytkownika zostało zablokowane przez administratora.', + 'auth/wrong-password': 'Nieprawidłowe hasło.', // Dodano z Twojego przykładu + + // === Błędy Tokenów i Sesji === + 'auth/id-token-expired': 'Sesja wygasła. Proszę zalogować się ponownie.', + 'auth/session-cookie-expired': 'Sesja wygasła. Proszę zalogować się ponownie.', + 'auth/id-token-revoked': 'Token sesji został unieważniony. Proszę zalogować się ponownie.', + 'auth/session-cookie-revoked': 'Token sesji został unieważniony. Proszę zalogować się ponownie.', + + // === Błędy Serwera i Limity === + 'auth/too-many-requests': + 'Wykryto zbyt wiele nieudanych prób. Proszę spróbować ponownie później.', + 'auth/internal-error': + 'Wystąpił nieoczekiwany błąd serwera. Jeśli problem się powtarza, skontaktuj się ze wsparciem.', + + // === Błędy Konfiguracji i Argumentów === + 'auth/invalid-argument': 'Wystąpił błąd danych. Proszę zweryfikować wprowadzone informacje.', + 'auth/operation-not-allowed': 'Metoda logowania jest wyłączona w ustawieniach projektu Firebase.', + 'auth/invalid-phone-number': + 'Wprowadzony numer telefonu jest nieprawidłowy (wymagany format E.164).', + 'auth/invalid-display-name': 'Nazwa użytkownika jest nieprawidłowa lub pusta.', + 'auth/unauthorized-continue-uri': + 'Domena przekierowania nie jest autoryzowana. Skontaktuj się z administratorem.', + + // === Błędy Admin SDK (rzadziej na kliencie) === + 'auth/claims-too-large': + 'Nie można ustawić atrybutów: przekroczono limit rozmiaru (1000 bajtów).', + 'auth/uid-already-exists': + 'Wewnętrzny identyfikator UID jest już używany przez innego użytkownika.', + + // === Domyślne (catch-all) === + default: 'Wystąpił nieznany błąd autoryzacji. Spróbuj ponownie lub skontaktuj się ze wsparciem.', +}; + +export const unavailabilityDictionary: { [reason in PostUnabilityReason]: string } = { + DISTANCE_RESTRICTION: + 'Nie możesz zgłosić zdarzenia ze swojej obecnej lokalizacji, ponieważ jest ona zbyt daleko od miejsca zdarzenia.', + REPUTATION_RESTRICTION: + 'Nie możesz zgłosić nowego zdarzenia, ponieważ twoja reputacja jest zbyt niska. Spróbuj ponownie później.', +}; diff --git a/Client/watchout/utils/types.ts b/Client/watchout/utils/types.ts index 79f6501..4e3dd99 100644 --- a/Client/watchout/utils/types.ts +++ b/Client/watchout/utils/types.ts @@ -1,3 +1,4 @@ +import { ActionAvailability } from 'utils/types'; export type Coordinates = { latitude: number; longitude: number; @@ -199,7 +200,16 @@ export type Alert = { placeName: string; }; -export type ActionAvailability = { +export type ActionAvailabilityRequest = { + lat: number; + long: number; + eventLat: number; + eventLong: number; +} + +export type ActionAvailabilityResponse = { canPost: boolean; - reason?: string; -}; \ No newline at end of file + reason?: PostUnabilityReason; +}; + +export type PostUnabilityReason = 'DISTANCE_RESTRICTION' | 'REPUTATION_RESTRICTION'; \ No newline at end of file From d572dd4bf769b9a3655feffff899f0d018472b3b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Puchyr?= Date: Sun, 7 Dec 2025 20:24:53 +0100 Subject: [PATCH 10/11] style: minor fixes --- .../watchout/components/Base/CustomChip.tsx | 1 - Client/watchout/features/auth/LoginScreen.tsx | 3 +- .../watchout/features/auth/SignUpScreen.tsx | 3 +- Client/watchout/features/auth/auth.ts | 5 ++- .../events/create/CreateEventBottomSheet.tsx | 12 +++---- .../events/create/useEventCreateForm.ts | 31 ++++++++++--------- .../features/settings/AccountSettings.tsx | 8 ++--- .../watchout/features/settings/Settings.tsx | 6 +--- .../features/settings/SettingsNavigator.tsx | 19 +----------- .../features/settings/settingsRoutes.ts | 17 ++++++++++ Client/watchout/utils/apiDefinition.ts | 12 +++---- Client/watchout/utils/types.ts | 7 ++--- 12 files changed, 58 insertions(+), 66 deletions(-) create mode 100644 Client/watchout/features/settings/settingsRoutes.ts diff --git a/Client/watchout/components/Base/CustomChip.tsx b/Client/watchout/components/Base/CustomChip.tsx index 16716e7..a8338d1 100644 --- a/Client/watchout/components/Base/CustomChip.tsx +++ b/Client/watchout/components/Base/CustomChip.tsx @@ -1,7 +1,6 @@ import { CustomSurface } from 'components/Layout/CustomSurface'; import { ViewStyle } from 'react-native'; import { Icon } from 'react-native-paper'; -import { View } from 'react-native-reanimated/lib/typescript/Animated'; type CustomChipProps = { icon: string; diff --git a/Client/watchout/features/auth/LoginScreen.tsx b/Client/watchout/features/auth/LoginScreen.tsx index 26e0e84..01da8ee 100644 --- a/Client/watchout/features/auth/LoginScreen.tsx +++ b/Client/watchout/features/auth/LoginScreen.tsx @@ -7,7 +7,8 @@ import { Button, Icon } from 'react-native-paper'; import { GoogleSignInButton } from 'features/auth/GoogleSignInButton'; import { signInWithEmail, resetPassword } from './auth'; import { AuthLayout } from 'components/Layout/AuthLayout'; -import { AuthError, firebaseAuthErrorMessages } from 'utils/AuthError'; +import { AuthError } from 'utils/AuthError'; +import { firebaseAuthErrorMessages } from 'utils/dictionaries'; export const LoginScreen = () => { const { navigate } = useNavigation(); diff --git a/Client/watchout/features/auth/SignUpScreen.tsx b/Client/watchout/features/auth/SignUpScreen.tsx index 2ca1788..183ad0e 100644 --- a/Client/watchout/features/auth/SignUpScreen.tsx +++ b/Client/watchout/features/auth/SignUpScreen.tsx @@ -7,7 +7,8 @@ import { Button, Icon } from 'react-native-paper'; import { GoogleSignInButton } from 'features/auth/GoogleSignInButton'; import { signUpEmail } from './auth'; import { AuthLayout } from 'components//Layout/AuthLayout'; -import { AuthError, firebaseAuthErrorMessages } from 'utils/AuthError'; +import { AuthError } from 'utils/AuthError'; +import { firebaseAuthErrorMessages } from 'utils/dictionaries'; export const SignUpScreen = () => { const { navigate } = useNavigation(); diff --git a/Client/watchout/features/auth/auth.ts b/Client/watchout/features/auth/auth.ts index 6fa231e..fdb6512 100644 --- a/Client/watchout/features/auth/auth.ts +++ b/Client/watchout/features/auth/auth.ts @@ -9,7 +9,6 @@ import { GoogleAuthProvider, signInWithCredential, getIdToken, - firebase, FirebaseAuthTypes, } from '@react-native-firebase/auth'; import { apiClient } from 'utils/apiClient'; @@ -38,7 +37,7 @@ export async function signUpEmail(email: string, password: string, displayName?: export async function signInWithEmail( email: string, password: string -): Promise { +): Promise { try { const auth = getAuth(); const cred = await signInWithEmailAndPassword(auth, email, password); @@ -93,7 +92,7 @@ export async function signInWithGoogleIdToken(idToken: string) { const firebaseIdToken = await getIdToken(user); await apiClient.post( - '/users/create', + API_ENDPOINTS.users.create, { firebaseUid: user.uid, email: user.email, diff --git a/Client/watchout/features/events/create/CreateEventBottomSheet.tsx b/Client/watchout/features/events/create/CreateEventBottomSheet.tsx index 7d620b4..9ef9f4b 100644 --- a/Client/watchout/features/events/create/CreateEventBottomSheet.tsx +++ b/Client/watchout/features/events/create/CreateEventBottomSheet.tsx @@ -23,6 +23,9 @@ type CreateEventProps = { }; export const CreateEventBottomSheet = ({ location, onSuccess, onClose }: CreateEventProps) => { + const { location: userLocation, loading: isUserLocationLoading } = useUserLocation(); + const isUserLocationFetched = !isUserLocationLoading && userLocation != null; + const { control, handleSubmit, @@ -33,10 +36,8 @@ export const CreateEventBottomSheet = ({ location, onSuccess, onClose }: CreateE geocodedAddress, eventBottomSheetRef, isLoading, - } = useEventCreateForm(location, onSuccess); + } = useEventCreateForm(location, userLocation, onSuccess); - const { location: userLocation, loading: isUserLocationLoading } = useUserLocation(); - const isUserLocationFetched = !isUserLocationLoading && userLocation != null; const { data: actionAvailability, isLoading: isActionAvailabilityLoading } = useActionAvailability( @@ -53,9 +54,6 @@ export const CreateEventBottomSheet = ({ location, onSuccess, onClose }: CreateE eventBottomSheetRef.current?.present(); }, [eventBottomSheetRef]); - console.log({ - isUserLocationFetched - }) return ( @@ -163,7 +161,7 @@ export const CreateEventBottomSheet = ({ location, onSuccess, onClose }: CreateE const ActionNotAvailableMessage = ({ reason }: { reason?: PostUnabilityReason }) => { const errorDescription = reason ? unavailabilityDictionary[reason] - : 'Chilowo nie jest możliwe zgłoszenie zdarzenia. Spróbuj ponownie później.'; + : 'Chwilowo nie jest możliwe zgłoszenie zdarzenia. Spróbuj ponownie później.'; return ( diff --git a/Client/watchout/features/events/create/useEventCreateForm.ts b/Client/watchout/features/events/create/useEventCreateForm.ts index 1a81c66..61da5c5 100644 --- a/Client/watchout/features/events/create/useEventCreateForm.ts +++ b/Client/watchout/features/events/create/useEventCreateForm.ts @@ -21,7 +21,11 @@ type SelectedEventType = { icon: string; } | null; -export const useEventCreateForm = (location: Coordinates, onSuccess: () => void) => { +export const useEventCreateForm = ( + location: Coordinates, + userLocation: Coordinates | null, + onSuccess: () => void +) => { const eventBottomSheetRef = useRef(null); const [selectedEventType, setSelectedEventType] = useState(null); @@ -44,12 +48,13 @@ export const useEventCreateForm = (location: Coordinates, onSuccess: () => void) const createEventMutation = useCreateEvent(); - const handleSetSelectedEventType = useCallback((eventType: SelectedEventType) => { - setSelectedEventType(eventType); - setValue('eventTypeId', eventType?.id || null); - }, [setValue]); - - const userLocation = useUserLocation(); + const handleSetSelectedEventType = useCallback( + (eventType: SelectedEventType) => { + setSelectedEventType(eventType); + setValue('eventTypeId', eventType?.id || null); + }, + [setValue] + ); const onSubmit = (values: FormData) => { const isValid = values.name && values.description && values.eventTypeId !== null; @@ -57,12 +62,8 @@ export const useEventCreateForm = (location: Coordinates, onSuccess: () => void) Alert.alert('Błąd', 'Wszystkie pola muszą być wypełnione.'); return; } - - if (!userLocation.location) { - Alert.alert( - 'Błąd', - 'Aby móc utworzyć zdarzenie, upewnij się, że usługi lokalizacyjne są włączone.' - ); + if (!userLocation) { + Alert.alert('Błąd', 'Nie udało się pobrać Twojej lokalizacji. Spróbuj ponownie.'); return; } @@ -73,8 +74,8 @@ export const useEventCreateForm = (location: Coordinates, onSuccess: () => void) description: values.description, latitude: location.latitude, longitude: location.longitude, - userLatitude: location.latitude, - userLongitude: location.longitude, + userLatitude: userLocation.latitude, + userLongitude: userLocation.longitude, endDate: dateInOneHour.toISOString(), images: values.images.map((image) => image.base64), eventTypeId: values.eventTypeId!, diff --git a/Client/watchout/features/settings/AccountSettings.tsx b/Client/watchout/features/settings/AccountSettings.tsx index 3b0e394..c70f7d9 100644 --- a/Client/watchout/features/settings/AccountSettings.tsx +++ b/Client/watchout/features/settings/AccountSettings.tsx @@ -1,5 +1,5 @@ -import React from 'react' -import { View } from 'react-native' +import React from 'react'; +import { View } from 'react-native'; import { Text } from 'components/Base/Text'; export const AccountSettings = () => { @@ -7,5 +7,5 @@ export const AccountSettings = () => { Konto - ) -} + ); +}; diff --git a/Client/watchout/features/settings/Settings.tsx b/Client/watchout/features/settings/Settings.tsx index cf7b5a5..dfce742 100644 --- a/Client/watchout/features/settings/Settings.tsx +++ b/Client/watchout/features/settings/Settings.tsx @@ -3,11 +3,7 @@ import { Text } from 'components/Base/Text'; import { PageWrapper } from 'components/Common/PageWrapper'; import { TouchableOpacity, View } from 'react-native'; import { Icon } from 'react-native-paper'; -import { settingsRoutes } from './SettingsNavigator'; - -type SettingsProps = { - options: { icon: string; label: string; link: string }[]; -}; +import { settingsRoutes } from './settingsRoutes'; export const Settings = () => { const navigation = useNavigation(); diff --git a/Client/watchout/features/settings/SettingsNavigator.tsx b/Client/watchout/features/settings/SettingsNavigator.tsx index 4bb6eea..e08c916 100644 --- a/Client/watchout/features/settings/SettingsNavigator.tsx +++ b/Client/watchout/features/settings/SettingsNavigator.tsx @@ -1,27 +1,10 @@ -import { Text } from 'components/Base/Text'; import { createStackNavigator } from '@react-navigation/stack'; import { Settings } from './Settings'; import { navigationTheme } from 'components/Base/navigationTheme'; -import { NotificationSettings } from './notifications/NotificationSettings'; -import { AccountSettings } from './AccountSettings'; +import { settingsRoutes } from './settingsRoutes'; const Stack = createStackNavigator(); -export const settingsRoutes = [ - { - icon: 'account-circle', - label: 'Konto', - component: AccountSettings, - link: 'AccountSettings', - }, - { - icon: 'bell', - label: 'Powiadomienia', - component: NotificationSettings, - link: 'NotificationSettings', - }, -]; - export const SettingsNavigator = () => { return ( ): string { .map((k) => { if (Array.isArray(params[k])) { return esc(k) + '=' + params[k].map((val: any) => `${esc(val)}`).join(','); - } - else if (params[k] instanceof Date) { + } else if (params[k] instanceof Date) { return `${esc(k)}=${esc((params[k] as Date).toISOString().replace('Z', ''))}`; - } - else if (typeof params[k] === 'object' && params[k] !== null) { + } else if (typeof params[k] === 'object' && params[k] !== null) { return queryParams(params[k]).slice(1); } return `${esc(k)}=${esc(params[k])}`; }) .join('&') ); -}; +} function paginationToQueryParams(pagination: PaginationRequest) { const params: Record = { @@ -81,4 +79,4 @@ function paginationToQueryParams(pagination: PaginationRequest) { } return queryParams(params); -}; +} diff --git a/Client/watchout/utils/types.ts b/Client/watchout/utils/types.ts index 4e3dd99..15aea61 100644 --- a/Client/watchout/utils/types.ts +++ b/Client/watchout/utils/types.ts @@ -1,4 +1,3 @@ -import { ActionAvailability } from 'utils/types'; export type Coordinates = { latitude: number; longitude: number; @@ -31,7 +30,7 @@ export type Event = { author: { id: number; reputation: number; - } + }; active: boolean; rating: number; ratingForCurrentUser: number; @@ -205,11 +204,11 @@ export type ActionAvailabilityRequest = { long: number; eventLat: number; eventLong: number; -} +}; export type ActionAvailabilityResponse = { canPost: boolean; reason?: PostUnabilityReason; }; -export type PostUnabilityReason = 'DISTANCE_RESTRICTION' | 'REPUTATION_RESTRICTION'; \ No newline at end of file +export type PostUnabilityReason = 'DISTANCE_RESTRICTION' | 'REPUTATION_RESTRICTION'; From 6951bb50098188bad72bd35098eb0429d8f340f9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Puchyr?= Date: Sun, 7 Dec 2025 21:40:32 +0100 Subject: [PATCH 11/11] ZPI-173 refactor: rewrite fetching user location, add block messaging for comments --- Client/watchout/components/NavigationBar.tsx | 2 +- .../location/UserLocationContext.tsx} | 42 +++++++-- .../events/comments/AddCommentModal.tsx | 17 ++-- .../features/events/comments/CommentList.tsx | 92 +++++++++++-------- .../events/create/CreateEventBottomSheet.tsx | 24 ++--- .../events/create/useActionAvailability.ts | 38 +++++--- .../events/create/useEventCreateForm.ts | 1 - .../events/details/EventBottomSheet.tsx | 2 +- Client/watchout/features/map/Map.tsx | 2 +- Client/watchout/utils/dictionaries.ts | 9 +- 10 files changed, 143 insertions(+), 86 deletions(-) rename Client/watchout/{features/map/useLocation.ts => components/location/UserLocationContext.tsx} (60%) diff --git a/Client/watchout/components/NavigationBar.tsx b/Client/watchout/components/NavigationBar.tsx index 6eccd1b..a928eb4 100644 --- a/Client/watchout/components/NavigationBar.tsx +++ b/Client/watchout/components/NavigationBar.tsx @@ -1,6 +1,6 @@ import { Appbar } from "react-native-paper"; import { Text } from "components/Base/Text"; -import { Button, getHeaderTitle } from '@react-navigation/elements'; +import { getHeaderTitle } from '@react-navigation/elements'; export const NavigationBar = ({ route, options }: any) => { const title = getHeaderTitle(options, route.name); diff --git a/Client/watchout/features/map/useLocation.ts b/Client/watchout/components/location/UserLocationContext.tsx similarity index 60% rename from Client/watchout/features/map/useLocation.ts rename to Client/watchout/components/location/UserLocationContext.tsx index 49f7da8..21faa3b 100644 --- a/Client/watchout/features/map/useLocation.ts +++ b/Client/watchout/components/location/UserLocationContext.tsx @@ -1,6 +1,7 @@ -import { useState, useEffect } from "react"; -import { PermissionsAndroid, Platform } from "react-native"; +import { useState, useEffect, createContext, ReactNode, useContext } from 'react'; +import { PermissionsAndroid, Platform } from 'react-native'; import Geolocation from '@react-native-community/geolocation'; +import { Coordinates } from 'utils/types'; const isUserLocationGranted = async () => { try { @@ -12,7 +13,7 @@ const isUserLocationGranted = async () => { buttonNeutral: 'Ask Me Later', buttonNegative: 'Cancel', buttonPositive: 'OK', - }, + } ); return granted === PermissionsAndroid.RESULTS.GRANTED; } catch (error) { @@ -21,17 +22,38 @@ const isUserLocationGranted = async () => { } }; +type UserLocationContextType = { + location: Coordinates | null; + isLoading: boolean; + hasPermission: boolean; + error: string | null; +}; + +export const UserLocationContext = createContext({ + location: null, + isLoading: false, + hasPermission: false, + error: null, +}); + export const useUserLocation = () => { + const context = useContext(UserLocationContext); + return context; +} + +export const UserLocationProvider = ({ children }: { children: ReactNode }) => { const [hasPermission, setHasPermission] = useState(false); const [location, setLocation] = useState<{ latitude: number; longitude: number } | null>(null); const [error, setError] = useState(null); - const [loading, setLoading] = useState(true); + const [isLoading, setIsLoading] = useState(true); useEffect(() => { const checkAndRequest = async () => { let permissionGranted = false; if (Platform.OS === 'android') { - const checkResult = await PermissionsAndroid.check(PermissionsAndroid.PERMISSIONS.ACCESS_FINE_LOCATION); + const checkResult = await PermissionsAndroid.check( + PermissionsAndroid.PERMISSIONS.ACCESS_FINE_LOCATION + ); if (checkResult) { permissionGranted = true; } else { @@ -52,10 +74,14 @@ export const useUserLocation = () => { } ); } - setLoading(false); + setIsLoading(false); }; checkAndRequest(); }, []); - return { hasPermission, location, error, loading }; -}; \ No newline at end of file + return ( + + {children} + + ); +}; diff --git a/Client/watchout/features/events/comments/AddCommentModal.tsx b/Client/watchout/features/events/comments/AddCommentModal.tsx index 217f286..7c7cbeb 100644 --- a/Client/watchout/features/events/comments/AddCommentModal.tsx +++ b/Client/watchout/features/events/comments/AddCommentModal.tsx @@ -1,12 +1,13 @@ -import { ActivityIndicator, Button, Icon } from 'react-native-paper'; +import { Button, Icon } from 'react-native-paper'; import { useState } from 'react'; import { View } from 'react-native'; import { useCreateComment } from 'features/events/comments/useCreateComment'; import { CustomModal } from 'components/Base/CustomModal'; import { CustomTextInput } from 'components/Base/CustomTextInput'; -import { useActionAvailability } from 'features/events/create/useActionAvailability'; import { Text } from 'components/Base/Text'; import { theme } from 'utils/theme'; +import { ActionAvailabilityResponse } from 'utils/types'; +import { commentReasonDictionary } from 'utils/dictionaries'; type AddCommentModalProps = { eventId: number; @@ -14,6 +15,7 @@ type AddCommentModalProps = { onClose: () => void; onSubmit: (comment: string) => void; onError: (error: unknown) => void; + availability: ActionAvailabilityResponse; }; export const AddCommentModal = ({ @@ -22,10 +24,10 @@ export const AddCommentModal = ({ onClose, onSubmit, onError, + availability, }: AddCommentModalProps) => { const [comment, setComment] = useState(''); const { mutate, isPending } = useCreateComment(); - const { data: availability, isPending: isAvailabilityPending } = useActionAvailability(); const handleSubmit = () => { mutate( @@ -45,18 +47,13 @@ export const AddCommentModal = ({ return ( <> - {isAvailabilityPending ? ( - - - - ) : !availability?.canPost ? ( + {!availability.canPost ? ( - Z powodu niskiej reputacji nie możesz obecnie dodawać komentarzy. Spróbuj ponownie - później. + {availability.reason != null && commentReasonDictionary[availability.reason]} + {comments?.empty ? ( @@ -105,7 +96,8 @@ export const CommentList = ({ eventId }: CommentListProps) => { ))} {comments.hasNextPage && ( - comments.fetchNextPage()} disabled={comments.isFetchingNextPage} style={{ paddingVertical: 16, alignItems: 'center' }}> @@ -114,7 +106,7 @@ export const CommentList = ({ eventId }: CommentListProps) => { ? 'Ładowanie...' : `Wyświetl więcej komentarzy (${(comments?.totalElements ?? 0) - currentCommentCount})`} - + )} {(!comments.hasNextPage || comments.totalElements === 0) && currentCommentCount > 0 && ( @@ -124,6 +116,34 @@ export const CommentList = ({ eventId }: CommentListProps) => { )} )} + + {availability != null && ( + setIsCommentModalOpen(false)} + onSubmit={handleCommentSubmit} + onError={handleCommentSubmitError} + availability={availability} + /> + )} + setCommentToDelete(null)} + content={ + + Czy na pewno chcesz usunąć ten komentarz? + + {comments?.content.find((c) => c.id === commentToDelete)?.content} + + + } + /> ); }; diff --git a/Client/watchout/features/events/create/CreateEventBottomSheet.tsx b/Client/watchout/features/events/create/CreateEventBottomSheet.tsx index 9ef9f4b..d570655 100644 --- a/Client/watchout/features/events/create/CreateEventBottomSheet.tsx +++ b/Client/watchout/features/events/create/CreateEventBottomSheet.tsx @@ -13,8 +13,8 @@ import { useActionAvailability } from './useActionAvailability'; import { Row } from 'components/Base/Row'; import { theme } from 'utils/theme'; import { Controller } from 'react-hook-form'; -import { useUserLocation } from 'features/map/useLocation'; -import { unavailabilityDictionary } from 'utils/dictionaries'; +import { eventReasonDictionary } from 'utils/dictionaries'; +import { useUserLocation } from 'components/location/UserLocationContext'; type CreateEventProps = { location: Coordinates; @@ -23,7 +23,7 @@ type CreateEventProps = { }; export const CreateEventBottomSheet = ({ location, onSuccess, onClose }: CreateEventProps) => { - const { location: userLocation, loading: isUserLocationLoading } = useUserLocation(); + const { location: userLocation, isLoading: isUserLocationLoading } = useUserLocation(); const isUserLocationFetched = !isUserLocationLoading && userLocation != null; const { @@ -38,17 +38,11 @@ export const CreateEventBottomSheet = ({ location, onSuccess, onClose }: CreateE isLoading, } = useEventCreateForm(location, userLocation, onSuccess); - - const { data: actionAvailability, isLoading: isActionAvailabilityLoading } = - useActionAvailability( - { - lat: userLocation?.latitude ?? 0, - long: userLocation?.longitude ?? 0, - eventLat: location.latitude, - eventLong: location.longitude, - }, - { isEnabled: isUserLocationFetched } - ); + const { availability: actionAvailability, isLoading: isActionAvailabilityLoading } = + useActionAvailability({ + eventLat: location.latitude, + eventLong: location.longitude, + }); useEffect(() => { eventBottomSheetRef.current?.present(); @@ -160,7 +154,7 @@ export const CreateEventBottomSheet = ({ location, onSuccess, onClose }: CreateE const ActionNotAvailableMessage = ({ reason }: { reason?: PostUnabilityReason }) => { const errorDescription = reason - ? unavailabilityDictionary[reason] + ? eventReasonDictionary[reason] : 'Chwilowo nie jest możliwe zgłoszenie zdarzenia. Spróbuj ponownie później.'; return ( diff --git a/Client/watchout/features/events/create/useActionAvailability.ts b/Client/watchout/features/events/create/useActionAvailability.ts index 848a677..ab0db85 100644 --- a/Client/watchout/features/events/create/useActionAvailability.ts +++ b/Client/watchout/features/events/create/useActionAvailability.ts @@ -1,17 +1,31 @@ import { useQuery } from '@tanstack/react-query'; +import { useUserLocation } from 'components/location/UserLocationContext'; import { apiClient } from 'utils/apiClient'; import { API_ENDPOINTS } from 'utils/apiDefinition'; -import { ActionAvailabilityRequest, ActionAvailabilityResponse } from 'utils/types'; +import { ActionAvailabilityResponse, Coordinates } from 'utils/types'; -export const useActionAvailability = ( - request: ActionAvailabilityRequest, - { isEnabled }: { isEnabled: boolean } -) => - useQuery({ - queryKey: ['actionAvailability', request], - queryFn: () => - apiClient - .get(API_ENDPOINTS.events.availability(request)) - .then((res) => res.data), - enabled: isEnabled, +export const useActionAvailability = ({ eventLat, eventLong }: { eventLat: number; eventLong: number }) => { + const { location: userLocation, isLoading: isUserLocationLoading } = useUserLocation(); + + const { data: availability, isLoading: isAvailabilityLoading } = useQuery({ + queryKey: ['actionAvailability', { eventLat, eventLong }, userLocation], + queryFn: () => { + if (userLocation == null) { + throw new Error('User location is required to check action availability'); + } + return apiClient + .get( + API_ENDPOINTS.events.availability({ + eventLat: eventLat, + eventLong: eventLong, + lat: userLocation.latitude, + long: userLocation.longitude, + }) + ) + .then((res) => res.data); + }, + enabled: !isUserLocationLoading && userLocation != null, }); + + return { availability, isLoading: isAvailabilityLoading || isUserLocationLoading }; +}; diff --git a/Client/watchout/features/events/create/useEventCreateForm.ts b/Client/watchout/features/events/create/useEventCreateForm.ts index 61da5c5..7502730 100644 --- a/Client/watchout/features/events/create/useEventCreateForm.ts +++ b/Client/watchout/features/events/create/useEventCreateForm.ts @@ -5,7 +5,6 @@ import { Alert } from 'react-native'; import { Coordinates, CreateEventRequest } from 'utils/types'; import { useCreateEvent } from './useEventCreate'; import { BottomSheetModal } from '@gorhom/bottom-sheet'; -import { useUserLocation } from 'features/map/useLocation'; import { useForm } from 'react-hook-form'; type FormData = { diff --git a/Client/watchout/features/events/details/EventBottomSheet.tsx b/Client/watchout/features/events/details/EventBottomSheet.tsx index 9c959df..f186e73 100644 --- a/Client/watchout/features/events/details/EventBottomSheet.tsx +++ b/Client/watchout/features/events/details/EventBottomSheet.tsx @@ -60,7 +60,7 @@ export const EventBottomSheet = ({ event, onClose }: EventBottomSheetProps) => { )} - + diff --git a/Client/watchout/features/map/Map.tsx b/Client/watchout/features/map/Map.tsx index 12d329c..20717bf 100644 --- a/Client/watchout/features/map/Map.tsx +++ b/Client/watchout/features/map/Map.tsx @@ -1,6 +1,6 @@ import { Dimensions, StyleSheet, View } from 'react-native'; import MapView, { LongPressEvent, Marker, PROVIDER_GOOGLE } from 'react-native-maps'; -import { useUserLocation } from 'features/map/useLocation'; +import { useUserLocation } from 'components/location/UserLocationContext'; import { Coordinates, Event, EventFilters } from 'utils/types'; import { Icon } from 'react-native-paper'; import { useRef, useState, useCallback, useEffect } from 'react'; diff --git a/Client/watchout/utils/dictionaries.ts b/Client/watchout/utils/dictionaries.ts index a2dface..fd616c2 100644 --- a/Client/watchout/utils/dictionaries.ts +++ b/Client/watchout/utils/dictionaries.ts @@ -49,9 +49,16 @@ export const firebaseAuthErrorMessages: { [code: string]: string } = { default: 'Wystąpił nieznany błąd autoryzacji. Spróbuj ponownie lub skontaktuj się ze wsparciem.', }; -export const unavailabilityDictionary: { [reason in PostUnabilityReason]: string } = { +export const eventReasonDictionary: { [reason in PostUnabilityReason]: string } = { DISTANCE_RESTRICTION: 'Nie możesz zgłosić zdarzenia ze swojej obecnej lokalizacji, ponieważ jest ona zbyt daleko od miejsca zdarzenia.', REPUTATION_RESTRICTION: 'Nie możesz zgłosić nowego zdarzenia, ponieważ twoja reputacja jest zbyt niska. Spróbuj ponownie później.', }; + +export const commentReasonDictionary: { [reason in PostUnabilityReason]: string } = { + DISTANCE_RESTRICTION: + 'Nie możesz dodać komentarza ze swojej obecnej lokalizacji, ponieważ jest ona zbyt daleko od miejsca zdarzenia.', + REPUTATION_RESTRICTION: + 'Nie możesz dodać komentarza, ponieważ twoja reputacja jest zbyt niska. Spróbuj ponownie później.', +};