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?.empty ? ( @@ -93,8 +84,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 && ( @@ -106,7 +96,8 @@ export const CommentList = ({ eventId }: CommentListProps) => { ))} {comments.hasNextPage && ( - comments.fetchNextPage()} disabled={comments.isFetchingNextPage} style={{ paddingVertical: 16, alignItems: 'center' }}> @@ -115,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 && ( @@ -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';