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

@@ -0,0 +1,46 @@
module.exports = {
"env": {
"es2021": true,
"node": true,
"react-native/react-native": true
},
"extends": [
"eslint:recommended",
"plugin:react/recommended",
"plugin:react-hooks/recommended",
"plugin:import/recommended"
],
"parserOptions": {
"ecmaFeatures": {
"jsx": true
},
"ecmaVersion": "latest",
"sourceType": "module"
},
"plugins": [
"react",
"react-native"
],
"rules": {
"react/react-in-jsx-scope": "off",
"react/prop-types": "off",
"no-unused-vars": "warn",
"semi": ["error", "always"],
"react-native/no-unused-styles": "warn",
"react-native/no-single-element-style-arrays": "warn",
'import/extensions': 'off',
},
"settings": {
"react": {
"version": "detect"
},
"import/ignore": [
"react-native"
],
'import/resolver': {
node: {
extensions: ['.js', '.jsx', '.ts', '.tsx'],
},
},
}
};

View File

@@ -43,7 +43,7 @@
],
"config": {
"googleMaps": {
"apiKey": "AIzaSyD0yuWIHFbsIDVfGQ9wEM3pOtVC2TgEO1U"
"apiKey": "AIzaSyAA0Y1vdDMsqtrg18ZaWx098uEgNAyJPq0"
}
}
},
@@ -55,6 +55,9 @@
],
"NSLocationAlwaysAndWhenInUseUsageDescription": "Your location is used to track you in the background.",
"NSLocationWhenInUseUsageDescription": "Location is used to track your movement."
},
"config": {
"googleMapsApiKey": "AIzaSyAA0Y1vdDMsqtrg18ZaWx098uEgNAyJPq0"
}
}
}

View File

@@ -1,3 +1,4 @@
// Expo
import { Unmatched } from 'expo-router';
export default Unmatched;

View File

