feat: people around radar

This commit is contained in:
Jo 2025-09-16 22:07:31 +02:00
parent 236121a73c
commit becb61967c
Signed by: devthejo
GPG key ID: 00CCA7A92B1D5351
8 changed files with 343 additions and 6 deletions

27
src/hooks/useRadarData.js Normal file
View file

@ -0,0 +1,27 @@
import { useLazyQuery } from "@apollo/client";
import { useCallback } from "react";
import { RADAR_PEOPLE_COUNT_QUERY } from "~/scenes/SendAlert/gql";
export default function useRadarData() {
const [fetchRadarData, { data, loading: isLoading, error }] = useLazyQuery(
RADAR_PEOPLE_COUNT_QUERY,
{
fetchPolicy: "network-only", // Always fetch fresh data
errorPolicy: "all",
},
);
const reset = useCallback(() => {
// Reset is handled by not calling the query again
// Apollo will manage the state internally
}, []);
return {
data: data?.getOneRadarPeopleCount,
isLoading,
error,
fetchRadarData,
reset,
hasLocation: true, // Location is now handled server-side via authentication
};
}

View file

@ -6,7 +6,7 @@ import { createStyles } from "~/theme";
import Text from "~/components/Text";
import { MaterialIcons } from "@expo/vector-icons";
export default function NotificationsButton() {
export default function NotificationsButton({ flex = 1 }) {
const navigation = useNavigation();
const { hasRegisteredRelatives } = useParamsState(["hasRegisteredRelatives"]);
const { newCount } = useNotificationsState(["newCount"]);
@ -50,9 +50,11 @@ export default function NotificationsButton() {
const styles = useStyles();
return (
<View>
<View style={{ flex }}>
<TouchableOpacity
style={styles.button}
accessibilityLabel={hasNewNotifications ? `Notifications - ${newCount} nouvelles notifications` : "Notifications"}
accessibilityRole="button"
onPress={() => navigation.navigate("Notifications")}
>
<MaterialIcons
@ -86,12 +88,13 @@ const useStyles = createStyles(({ theme: { colors } }) => ({
button: {
flexDirection: "row",
alignItems: "center",
alignSelf: "flex-end",
alignSelf: "stretch",
marginTop: 10,
marginBottom: 10,
backgroundColor: colors.surface,
borderRadius: 8,
width: "100%",
flex: 1,
minHeight: 48, // Consistent with RadarButton for accessibility
paddingVertical: 12,
paddingHorizontal: 16,
shadowColor: colors.text,

View file

@ -0,0 +1,57 @@
import React from "react";
import { View } from "react-native";
import { IconButton } from "react-native-paper";
import { MaterialCommunityIcons } from "@expo/vector-icons";
import { createStyles } from "~/theme";
export default function RadarButton({ onPress, isLoading = false, flex = 0.22 }) {
const styles = useStyles();
return (
<View style={[styles.container, { flex }]}>
<IconButton
accessibilityLabel="Radar - Voir les utilisateurs Alerte-Secours prêts à porter secours aux alentours"
mode="contained"
size={24}
style={styles.button}
onPress={onPress}
disabled={isLoading}
icon={({ size, color }) => (
<MaterialCommunityIcons
name="radar"
size={size}
color={color}
style={styles.icon}
/>
)}
/>
</View>
);
}
const useStyles = createStyles(({ wp, hp, theme: { colors, custom } }) => ({
container: {
alignItems: "center",
justifyContent: "center",
marginTop: 10,
marginBottom: 10,
flex: 1, // Stretch to fill available space
},
button: {
backgroundColor: colors.primary,
elevation: 4,
shadowColor: "#000",
shadowOffset: {
width: 0,
height: 2,
},
shadowOpacity: 0.25,
shadowRadius: 3.84,
minHeight: 48, // Match minimum touch target height
width: "100%",
maxWidth: 48, // Keep it square for radar button
},
icon: {
color: colors.onPrimary,
},
}));

View file

@ -0,0 +1,176 @@
import React from "react";
import { View, Text } from "react-native";
import { Modal, Portal, Button, ActivityIndicator } from "react-native-paper";
import { MaterialCommunityIcons } from "@expo/vector-icons";
import { createStyles, useTheme } from "~/theme";
export default function RadarModal({
visible,
onDismiss,
peopleCount = null,
isLoading = false,
error = null,
}) {
const { colors } = useTheme();
const styles = useStyles();
const renderContent = () => {
if (isLoading) {
return (
<View style={styles.loadingContainer}>
<ActivityIndicator size="large" color={colors.primary} />
<Text style={styles.loadingText}>
Recherche d'utilisateurs Alerte-Secours disponibles aux alentours...
</Text>
</View>
);
}
if (error) {
return (
<View style={styles.errorContainer}>
<MaterialCommunityIcons
name="alert-circle"
size={48}
color={colors.error}
/>
<Text style={styles.errorTitle}>Erreur</Text>
<Text style={styles.errorText}>
Impossible de récupérer les informations. Vérifiez votre connexion
et votre localisation.
</Text>
</View>
);
}
if (peopleCount !== null) {
return (
<View style={styles.successContainer}>
<Text style={styles.countText}>{peopleCount}</Text>
<Text style={styles.descriptionText}>
{peopleCount === 0
? "Aucun utilisateur d'Alerte-Secours disponible pour assistance dans un rayon de 25 km"
: peopleCount === 1
? "utilisateur d'Alerte-Secours prêt à porter secours dans un rayon de 25 km"
: "utilisateurs d'Alerte-Secours prêts à porter secours dans un rayon de 25 km"}
</Text>
</View>
);
}
return null;
};
return (
<Portal>
<Modal
visible={visible}
onDismiss={onDismiss}
contentContainerStyle={[
styles.modalContainer,
{ backgroundColor: colors.surface },
]}
>
<View style={styles.modalHeader}>
<MaterialCommunityIcons
name="radar"
size={32}
color={colors.primary}
style={styles.modalIcon}
/>
<Text style={styles.modalTitle}>Utilisateurs aux alentours</Text>
</View>
<View style={styles.content}>
{renderContent()}
<View style={styles.buttonContainer}>
<Button
mode="contained"
onPress={onDismiss}
style={styles.closeButton}
>
Fermer
</Button>
</View>
</View>
</Modal>
</Portal>
);
}
const useStyles = createStyles(({ wp, hp, scaleText, theme: { colors } }) => ({
modalContainer: {
margin: wp(5),
borderRadius: 8,
padding: wp(5),
elevation: 5,
},
content: {
alignItems: "center",
},
loadingContainer: {
alignItems: "center",
paddingVertical: hp(3),
},
loadingText: {
...scaleText({ fontSize: 16 }),
color: colors.onSurface,
marginTop: hp(2),
},
errorContainer: {
alignItems: "center",
paddingVertical: hp(2),
},
errorTitle: {
...scaleText({ fontSize: 18 }),
fontWeight: "bold",
color: colors.error,
marginTop: hp(1),
marginBottom: hp(1),
},
errorText: {
...scaleText({ fontSize: 14 }),
color: colors.onSurface,
textAlign: "center",
lineHeight: 20,
},
successContainer: {
alignItems: "center",
paddingVertical: hp(2),
},
countText: {
...scaleText({ fontSize: 36 }),
fontWeight: "bold",
color: colors.primary,
marginTop: hp(1),
},
descriptionText: {
...scaleText({ fontSize: 16 }),
color: colors.onSurface,
textAlign: "center",
marginTop: hp(1),
},
buttonContainer: {
marginTop: hp(3),
width: "100%",
},
closeButton: {
borderRadius: 8,
},
modalHeader: {
flexDirection: "row",
alignItems: "center",
justifyContent: "center",
marginBottom: hp(2),
},
modalIcon: {
marginRight: wp(2),
},
modalTitle: {
...scaleText({ fontSize: 20 }),
fontWeight: "bold",
color: colors.onSurface,
textAlign: "center",
},
}));

View file

@ -0,0 +1,21 @@
import React from "react";
import { View } from "react-native";
import { createStyles } from "~/theme";
export default function TopButtonsBar({ children }) {
const styles = useStyles();
return <View style={styles.container}>{children}</View>;
}
const useStyles = createStyles(({ wp, hp }) => ({
container: {
flexDirection: "row",
alignItems: "stretch", // Ensures both buttons have same height
justifyContent: "space-between",
marginTop: hp(1),
marginBottom: hp(1),
gap: wp(3), // Slightly more space between the buttons
minHeight: 48, // Minimum touch target height for accessibility
},
}));

View file

@ -0,0 +1,9 @@
import { gql } from "@apollo/client";
export const RADAR_PEOPLE_COUNT_QUERY = gql`
query radarPeopleCount {
getOneRadarPeopleCount {
count
}
}
`;

View file

@ -13,6 +13,10 @@ import HelpBlock from "./HelpBlock";
import RegisterRelativesButton from "./RegisterRelativesButton";
import NotificationsButton from "./NotificationsButton";
import ContributeButton from "./ContributeButton";
import RadarButton from "./RadarButton";
import RadarModal from "./RadarModal";
import TopButtonsBar from "./TopButtonsBar";
import useRadarData from "~/hooks/useRadarData";
export default function SendAlert() {
const navigation = useNavigation();
@ -20,10 +24,35 @@ export default function SendAlert() {
const styles = useStyles();
const [helpVisible, setHelpVisible] = useState(false);
const [radarModalVisible, setRadarModalVisible] = useState(false);
const {
data: radarData,
isLoading: radarIsLoading,
error: radarError,
fetchRadarData,
reset: resetRadarData,
hasLocation,
} = useRadarData();
function toggleHelp() {
setHelpVisible(!helpVisible);
}
const handleRadarPress = useCallback(() => {
if (!hasLocation) {
// Could show a location permission alert here
return;
}
setRadarModalVisible(true);
fetchRadarData();
}, [hasLocation, fetchRadarData]);
const handleRadarModalClose = useCallback(() => {
setRadarModalVisible(false);
resetRadarData();
}, [resetRadarData]);
const navigateTo = useCallback(
(navOpts) =>
navigation.dispatch({
@ -70,7 +99,14 @@ export default function SendAlert() {
return (
<ScrollView>
<View style={styles.container}>
<NotificationsButton />
<TopButtonsBar>
<NotificationsButton flex={0.78} />
<RadarButton
onPress={handleRadarPress}
isLoading={radarIsLoading}
flex={0.22}
/>
</TopButtonsBar>
<View style={styles.head}>
<Title style={styles.title}>Quelle est votre situation ?</Title>
@ -218,6 +254,14 @@ export default function SendAlert() {
</View>
<ContributeButton />
<RadarModal
visible={radarModalVisible}
onDismiss={handleRadarModalClose}
peopleCount={radarData?.count}
isLoading={radarIsLoading}
error={radarError}
/>
</View>
</ScrollView>
);