diff --git a/Client/watchout/App.tsx b/Client/watchout/App.tsx
index cc06b17..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,11 @@ 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)
.configure({ name: 'WatchOut' })
@@ -54,6 +53,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 +63,7 @@ export default function App() {
};
if (!loaded) {
- return (
- <>
-
-
-
- Ładowanie...
-
-
-
- >
- );
+ return ;
}
return (
diff --git a/Client/watchout/components/Base/CustomChip.tsx b/Client/watchout/components/Base/CustomChip.tsx
new file mode 100644
index 0000000..a8338d1
--- /dev/null
+++ b/Client/watchout/components/Base/CustomChip.tsx
@@ -0,0 +1,30 @@
+import { CustomSurface } from 'components/Layout/CustomSurface';
+import { ViewStyle } from 'react-native';
+import { Icon } from 'react-native-paper';
+
+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/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..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',
@@ -55,16 +57,18 @@ export const AppNavigator = () => {
const { user, loading } = useAuth();
const { dismissAll } = useBottomSheetModal();
- if (loading) return null;
+ if (loading) {
+ return ;
+ }
+ const isUserLoggedIn = user != null && user.isEmailVerified;
- if (!user) {
+ if (!isUserLoggedIn) {
return (
- }>
-
-
-
+
+
+
+
);
}
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 78d4fc1..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,16 +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 [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 {
@@ -51,9 +74,14 @@ export const useUserLocation = () => {
}
);
}
+ setIsLoading(false);
};
checkAndRequest();
}, []);
- return { hasPermission, location, error };
-};
\ No newline at end of file
+ return (
+
+ {children}
+
+ );
+};
diff --git a/Client/watchout/features/auth/LoginScreen.tsx b/Client/watchout/features/auth/LoginScreen.tsx
index e6f46ae..01da8ee 100644
--- a/Client/watchout/features/auth/LoginScreen.tsx
+++ b/Client/watchout/features/auth/LoginScreen.tsx
@@ -7,9 +7,11 @@ 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 } from 'utils/AuthError';
+import { firebaseAuthErrorMessages } from 'utils/dictionaries';
export const LoginScreen = () => {
- const navigation = useNavigation();
+ const { navigate } = useNavigation();
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
@@ -19,8 +21,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 +45,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 +106,7 @@ export const LoginScreen = () => {
Zaloguj się
- navigation.navigate('SignUp' as never)}>
+ navigate('SignUp' as never)}>
Nie masz konta? Zarejestruj się
@@ -110,7 +129,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..183ad0e 100644
--- a/Client/watchout/features/auth/SignUpScreen.tsx
+++ b/Client/watchout/features/auth/SignUpScreen.tsx
@@ -7,9 +7,11 @@ 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 } from 'utils/AuthError';
+import { firebaseAuthErrorMessages } from 'utils/dictionaries';
export const SignUpScreen = () => {
- const navigation = useNavigation();
+ const { navigate } = useNavigation();
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const [displayName, setDisplayName] = useState('');
@@ -17,10 +19,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 +81,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..fdb6512 100644
--- a/Client/watchout/features/auth/auth.ts
+++ b/Client/watchout/features/auth/auth.ts
@@ -1,16 +1,19 @@
-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,
+ 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 +29,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 +45,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 +61,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 +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/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/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.hasNextPage || comments.totalElements === 0) && currentCommentCount > 0 && (
@@ -125,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 36c57a9..d570655 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';
@@ -12,6 +12,9 @@ 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';
+import { eventReasonDictionary } from 'utils/dictionaries';
+import { useUserLocation } from 'components/location/UserLocationContext';
type CreateEventProps = {
location: Coordinates;
@@ -20,21 +23,26 @@ type CreateEventProps = {
};
export const CreateEventBottomSheet = ({ location, onSuccess, onClose }: CreateEventProps) => {
+ const { location: userLocation, isLoading: isUserLocationLoading } = useUserLocation();
+ const isUserLocationFetched = !isUserLocationLoading && userLocation != null;
+
const {
- formData,
+ control,
handleSubmit,
handleSetSelectedEventType,
eventTypeModalVisible,
setEventTypeModalVisible,
selectedEventType,
- updateField,
geocodedAddress,
eventBottomSheetRef,
isLoading,
- } = useEventCreateForm(location, onSuccess);
+ } = useEventCreateForm(location, userLocation, onSuccess);
- const { data: actionAvailability, isLoading: isActionAvailabilityLoading } =
- useActionAvailability();
+ const { availability: actionAvailability, isLoading: isActionAvailabilityLoading } =
+ useActionAvailability({
+ eventLat: location.latitude,
+ eventLong: location.longitude,
+ });
useEffect(() => {
eventBottomSheetRef.current?.present();
@@ -44,7 +52,21 @@ export const CreateEventBottomSheet = ({ location, onSuccess, onClose }: CreateE
- {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 ? (
@@ -53,35 +75,55 @@ export const CreateEventBottomSheet = ({ location, onSuccess, onClose }: CreateE
- updateField('name', value)}
+ (
+
+ )}
/>
- updateField('description', value)}
- multiline
- numberOfLines={3}
+ (
+
+ )}
+ />
+ (
+ setEventTypeModalVisible(true)}>
+
+
+ )}
/>
-
- setEventTypeModalVisible(true)}>
-
-
- updateField('images', images)}
+ (
+
+ )}
/>
) : (
-
+
)}
@@ -110,18 +152,20 @@ export const CreateEventBottomSheet = ({ location, onSuccess, onClose }: CreateE
);
};
-const ActionNotAvailableMessage = () => {
+const ActionNotAvailableMessage = ({ reason }: { reason?: PostUnabilityReason }) => {
+ const errorDescription = reason
+ ? eventReasonDictionary[reason]
+ : 'Chwilowo nie jest możliwe zgłoszenie zdarzenia. Spróbuj ponownie później.';
+
return (
-
+
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..ab0db85 100644
--- a/Client/watchout/features/events/create/useActionAvailability.ts
+++ b/Client/watchout/features/events/create/useActionAvailability.ts
@@ -1,11 +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 { ActionAvailability } from 'utils/types';
+import { ActionAvailabilityResponse, Coordinates } from 'utils/types';
-export const useActionAvailability = () =>
- useQuery({
- queryKey: ['actionAvailability'],
- queryFn: () =>
- apiClient.get(API_ENDPOINTS.events.availability).then((res) => res.data),
+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 9ff9cda..7502730 100644
--- a/Client/watchout/features/events/create/useEventCreateForm.ts
+++ b/Client/watchout/features/events/create/useEventCreateForm.ts
@@ -5,7 +5,7 @@ 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 = {
name: string;
@@ -20,17 +20,23 @@ 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);
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 +47,60 @@ 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: [] });
- }, []);
-
- const userLocation = useUserLocation();
-
- const handleSubmit = useCallback(() => {
- if (!validateForm()) {
+ 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;
+ 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.');
+ if (!userLocation) {
+ Alert.alert('Błąd', 'Nie udało się pobrać Twojej lokalizacji. Spróbuj ponownie.');
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: userLocation.latitude,
+ userLongitude: userLocation.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/features/events/details/EventBottomSheet.tsx b/Client/watchout/features/events/details/EventBottomSheet.tsx
index 8f80a1b..f186e73 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,10 +51,16 @@ export const EventBottomSheet = ({ event, onClose }: EventBottomSheetProps) => {
-
+ {event.isAuthor ? (
+
+ Jako autor zdarzenia nie możesz ocenić własnego zgłoszenia.
+
+ ) : (
+
+ )}
-
+
diff --git a/Client/watchout/features/events/details/EventDetails.tsx b/Client/watchout/features/events/details/EventDetails.tsx
index 456da95..eaa4a95 100644
--- a/Client/watchout/features/events/details/EventDetails.tsx
+++ b/Client/watchout/features/events/details/EventDetails.tsx
@@ -1,11 +1,11 @@
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 { ReportStatusIcons } from './ReportStatusIcons';
type EventDetailsProps = {
event: Event;
@@ -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);
@@ -41,6 +41,9 @@ export const EventDetails = ({ event }: EventDetailsProps) => {
Zgłoszono: {reportedDateText}
+
+
+
{event.description}
{event.images.length > 0 && (
@@ -86,6 +89,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..7de884d
--- /dev/null
+++ b/Client/watchout/features/events/details/ReportStatusIcons.tsx
@@ -0,0 +1,60 @@
+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);
+
+ 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/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/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/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..c70f7d9
--- /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/features/settings/Settings.tsx b/Client/watchout/features/settings/Settings.tsx
index 3a227f3..dfce742 100644
--- a/Client/watchout/features/settings/Settings.tsx
+++ b/Client/watchout/features/settings/Settings.tsx
@@ -3,18 +3,15 @@ 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 './settingsRoutes';
-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..e08c916 100644
--- a/Client/watchout/features/settings/SettingsNavigator.tsx
+++ b/Client/watchout/features/settings/SettingsNavigator.tsx
@@ -1,41 +1,19 @@
-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 { settingsRoutes } from './settingsRoutes';
const Stack = createStackNavigator();
-export const settingsRoutes = [
- {
- icon: 'account-circle',
- label: 'Konto',
- component: () => Konto,
- link: 'AccountSettings',
- },
- {
- icon: 'bell',
- label: 'Powiadomienia',
- component: () => ,
- link: 'NotificationSettings',
- },
- {
- icon: 'lock',
- label: 'Prywatność',
- component: () => Prywatność,
- link: 'PrivacySettings',
- },
-];
-
export const SettingsNavigator = () => {
return (
+ ...navigationTheme,
+ }}>
}
+ component={Settings}
options={{ headerShown: false }}
/>
{settingsRoutes.map((option) => (
diff --git a/Client/watchout/features/settings/settingsRoutes.ts b/Client/watchout/features/settings/settingsRoutes.ts
new file mode 100644
index 0000000..8971225
--- /dev/null
+++ b/Client/watchout/features/settings/settingsRoutes.ts
@@ -0,0 +1,17 @@
+import { AccountSettings } from "./AccountSettings";
+import { NotificationSettings } from "./notifications/NotificationSettings";
+
+export const settingsRoutes = [
+ {
+ icon: 'account-circle',
+ label: 'Konto',
+ component: AccountSettings,
+ link: 'AccountSettings',
+ },
+ {
+ icon: 'bell',
+ label: 'Powiadomienia',
+ component: NotificationSettings,
+ link: 'NotificationSettings',
+ },
+];
\ No newline at end of file
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",
diff --git a/Client/watchout/utils/AuthError.ts b/Client/watchout/utils/AuthError.ts
new file mode 100644
index 0000000..352c789
--- /dev/null
+++ b/Client/watchout/utils/AuthError.ts
@@ -0,0 +1,6 @@
+export class AuthError extends Error {
+ constructor(message: string) {
+ super(message);
+ this.name = 'AuthError';
+ }
+}
\ No newline at end of file
diff --git a/Client/watchout/utils/apiDefinition.ts b/Client/watchout/utils/apiDefinition.ts
index 8e11e52..46b8104 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: {
@@ -42,6 +42,9 @@ export const API_ENDPOINTS = {
add: 'fcm-tokens',
get: 'fcm-tokens',
},
+ users: {
+ create: 'users/create',
+ },
};
// utility functions
@@ -54,18 +57,16 @@ function queryParams(params: Record): 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 = {
@@ -78,4 +79,4 @@ function paginationToQueryParams(pagination: PaginationRequest) {
}
return queryParams(params);
-};
+}
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/dictionaries.ts b/Client/watchout/utils/dictionaries.ts
new file mode 100644
index 0000000..fd616c2
--- /dev/null
+++ b/Client/watchout/utils/dictionaries.ts
@@ -0,0 +1,64 @@
+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 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.',
+};
diff --git a/Client/watchout/utils/types.ts b/Client/watchout/utils/types.ts
index e1fd18f..15aea61 100644
--- a/Client/watchout/utils/types.ts
+++ b/Client/watchout/utils/types.ts
@@ -24,12 +24,17 @@ export type Event = {
images: string[];
latitude: number;
longitude: number;
- reportedDate: string;
- endDate: string;
+ reportedDate: Date;
+ endDate: Date;
eventType: EventType;
+ author: {
+ id: number;
+ reputation: number;
+ };
active: boolean;
rating: number;
ratingForCurrentUser: number;
+ isAuthor: boolean;
};
export type Outage = {
@@ -109,8 +114,8 @@ export type AddLocationRequest = {
weather: boolean;
eventTypes?: number[];
};
+ notificationsEnable: boolean;
};
- notificationsEnable: boolean;
};
export type UpdateLocationRequest = AddLocationRequest;
@@ -194,7 +199,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';