@@ -1,9 +1,11 @@
// Expo
import { Slot } from 'expo-router';
import SocketProvider from "../context/socketContext";
// Contexts
import { SocketProvider } from "../context/socketContext";
import { TeamConnexionProvider } from "../context/teamConnexionContext";
import { TeamProvider } from "../context/teamContext";
export default function Layout() {
const Layout = () => {
return (
<SocketProvider>
<TeamConnexionProvider>
@@ -13,4 +15,6 @@ export default function Layout() {
</TeamConnexionProvider>
</SocketProvider>
);
}
};
export default Layout;

View File

@@ -1,600 +0,0 @@
// React
import { useState, useEffect, useRef, Fragment } from 'react';
import { ScrollView, View, Text, Image, Alert, StyleSheet, TouchableOpacity, TouchableHighlight } from 'react-native';
import MapView, { Marker, Circle, Polygon } from 'react-native-maps';
// Expo
import { useRouter } from 'expo-router';
// Components
import CustomImage from '../components/image';
import CustomTextInput from '../components/input';
import Stat from '../components/stat';
import Collapsible from 'react-native-collapsible';
import LinearGradient from 'react-native-linear-gradient';
// Other
import { useSocket } from '../context/socketContext';
import { useTeamContext } from '../context/teamContext';
import { useTeamConnexion } from '../context/teamConnexionContext';
import { useTimeDifference } from '../hook/useTimeDifference';
import { GameState } from '../util/gameState';
import useGame from '../hook/useGame';
const backgroundColor = '#f5f5f5';
const initialRegion = {latitude: 48.864, longitude: 2.342, latitudeDelta: 0, longitudeDelta: 50} // France centrée sur Paris
const zoneTypes = {
circle: "circle",
polygon: "polygon"
}
export default function Display() {
const arrowUp = require('../assets/images/arrow.png');
const [collapsibleState, setCollapsibleState] = useState(true);
const [bottomContainerHeight, setBottomContainerHeight] = useState(0);
const router = useRouter();
const {SERVER_URL} = useSocket();
const {messages, zoneType, zoneExtremities, nextZoneDate, isShrinking, location, startLocationTracking, stopLocationTracking, gameState, startDate} = useTeamContext();
const {loggedIn, logout, loading} = useTeamConnexion();
const {sendCurrentPosition, capture, enemyLocation, enemyName, startingArea, captureCode, name, ready, captured, lastSentLocation, locationSendDeadline, teamId, outOfZone, outOfZoneDeadline, distance, finishDate, nCaptures, nSentLocation, hasHandicap, enemyHasHandicap} = useGame();
const [enemyCaptureCode, setEnemyCaptureCode] = useState("");
const [timeLeftSendLocation] = useTimeDifference(locationSendDeadline, 1000);
const [timeLeftNextZone] = useTimeDifference(nextZoneDate, 1000);
const [timeLeftOutOfZone] = useTimeDifference(outOfZoneDeadline, 1000);
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"};
const mapRef = useRef(null);
const [centerMap, setCenterMap] = useState(true);
// Router
useEffect(() => {
if (!loading) {
if (!loggedIn) {
router.replace("/");
}
}
}, [loggedIn, loading]);
// Activating geolocation tracking
useEffect(() => {
if (loggedIn) {
startLocationTracking();
} else {
stopLocationTracking();
}
}, [loggedIn, gameState, captured]);
// Refresh the image
useEffect(() => {
setEnemyImageURI(`${SERVER_URL}/photo/enemy?team=${teamId}&t=${new Date().getTime()}`);
}, [enemyName, teamId]);
// Capture state update
useEffect(() => {
if (captureStatus == 2 || captureStatus == 3) {
const timeout = setTimeout(() => {
setCaptureStatus(0);
}, 3000);
return () => clearTimeout(timeout);
}
}, [captureStatus]);
// 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]);
// 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, timeSinceStart]);
function toggleCollapsible() {
setCollapsibleState(!collapsibleState);
};
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("");
}
}
function formatTimeMinutes(time) {
// time is in seconds
if (!Number.isInteger(time)) return "Inconnue";
if (time < 0) time = 0;
const minutes = Math.floor(time / 60);
const seconds = Math.floor(time % 60);
return String(minutes).padStart(2,"0") + ":" + String(seconds).padStart(2,"0");
}
function formatTimeHours(time) {
// time is in seconds
if (!Number.isInteger(time)) return "Inconnue";
if (time < 0) time = 0;
const hours = Math.floor(time / 3600);
const minutes = Math.floor(time / 60 % 60);
const seconds = Math.floor(time % 60);
return String(hours).padStart(2,"0") + ":" + String(minutes).padStart(2,"0") + ":" + String(seconds).padStart(2,"0");
}
const Logout = () => {
return (
<TouchableOpacity style={{width: 40, height: 40}} onPress={logout}>
<Image source={require('../assets/images/logout.png')} style={{width: 40, height: 40}} resizeMode="contain"></Image>
</TouchableOpacity>
);
}
const Settings = () => {
return (
<TouchableOpacity style={{width: 40, height: 40}} onPress={() => Alert.alert("Settings")}>
<Image source={require('../assets/images/cogwheel.png')} style={{width: 40, height: 40}} resizeMode="contain"></Image>
</TouchableOpacity>
);
}
const TeamName = () => {
return(
<Text style={{fontSize: 36, fontWeight: "bold", textAlign: "center"}}>{(name ?? "Indisponible")}</Text>
);
}
const GameLog = () => {
return (
<TouchableOpacity style={{width:"100%"}}>
{ gameState == GameState.SETUP && <Text style={styles.gameState}>{messages?.waiting || "Préparation de la partie"}</Text>}
{ gameState == GameState.PLACEMENT && <Text style={styles.gameState}>Phase de placement</Text>}
{ gameState == GameState.PLAYING && !outOfZone && <Text style={styles.gameState}>La partie est en cours</Text>}
{ gameState == GameState.PLAYING && outOfZone && !hasHandicap && <Text style={styles.gameStateOutOfZone}>{`Veuillez retourner dans la zone\nHandicap dans ${formatTimeMinutes(-timeLeftOutOfZone)}`}</Text>}
{ gameState == GameState.PLAYING && hasHandicap && <Text style={styles.gameStateOutOfZone}>{`Veuillez retourner dans la zone\nVotre position est révélée en continue`}</Text>}
{ gameState == GameState.FINISHED && <Text style={styles.gameState}>La partie est terminée</Text>}
</TouchableOpacity>
);
}
const TimeBeforeNextZone = () => {
return (
<View style={{width: "100%", alignItems: "center", justifyContent: "center"}}>
<Text style={{fontSize: 15}}>{isShrinking ? "Réduction de la zone" : "Durée de la zone"}</Text>
<Text style={{fontSize: 30, fontWeight: "bold"}}>{formatTimeMinutes(-timeLeftNextZone)}</Text>
</View>
);
}
const TimeBeforeNextPosition = () => {
return (
<View style={{width: "100%", alignItems: "center", justifyContent: "center"}}>
<Text style={{fontSize: 15}}>Position envoyée dans</Text>
<Text style={{fontSize: 30, fontWeight: "bold"}}>{ !hasHandicap ? formatTimeMinutes(-timeLeftSendLocation) : "00:00"}</Text>
</View>
);
}
const Timers = () => {
return (
<View style={styles.timersContainer}>
<View style={styles.zoneTimerContainer}>
{ TimeBeforeNextZone() }
</View>
<View style={styles.positionTimerContainer}>
{ TimeBeforeNextPosition() }
</View>
</View>
);
}
const Ready = () => {
return (
<View style={styles.timersContainer}>
<View style={[styles.readyIndicator, {backgroundColor: ready ? "#3C3" : "#C33"}]}>
<Text style={{color: '#fff', fontSize: 16}}>{ready ? "Placé" : "Non placé"}</Text>
</View>
</View>
);
}
const CapturedMessage = () => {
return (
<View style={[styles.timersContainer, {height: 61}]}>
<Text style={{fontSize: 20}}>{messages?.captured || "Vous avez été éliminé..."}</Text>
</View>
);
}
const EndGameMessage = () => {
return (
<View style={[styles.timersContainer, {height: 61}]}>
{captured && <Text style={{fontSize: 20}}>{messages?.loser || "Vous avez perdu..."}</Text>}
{!captured && <Text style={{fontSize: 20}}>{messages?.winner || "Vous avez gagné !"}</Text>}
</View>
);
}
const Zones = () => {
const latToLatitude = (pos) => ({latitude: pos.lat, longitude: pos.lng});
return (
<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>
);
}
const Map = () => {
return (
<MapView ref={mapRef} style={{flex: 1}} initialRegion={initialRegion} 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 && <Zones/>}
{ 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>
);
}
const UpdatePositionButton = () => {
return ( !hasHandicap &&
<TouchableOpacity style={styles.updatePositionContainer} onPress={sendCurrentPosition}>
<Image source={require("../assets/images/update_position.png")} style={{width: 40, height: 40}} resizeMode="contain"></Image>
</TouchableOpacity>
);
}
const CenterMapButton = () => {
return (
<TouchableOpacity style={styles.centerMapContainer} onPress={() => setCenterMap(true)}>
<Image source={require("../assets/images/centerMap.png")} style={{width: 30, height: 30}} resizeMode="contain"></Image>
</TouchableOpacity>
);
}
const LayerButton = () => {
return(
<TouchableOpacity style={styles.layerContainer} onPress={() => Alert.alert("Layer")}>
<Image source={require('../assets/images/path.png')} style={{width: 40, height: 40}} resizeMode="contain"></Image>
</TouchableOpacity>
);
}
const CollapsibleButton = () => {
return (
<TouchableHighlight onPress={toggleCollapsible} style={styles.collapsibleButton} underlayColor="#d9d9d9">
<Image source={arrowUp} style={{width: 20, height: 20, transform: [{ scaleY: collapsibleState ? 1 : -1 }] }} resizeMode="contain"></Image>
</TouchableHighlight>
);
}
const TeamCaptureCode = () => {
return (
<Text style={{fontSize: 22, fontWeight: "bold", textAlign: "center"}}>Code de {(name ?? "Indisponible")} : {String(captureCode).padStart(4,"0")}</Text>
);
}
const ChasedTeamImage = () => {
return (
<View style={styles.imageContainer}>
{<Text style={{fontSize: 15, margin: 5}}>{"Cible (" + (enemyName ?? "Indisponible") + ")"}</Text>}
{<CustomImage source={{ uri : enemyImageURI }} canZoom/>}
</View>
);
}
const CaptureCode = () => {
return (
<View style={styles.actionsLeftContainer}>
<CustomTextInput style={{borderColor: captureStatusColor[captureStatus]}} value={enemyCaptureCode} inputMode="numeric" placeholder="Code cible" onChangeText={setEnemyCaptureCode}/>
</View>
);
}
const CaptureButton = () => {
return (
<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>
);
}
const Stats = () => {
return (
<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"}>{formatTimeHours((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>
);
}
return (
<View style={styles.globalContainer}>
<View style={styles.topContainer}>
<View style={styles.topheadContainer}>
{ Logout() }
{ false && Settings() }
</View>
<View style={styles.teamNameContainer}>
{ TeamName() }
</View>
<View style={styles.logContainer}>
{ GameLog() }
</View>
{ gameState == GameState.PLACEMENT &&
Ready()
}
{ gameState == GameState.PLAYING && !captured && <Fragment>
{Timers()}
{enemyHasHandicap && <Text style={{fontSize: 18, marginTop: 6, fontWeight: "bold"}}>Position ennemie révélée en continue !</Text>}
</Fragment>}
{ gameState == GameState.PLAYING && captured &&
CapturedMessage()
}
{ gameState == GameState.FINISHED &&
EndGameMessage()
}
</View>
<View style={styles.bottomContainer} onLayout={(event) => {setBottomContainerHeight(event.nativeEvent.layout.height)}}>
<View style={styles.mapContainer}>
{ Map() }
<LinearGradient colors={['rgba(0,0,0,0.3)', 'rgba(0,0,0,0)']} style={{height: 40, width: "100%", position: "absolute"}}/>
{ !centerMap && CenterMapButton() }
{ false && gameState == GameState.PLAYING && !captured &&
<View style={styles.toolBarLeft}>
{ LayerButton() }
</View>
}
{ gameState == GameState.PLAYING && !captured &&
<View style={styles.toolBarRight}>
{ UpdatePositionButton() }
</View>
}
</View>
{ (gameState == GameState.PLAYING || gameState == GameState.FINISHED) &&
<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}>
{ CollapsibleButton() }
<Collapsible style={[styles.collapsibleWindow, {height: bottomContainerHeight - 44}]} title="Collapse" collapsed={collapsibleState}>
<ScrollView contentContainerStyle={styles.collapsibleContent}>
{ gameState == GameState.PLAYING && TeamCaptureCode() }
{ gameState == GameState.PLAYING && !hasHandicap && <Fragment>
{ ChasedTeamImage() }
<View style={styles.actionsContainer}>
{ CaptureCode() }
{ CaptureButton() }
</View>
</Fragment>}
{ Stats() }
</ScrollView>
</Collapsible>
</View>
</View>
}
</View>
</View>
);
}
const styles = StyleSheet.create({
globalContainer: {
backgroundColor: backgroundColor,
flex: 1,
},
topContainer: {
width: '100%',
alignItems: 'center',
padding: 15,
},
topheadContainer: {
width: "100%",
flexDirection: "row",
justifyContent: 'space-between'
},
teamNameContainer: {
width: '100%',
alignItems: 'center',
justifyContent: 'center'
},
logContainer: {
width: '100%',
alignItems: 'center',
justifyContent: 'center',
marginTop: 15
},
gameState: {
borderWidth: 2,
borderRadius: 10,
width: "100%",
backgroundColor: 'white',
fontSize: 18,
padding: 10,
},
gameStateOutOfZone: {
borderWidth: 2,
borderRadius: 10,
width: "100%",
backgroundColor: 'white',
fontSize: 18,
padding: 10,
borderColor: 'red'
},
timersContainer: {
width: '100%',
alignItems: 'center',
justifyContent: 'center',
flexDirection: 'row',
marginTop: 15
},
zoneTimerContainer: {
width: "50%",
alignItems: 'center',
justifyContent: 'center',
},
positionTimerContainer: {
width: "50%",
alignItems: 'center',
justifyContent: 'center',
},
readyIndicator: {
width: "100%",
maxWidth: 240,
height: 61,
alignItems: 'center',
justifyContent: 'center',
padding: 3,
borderRadius: 10
},
bottomContainer: {
flex: 1,
},
mapContainer: {
flex: 1,
width: '100%',
borderTopLeftRadius: 30,
borderTopRightRadius: 30,
overflow: 'hidden',
},
outerDrawerContainer: {
position: 'absolute',
bottom: 0,
left: 0,
right: 0,
},
innerDrawerContainer: {
width: "100%",
backgroundColor: backgroundColor,
borderTopLeftRadius: 30,
borderTopRightRadius: 30,
overflow: 'hidden',
},
collapsibleButton: {
justifyContent: 'center',
alignItems: 'center',
width: "100%",
height: 45
},
collapsibleWindow: {
width: "100%",
justifyContent: 'center',
backgroundColor: backgroundColor,
},
collapsibleContent: {
paddingHorizontal: 15,
},
centerMapContainer: {
position: 'absolute',
right: 20,
top: 20,
width: 40,
height: 40,
borderRadius: 20,
backgroundColor: 'white',
borderWidth: 2,
borderColor: 'black',
alignItems: 'center',
justifyContent: 'center',
},
toolBarLeft: {
position: 'absolute',
left: 30,
bottom: 80
},
toolBarRight: {
position: 'absolute',
right: 30,
bottom: 80
},
updatePositionContainer: {
width: 60,
height: 60,
borderRadius: 30,
backgroundColor: 'white',
borderWidth: 4,
borderColor: 'black',
alignItems: 'center',
justifyContent: 'center',
},
layerContainer: {
width: 60,
height: 60,
borderRadius: 30,
backgroundColor: 'white',
borderWidth: 4,
borderColor: 'black',
alignItems: 'center',
justifyContent: 'center',
},
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

@@ -4,18 +4,19 @@ import { ScrollView, View, Text, StyleSheet, Image, Alert } from 'react-native';
// Expo
import { useRouter } from 'expo-router';
// Components
import CustomButton from '../components/button';
import CustomImage from '../components/image';
import CustomTextInput from '../components/input';
// Other
import { useSocket } from '../context/socketContext';
import { useTeamContext } from '../context/teamContext';
import { CustomButton } from '../components/button';
import { CustomImage } from '../components/image';
import { CustomTextInput } from '../components/input';
// Contexts
import { useSocket } from "../context/socketContext";
import { useTeamConnexion } from "../context/teamConnexionContext";
import { useTeamContext } from "../context/teamContext";
// Hooks
import { usePickImage } from '../hook/usePickImage';
// Util
import { Colors } from '../util/colors';
const backgroundColor = '#f5f5f5';
export default function Index() {
const Index = () => {
const router = useRouter();
const {SERVER_URL} = useSocket();
const {login, loggedIn, loading} = useTeamConnexion();
@@ -27,14 +28,14 @@ export default function Index() {
// Disbaling location tracking
useEffect(() => {
stopLocationTracking();
}, []);
}, [stopLocationTracking]);
// Routeur
useEffect(() => {
if (!loading && loggedIn) {
router.replace("/display");
router.replace("/interface");
}
}, [loggedIn, loading]);
}, [router, loggedIn, loading]);
function handleSubmit() {
if (!isSubmitting && !loading) {
@@ -69,7 +70,7 @@ export default function Index() {
<CustomTextInput value={teamID} inputMode="numeric" placeholder="ID de l'équipe" style={styles.input} onChangeText={setTeamID}/>
</View>
<View style={styles.subContainer}>
<Text style={{fontSize: 15}}>Appuyer pour changer la photo d'équipe</Text>
<Text style={{fontSize: 15}}>Appuyer pour changer la photo d&apos;équipe</Text>
<Text style={{fontSize: 13, marginBottom: 3}}>(Le haut du corps doit être visible)</Text>
<CustomImage source={image ? {uri: image.uri} : require('../assets/images/missing_image.jpg')} onPress={pickImage}/>
</View>
@@ -79,14 +80,16 @@ export default function Index() {
</View>
</ScrollView>
);
}
};
export default Index;
const styles = StyleSheet.create({
container: {
flexGrow: 1,
alignItems: 'center',
paddingVertical: 20,
backgroundColor: backgroundColor
backgroundColor: Colors.background
},
transitionContainer: {
flexGrow: 1,

View File

@@ -0,0 +1,172 @@
// React
import { useState, useEffect, Fragment } from 'react';
import { View, Text, Image, Alert, StyleSheet, TouchableOpacity } from 'react-native';
// Expo
import { useRouter } from 'expo-router';
// Components
import { CustomMap } from '../components/map';
import { Drawer } from '../components/drawer';
// Contexts
import { useTeamConnexion } from '../context/teamConnexionContext';
import { useTeamContext } from '../context/teamContext';
// Hooks
import { useGame } from '../hook/useGame';
import { useTimeDifference } from '../hook/useTimeDifference';
// Util
import { GameState } from '../util/gameState';
import { TimerMMSS } from '../components/timer';
import { secondsToMMSS } from '../util/format';
import { Colors } from '../util/colors';
const Interface = () => {
const router = useRouter();
const {messages, nextZoneDate, isShrinking, startLocationTracking, stopLocationTracking, gameState} = useTeamContext();
const {loggedIn, logout, loading} = useTeamConnexion();
const {name, ready, captured, locationSendDeadline, outOfZone, outOfZoneDeadline, hasHandicap, enemyHasHandicap} = useGame();
const [timeLeftSendLocation] = useTimeDifference(locationSendDeadline, 1000);
const [timeLeftNextZone] = useTimeDifference(nextZoneDate, 1000);
const [timeLeftOutOfZone] = useTimeDifference(outOfZoneDeadline, 1000);
const [bottomContainerHeight, setBottomContainerHeight] = useState(0);
// Router
useEffect(() => {
if (!loading) {
if (!loggedIn) {
router.replace("/");
}
}
}, [router, loggedIn, loading]);
// Activating geolocation tracking
useEffect(() => {
if (loggedIn) {
startLocationTracking();
} else {
stopLocationTracking();
}
}, [startLocationTracking, stopLocationTracking, loggedIn]);
return (
<View style={styles.globalContainer}>
<View style={styles.topContainer}>
<View style={styles.topheadContainer}>
<TouchableOpacity style={{width: 40, height: 40}} onPress={logout}>
<Image source={require('../assets/images/logout.png')} style={{width: 40, height: 40}} resizeMode="contain"></Image>
</TouchableOpacity>
<TouchableOpacity style={{width: 40, height: 40}} onPress={() => Alert.alert("Settings")}>
<Image source={require('../assets/images/cogwheel.png')} style={{width: 40, height: 40}} resizeMode="contain"></Image>
</TouchableOpacity>
</View>
<View style={styles.teamNameContainer}>
<Text style={{fontSize: 36, fontWeight: "bold", textAlign: "center"}}>{(name ?? "Indisponible")}</Text>
</View>
<View style={styles.logContainer}>
<TouchableOpacity style={{width:"100%"}}>
{ gameState == GameState.SETUP && <Text style={styles.gameState}>{messages?.waiting || "Préparation de la partie"}</Text>}
{ gameState == GameState.PLACEMENT && <Text style={styles.gameState}>Phase de placement</Text>}
{ gameState == GameState.PLAYING && !outOfZone && <Text style={styles.gameState}>La partie est en cours</Text>}
{ gameState == GameState.PLAYING && outOfZone && !hasHandicap && <Text style={styles.gameStateOutOfZone}>{`Veuillez retourner dans la zone\nHandicap dans ${secondsToMMSS(-timeLeftOutOfZone)}`}</Text>}
{ gameState == GameState.PLAYING && hasHandicap && <Text style={styles.gameStateOutOfZone}>{`Veuillez retourner dans la zone\nVotre position est révélée en continue`}</Text>}
{ gameState == GameState.FINISHED && <Text style={styles.gameState}>La partie est terminée</Text>}
</TouchableOpacity>
</View>
{ gameState == GameState.PLACEMENT &&
<View style={styles.timersContainer}>
<View style={[styles.readyIndicator, {backgroundColor: ready ? "#3C3" : "#C33"}]}>
<Text style={{color: '#fff', fontSize: 16}}>{ready ? "Placé" : "Non placé"}</Text>
</View>
</View>
}
{ gameState == GameState.PLAYING && !captured && <Fragment>
<View style={styles.timersContainer}>
<TimerMMSS style={{width: "50%"}} title={isShrinking ? "Réduction de la zone" : "Durée de la zone"} seconds={-timeLeftNextZone} />
<TimerMMSS style={{width: "50%"}} title={"Position envoyée dans"} seconds={!hasHandicap ? -timeLeftSendLocation: 0} />
</View>
{enemyHasHandicap &&
<Text style={{fontSize: 18, marginTop: 6, fontWeight: "bold"}}>Position ennemie révélée en continue !</Text>
}
</Fragment>}
{ gameState == GameState.PLAYING && captured &&
<View style={[styles.timersContainer, {height: 61}]}>
<Text style={{fontSize: 20}}>{messages?.captured || "Vous avez été éliminé..."}</Text>
</View>
}
{ gameState == GameState.FINISHED &&
<View style={[styles.timersContainer, {height: 61}]}>
{captured && <Text style={{fontSize: 20}}>{captured ? (messages?.loser || "Vous avez perdu...") : (messages?.winner || "Vous avez gagné !")}</Text>}
</View>
}
</View>
<View style={styles.bottomContainer} onLayout={(event) => setBottomContainerHeight(event.nativeEvent.layout.height)}>
<CustomMap/>
<Drawer height={bottomContainerHeight}/>
</View>
</View>
);
};
export default Interface;
const styles = StyleSheet.create({
globalContainer: {
backgroundColor: Colors.background,
flex: 1,
},
topContainer: {
width: '100%',
alignItems: 'center',
padding: 15,
},
topheadContainer: {
width: "100%",
flexDirection: "row",
justifyContent: 'space-between'
},
teamNameContainer: {
width: '100%',
alignItems: 'center',
justifyContent: 'center'
},
logContainer: {
width: '100%',
alignItems: 'center',
justifyContent: 'center',
marginTop: 15
},
gameState: {
borderWidth: 2,
borderRadius: 10,
width: "100%",
backgroundColor: 'white',
fontSize: 18,
padding: 10,
},
gameStateOutOfZone: {
borderWidth: 2,
borderRadius: 10,
width: "100%",
backgroundColor: 'white',
fontSize: 18,
padding: 10,
borderColor: 'red'
},
timersContainer: {
width: '100%',
alignItems: 'center',
justifyContent: 'center',
flexDirection: 'row',
marginTop: 15
},
readyIndicator: {
width: "100%",
maxWidth: 240,
height: 61,
alignItems: 'center',
justifyContent: 'center',
padding: 3,
borderRadius: 10
},
bottomContainer: {
flex: 1,
}
});

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',
}
});

View File

@@ -1,8 +1,9 @@
import { createContext, useContext, useMemo } from "react";
import { io } from "socket.io-client";
const SOCKET_URL = `ws://0.0.0.0/player`;
const SERVER_URL = `http://0.0.0.0/back`;
const IP = "172.16.1.180";
const SOCKET_URL = `ws://${IP}/player`;
const SERVER_URL = `http://${IP}/back`;
export const teamSocket = io(SOCKET_URL, {
path: "/back/socket.io",
@@ -10,13 +11,14 @@ export const teamSocket = io(SOCKET_URL, {
export const SocketContext = createContext();
export default function SocketProvider({ children }) {
const value = useMemo(() => ({ teamSocket, SERVER_URL }), [teamSocket]);
export const SocketProvider = ({ children }) => {
const value = useMemo(() => ({ teamSocket, SERVER_URL }), []);
return (
<SocketContext.Provider value={value}>{children}</SocketContext.Provider>
);
}
};
export function useSocket() {
export const useSocket = () => {
return useContext(SocketContext);
}
};

View File

@@ -4,22 +4,19 @@ import { useSocketAuth } from "../hook/useSocketAuth";
const teamConnexionContext = createContext();
const TeamConnexionProvider = ({ children }) => {
export const TeamConnexionProvider = ({ children }) => {
const { teamSocket } = useSocket();
const { login, password: teamId, loggedIn, loading, logout } = useSocketAuth(teamSocket, "team_password");
const value = useMemo(() => ({ teamId, login, logout, loggedIn, loading}), [teamId, login, loggedIn, loading]);
const value = useMemo(() => ({ teamId, login, logout, loggedIn, loading}), [teamId, login, logout, loggedIn, loading]);
return (
<teamConnexionContext.Provider value={value}>
{children}
</teamConnexionContext.Provider>
);
}
};
function useTeamConnexion() {
export const useTeamConnexion = () => {
return useContext(teamConnexionContext);
}
export { TeamConnexionProvider, useTeamConnexion };
};

View File

@@ -1,14 +1,14 @@
import { useLocation } from "../hook/useLocation";
import { useSocketListener } from "../hook/useSocketListener";
import { createContext, useContext, useMemo, useState } from "react";
import { useSocket } from "./socketContext";
import { GameState } from "../util/gameState";
import useSendDeviceInfo from "../hook/useSendDeviceInfo";
import { useTeamConnexion } from "./teamConnexionContext";
import { GameState } from "../util/gameState";
import { useSendDeviceInfo } from "../hook/useSendDeviceInfo";
import { useLocation } from "../hook/useLocation";
import { useSocketListener } from "../hook/useSocketListener";
const teamContext = createContext();
function TeamProvider({children}) {
export const TeamProvider = ({children}) => {
const {teamSocket} = useSocket();
const [location, getLocationAuthorization, startLocationTracking, stopLocationTracking] = useLocation(5000, 10);
// update_team
@@ -28,7 +28,7 @@ function TeamProvider({children}) {
useSendDeviceInfo();
useSocketListener(teamSocket, "update_team", (data) => {
setTeamInfos(teamInfos => ({...teamInfos, ...data}))
setTeamInfos(teamInfos => ({...teamInfos, ...data}));
});
useSocketListener(teamSocket, "game_state", (data) => {
@@ -54,17 +54,15 @@ function TeamProvider({children}) {
const value = useMemo(() => (
{teamInfos, gameState, startDate, zoneType, zoneExtremities, nextZoneDate, messages, location, getLocationAuthorization, startLocationTracking, stopLocationTracking}
), [teamInfos, gameState, startDate, zoneType, zoneExtremities, nextZoneDate, messages, location]);
), [teamInfos, gameState, startDate, zoneType, zoneExtremities, nextZoneDate, messages, location, getLocationAuthorization, startLocationTracking, stopLocationTracking]);
return (
<teamContext.Provider value={value}>
{children}
</teamContext.Provider>
);
}
};
function useTeamContext() {
export const useTeamContext = () => {
return useContext(teamContext);
}
export { TeamProvider, useTeamContext };
};

View File

@@ -2,13 +2,13 @@ import { useSocket } from "../context/socketContext";
import { useTeamConnexion } from "../context/teamConnexionContext";
import { useTeamContext } from "../context/teamContext";
export default function useGame() {
export const useGame = () => {
const { teamSocket } = useSocket();
const { teamId } = useTeamConnexion();
const { teamInfos } = useTeamContext();
function sendCurrentPosition() {
console.log("Reveal position.")
console.log("Reveal position.");
teamSocket.emit("send_position");
}
@@ -30,4 +30,4 @@ export default function useGame() {
}
return {...teamInfos, sendCurrentPosition, capture, teamId};
}
};

View File

@@ -1,7 +1,7 @@
import AsyncStorage from '@react-native-async-storage/async-storage';
import { useEffect, useState } from "react";
export function useLocalStorage(key, initialValue) {
export const useLocalStorage = (key, initialValue) => {
const [storedValue, setStoredValue] = useState(initialValue);
const [loading, setLoading] = useState(true);
@@ -16,7 +16,7 @@ export function useLocalStorage(key, initialValue) {
setLoading(false);
}
fetchData();
}, []);
}, [initialValue, key]);
const setValue = async value => {
try {
@@ -28,7 +28,7 @@ export function useLocalStorage(key, initialValue) {
} catch (error) {
console.log(error);
}
}
};
return [storedValue, setValue, loading];
}
};

View File

@@ -4,7 +4,7 @@ import { defineTask, isTaskRegisteredAsync } from "expo-task-manager";
import * as Location from 'expo-location';
import { useSocket } from "../context/socketContext";
export function useLocation(timeInterval, distanceInterval) {
export const useLocation = (timeInterval, distanceInterval) => {
const [location, setLocation] = useState(null); // [latitude, longitude]
const { teamSocket } = useSocket();
const LOCATION_TASK_NAME = "background-location-task";
@@ -40,7 +40,7 @@ export function useLocation(timeInterval, distanceInterval) {
console.log("Sending position :", new_location);
teamSocket.emit("update_position", new_location);
} else {
console.log("No location measured.")
console.log("No location measured.");
}
}
});
@@ -64,7 +64,7 @@ export function useLocation(timeInterval, distanceInterval) {
if (await getLocationAuthorization()) {
if (!(await isTaskRegisteredAsync(LOCATION_TASK_NAME))) {
await Location.startLocationUpdatesAsync(LOCATION_TASK_NAME, locationUpdateParameters);
console.log("Location tracking started.")
console.log("Location tracking started.");
}
}
}
@@ -72,9 +72,9 @@ export function useLocation(timeInterval, distanceInterval) {
async function stopLocationTracking() {
if (await isTaskRegisteredAsync(LOCATION_TASK_NAME)) {
await Location.stopLocationUpdatesAsync(LOCATION_TASK_NAME);
console.log("Location tracking stopped.")
console.log("Location tracking stopped.");
}
}
return [location, getLocationAuthorization, startLocationTracking, stopLocationTracking];
}
};

View File

@@ -2,7 +2,7 @@ import { useState, } from 'react';
import { Alert } from 'react-native';
import { launchImageLibraryAsync, requestMediaLibraryPermissionsAsync } from 'expo-image-picker';
export function usePickImage() {
export const usePickImage = () => {
const [image, setImage] = useState(null);
const pickImage = async () => {
@@ -33,7 +33,7 @@ export function usePickImage() {
console.error('Error picking image;', error);
Alert.alert('Erreur', "Une erreur est survenue lors de la sélection d'une image.");
}
}
};
function sendImage(location) {
if (image) {
@@ -55,4 +55,4 @@ export function usePickImage() {
}
return {image, pickImage, sendImage};
}
};

View File

@@ -3,7 +3,7 @@ import DeviceInfo from 'react-native-device-info';
import { useSocket } from "../context/socketContext";
import { useTeamConnexion } from "../context/teamConnexionContext";
export default function useSendDeviceInfo() {
export const useSendDeviceInfo = () => {
const batteryUpdateTimeout = 5*60*1000;
const { teamSocket } = useSocket();
const {loggedIn} = useTeamConnexion();
@@ -28,8 +28,8 @@ export default function useSendDeviceInfo() {
const batteryCheckInterval = setInterval(() => sendBattery(), batteryUpdateTimeout);
return () => {clearInterval(batteryCheckInterval)};
}, [loggedIn]);
return () => clearInterval(batteryCheckInterval);
}, [batteryUpdateTimeout, loggedIn, teamSocket]);
return null;
}
};

