From 0768609ada02b3a14ac3c3f05bb65314c430a522 Mon Sep 17 00:00:00 2001 From: Sebastien Riviere Date: Sun, 15 Feb 2026 19:23:29 +0100 Subject: [PATCH] eslint for mobile + new maps API key + cleaning --- mobile/traque-app/.eslintrc.js | 46 ++ mobile/traque-app/app.json | 5 +- .../app/{+not-found.js => +not-found.jsx} | 1 + .../app/{_layout.js => _layout.jsx} | 10 +- mobile/traque-app/app/display.js | 600 ------------------ mobile/traque-app/app/{index.js => index.jsx} | 33 +- mobile/traque-app/app/interface.jsx | 172 +++++ .../components/{button.js => button.jsx} | 3 +- mobile/traque-app/components/drawer.jsx | 182 ++++++ .../components/{image.js => image.jsx} | 5 +- .../components/{input.js => input.jsx} | 3 +- mobile/traque-app/components/map.jsx | 114 ++++ .../components/{stat.js => stat.jsx} | 3 +- mobile/traque-app/components/timer.jsx | 20 + mobile/traque-app/context/socketContext.jsx | 16 +- .../context/teamConnexionContext.jsx | 13 +- mobile/traque-app/context/teamContext.jsx | 22 +- mobile/traque-app/hook/useGame.jsx | 6 +- mobile/traque-app/hook/useLocalStorage.jsx | 8 +- mobile/traque-app/hook/useLocation.jsx | 10 +- mobile/traque-app/hook/usePickImage.jsx | 6 +- mobile/traque-app/hook/useSendDeviceInfo.jsx | 8 +- mobile/traque-app/hook/useSocketAuth.jsx | 6 +- mobile/traque-app/hook/useSocketListener.jsx | 8 +- mobile/traque-app/hook/useTimeDifference.jsx | 6 +- mobile/traque-app/package-lock.json | 120 ++-- mobile/traque-app/package.json | 6 +- mobile/traque-app/util/colors.js | 3 + mobile/traque-app/util/constants.js | 13 + mobile/traque-app/util/format.js | 16 + mobile/traque-app/util/gameState.js | 2 +- server/traque-front/.eslintrc.json | 3 - server/traque-front/postcss.config.js | 8 +- server/traque-front/tailwind.config.js | 49 +- 34 files changed, 765 insertions(+), 761 deletions(-) create mode 100644 mobile/traque-app/.eslintrc.js rename mobile/traque-app/app/{+not-found.js => +not-found.jsx} (89%) rename mobile/traque-app/app/{_layout.js => _layout.jsx} (75%) delete mode 100644 mobile/traque-app/app/display.js rename mobile/traque-app/app/{index.js => index.jsx} (85%) create mode 100644 mobile/traque-app/app/interface.jsx rename mobile/traque-app/components/{button.js => button.jsx} (90%) create mode 100644 mobile/traque-app/components/drawer.jsx rename mobile/traque-app/components/{image.js => image.jsx} (93%) rename mobile/traque-app/components/{input.js => input.jsx} (84%) create mode 100644 mobile/traque-app/components/map.jsx rename mobile/traque-app/components/{stat.js => stat.jsx} (88%) create mode 100644 mobile/traque-app/components/timer.jsx create mode 100644 mobile/traque-app/util/colors.js create mode 100644 mobile/traque-app/util/constants.js create mode 100644 mobile/traque-app/util/format.js delete mode 100644 server/traque-front/.eslintrc.json diff --git a/mobile/traque-app/.eslintrc.js b/mobile/traque-app/.eslintrc.js new file mode 100644 index 0000000..5e0f171 --- /dev/null +++ b/mobile/traque-app/.eslintrc.js @@ -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'], + }, + }, + } +}; \ No newline at end of file diff --git a/mobile/traque-app/app.json b/mobile/traque-app/app.json index 0d6bf8e..1bee4fb 100644 --- a/mobile/traque-app/app.json +++ b/mobile/traque-app/app.json @@ -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" } } } diff --git a/mobile/traque-app/app/+not-found.js b/mobile/traque-app/app/+not-found.jsx similarity index 89% rename from mobile/traque-app/app/+not-found.js rename to mobile/traque-app/app/+not-found.jsx index f2f1dd7..f011680 100644 --- a/mobile/traque-app/app/+not-found.js +++ b/mobile/traque-app/app/+not-found.jsx @@ -1,3 +1,4 @@ +// Expo import { Unmatched } from 'expo-router'; export default Unmatched; diff --git a/mobile/traque-app/app/_layout.js b/mobile/traque-app/app/_layout.jsx similarity index 75% rename from mobile/traque-app/app/_layout.js rename to mobile/traque-app/app/_layout.jsx index b2f64fc..cbe85b9 100644 --- a/mobile/traque-app/app/_layout.js +++ b/mobile/traque-app/app/_layout.jsx @@ -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 ( @@ -13,4 +15,6 @@ export default function Layout() { ); -} +}; + +export default Layout; diff --git a/mobile/traque-app/app/display.js b/mobile/traque-app/app/display.js deleted file mode 100644 index f1f1088..0000000 --- a/mobile/traque-app/app/display.js +++ /dev/null @@ -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 ( - - - - ); - } - - const Settings = () => { - return ( - Alert.alert("Settings")}> - - - ); - } - - const TeamName = () => { - return( - {(name ?? "Indisponible")} - ); - } - - const GameLog = () => { - return ( - - { gameState == GameState.SETUP && {messages?.waiting || "Préparation de la partie"}} - { gameState == GameState.PLACEMENT && Phase de placement} - { gameState == GameState.PLAYING && !outOfZone && La partie est en cours} - { gameState == GameState.PLAYING && outOfZone && !hasHandicap && {`Veuillez retourner dans la zone\nHandicap dans ${formatTimeMinutes(-timeLeftOutOfZone)}`}} - { gameState == GameState.PLAYING && hasHandicap && {`Veuillez retourner dans la zone\nVotre position est révélée en continue`}} - { gameState == GameState.FINISHED && La partie est terminée} - - ); - } - - const TimeBeforeNextZone = () => { - return ( - - {isShrinking ? "Réduction de la zone" : "Durée de la zone"} - {formatTimeMinutes(-timeLeftNextZone)} - - ); - } - - const TimeBeforeNextPosition = () => { - return ( - - Position envoyée dans - { !hasHandicap ? formatTimeMinutes(-timeLeftSendLocation) : "00:00"} - - ); - } - - const Timers = () => { - return ( - - - { TimeBeforeNextZone() } - - - { TimeBeforeNextPosition() } - - - ); - } - - const Ready = () => { - return ( - - - {ready ? "Placé" : "Non placé"} - - - ); - } - - const CapturedMessage = () => { - return ( - - {messages?.captured || "Vous avez été éliminé..."} - - ); - } - - const EndGameMessage = () => { - return ( - - {captured && {messages?.loser || "Vous avez perdu..."}} - {!captured && {messages?.winner || "Vous avez gagné !"}} - - ); - } - - const Zones = () => { - const latToLatitude = (pos) => ({latitude: pos.lat, longitude: pos.lng}); - - return ( - - { zoneType == zoneTypes.circle && zoneExtremities.begin && } - { zoneType == zoneTypes.circle && zoneExtremities.end && } - { zoneType == zoneTypes.polygon && zoneExtremities.begin && latToLatitude(pos))} strokeColor="red" fillColor="rgba(255,0,0,0.1)" strokeWidth={2} /> } - { zoneType == zoneTypes.polygon && zoneExtremities.end && latToLatitude(pos))} strokeColor="green" fillColor="rgba(0,255,0,0.1)" strokeWidth={2} /> } - - ); - } - - const Map = () => { - return ( - setCenterMap(false)} toolbarEnabled={false}> - { gameState == GameState.PLACEMENT && startingArea && } - { gameState == GameState.PLAYING && zoneExtremities && } - { location && - Alert.alert("Position actuelle", "Ceci est votre position")}> - - - } - { gameState == GameState.PLAYING && lastSentLocation && !hasHandicap && - Alert.alert("Position envoyée", "Ceci est votre dernière position connue par le serveur")}> - - - } - { gameState == GameState.PLAYING && enemyLocation && !hasHandicap && - - Alert.alert("Position ennemie", "Ceci est la dernière position de vos ennemis connue")}/> - - } - - ); - } - - const UpdatePositionButton = () => { - return ( !hasHandicap && - - - - ); - } - - const CenterMapButton = () => { - return ( - setCenterMap(true)}> - - - ); - } - - const LayerButton = () => { - return( - Alert.alert("Layer")}> - - - ); - } - - const CollapsibleButton = () => { - return ( - - - - ); - } - - const TeamCaptureCode = () => { - return ( - Code de {(name ?? "Indisponible")} : {String(captureCode).padStart(4,"0")} - ); - } - - const ChasedTeamImage = () => { - return ( - - {{"Cible (" + (enemyName ?? "Indisponible") + ")"}} - {} - - ); - } - - const CaptureCode = () => { - return ( - - - - ); - } - - const CaptureButton = () => { - return ( - - - - - - ); - } - - const Stats = () => { - return ( - - - {Math.floor(distance / 100) / 10}km - {formatTimeHours((finishDate ? Math.floor((finishDate - startDate) / 1000) : timeSinceStart))} - {avgSpeed}km/h - - - {nCaptures} - {nSentLocation} - - - ); - } - - return ( - - - - { Logout() } - { false && Settings() } - - - { TeamName() } - - - { GameLog() } - - { gameState == GameState.PLACEMENT && - Ready() - } - { gameState == GameState.PLAYING && !captured && - {Timers()} - {enemyHasHandicap && Position ennemie révélée en continue !} - } - { gameState == GameState.PLAYING && captured && - CapturedMessage() - } - { gameState == GameState.FINISHED && - EndGameMessage() - } - - {setBottomContainerHeight(event.nativeEvent.layout.height)}}> - - { Map() } - - { !centerMap && CenterMapButton() } - { false && gameState == GameState.PLAYING && !captured && - - { LayerButton() } - - } - { gameState == GameState.PLAYING && !captured && - - { UpdatePositionButton() } - - } - - { (gameState == GameState.PLAYING || gameState == GameState.FINISHED) && - - - - { CollapsibleButton() } - - - { gameState == GameState.PLAYING && TeamCaptureCode() } - { gameState == GameState.PLAYING && !hasHandicap && - { ChasedTeamImage() } - - { CaptureCode() } - { CaptureButton() } - - } - { Stats() } - - - - - } - - - ); -} - -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' - }, -}); diff --git a/mobile/traque-app/app/index.js b/mobile/traque-app/app/index.jsx similarity index 85% rename from mobile/traque-app/app/index.js rename to mobile/traque-app/app/index.jsx index f5accba..347dd40 100644 --- a/mobile/traque-app/app/index.js +++ b/mobile/traque-app/app/index.jsx @@ -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() { - Appuyer pour changer la photo d'équipe + Appuyer pour changer la photo d'équipe (Le haut du corps doit être visible) @@ -79,14 +80,16 @@ export default function Index() { ); -} +}; + +export default Index; const styles = StyleSheet.create({ container: { flexGrow: 1, alignItems: 'center', paddingVertical: 20, - backgroundColor: backgroundColor + backgroundColor: Colors.background }, transitionContainer: { flexGrow: 1, diff --git a/mobile/traque-app/app/interface.jsx b/mobile/traque-app/app/interface.jsx new file mode 100644 index 0000000..74371b2 --- /dev/null +++ b/mobile/traque-app/app/interface.jsx @@ -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 ( + + + + + + + Alert.alert("Settings")}> + + + + + {(name ?? "Indisponible")} + + + + { gameState == GameState.SETUP && {messages?.waiting || "Préparation de la partie"}} + { gameState == GameState.PLACEMENT && Phase de placement} + { gameState == GameState.PLAYING && !outOfZone && La partie est en cours} + { gameState == GameState.PLAYING && outOfZone && !hasHandicap && {`Veuillez retourner dans la zone\nHandicap dans ${secondsToMMSS(-timeLeftOutOfZone)}`}} + { gameState == GameState.PLAYING && hasHandicap && {`Veuillez retourner dans la zone\nVotre position est révélée en continue`}} + { gameState == GameState.FINISHED && La partie est terminée} + + + { gameState == GameState.PLACEMENT && + + + {ready ? "Placé" : "Non placé"} + + + } + { gameState == GameState.PLAYING && !captured && + + + + + {enemyHasHandicap && + Position ennemie révélée en continue ! + } + } + { gameState == GameState.PLAYING && captured && + + {messages?.captured || "Vous avez été éliminé..."} + + } + { gameState == GameState.FINISHED && + + {captured && {captured ? (messages?.loser || "Vous avez perdu...") : (messages?.winner || "Vous avez gagné !")}} + + } + + setBottomContainerHeight(event.nativeEvent.layout.height)}> + + + + + ); +}; + +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, + } +}); diff --git a/mobile/traque-app/components/button.js b/mobile/traque-app/components/button.jsx similarity index 90% rename from mobile/traque-app/components/button.js rename to mobile/traque-app/components/button.jsx index 8af3355..bde22a2 100644 --- a/mobile/traque-app/components/button.js +++ b/mobile/traque-app/components/button.jsx @@ -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 ( diff --git a/mobile/traque-app/components/drawer.jsx b/mobile/traque-app/components/drawer.jsx new file mode 100644 index 0000000..7365a7a --- /dev/null +++ b/mobile/traque-app/components/drawer.jsx @@ -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 ( + + + + setCollapsibleState(!collapsibleState)} style={styles.collapsibleButton} underlayColor="#d9d9d9"> + + + + + { gameState == GameState.PLAYING && + Code de {(name ?? "Indisponible")} : {String(captureCode).padStart(4,"0")} + } + { gameState == GameState.PLAYING && !hasHandicap && + + {{"Cible (" + (enemyName ?? "Indisponible") + ")"}} + {} + + + + + + + + + + + + } + + + {Math.floor(distance / 100) / 10}km + {secondsToHHMMSS((finishDate ? Math.floor((finishDate - startDate) / 1000) : timeSinceStart))} + {avgSpeed}km/h + + + {nCaptures} + {nSentLocation} + + + + + + + ); +}; + +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' + }, +}); diff --git a/mobile/traque-app/components/image.js b/mobile/traque-app/components/image.jsx similarity index 93% rename from mobile/traque-app/components/image.js rename to mobile/traque-app/components/image.jsx index 646ec92..8b58407 100644 --- a/mobile/traque-app/components/image.js +++ b/mobile/traque-app/components/image.jsx @@ -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 }) { /> ); -} +}; const styles = StyleSheet.create({ container: { diff --git a/mobile/traque-app/components/input.js b/mobile/traque-app/components/input.jsx similarity index 84% rename from mobile/traque-app/components/input.js rename to mobile/traque-app/components/input.jsx index a564de3..1148506 100644 --- a/mobile/traque-app/components/input.js +++ b/mobile/traque-app/components/input.jsx @@ -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 ( { + 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 ( + + setCenterMap(false)} toolbarEnabled={false}> + { gameState == GameState.PLACEMENT && startingArea && + + } + { gameState == GameState.PLAYING && zoneExtremities && + + { zoneType == ZoneTypes.circle && zoneExtremities.begin && } + { zoneType == ZoneTypes.circle && zoneExtremities.end && } + { zoneType == ZoneTypes.polygon && zoneExtremities.begin && latToLatitude(pos))} strokeColor="red" fillColor="rgba(255,0,0,0.1)" strokeWidth={2} /> } + { zoneType == ZoneTypes.polygon && zoneExtremities.end && latToLatitude(pos))} strokeColor="green" fillColor="rgba(0,255,0,0.1)" strokeWidth={2} /> } + + } + { location && + Alert.alert("Position actuelle", "Ceci est votre position")}> + + + } + { gameState == GameState.PLAYING && lastSentLocation && !hasHandicap && + Alert.alert("Position envoyée", "Ceci est votre dernière position connue par le serveur")}> + + + } + { gameState == GameState.PLAYING && enemyLocation && !hasHandicap && + + Alert.alert("Position ennemie", "Ceci est la dernière position de vos ennemis connue")}/> + + } + + + { !centerMap && + setCenterMap(true)}> + + + } + { gameState == GameState.PLAYING && !captured && + + { !hasHandicap && + + + + } + + } + + ); +}; + +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', + }, +}); diff --git a/mobile/traque-app/components/stat.js b/mobile/traque-app/components/stat.jsx similarity index 88% rename from mobile/traque-app/components/stat.js rename to mobile/traque-app/components/stat.jsx index 370f39d..714a093 100644 --- a/mobile/traque-app/components/stat.js +++ b/mobile/traque-app/components/stat.jsx @@ -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 ( Alert.alert("Info", description) : null}> diff --git a/mobile/traque-app/components/timer.jsx b/mobile/traque-app/components/timer.jsx new file mode 100644 index 0000000..8d69bc3 --- /dev/null +++ b/mobile/traque-app/components/timer.jsx @@ -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 ( + + {title} + {secondsToMMSS(seconds)} + + ); +}; + +const styles = StyleSheet.create({ + container: { + alignItems: 'center', + justifyContent: 'center', + } +}); diff --git a/mobile/traque-app/context/socketContext.jsx b/mobile/traque-app/context/socketContext.jsx index 88d2749..59d9329 100644 --- a/mobile/traque-app/context/socketContext.jsx +++ b/mobile/traque-app/context/socketContext.jsx @@ -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 ( {children} ); -} +}; -export function useSocket() { +export const useSocket = () => { return useContext(SocketContext); -} +}; diff --git a/mobile/traque-app/context/teamConnexionContext.jsx b/mobile/traque-app/context/teamConnexionContext.jsx index 2a76591..3b63297 100644 --- a/mobile/traque-app/context/teamConnexionContext.jsx +++ b/mobile/traque-app/context/teamConnexionContext.jsx @@ -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 ( {children} ); -} +}; -function useTeamConnexion() { +export const useTeamConnexion = () => { return useContext(teamConnexionContext); -} - -export { TeamConnexionProvider, useTeamConnexion }; - +}; diff --git a/mobile/traque-app/context/teamContext.jsx b/mobile/traque-app/context/teamContext.jsx index b5e338f..3f4388e 100644 --- a/mobile/traque-app/context/teamContext.jsx +++ b/mobile/traque-app/context/teamContext.jsx @@ -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 ( {children} ); -} +}; -function useTeamContext() { +export const useTeamContext = () => { return useContext(teamContext); -} - -export { TeamProvider, useTeamContext }; +}; diff --git a/mobile/traque-app/hook/useGame.jsx b/mobile/traque-app/hook/useGame.jsx index ec1f07f..a02bdc1 100644 --- a/mobile/traque-app/hook/useGame.jsx +++ b/mobile/traque-app/hook/useGame.jsx @@ -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}; -} \ No newline at end of file +}; diff --git a/mobile/traque-app/hook/useLocalStorage.jsx b/mobile/traque-app/hook/useLocalStorage.jsx index fcbdfb2..946aca8 100644 --- a/mobile/traque-app/hook/useLocalStorage.jsx +++ b/mobile/traque-app/hook/useLocalStorage.jsx @@ -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]; -} \ No newline at end of file +}; diff --git a/mobile/traque-app/hook/useLocation.jsx b/mobile/traque-app/hook/useLocation.jsx index c63cbc3..e8936d5 100644 --- a/mobile/traque-app/hook/useLocation.jsx +++ b/mobile/traque-app/hook/useLocation.jsx @@ -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]; -} +}; diff --git a/mobile/traque-app/hook/usePickImage.jsx b/mobile/traque-app/hook/usePickImage.jsx index c200170..778365b 100644 --- a/mobile/traque-app/hook/usePickImage.jsx +++ b/mobile/traque-app/hook/usePickImage.jsx @@ -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}; -} +}; diff --git a/mobile/traque-app/hook/useSendDeviceInfo.jsx b/mobile/traque-app/hook/useSendDeviceInfo.jsx index 72e9ef0..868b46e 100644 --- a/mobile/traque-app/hook/useSendDeviceInfo.jsx +++ b/mobile/traque-app/hook/useSendDeviceInfo.jsx @@ -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; -} +}; diff --git a/mobile/traque-app/hook/useSocketAuth.jsx b/mobile/traque-app/hook/useSocketAuth.jsx index 1c91484..267c598 100644 --- a/mobile/traque-app/hook/useSocketAuth.jsx +++ b/mobile/traque-app/hook/useSocketAuth.jsx @@ -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}; -} \ No newline at end of file +}; diff --git a/mobile/traque-app/hook/useSocketListener.jsx b/mobile/traque-app/hook/useSocketListener.jsx index 6f9adc3..2d20298 100644 --- a/mobile/traque-app/hook/useSocketListener.jsx +++ b/mobile/traque-app/hook/useSocketListener.jsx @@ -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); - } - }, []); -} \ No newline at end of file + }; + }, [callback, event, socket]); +}; diff --git a/mobile/traque-app/hook/useTimeDifference.jsx b/mobile/traque-app/hook/useTimeDifference.jsx index d3ae6f5..9ee818b 100644 --- a/mobile/traque-app/hook/useTimeDifference.jsx +++ b/mobile/traque-app/hook/useTimeDifference.jsx @@ -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]; -} +}; diff --git a/mobile/traque-app/package-lock.json b/mobile/traque-app/package-lock.json index ace3ab3..6a562b7 100644 --- a/mobile/traque-app/package-lock.json +++ b/mobile/traque-app/package-lock.json @@ -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" diff --git a/mobile/traque-app/package.json b/mobile/traque-app/package.json index 9fce0da..8281c79 100644 --- a/mobile/traque-app/package.json +++ b/mobile/traque-app/package.json @@ -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 } diff --git a/mobile/traque-app/util/colors.js b/mobile/traque-app/util/colors.js new file mode 100644 index 0000000..bad3068 --- /dev/null +++ b/mobile/traque-app/util/colors.js @@ -0,0 +1,3 @@ +export const Colors = { + background: '#f5f5f5' +}; diff --git a/mobile/traque-app/util/constants.js b/mobile/traque-app/util/constants.js new file mode 100644 index 0000000..ec0be5e --- /dev/null +++ b/mobile/traque-app/util/constants.js @@ -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" +}; \ No newline at end of file diff --git a/mobile/traque-app/util/format.js b/mobile/traque-app/util/format.js new file mode 100644 index 0000000..1069750 --- /dev/null +++ b/mobile/traque-app/util/format.js @@ -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"); +}; diff --git a/mobile/traque-app/util/gameState.js b/mobile/traque-app/util/gameState.js index e5514f7..3333b42 100644 --- a/mobile/traque-app/util/gameState.js +++ b/mobile/traque-app/util/gameState.js @@ -3,4 +3,4 @@ export const GameState = { PLACEMENT: "placement", PLAYING: "playing", FINISHED: "finished" -} \ No newline at end of file +}; diff --git a/server/traque-front/.eslintrc.json b/server/traque-front/.eslintrc.json deleted file mode 100644 index bffb357..0000000 --- a/server/traque-front/.eslintrc.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "extends": "next/core-web-vitals" -} diff --git a/server/traque-front/postcss.config.js b/server/traque-front/postcss.config.js index 12a703d..70cccb4 100644 --- a/server/traque-front/postcss.config.js +++ b/server/traque-front/postcss.config.js @@ -1,6 +1,4 @@ -module.exports = { - plugins: { - tailwindcss: {}, - autoprefixer: {}, - }, +export const plugins = { + tailwindcss: {}, + autoprefixer: {}, }; diff --git a/server/traque-front/tailwind.config.js b/server/traque-front/tailwind.config.js index 584744e..f421409 100644 --- a/server/traque-front/tailwind.config.js +++ b/server/traque-front/tailwind.config.js @@ -1,26 +1,29 @@ /** @type {import('tailwindcss').Config} */ -module.exports = { - theme: { - extend: { - colors: { - 'custom-green': '#19e119', - 'custom-red': '#e11919', - 'custom-orange': '#fa6400', - 'custom-blue': '#1e90ff', - 'custom-grey': '#808080', - 'custom-light-blue': '#80b3ff' - } + +export const theme = { + extend: { + colors: { + 'custom-green': '#19e119', + 'custom-red': '#e11919', + 'custom-orange': '#fa6400', + 'custom-blue': '#1e90ff', + 'custom-grey': '#808080', + 'custom-light-blue': '#80b3ff' } - }, - mode: 'jit', - content: [ - "./pages/**/*.{js,ts,jsx,tsx,mdx}", - "./components/**/*.{js,ts,jsx,tsx,mdx}", - "./app/**/*.{js,ts,jsx,tsx,mdx}", - ], - fontFamily: { - sans: ["Inter", "sans-serif"], - serif: ["Merriweather", "serif"], - }, - plugins: [], + } }; + +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}", +]; + +export const fontFamily = { + sans: ["Inter", "sans-serif"], + serif: ["Merriweather", "serif"], +}; + +export const plugins = [];