feat: people around radar
This commit is contained in:
parent
236121a73c
commit
becb61967c
8 changed files with 343 additions and 6 deletions
|
@ -279,4 +279,4 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"packageManager": "yarn@4.5.3"
|
"packageManager": "yarn@4.5.3"
|
||||||
}
|
}
|
||||||
|
|
27
src/hooks/useRadarData.js
Normal file
27
src/hooks/useRadarData.js
Normal 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
|
||||||
|
};
|
||||||
|
}
|
|
@ -6,7 +6,7 @@ import { createStyles } from "~/theme";
|
||||||
import Text from "~/components/Text";
|
import Text from "~/components/Text";
|
||||||
import { MaterialIcons } from "@expo/vector-icons";
|
import { MaterialIcons } from "@expo/vector-icons";
|
||||||
|
|
||||||
export default function NotificationsButton() {
|
export default function NotificationsButton({ flex = 1 }) {
|
||||||
const navigation = useNavigation();
|
const navigation = useNavigation();
|
||||||
const { hasRegisteredRelatives } = useParamsState(["hasRegisteredRelatives"]);
|
const { hasRegisteredRelatives } = useParamsState(["hasRegisteredRelatives"]);
|
||||||
const { newCount } = useNotificationsState(["newCount"]);
|
const { newCount } = useNotificationsState(["newCount"]);
|
||||||
|
@ -50,9 +50,11 @@ export default function NotificationsButton() {
|
||||||
const styles = useStyles();
|
const styles = useStyles();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View>
|
<View style={{ flex }}>
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
style={styles.button}
|
style={styles.button}
|
||||||
|
accessibilityLabel={hasNewNotifications ? `Notifications - ${newCount} nouvelles notifications` : "Notifications"}
|
||||||
|
accessibilityRole="button"
|
||||||
onPress={() => navigation.navigate("Notifications")}
|
onPress={() => navigation.navigate("Notifications")}
|
||||||
>
|
>
|
||||||
<MaterialIcons
|
<MaterialIcons
|
||||||
|
@ -86,12 +88,13 @@ const useStyles = createStyles(({ theme: { colors } }) => ({
|
||||||
button: {
|
button: {
|
||||||
flexDirection: "row",
|
flexDirection: "row",
|
||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
alignSelf: "flex-end",
|
alignSelf: "stretch",
|
||||||
marginTop: 10,
|
marginTop: 10,
|
||||||
marginBottom: 10,
|
marginBottom: 10,
|
||||||
backgroundColor: colors.surface,
|
backgroundColor: colors.surface,
|
||||||
borderRadius: 8,
|
borderRadius: 8,
|
||||||
width: "100%",
|
flex: 1,
|
||||||
|
minHeight: 48, // Consistent with RadarButton for accessibility
|
||||||
paddingVertical: 12,
|
paddingVertical: 12,
|
||||||
paddingHorizontal: 16,
|
paddingHorizontal: 16,
|
||||||
shadowColor: colors.text,
|
shadowColor: colors.text,
|
||||||
|
|
57
src/scenes/SendAlert/RadarButton.js
Normal file
57
src/scenes/SendAlert/RadarButton.js
Normal 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,
|
||||||
|
},
|
||||||
|
}));
|
176
src/scenes/SendAlert/RadarModal.js
Normal file
176
src/scenes/SendAlert/RadarModal.js
Normal 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",
|
||||||
|
},
|
||||||
|
}));
|
21
src/scenes/SendAlert/TopButtonsBar.js
Normal file
21
src/scenes/SendAlert/TopButtonsBar.js
Normal 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
|
||||||
|
},
|
||||||
|
}));
|
9
src/scenes/SendAlert/gql.js
Normal file
9
src/scenes/SendAlert/gql.js
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
import { gql } from "@apollo/client";
|
||||||
|
|
||||||
|
export const RADAR_PEOPLE_COUNT_QUERY = gql`
|
||||||
|
query radarPeopleCount {
|
||||||
|
getOneRadarPeopleCount {
|
||||||
|
count
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
|
@ -13,6 +13,10 @@ import HelpBlock from "./HelpBlock";
|
||||||
import RegisterRelativesButton from "./RegisterRelativesButton";
|
import RegisterRelativesButton from "./RegisterRelativesButton";
|
||||||
import NotificationsButton from "./NotificationsButton";
|
import NotificationsButton from "./NotificationsButton";
|
||||||
import ContributeButton from "./ContributeButton";
|
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() {
|
export default function SendAlert() {
|
||||||
const navigation = useNavigation();
|
const navigation = useNavigation();
|
||||||
|
@ -20,10 +24,35 @@ export default function SendAlert() {
|
||||||
const styles = useStyles();
|
const styles = useStyles();
|
||||||
|
|
||||||
const [helpVisible, setHelpVisible] = useState(false);
|
const [helpVisible, setHelpVisible] = useState(false);
|
||||||
|
const [radarModalVisible, setRadarModalVisible] = useState(false);
|
||||||
|
|
||||||
|
const {
|
||||||
|
data: radarData,
|
||||||
|
isLoading: radarIsLoading,
|
||||||
|
error: radarError,
|
||||||
|
fetchRadarData,
|
||||||
|
reset: resetRadarData,
|
||||||
|
hasLocation,
|
||||||
|
} = useRadarData();
|
||||||
|
|
||||||
function toggleHelp() {
|
function toggleHelp() {
|
||||||
setHelpVisible(!helpVisible);
|
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(
|
const navigateTo = useCallback(
|
||||||
(navOpts) =>
|
(navOpts) =>
|
||||||
navigation.dispatch({
|
navigation.dispatch({
|
||||||
|
@ -70,7 +99,14 @@ export default function SendAlert() {
|
||||||
return (
|
return (
|
||||||
<ScrollView>
|
<ScrollView>
|
||||||
<View style={styles.container}>
|
<View style={styles.container}>
|
||||||
<NotificationsButton />
|
<TopButtonsBar>
|
||||||
|
<NotificationsButton flex={0.78} />
|
||||||
|
<RadarButton
|
||||||
|
onPress={handleRadarPress}
|
||||||
|
isLoading={radarIsLoading}
|
||||||
|
flex={0.22}
|
||||||
|
/>
|
||||||
|
</TopButtonsBar>
|
||||||
|
|
||||||
<View style={styles.head}>
|
<View style={styles.head}>
|
||||||
<Title style={styles.title}>Quelle est votre situation ?</Title>
|
<Title style={styles.title}>Quelle est votre situation ?</Title>
|
||||||
|
@ -218,6 +254,14 @@ export default function SendAlert() {
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
<ContributeButton />
|
<ContributeButton />
|
||||||
|
|
||||||
|
<RadarModal
|
||||||
|
visible={radarModalVisible}
|
||||||
|
onDismiss={handleRadarModalClose}
|
||||||
|
peopleCount={radarData?.count}
|
||||||
|
isLoading={radarIsLoading}
|
||||||
|
error={radarError}
|
||||||
|
/>
|
||||||
</View>
|
</View>
|
||||||
</ScrollView>
|
</ScrollView>
|
||||||
);
|
);
|
||||||
|
|
Loading…
Add table
Reference in a new issue