View File

@@ -4,7 +4,7 @@ import { useLocalStorage } from './useLocalStorage';
const LOGIN_MESSAGE = "login";
const LOGOUT_MESSAGE = "logout";
export function useSocketAuth(socket, passwordName) {
export const useSocketAuth = (socket, passwordName) => {
const [loggedIn, setLoggedIn] = useState(false);
const [loading, setLoading] = useState(true);
const [waitingForResponse, setWaitingForResponse] = useState(false);
@@ -29,7 +29,7 @@ export function useSocketAuth(socket, passwordName) {
});
setHasTriedSavedPassword(true);
}
}, [loading]);
}, [hasTriedSavedPassword, loading, savedPassword, socket]);
function login(password) {
console.log("Try to log in with :", password);
@@ -69,4 +69,4 @@ export function useSocketAuth(socket, passwordName) {
}, [waitingForResponse, savedPasswordLoading]);
return {login, logout, password: savedPassword, loggedIn, loading};
}
};

View File

@@ -1,10 +1,10 @@
import { useEffect } from "react";
export function useSocketListener(socket, event, callback) {
export const useSocketListener = (socket, event, callback) => {
useEffect(() => {
socket.on(event,callback);
return () => {
socket.off(event, callback);
}
}, []);
}
};
}, [callback, event, socket]);
};

