eslint for mobile + new maps API key + cleaning

This commit is contained in:
Sebastien Riviere
2026-02-15 19:23:29 +01:00
parent c1f1688794
commit 0768609ada
34 changed files with 765 additions and 761 deletions

View File

@@ -1,7 +1,8 @@
// React
import { forwardRef } from 'react';
import { TouchableHighlight, View, Text, StyleSheet } from "react-native";
export default CustomButton = forwardRef(function CustomButton({ label, onPress }, ref) {
export const CustomButton = forwardRef(function CustomButton({ label, onPress }, ref) {
return (
<View style={styles.buttonContainer}>
<TouchableHighlight style={styles.button} onPress={onPress} ref={ref}>

View File

@@ -0,0 +1,182 @@
// React
import { useState, useEffect, Fragment } from 'react';
import { ScrollView, View, Text, Image, StyleSheet, TouchableOpacity, TouchableHighlight, Alert } from 'react-native';
import Collapsible from 'react-native-collapsible';
import LinearGradient from 'react-native-linear-gradient';
// Components
import { CustomImage } from './image';
import { CustomTextInput } from './input';
import { Stat } from './stat';
// Contexts
import { useTeamContext } from '../context/teamContext';
import { useSocket } from '../context/socketContext';
// Hooks
import { useTimeDifference } from '../hook/useTimeDifference';
import { useGame } from '../hook/useGame';
// Util
import { GameState } from '../util/gameState';
import { Colors } from '../util/colors';
import { secondsToHHMMSS } from '../util/format';
export const Drawer = ({ height }) => {
const [collapsibleState, setCollapsibleState] = useState(true);
const [enemyCaptureCode, setEnemyCaptureCode] = useState("");
const {SERVER_URL} = useSocket();
const {gameState, startDate} = useTeamContext();
const {capture, enemyName, captureCode, name, teamId, distance, finishDate, nCaptures, nSentLocation, hasHandicap} = useGame();
const [timeSinceStart] = useTimeDifference(startDate, 1000);
const [avgSpeed, setAvgSpeed] = useState(0); // Speed in m/s
const [enemyImageURI, setEnemyImageURI] = useState("../assets/images/missing_image.jpg");
const [captureStatus, setCaptureStatus] = useState(0); // 0 : no capture | 1 : waiting for response from server | 2 : capture failed | 3 : capture succesful
const captureStatusColor = {0: "#777", 1: "#FFA500", 2: "#FF6B6B", 3: "#81C784"};
// Capture state update
useEffect(() => {
if (captureStatus == 2 || captureStatus == 3) {
const timeout = setTimeout(() => {
setCaptureStatus(0);
}, 3000);
return () => clearTimeout(timeout);
}
}, [captureStatus]);
// Refresh the image
useEffect(() => {
setEnemyImageURI(`${SERVER_URL}/photo/enemy?team=${teamId}&t=${new Date().getTime()}`);
}, [SERVER_URL, enemyName, teamId]);
// Update the average speed
useEffect(() => {
const hours = (finishDate ? (finishDate - startDate) : timeSinceStart*1000) / 1000 / 3600;
const km = distance / 1000;
setAvgSpeed(Math.floor(km / hours * 10) / 10);
}, [distance, finishDate, startDate, timeSinceStart]);
if (gameState != GameState.PLAYING) return;
function handleCapture() {
if (captureStatus != 1) {
setCaptureStatus(1);
capture(enemyCaptureCode)
.then((response) => {
if (response.hasCaptured) {
setCaptureStatus(3);
} else {
setCaptureStatus(2);
}
})
.catch(() => {
Alert.alert("Échec", "La connexion au serveur a échoué.");
setCaptureStatus(2);
});
setEnemyCaptureCode("");
}
}
return (
<View style={styles.outerDrawerContainer}>
<LinearGradient colors={['rgba(0,0,0,0)', 'rgba(0,0,0,0.5)']} style={{height: 70, width: "100%", position: "absolute", top: -30}}/>
<View style={styles.innerDrawerContainer}>
<TouchableHighlight onPress={() => setCollapsibleState(!collapsibleState)} style={styles.collapsibleButton} underlayColor="#d9d9d9">
<Image source={require('../assets/images/arrow.png')} style={{width: 20, height: 20, transform: [{ scaleY: collapsibleState ? 1 : -1 }] }} resizeMode="contain"></Image>
</TouchableHighlight>
<Collapsible style={[styles.collapsibleWindow, {height: height - 44}]} title="Collapse" collapsed={collapsibleState}>
<ScrollView contentContainerStyle={styles.collapsibleContent}>
{ gameState == GameState.PLAYING &&
<Text style={{fontSize: 22, fontWeight: "bold", textAlign: "center"}}>Code de {(name ?? "Indisponible")} : {String(captureCode).padStart(4,"0")}</Text>
}
{ gameState == GameState.PLAYING && !hasHandicap && <Fragment>
<View style={styles.imageContainer}>
{<Text style={{fontSize: 15, margin: 5}}>{"Cible (" + (enemyName ?? "Indisponible") + ")"}</Text>}
{<CustomImage source={{ uri : enemyImageURI }} canZoom/>}
</View>
<View style={styles.actionsContainer}>
<View style={styles.actionsLeftContainer}>
<CustomTextInput style={{borderColor: captureStatusColor[captureStatus]}} value={enemyCaptureCode} inputMode="numeric" placeholder="Code cible" onChangeText={setEnemyCaptureCode}/>
</View>
<View style={styles.actionsRightContainer}>
<TouchableOpacity style={styles.button} onPress={handleCapture}>
<Image source={require("../assets/images/target/white.png")} style={{width: 40, height: 40}} resizeMode="contain"/>
</TouchableOpacity>
</View>
</View>
</Fragment>}
<View style={{gap: 15, width: "100%", marginVertical: 15}}>
<View style={{flexDirection: "row", justifyContent: "space-around"}}>
<Stat source={require('../assets/images/distance.png')} description={"Distance parcourue"}>{Math.floor(distance / 100) / 10}km</Stat>
<Stat source={require('../assets/images/time.png')} description={"Temps écoulé au format HH:MM:SS"}>{secondsToHHMMSS((finishDate ? Math.floor((finishDate - startDate) / 1000) : timeSinceStart))}</Stat>
<Stat source={require('../assets/images/running.png')} description={"Vitesse moyenne"}>{avgSpeed}km/h</Stat>
</View>
<View style={{flexDirection: "row", justifyContent: "space-around"}}>
<Stat source={require('../assets/images/target/black.png')} description={"Nombre total de captures par votre équipe"}>{nCaptures}</Stat>
<Stat source={require('../assets/images/update_position.png')} description={"Nombre total d'envois de votre position"}>{nSentLocation}</Stat>
</View>
</View>
</ScrollView>
</Collapsible>
</View>
</View>
);
};
const styles = StyleSheet.create({
outerDrawerContainer: {
position: 'absolute',
bottom: 0,
left: 0,
right: 0,
},
innerDrawerContainer: {
width: "100%",
backgroundColor: Colors.background,
borderTopLeftRadius: 30,
borderTopRightRadius: 30,
overflow: 'hidden',
},
collapsibleButton: {
justifyContent: 'center',
alignItems: 'center',
width: "100%",
height: 45
},
collapsibleWindow: {
width: "100%",
justifyContent: 'center',
backgroundColor: Colors.background,
},
collapsibleContent: {
paddingHorizontal: 15,
},
imageContainer: {
width: "100%",
alignItems: "center",
justifyContent: "center",
marginTop: 15
},
actionsContainer: {
flexDirection: "row",
width: "100%",
alignItems: 'center',
justifyContent: 'space-between',
marginTop: 15
},
actionsLeftContainer: {
flexGrow: 1,
alignItems: 'center',
justifyContent: 'center',
marginRight: 15
},
actionsRightContainer: {
width: 100,
alignItems: 'center',
justifyContent: 'center'
},
button: {
borderRadius: 12,
width: '100%',
height: 75,
alignItems: 'center',
justifyContent: 'center',
backgroundColor: '#444'
},
});

View File

@@ -1,8 +1,9 @@
// React
import { useState } from 'react';
import { StyleSheet, View, Image, TouchableOpacity } from "react-native";
import ImageViewing from 'react-native-image-viewing';
export default function CustomImage({ source, canZoom, onPress }) {
export const CustomImage = ({ source, canZoom, onPress }) => {
// canZoom : boolean
const [isModalVisible, setIsModalVisible] = useState(false);
@@ -20,7 +21,7 @@ export default function CustomImage({ source, canZoom, onPress }) {
/>
</View>
);
}
};
const styles = StyleSheet.create({
container: {

View File

@@ -1,6 +1,7 @@
// React
import { TextInput, StyleSheet } from 'react-native';
export default function CustomTextInput({ style, value, inputMode, placeholder, onChangeText }) {
export const CustomTextInput = ({ style, value, inputMode, placeholder, onChangeText }) => {
return (
<TextInput
value={value}

View File

@@ -0,0 +1,114 @@
// React
import { useState, useEffect, useRef, Fragment } from 'react';
import { View, Image, Alert, StyleSheet, TouchableOpacity } from 'react-native';
import MapView, { Marker, Circle, Polygon } from 'react-native-maps';
import LinearGradient from 'react-native-linear-gradient';
// Contexts
import { useTeamContext } from '../context/teamContext';
// Hooks
import { useGame } from '../hook/useGame';
// Util
import { GameState } from '../util/gameState';
import { ZoneTypes, InitialRegions } from '../util/constants';
export const CustomMap = () => {
const {zoneType, zoneExtremities, location, gameState} = useTeamContext();
const {sendCurrentPosition, enemyLocation, startingArea, captured, lastSentLocation, hasHandicap} = useGame();
const mapRef = useRef(null);
const [centerMap, setCenterMap] = useState(true);
// Center the map on user position
useEffect(() => {
if (centerMap && mapRef.current && location) {
mapRef.current.animateToRegion({latitude: location[0], longitude: location[1], latitudeDelta: 0, longitudeDelta: 0.02}, 1000);
}
}, [centerMap, mapRef, location]);
const latToLatitude = (pos) => ({latitude: pos.lat, longitude: pos.lng});
return (
<View style={styles.mapContainer}>
<MapView ref={mapRef} style={{flex: 1}} initialRegion={InitialRegions.paris} mapType="standard" onTouchMove={() => setCenterMap(false)} toolbarEnabled={false}>
{ gameState == GameState.PLACEMENT && startingArea &&
<Circle center={{ latitude: startingArea.center.lat, longitude: startingArea.center.lng }} radius={startingArea.radius} strokeWidth={2} strokeColor={`rgba(0, 0, 255, 1)`} fillColor={`rgba(0, 0, 255, 0.2)`}/>
}
{ gameState == GameState.PLAYING && zoneExtremities &&
<Fragment>
{ zoneType == ZoneTypes.circle && zoneExtremities.begin && <Circle center={latToLatitude(zoneExtremities.begin.center)} radius={zoneExtremities.begin.radius} strokeColor="red" fillColor="rgba(255,0,0,0.1)" strokeWidth={2} />}
{ zoneType == ZoneTypes.circle && zoneExtremities.end && <Circle center={latToLatitude(zoneExtremities.end.center)} radius={zoneExtremities.end.radius} strokeColor="green" fillColor="rgba(0,255,0,0.1)" strokeWidth={2} />}
{ zoneType == ZoneTypes.polygon && zoneExtremities.begin && <Polygon coordinates={zoneExtremities.begin.polygon.map(pos => latToLatitude(pos))} strokeColor="red" fillColor="rgba(255,0,0,0.1)" strokeWidth={2} /> }
{ zoneType == ZoneTypes.polygon && zoneExtremities.end && <Polygon coordinates={zoneExtremities.end.polygon.map(pos => latToLatitude(pos))} strokeColor="green" fillColor="rgba(0,255,0,0.1)" strokeWidth={2} /> }
</Fragment>
}
{ location &&
<Marker coordinate={{ latitude: location[0], longitude: location[1] }} anchor={{ x: 0.33, y: 0.33 }} onPress={() => Alert.alert("Position actuelle", "Ceci est votre position")}>
<Image source={require("../assets/images/marker/blue.png")} style={{width: 24, height: 24}} resizeMode="contain"/>
</Marker>
}
{ gameState == GameState.PLAYING && lastSentLocation && !hasHandicap &&
<Marker coordinate={{ latitude: lastSentLocation[0], longitude: lastSentLocation[1] }} anchor={{ x: 0.33, y: 0.33 }} onPress={() => Alert.alert("Position envoyée", "Ceci est votre dernière position connue par le serveur")}>
<Image source={require("../assets/images/marker/grey.png")} style={{width: 24, height: 24}} resizeMode="contain"/>
</Marker>
}
{ gameState == GameState.PLAYING && enemyLocation && !hasHandicap &&
<Marker coordinate={{ latitude: enemyLocation[0], longitude: enemyLocation[1] }} anchor={{ x: 0.33, y: 0.33 }}>
<Image source={require("../assets/images/marker/red.png")} style={{width: 24, height: 24}} resizeMode="contain" onPress={() => Alert.alert("Position ennemie", "Ceci est la dernière position de vos ennemis connue")}/>
</Marker>
}
</MapView>
<LinearGradient colors={['rgba(0,0,0,0.3)', 'rgba(0,0,0,0)']} style={{height: 40, width: "100%", position: "absolute"}}/>
{ !centerMap &&
<TouchableOpacity style={styles.centerMapContainer} onPress={() => setCenterMap(true)}>
<Image source={require("../assets/images/centerMap.png")} style={{width: 30, height: 30}} resizeMode="contain"></Image>
</TouchableOpacity>
}
{ gameState == GameState.PLAYING && !captured &&
<View style={styles.toolBarRight}>
{ !hasHandicap &&
<TouchableOpacity style={styles.updatePositionContainer} onPress={sendCurrentPosition}>
<Image source={require("../assets/images/update_position.png")} style={{width: 40, height: 40}} resizeMode="contain"></Image>
</TouchableOpacity>
}
</View>
}
</View>
);
};
const styles = StyleSheet.create({
mapContainer: {
flex: 1,
width: '100%',
borderTopLeftRadius: 30,
borderTopRightRadius: 30,
overflow: 'hidden',
},
centerMapContainer: {
position: 'absolute',
right: 20,
top: 20,
width: 40,
height: 40,
borderRadius: 20,
backgroundColor: 'white',
borderWidth: 2,
borderColor: 'black',
alignItems: 'center',
justifyContent: 'center',
},
toolBarRight: {
position: 'absolute',
right: 30,
bottom: 80
},
updatePositionContainer: {
width: 60,
height: 60,
borderRadius: 30,
backgroundColor: 'white',
borderWidth: 4,
borderColor: 'black',
alignItems: 'center',
justifyContent: 'center',
},
});

View File

@@ -1,6 +1,7 @@
// React
import { TouchableOpacity, View, Image, Text, Alert } from 'react-native';
export default function Stat({ children, source, description }) {
export const Stat = ({ children, source, description }) => {
return (
<TouchableOpacity onPress={description ? () => Alert.alert("Info", description) : null}>
<View style={{height: 30, flexDirection: "row", justifyContent: 'center', alignItems: 'center'}}>

View File

@@ -0,0 +1,20 @@
// React
import { View, Text, StyleSheet } from 'react-native';
// Util
import { secondsToMMSS } from '../util/format';
export const TimerMMSS = ({ title, seconds, style }) => {
return (
<View style={[styles.container, style]}>
<Text style={{fontSize: 15}}>{title}</Text>
<Text style={{fontSize: 30, fontWeight: "bold"}}>{secondsToMMSS(seconds)}</Text>
</View>
);
};
const styles = StyleSheet.create({
container: {
alignItems: 'center',
justifyContent: 'center',
}
});