View File

@@ -1,6 +1,6 @@
import { useEffect, useState } from "react";
export function useTimeDifference(refTime, timeout) {
export const useTimeDifference = (refTime, timeout) => {
// If refTime is in the past, time will be positive
// If refTime is in the future, time will be negative
// The time is updated every timeout milliseconds
@@ -15,7 +15,7 @@ export function useTimeDifference(refTime, timeout) {
const interval = setInterval(updateTime, timeout);
return () => clearInterval(interval);
}, [refTime]);
}, [refTime, timeout]);
return [time];
}
};

View File

@@ -43,7 +43,11 @@
},
"devDependencies": {
"@babel/core": "^7.20.0",
"@react-native-community/cli": "latest"
"@react-native-community/cli": "latest",
"eslint": "^7.32.0",
"eslint-plugin-import": "^2.32.0",
"eslint-plugin-react": "^7.37.5",
"eslint-plugin-react-native": "^5.0.0"
}
},
"node_modules/@0no-co/graphql.web": {
@@ -2531,9 +2535,9 @@
}
},
"node_modules/@eslint/eslintrc/node_modules/js-yaml": {
"version": "3.14.1",
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz",
"integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==",
"version": "3.14.2",
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.2.tgz",
"integrity": "sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg==",
"license": "MIT",
"dependencies": {
"argparse": "^1.0.7",
@@ -7062,9 +7066,9 @@
}
},
"node_modules/@typescript-eslint/eslint-plugin/node_modules/semver": {
"version": "7.7.2",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz",
"integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==",
"version": "7.7.4",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz",
"integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==",
"license": "ISC",
"bin": {
"semver": "bin/semver.js"
@@ -8460,9 +8464,9 @@
}
},
"node_modules/axe-core": {
"version": "4.10.3",
"resolved": "https://registry.npmjs.org/axe-core/-/axe-core-4.10.3.tgz",
"integrity": "sha512-Xm7bpRXnDSX2YE2YFfBk2FnF0ep6tmG7xPh8iHee8MIcrgq762Nkce856dYtJYLkuIoYZvGfTs/PbZhideTcEg==",
"version": "4.11.1",
"resolved": "https://registry.npmjs.org/axe-core/-/axe-core-4.11.1.tgz",
"integrity": "sha512-BASOg+YwO2C+346x3LZOeoovTIoTrRqEsqMa6fmfAV0P+U9mFr9NsyOEpiYvFjbc64NMrSswhV50WdXzdb/Z5A==",
"license": "MPL-2.0",
"engines": {
"node": ">=4"
@@ -12950,9 +12954,9 @@
}
},
"node_modules/es-abstract": {
"version": "1.24.0",
"resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.24.0.tgz",
"integrity": "sha512-WSzPgsdLtTcQwm4CROfS5ju2Wa1QQcVeT37jFjYzdFz1r9ahadC8B8/a4qxJxM+09F18iumCdRmlr96ZYkQvEg==",
"version": "1.24.1",
"resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.24.1.tgz",
"integrity": "sha512-zHXBLhP+QehSSbsS9Pt23Gg964240DPd6QCf8WpkqEXxQ7fhdZzYsocOr5u7apWonsS5EjZDmTF+/slGMyasvw==",
"license": "MIT",
"dependencies": {
"array-buffer-byte-length": "^1.0.2",
@@ -13042,26 +13046,26 @@
}
},
"node_modules/es-iterator-helpers": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/es-iterator-helpers/-/es-iterator-helpers-1.2.1.tgz",
"integrity": "sha512-uDn+FE1yrDzyC0pCo961B2IHbdM8y/ACZsKD4dG6WqrjV53BADjwa7D+1aom2rsNVfLyDgU/eigvlJGJ08OQ4w==",
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/es-iterator-helpers/-/es-iterator-helpers-1.2.2.tgz",
"integrity": "sha512-BrUQ0cPTB/IwXj23HtwHjS9n7O4h9FX94b4xc5zlTHxeLgTAdzYUDyy6KdExAl9lbN5rtfe44xpjpmj9grxs5w==",
"license": "MIT",
"dependencies": {
"call-bind": "^1.0.8",
"call-bound": "^1.0.3",
"call-bound": "^1.0.4",
"define-properties": "^1.2.1",
"es-abstract": "^1.23.6",
"es-abstract": "^1.24.1",
"es-errors": "^1.3.0",
"es-set-tostringtag": "^2.0.3",
"es-set-tostringtag": "^2.1.0",
"function-bind": "^1.1.2",
"get-intrinsic": "^1.2.6",
"get-intrinsic": "^1.3.0",
"globalthis": "^1.0.4",
"gopd": "^1.2.0",
"has-property-descriptors": "^1.0.2",
"has-proto": "^1.2.0",
"has-symbols": "^1.1.0",
"internal-slot": "^1.1.0",
"iterator.prototype": "^1.1.4",
"iterator.prototype": "^1.1.5",
"safe-array-concat": "^1.1.3"
},
"engines": {
@@ -13334,9 +13338,9 @@
}
},
"node_modules/eslint-module-utils": {
"version": "2.12.0",
"resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.12.0.tgz",
"integrity": "sha512-wALZ0HFoytlyh/1+4wuZ9FJCD/leWHQzzrxJ8+rebyReSLk7LApMyd3WJaLVoN+D5+WIdJyDK1c6JnE65V4Zyg==",
"version": "2.12.1",
"resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.12.1.tgz",
"integrity": "sha512-L8jSWTze7K2mTg0vos/RuLRS5soomksDPoJLXIslC7c8Wmut3bx7CPpJijDcBZtxQ5lrbUdM+s0OlNbz0DCDNw==",
"license": "MIT",
"dependencies": {
"debug": "^3.2.7"
@@ -13376,29 +13380,29 @@
}
},
"node_modules/eslint-plugin-import": {
"version": "2.31.0",
"resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.31.0.tgz",
"integrity": "sha512-ixmkI62Rbc2/w8Vfxyh1jQRTdRTF52VxwRVHl/ykPAmqG+Nb7/kNn+byLP0LxPgI7zWA16Jt82SybJInmMia3A==",
"version": "2.32.0",
"resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.32.0.tgz",
"integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==",
"license": "MIT",
"dependencies": {
"@rtsao/scc": "^1.1.0",
"array-includes": "^3.1.8",
"array.prototype.findlastindex": "^1.2.5",
"array.prototype.flat": "^1.3.2",
"array.prototype.flatmap": "^1.3.2",
"array-includes": "^3.1.9",
"array.prototype.findlastindex": "^1.2.6",
"array.prototype.flat": "^1.3.3",
"array.prototype.flatmap": "^1.3.3",
"debug": "^3.2.7",
"doctrine": "^2.1.0",
"eslint-import-resolver-node": "^0.3.9",
"eslint-module-utils": "^2.12.0",
"eslint-module-utils": "^2.12.1",
"hasown": "^2.0.2",
"is-core-module": "^2.15.1",
"is-core-module": "^2.16.1",
"is-glob": "^4.0.3",
"minimatch": "^3.1.2",
"object.fromentries": "^2.0.8",
"object.groupby": "^1.0.3",
"object.values": "^1.2.0",
"object.values": "^1.2.1",
"semver": "^6.3.1",
"string.prototype.trimend": "^1.0.8",
"string.prototype.trimend": "^1.0.9",
"tsconfig-paths": "^3.15.0"
},
"engines": {
@@ -13532,6 +13536,26 @@
"eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0"
}
},
"node_modules/eslint-plugin-react-native": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/eslint-plugin-react-native/-/eslint-plugin-react-native-5.0.0.tgz",
"integrity": "sha512-VyWlyCC/7FC/aONibOwLkzmyKg4j9oI8fzrk9WYNs4I8/m436JuOTAFwLvEn1CVvc7La4cPfbCyspP4OYpP52Q==",
"dev": true,
"license": "MIT",
"dependencies": {
"eslint-plugin-react-native-globals": "^0.1.1"
},
"peerDependencies": {
"eslint": "^3.17.0 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9"
}
},
"node_modules/eslint-plugin-react-native-globals": {
"version": "0.1.2",
"resolved": "https://registry.npmjs.org/eslint-plugin-react-native-globals/-/eslint-plugin-react-native-globals-0.1.2.tgz",
"integrity": "sha512-9aEPf1JEpiTjcFAmmyw8eiIXmcNZOqaZyHO77wgm0/dWfT/oxC1SrIq8ET38pMxHYrcB6Uew+TzUVsBeczF88g==",
"dev": true,
"license": "MIT"
},
"node_modules/eslint-plugin-react/node_modules/doctrine": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz",
@@ -13685,7 +13709,7 @@
"version": "7.2.3",
"resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
"integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==",
"deprecated": "Glob versions prior to v9 are no longer supported",
"deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me",
"license": "ISC",
"dependencies": {
"fs.realpath": "^1.0.0",
@@ -13703,9 +13727,9 @@
}
},
"node_modules/eslint-plugin-testing-library/node_modules/semver": {
"version": "7.7.2",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz",
"integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==",
"version": "7.7.4",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz",
"integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==",
"license": "ISC",
"bin": {
"semver": "bin/semver.js"
@@ -13949,9 +13973,9 @@
}
},
"node_modules/eslint/node_modules/js-yaml": {
"version": "3.14.1",
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz",
"integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==",
"version": "3.14.2",
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.2.tgz",
"integrity": "sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg==",
"license": "MIT",
"dependencies": {
"argparse": "^1.0.7",
@@ -13968,9 +13992,9 @@
"license": "MIT"
},
"node_modules/eslint/node_modules/semver": {
"version": "7.7.2",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz",
"integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==",
"version": "7.7.4",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz",
"integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==",
"license": "ISC",
"bin": {
"semver": "bin/semver.js"
@@ -14055,9 +14079,9 @@
}
},
"node_modules/esquery": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz",
"integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==",
"version": "1.7.0",
"resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz",
"integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==",
"license": "BSD-3-Clause",
"dependencies": {
"estraverse": "^5.1.0"

View File

@@ -44,7 +44,11 @@
},
"devDependencies": {
"@babel/core": "^7.20.0",
"@react-native-community/cli": "latest"
"@react-native-community/cli": "latest",
"eslint": "^7.32.0",
"eslint-plugin-import": "^2.32.0",
"eslint-plugin-react": "^7.37.5",
"eslint-plugin-react-native": "^5.0.0"
},
"private": true
}

View File

@@ -0,0 +1,3 @@
export const Colors = {
background: '#f5f5f5'
};

View File

@@ -0,0 +1,13 @@
export const InitialRegions = {
paris : {
latitude: 48.864,
longitude: 2.342,
latitudeDelta: 0,
longitudeDelta: 50
}
};
export const ZoneTypes = {
circle: "circle",
polygon: "polygon"
};

View File

@@ -0,0 +1,16 @@
export const secondsToMMSS = (seconds) => {
if (!Number.isInteger(seconds)) return "Inconnue";
if (seconds < 0) seconds = 0;
const strMinutes = String(Math.floor(seconds / 60));
const strSeconds = String(Math.floor(seconds % 60));
return strMinutes.padStart(2,"0") + ":" + strSeconds.padStart(2,"0");
};
export const secondsToHHMMSS = (seconds) => {
if (!Number.isInteger(seconds)) return "Inconnue";
if (seconds < 0) seconds = 0;
const strHours = String(Math.floor(seconds / 3600));
const strMinutes = String(Math.floor(seconds / 60 % 60));
const strSeconds = String(Math.floor(seconds % 60));
return strHours.padStart(2,"0") + ":" + strMinutes.padStart(2,"0") + ":" + strSeconds.padStart(2,"0");
};

View File

@@ -3,4 +3,4 @@ export const GameState = {
PLACEMENT: "placement",
PLAYING: "playing",
FINISHED: "finished"
}
};

View File

@@ -1,3 +0,0 @@
{
"extends": "next/core-web-vitals"
}

View File

@@ -1,6 +1,4 @@
module.exports = {
plugins: {
export const plugins = {
tailwindcss: {},
autoprefixer: {},
},
};

View File

@@ -1,6 +1,6 @@
/** @type {import('tailwindcss').Config} */
module.exports = {
theme: {
export const theme = {
extend: {
colors: {
'custom-green': '#19e119',
@@ -11,16 +11,19 @@ module.exports = {
'custom-light-blue': '#80b3ff'
}
}
},
mode: 'jit',
content: [
};
export const mode = 'jit';
export const content = [
"./pages/**/*.{js,ts,jsx,tsx,mdx}",
"./components/**/*.{js,ts,jsx,tsx,mdx}",
"./app/**/*.{js,ts,jsx,tsx,mdx}",
],
fontFamily: {
];
export const fontFamily = {
sans: ["Inter", "sans-serif"],
serif: ["Merriweather", "serif"],
},
plugins: [],
};
export const plugins = [];