mirror of
https://git.rezel.net/LudoTech/traque.git
synced 2026-04-10 16:30:18 +02:00
eslint for mobile + new maps API key + cleaning
This commit is contained in:
46
mobile/traque-app/.eslintrc.js
Normal file
46
mobile/traque-app/.eslintrc.js
Normal 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'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -43,7 +43,7 @@
|
|||||||
],
|
],
|
||||||
"config": {
|
"config": {
|
||||||
"googleMaps": {
|
"googleMaps": {
|
||||||
"apiKey": "AIzaSyD0yuWIHFbsIDVfGQ9wEM3pOtVC2TgEO1U"
|
"apiKey": "AIzaSyAA0Y1vdDMsqtrg18ZaWx098uEgNAyJPq0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -55,6 +55,9 @@
|
|||||||
],
|
],
|
||||||
"NSLocationAlwaysAndWhenInUseUsageDescription": "Your location is used to track you in the background.",
|
"NSLocationAlwaysAndWhenInUseUsageDescription": "Your location is used to track you in the background.",
|
||||||
"NSLocationWhenInUseUsageDescription": "Location is used to track your movement."
|
"NSLocationWhenInUseUsageDescription": "Location is used to track your movement."
|
||||||
|
},
|
||||||
|
"config": {
|
||||||
|
"googleMapsApiKey": "AIzaSyAA0Y1vdDMsqtrg18ZaWx098uEgNAyJPq0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
// Expo
|
||||||
import { Unmatched } from 'expo-router';
|
import { Unmatched } from 'expo-router';
|
||||||
|
|
||||||
export default Unmatched;
|
export default Unmatched;
|
||||||
@@ -1,9 +1,11 @@
|
|||||||
|
// Expo
|
||||||
import { Slot } from 'expo-router';
|
import { Slot } from 'expo-router';
|
||||||
import SocketProvider from "../context/socketContext";
|
// Contexts
|
||||||
|
import { SocketProvider } from "../context/socketContext";
|
||||||
import { TeamConnexionProvider } from "../context/teamConnexionContext";
|
import { TeamConnexionProvider } from "../context/teamConnexionContext";
|
||||||
import { TeamProvider } from "../context/teamContext";
|
import { TeamProvider } from "../context/teamContext";
|
||||||
|
|
||||||
export default function Layout() {
|
const Layout = () => {
|
||||||
return (
|
return (
|
||||||
<SocketProvider>
|
<SocketProvider>
|
||||||
<TeamConnexionProvider>
|
<TeamConnexionProvider>
|
||||||
@@ -13,4 +15,6 @@ export default function Layout() {
|
|||||||
</TeamConnexionProvider>
|
</TeamConnexionProvider>
|
||||||
</SocketProvider>
|
</SocketProvider>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
|
export default Layout;
|
||||||
@@ -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'
|
|
||||||
},
|
|
||||||
});
|
|
||||||
@@ -4,18 +4,19 @@ import { ScrollView, View, Text, StyleSheet, Image, Alert } from 'react-native';
|
|||||||
// Expo
|
// Expo
|
||||||
import { useRouter } from 'expo-router';
|
import { useRouter } from 'expo-router';
|
||||||
// Components
|
// Components
|
||||||
import CustomButton from '../components/button';
|
import { CustomButton } from '../components/button';
|
||||||
import CustomImage from '../components/image';
|
import { CustomImage } from '../components/image';
|
||||||
import CustomTextInput from '../components/input';
|
import { CustomTextInput } from '../components/input';
|
||||||
// Other
|
// Contexts
|
||||||
import { useSocket } from '../context/socketContext';
|
import { useSocket } from "../context/socketContext";
|
||||||
import { useTeamContext } from '../context/teamContext';
|
|
||||||
import { useTeamConnexion } from "../context/teamConnexionContext";
|
import { useTeamConnexion } from "../context/teamConnexionContext";
|
||||||
|
import { useTeamContext } from "../context/teamContext";
|
||||||
|
// Hooks
|
||||||
import { usePickImage } from '../hook/usePickImage';
|
import { usePickImage } from '../hook/usePickImage';
|
||||||
|
// Util
|
||||||
|
import { Colors } from '../util/colors';
|
||||||
|
|
||||||
const backgroundColor = '#f5f5f5';
|
const Index = () => {
|
||||||
|
|
||||||
export default function Index() {
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const {SERVER_URL} = useSocket();
|
const {SERVER_URL} = useSocket();
|
||||||
const {login, loggedIn, loading} = useTeamConnexion();
|
const {login, loggedIn, loading} = useTeamConnexion();
|
||||||
@@ -27,14 +28,14 @@ export default function Index() {
|
|||||||
// Disbaling location tracking
|
// Disbaling location tracking
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
stopLocationTracking();
|
stopLocationTracking();
|
||||||
}, []);
|
}, [stopLocationTracking]);
|
||||||
|
|
||||||
// Routeur
|
// Routeur
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!loading && loggedIn) {
|
if (!loading && loggedIn) {
|
||||||
router.replace("/display");
|
router.replace("/interface");
|
||||||
}
|
}
|
||||||
}, [loggedIn, loading]);
|
}, [router, loggedIn, loading]);
|
||||||
|
|
||||||
function handleSubmit() {
|
function handleSubmit() {
|
||||||
if (!isSubmitting && !loading) {
|
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}/>
|
<CustomTextInput value={teamID} inputMode="numeric" placeholder="ID de l'équipe" style={styles.input} onChangeText={setTeamID}/>
|
||||||
</View>
|
</View>
|
||||||
<View style={styles.subContainer}>
|
<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'équipe</Text>
|
||||||
<Text style={{fontSize: 13, marginBottom: 3}}>(Le haut du corps doit être visible)</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}/>
|
<CustomImage source={image ? {uri: image.uri} : require('../assets/images/missing_image.jpg')} onPress={pickImage}/>
|
||||||
</View>
|
</View>
|
||||||
@@ -79,14 +80,16 @@ export default function Index() {
|
|||||||
</View>
|
</View>
|
||||||
</ScrollView>
|
</ScrollView>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
|
export default Index;
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
const styles = StyleSheet.create({
|
||||||
container: {
|
container: {
|
||||||
flexGrow: 1,
|
flexGrow: 1,
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
paddingVertical: 20,
|
paddingVertical: 20,
|
||||||
backgroundColor: backgroundColor
|
backgroundColor: Colors.background
|
||||||
},
|
},
|
||||||
transitionContainer: {
|
transitionContainer: {
|
||||||
flexGrow: 1,
|
flexGrow: 1,
|
||||||
172
mobile/traque-app/app/interface.jsx
Normal file
172
mobile/traque-app/app/interface.jsx
Normal 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,
|
||||||
|
}
|
||||||
|
});
|
||||||
@@ -1,7 +1,8 @@
|
|||||||
|
// React
|
||||||
import { forwardRef } from 'react';
|
import { forwardRef } from 'react';
|
||||||
import { TouchableHighlight, View, Text, StyleSheet } from "react-native";
|
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 (
|
return (
|
||||||
<View style={styles.buttonContainer}>
|
<View style={styles.buttonContainer}>
|
||||||
<TouchableHighlight style={styles.button} onPress={onPress} ref={ref}>
|
<TouchableHighlight style={styles.button} onPress={onPress} ref={ref}>
|
||||||
182
mobile/traque-app/components/drawer.jsx
Normal file
182
mobile/traque-app/components/drawer.jsx
Normal 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'
|
||||||
|
},
|
||||||
|
});
|
||||||
@@ -1,8 +1,9 @@
|
|||||||
|
// React
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { StyleSheet, View, Image, TouchableOpacity } from "react-native";
|
import { StyleSheet, View, Image, TouchableOpacity } from "react-native";
|
||||||
import ImageViewing from 'react-native-image-viewing';
|
import ImageViewing from 'react-native-image-viewing';
|
||||||
|
|
||||||
export default function CustomImage({ source, canZoom, onPress }) {
|
export const CustomImage = ({ source, canZoom, onPress }) => {
|
||||||
// canZoom : boolean
|
// canZoom : boolean
|
||||||
const [isModalVisible, setIsModalVisible] = useState(false);
|
const [isModalVisible, setIsModalVisible] = useState(false);
|
||||||
|
|
||||||
@@ -20,7 +21,7 @@ export default function CustomImage({ source, canZoom, onPress }) {
|
|||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
const styles = StyleSheet.create({
|
||||||
container: {
|
container: {
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
|
// React
|
||||||
import { TextInput, StyleSheet } from 'react-native';
|
import { TextInput, StyleSheet } from 'react-native';
|
||||||
|
|
||||||
export default function CustomTextInput({ style, value, inputMode, placeholder, onChangeText }) {
|
export const CustomTextInput = ({ style, value, inputMode, placeholder, onChangeText }) => {
|
||||||
return (
|
return (
|
||||||
<TextInput
|
<TextInput
|
||||||
value={value}
|
value={value}
|
||||||
114
mobile/traque-app/components/map.jsx
Normal file
114
mobile/traque-app/components/map.jsx
Normal 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',
|
||||||
|
},
|
||||||
|
});
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
|
// React
|
||||||
import { TouchableOpacity, View, Image, Text, Alert } from 'react-native';
|
import { TouchableOpacity, View, Image, Text, Alert } from 'react-native';
|
||||||
|
|
||||||
export default function Stat({ children, source, description }) {
|
export const Stat = ({ children, source, description }) => {
|
||||||
return (
|
return (
|
||||||
<TouchableOpacity onPress={description ? () => Alert.alert("Info", description) : null}>
|
<TouchableOpacity onPress={description ? () => Alert.alert("Info", description) : null}>
|
||||||
<View style={{height: 30, flexDirection: "row", justifyContent: 'center', alignItems: 'center'}}>
|
<View style={{height: 30, flexDirection: "row", justifyContent: 'center', alignItems: 'center'}}>
|
||||||
20
mobile/traque-app/components/timer.jsx
Normal file
20
mobile/traque-app/components/timer.jsx
Normal 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',
|
||||||
|
}
|
||||||
|
});
|
||||||
@@ -1,8 +1,9 @@
|
|||||||
import { createContext, useContext, useMemo } from "react";
|
import { createContext, useContext, useMemo } from "react";
|
||||||
import { io } from "socket.io-client";
|
import { io } from "socket.io-client";
|
||||||
|
|
||||||
const SOCKET_URL = `ws://0.0.0.0/player`;
|
const IP = "172.16.1.180";
|
||||||
const SERVER_URL = `http://0.0.0.0/back`;
|
const SOCKET_URL = `ws://${IP}/player`;
|
||||||
|
const SERVER_URL = `http://${IP}/back`;
|
||||||
|
|
||||||
export const teamSocket = io(SOCKET_URL, {
|
export const teamSocket = io(SOCKET_URL, {
|
||||||
path: "/back/socket.io",
|
path: "/back/socket.io",
|
||||||
@@ -10,13 +11,14 @@ export const teamSocket = io(SOCKET_URL, {
|
|||||||
|
|
||||||
export const SocketContext = createContext();
|
export const SocketContext = createContext();
|
||||||
|
|
||||||
export default function SocketProvider({ children }) {
|
export const SocketProvider = ({ children }) => {
|
||||||
const value = useMemo(() => ({ teamSocket, SERVER_URL }), [teamSocket]);
|
const value = useMemo(() => ({ teamSocket, SERVER_URL }), []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SocketContext.Provider value={value}>{children}</SocketContext.Provider>
|
<SocketContext.Provider value={value}>{children}</SocketContext.Provider>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
export function useSocket() {
|
export const useSocket = () => {
|
||||||
return useContext(SocketContext);
|
return useContext(SocketContext);
|
||||||
}
|
};
|
||||||
|
|||||||
@@ -4,22 +4,19 @@ import { useSocketAuth } from "../hook/useSocketAuth";
|
|||||||
|
|
||||||
const teamConnexionContext = createContext();
|
const teamConnexionContext = createContext();
|
||||||
|
|
||||||
const TeamConnexionProvider = ({ children }) => {
|
export const TeamConnexionProvider = ({ children }) => {
|
||||||
const { teamSocket } = useSocket();
|
const { teamSocket } = useSocket();
|
||||||
const { login, password: teamId, loggedIn, loading, logout } = useSocketAuth(teamSocket, "team_password");
|
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 (
|
return (
|
||||||
<teamConnexionContext.Provider value={value}>
|
<teamConnexionContext.Provider value={value}>
|
||||||
{children}
|
{children}
|
||||||
</teamConnexionContext.Provider>
|
</teamConnexionContext.Provider>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
function useTeamConnexion() {
|
export const useTeamConnexion = () => {
|
||||||
return useContext(teamConnexionContext);
|
return useContext(teamConnexionContext);
|
||||||
}
|
};
|
||||||
|
|
||||||
export { TeamConnexionProvider, useTeamConnexion };
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,14 +1,14 @@
|
|||||||
import { useLocation } from "../hook/useLocation";
|
|
||||||
import { useSocketListener } from "../hook/useSocketListener";
|
|
||||||
import { createContext, useContext, useMemo, useState } from "react";
|
import { createContext, useContext, useMemo, useState } from "react";
|
||||||
import { useSocket } from "./socketContext";
|
import { useSocket } from "./socketContext";
|
||||||
import { GameState } from "../util/gameState";
|
|
||||||
import useSendDeviceInfo from "../hook/useSendDeviceInfo";
|
|
||||||
import { useTeamConnexion } from "./teamConnexionContext";
|
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();
|
const teamContext = createContext();
|
||||||
|
|
||||||
function TeamProvider({children}) {
|
export const TeamProvider = ({children}) => {
|
||||||
const {teamSocket} = useSocket();
|
const {teamSocket} = useSocket();
|
||||||
const [location, getLocationAuthorization, startLocationTracking, stopLocationTracking] = useLocation(5000, 10);
|
const [location, getLocationAuthorization, startLocationTracking, stopLocationTracking] = useLocation(5000, 10);
|
||||||
// update_team
|
// update_team
|
||||||
@@ -28,7 +28,7 @@ function TeamProvider({children}) {
|
|||||||
useSendDeviceInfo();
|
useSendDeviceInfo();
|
||||||
|
|
||||||
useSocketListener(teamSocket, "update_team", (data) => {
|
useSocketListener(teamSocket, "update_team", (data) => {
|
||||||
setTeamInfos(teamInfos => ({...teamInfos, ...data}))
|
setTeamInfos(teamInfos => ({...teamInfos, ...data}));
|
||||||
});
|
});
|
||||||
|
|
||||||
useSocketListener(teamSocket, "game_state", (data) => {
|
useSocketListener(teamSocket, "game_state", (data) => {
|
||||||
@@ -54,17 +54,15 @@ function TeamProvider({children}) {
|
|||||||
|
|
||||||
const value = useMemo(() => (
|
const value = useMemo(() => (
|
||||||
{teamInfos, gameState, startDate, zoneType, zoneExtremities, nextZoneDate, messages, location, getLocationAuthorization, startLocationTracking, stopLocationTracking}
|
{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 (
|
return (
|
||||||
<teamContext.Provider value={value}>
|
<teamContext.Provider value={value}>
|
||||||
{children}
|
{children}
|
||||||
</teamContext.Provider>
|
</teamContext.Provider>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
function useTeamContext() {
|
export const useTeamContext = () => {
|
||||||
return useContext(teamContext);
|
return useContext(teamContext);
|
||||||
}
|
};
|
||||||
|
|
||||||
export { TeamProvider, useTeamContext };
|
|
||||||
|
|||||||
@@ -2,13 +2,13 @@ import { useSocket } from "../context/socketContext";
|
|||||||
import { useTeamConnexion } from "../context/teamConnexionContext";
|
import { useTeamConnexion } from "../context/teamConnexionContext";
|
||||||
import { useTeamContext } from "../context/teamContext";
|
import { useTeamContext } from "../context/teamContext";
|
||||||
|
|
||||||
export default function useGame() {
|
export const useGame = () => {
|
||||||
const { teamSocket } = useSocket();
|
const { teamSocket } = useSocket();
|
||||||
const { teamId } = useTeamConnexion();
|
const { teamId } = useTeamConnexion();
|
||||||
const { teamInfos } = useTeamContext();
|
const { teamInfos } = useTeamContext();
|
||||||
|
|
||||||
function sendCurrentPosition() {
|
function sendCurrentPosition() {
|
||||||
console.log("Reveal position.")
|
console.log("Reveal position.");
|
||||||
teamSocket.emit("send_position");
|
teamSocket.emit("send_position");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -30,4 +30,4 @@ export default function useGame() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return {...teamInfos, sendCurrentPosition, capture, teamId};
|
return {...teamInfos, sendCurrentPosition, capture, teamId};
|
||||||
}
|
};
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import AsyncStorage from '@react-native-async-storage/async-storage';
|
import AsyncStorage from '@react-native-async-storage/async-storage';
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
|
|
||||||
export function useLocalStorage(key, initialValue) {
|
export const useLocalStorage = (key, initialValue) => {
|
||||||
const [storedValue, setStoredValue] = useState(initialValue);
|
const [storedValue, setStoredValue] = useState(initialValue);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
|
|
||||||
@@ -16,7 +16,7 @@ export function useLocalStorage(key, initialValue) {
|
|||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
fetchData();
|
fetchData();
|
||||||
}, []);
|
}, [initialValue, key]);
|
||||||
|
|
||||||
const setValue = async value => {
|
const setValue = async value => {
|
||||||
try {
|
try {
|
||||||
@@ -28,7 +28,7 @@ export function useLocalStorage(key, initialValue) {
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log(error);
|
console.log(error);
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
return [storedValue, setValue, loading];
|
return [storedValue, setValue, loading];
|
||||||
}
|
};
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { defineTask, isTaskRegisteredAsync } from "expo-task-manager";
|
|||||||
import * as Location from 'expo-location';
|
import * as Location from 'expo-location';
|
||||||
import { useSocket } from "../context/socketContext";
|
import { useSocket } from "../context/socketContext";
|
||||||
|
|
||||||
export function useLocation(timeInterval, distanceInterval) {
|
export const useLocation = (timeInterval, distanceInterval) => {
|
||||||
const [location, setLocation] = useState(null); // [latitude, longitude]
|
const [location, setLocation] = useState(null); // [latitude, longitude]
|
||||||
const { teamSocket } = useSocket();
|
const { teamSocket } = useSocket();
|
||||||
const LOCATION_TASK_NAME = "background-location-task";
|
const LOCATION_TASK_NAME = "background-location-task";
|
||||||
@@ -40,7 +40,7 @@ export function useLocation(timeInterval, distanceInterval) {
|
|||||||
console.log("Sending position :", new_location);
|
console.log("Sending position :", new_location);
|
||||||
teamSocket.emit("update_position", new_location);
|
teamSocket.emit("update_position", new_location);
|
||||||
} else {
|
} 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 getLocationAuthorization()) {
|
||||||
if (!(await isTaskRegisteredAsync(LOCATION_TASK_NAME))) {
|
if (!(await isTaskRegisteredAsync(LOCATION_TASK_NAME))) {
|
||||||
await Location.startLocationUpdatesAsync(LOCATION_TASK_NAME, locationUpdateParameters);
|
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() {
|
async function stopLocationTracking() {
|
||||||
if (await isTaskRegisteredAsync(LOCATION_TASK_NAME)) {
|
if (await isTaskRegisteredAsync(LOCATION_TASK_NAME)) {
|
||||||
await Location.stopLocationUpdatesAsync(LOCATION_TASK_NAME);
|
await Location.stopLocationUpdatesAsync(LOCATION_TASK_NAME);
|
||||||
console.log("Location tracking stopped.")
|
console.log("Location tracking stopped.");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return [location, getLocationAuthorization, startLocationTracking, stopLocationTracking];
|
return [location, getLocationAuthorization, startLocationTracking, stopLocationTracking];
|
||||||
}
|
};
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { useState, } from 'react';
|
|||||||
import { Alert } from 'react-native';
|
import { Alert } from 'react-native';
|
||||||
import { launchImageLibraryAsync, requestMediaLibraryPermissionsAsync } from 'expo-image-picker';
|
import { launchImageLibraryAsync, requestMediaLibraryPermissionsAsync } from 'expo-image-picker';
|
||||||
|
|
||||||
export function usePickImage() {
|
export const usePickImage = () => {
|
||||||
const [image, setImage] = useState(null);
|
const [image, setImage] = useState(null);
|
||||||
|
|
||||||
const pickImage = async () => {
|
const pickImage = async () => {
|
||||||
@@ -33,7 +33,7 @@ export function usePickImage() {
|
|||||||
console.error('Error picking image;', error);
|
console.error('Error picking image;', error);
|
||||||
Alert.alert('Erreur', "Une erreur est survenue lors de la sélection d'une image.");
|
Alert.alert('Erreur', "Une erreur est survenue lors de la sélection d'une image.");
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
function sendImage(location) {
|
function sendImage(location) {
|
||||||
if (image) {
|
if (image) {
|
||||||
@@ -55,4 +55,4 @@ export function usePickImage() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return {image, pickImage, sendImage};
|
return {image, pickImage, sendImage};
|
||||||
}
|
};
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import DeviceInfo from 'react-native-device-info';
|
|||||||
import { useSocket } from "../context/socketContext";
|
import { useSocket } from "../context/socketContext";
|
||||||
import { useTeamConnexion } from "../context/teamConnexionContext";
|
import { useTeamConnexion } from "../context/teamConnexionContext";
|
||||||
|
|
||||||
export default function useSendDeviceInfo() {
|
export const useSendDeviceInfo = () => {
|
||||||
const batteryUpdateTimeout = 5*60*1000;
|
const batteryUpdateTimeout = 5*60*1000;
|
||||||
const { teamSocket } = useSocket();
|
const { teamSocket } = useSocket();
|
||||||
const {loggedIn} = useTeamConnexion();
|
const {loggedIn} = useTeamConnexion();
|
||||||
@@ -28,8 +28,8 @@ export default function useSendDeviceInfo() {
|
|||||||
|
|
||||||
const batteryCheckInterval = setInterval(() => sendBattery(), batteryUpdateTimeout);
|
const batteryCheckInterval = setInterval(() => sendBattery(), batteryUpdateTimeout);
|
||||||
|
|
||||||
return () => {clearInterval(batteryCheckInterval)};
|
return () => clearInterval(batteryCheckInterval);
|
||||||
}, [loggedIn]);
|
}, [batteryUpdateTimeout, loggedIn, teamSocket]);
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
};
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { useLocalStorage } from './useLocalStorage';
|
|||||||
const LOGIN_MESSAGE = "login";
|
const LOGIN_MESSAGE = "login";
|
||||||
const LOGOUT_MESSAGE = "logout";
|
const LOGOUT_MESSAGE = "logout";
|
||||||
|
|
||||||
export function useSocketAuth(socket, passwordName) {
|
export const useSocketAuth = (socket, passwordName) => {
|
||||||
const [loggedIn, setLoggedIn] = useState(false);
|
const [loggedIn, setLoggedIn] = useState(false);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [waitingForResponse, setWaitingForResponse] = useState(false);
|
const [waitingForResponse, setWaitingForResponse] = useState(false);
|
||||||
@@ -29,7 +29,7 @@ export function useSocketAuth(socket, passwordName) {
|
|||||||
});
|
});
|
||||||
setHasTriedSavedPassword(true);
|
setHasTriedSavedPassword(true);
|
||||||
}
|
}
|
||||||
}, [loading]);
|
}, [hasTriedSavedPassword, loading, savedPassword, socket]);
|
||||||
|
|
||||||
function login(password) {
|
function login(password) {
|
||||||
console.log("Try to log in with :", password);
|
console.log("Try to log in with :", password);
|
||||||
@@ -69,4 +69,4 @@ export function useSocketAuth(socket, passwordName) {
|
|||||||
}, [waitingForResponse, savedPasswordLoading]);
|
}, [waitingForResponse, savedPasswordLoading]);
|
||||||
|
|
||||||
return {login, logout, password: savedPassword, loggedIn, loading};
|
return {login, logout, password: savedPassword, loggedIn, loading};
|
||||||
}
|
};
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
import { useEffect } from "react";
|
import { useEffect } from "react";
|
||||||
|
|
||||||
export function useSocketListener(socket, event, callback) {
|
export const useSocketListener = (socket, event, callback) => {
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
socket.on(event,callback);
|
socket.on(event,callback);
|
||||||
return () => {
|
return () => {
|
||||||
socket.off(event, callback);
|
socket.off(event, callback);
|
||||||
}
|
};
|
||||||
}, []);
|
}, [callback, event, socket]);
|
||||||
}
|
};
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { useEffect, useState } from "react";
|
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 past, time will be positive
|
||||||
// If refTime is in the future, time will be negative
|
// If refTime is in the future, time will be negative
|
||||||
// The time is updated every timeout milliseconds
|
// The time is updated every timeout milliseconds
|
||||||
@@ -15,7 +15,7 @@ export function useTimeDifference(refTime, timeout) {
|
|||||||
const interval = setInterval(updateTime, timeout);
|
const interval = setInterval(updateTime, timeout);
|
||||||
|
|
||||||
return () => clearInterval(interval);
|
return () => clearInterval(interval);
|
||||||
}, [refTime]);
|
}, [refTime, timeout]);
|
||||||
|
|
||||||
return [time];
|
return [time];
|
||||||
}
|
};
|
||||||
|
|||||||
120
mobile/traque-app/package-lock.json
generated
120
mobile/traque-app/package-lock.json
generated
@@ -43,7 +43,11 @@
|
|||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/core": "^7.20.0",
|
"@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": {
|
"node_modules/@0no-co/graphql.web": {
|
||||||
@@ -2531,9 +2535,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@eslint/eslintrc/node_modules/js-yaml": {
|
"node_modules/@eslint/eslintrc/node_modules/js-yaml": {
|
||||||
"version": "3.14.1",
|
"version": "3.14.2",
|
||||||
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz",
|
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.2.tgz",
|
||||||
"integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==",
|
"integrity": "sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"argparse": "^1.0.7",
|
"argparse": "^1.0.7",
|
||||||
@@ -7062,9 +7066,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@typescript-eslint/eslint-plugin/node_modules/semver": {
|
"node_modules/@typescript-eslint/eslint-plugin/node_modules/semver": {
|
||||||
"version": "7.7.2",
|
"version": "7.7.4",
|
||||||
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz",
|
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz",
|
||||||
"integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==",
|
"integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==",
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"bin": {
|
"bin": {
|
||||||
"semver": "bin/semver.js"
|
"semver": "bin/semver.js"
|
||||||
@@ -8460,9 +8464,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/axe-core": {
|
"node_modules/axe-core": {
|
||||||
"version": "4.10.3",
|
"version": "4.11.1",
|
||||||
"resolved": "https://registry.npmjs.org/axe-core/-/axe-core-4.10.3.tgz",
|
"resolved": "https://registry.npmjs.org/axe-core/-/axe-core-4.11.1.tgz",
|
||||||
"integrity": "sha512-Xm7bpRXnDSX2YE2YFfBk2FnF0ep6tmG7xPh8iHee8MIcrgq762Nkce856dYtJYLkuIoYZvGfTs/PbZhideTcEg==",
|
"integrity": "sha512-BASOg+YwO2C+346x3LZOeoovTIoTrRqEsqMa6fmfAV0P+U9mFr9NsyOEpiYvFjbc64NMrSswhV50WdXzdb/Z5A==",
|
||||||
"license": "MPL-2.0",
|
"license": "MPL-2.0",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=4"
|
"node": ">=4"
|
||||||
@@ -12950,9 +12954,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/es-abstract": {
|
"node_modules/es-abstract": {
|
||||||
"version": "1.24.0",
|
"version": "1.24.1",
|
||||||
"resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.24.0.tgz",
|
"resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.24.1.tgz",
|
||||||
"integrity": "sha512-WSzPgsdLtTcQwm4CROfS5ju2Wa1QQcVeT37jFjYzdFz1r9ahadC8B8/a4qxJxM+09F18iumCdRmlr96ZYkQvEg==",
|
"integrity": "sha512-zHXBLhP+QehSSbsS9Pt23Gg964240DPd6QCf8WpkqEXxQ7fhdZzYsocOr5u7apWonsS5EjZDmTF+/slGMyasvw==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"array-buffer-byte-length": "^1.0.2",
|
"array-buffer-byte-length": "^1.0.2",
|
||||||
@@ -13042,26 +13046,26 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/es-iterator-helpers": {
|
"node_modules/es-iterator-helpers": {
|
||||||
"version": "1.2.1",
|
"version": "1.2.2",
|
||||||
"resolved": "https://registry.npmjs.org/es-iterator-helpers/-/es-iterator-helpers-1.2.1.tgz",
|
"resolved": "https://registry.npmjs.org/es-iterator-helpers/-/es-iterator-helpers-1.2.2.tgz",
|
||||||
"integrity": "sha512-uDn+FE1yrDzyC0pCo961B2IHbdM8y/ACZsKD4dG6WqrjV53BADjwa7D+1aom2rsNVfLyDgU/eigvlJGJ08OQ4w==",
|
"integrity": "sha512-BrUQ0cPTB/IwXj23HtwHjS9n7O4h9FX94b4xc5zlTHxeLgTAdzYUDyy6KdExAl9lbN5rtfe44xpjpmj9grxs5w==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"call-bind": "^1.0.8",
|
"call-bind": "^1.0.8",
|
||||||
"call-bound": "^1.0.3",
|
"call-bound": "^1.0.4",
|
||||||
"define-properties": "^1.2.1",
|
"define-properties": "^1.2.1",
|
||||||
"es-abstract": "^1.23.6",
|
"es-abstract": "^1.24.1",
|
||||||
"es-errors": "^1.3.0",
|
"es-errors": "^1.3.0",
|
||||||
"es-set-tostringtag": "^2.0.3",
|
"es-set-tostringtag": "^2.1.0",
|
||||||
"function-bind": "^1.1.2",
|
"function-bind": "^1.1.2",
|
||||||
"get-intrinsic": "^1.2.6",
|
"get-intrinsic": "^1.3.0",
|
||||||
"globalthis": "^1.0.4",
|
"globalthis": "^1.0.4",
|
||||||
"gopd": "^1.2.0",
|
"gopd": "^1.2.0",
|
||||||
"has-property-descriptors": "^1.0.2",
|
"has-property-descriptors": "^1.0.2",
|
||||||
"has-proto": "^1.2.0",
|
"has-proto": "^1.2.0",
|
||||||
"has-symbols": "^1.1.0",
|
"has-symbols": "^1.1.0",
|
||||||
"internal-slot": "^1.1.0",
|
"internal-slot": "^1.1.0",
|
||||||
"iterator.prototype": "^1.1.4",
|
"iterator.prototype": "^1.1.5",
|
||||||
"safe-array-concat": "^1.1.3"
|
"safe-array-concat": "^1.1.3"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
@@ -13334,9 +13338,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/eslint-module-utils": {
|
"node_modules/eslint-module-utils": {
|
||||||
"version": "2.12.0",
|
"version": "2.12.1",
|
||||||
"resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.12.0.tgz",
|
"resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.12.1.tgz",
|
||||||
"integrity": "sha512-wALZ0HFoytlyh/1+4wuZ9FJCD/leWHQzzrxJ8+rebyReSLk7LApMyd3WJaLVoN+D5+WIdJyDK1c6JnE65V4Zyg==",
|
"integrity": "sha512-L8jSWTze7K2mTg0vos/RuLRS5soomksDPoJLXIslC7c8Wmut3bx7CPpJijDcBZtxQ5lrbUdM+s0OlNbz0DCDNw==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"debug": "^3.2.7"
|
"debug": "^3.2.7"
|
||||||
@@ -13376,29 +13380,29 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/eslint-plugin-import": {
|
"node_modules/eslint-plugin-import": {
|
||||||
"version": "2.31.0",
|
"version": "2.32.0",
|
||||||
"resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.31.0.tgz",
|
"resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.32.0.tgz",
|
||||||
"integrity": "sha512-ixmkI62Rbc2/w8Vfxyh1jQRTdRTF52VxwRVHl/ykPAmqG+Nb7/kNn+byLP0LxPgI7zWA16Jt82SybJInmMia3A==",
|
"integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@rtsao/scc": "^1.1.0",
|
"@rtsao/scc": "^1.1.0",
|
||||||
"array-includes": "^3.1.8",
|
"array-includes": "^3.1.9",
|
||||||
"array.prototype.findlastindex": "^1.2.5",
|
"array.prototype.findlastindex": "^1.2.6",
|
||||||
"array.prototype.flat": "^1.3.2",
|
"array.prototype.flat": "^1.3.3",
|
||||||
"array.prototype.flatmap": "^1.3.2",
|
"array.prototype.flatmap": "^1.3.3",
|
||||||
"debug": "^3.2.7",
|
"debug": "^3.2.7",
|
||||||
"doctrine": "^2.1.0",
|
"doctrine": "^2.1.0",
|
||||||
"eslint-import-resolver-node": "^0.3.9",
|
"eslint-import-resolver-node": "^0.3.9",
|
||||||
"eslint-module-utils": "^2.12.0",
|
"eslint-module-utils": "^2.12.1",
|
||||||
"hasown": "^2.0.2",
|
"hasown": "^2.0.2",
|
||||||
"is-core-module": "^2.15.1",
|
"is-core-module": "^2.16.1",
|
||||||
"is-glob": "^4.0.3",
|
"is-glob": "^4.0.3",
|
||||||
"minimatch": "^3.1.2",
|
"minimatch": "^3.1.2",
|
||||||
"object.fromentries": "^2.0.8",
|
"object.fromentries": "^2.0.8",
|
||||||
"object.groupby": "^1.0.3",
|
"object.groupby": "^1.0.3",
|
||||||
"object.values": "^1.2.0",
|
"object.values": "^1.2.1",
|
||||||
"semver": "^6.3.1",
|
"semver": "^6.3.1",
|
||||||
"string.prototype.trimend": "^1.0.8",
|
"string.prototype.trimend": "^1.0.9",
|
||||||
"tsconfig-paths": "^3.15.0"
|
"tsconfig-paths": "^3.15.0"
|
||||||
},
|
},
|
||||||
"engines": {
|
"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"
|
"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": {
|
"node_modules/eslint-plugin-react/node_modules/doctrine": {
|
||||||
"version": "2.1.0",
|
"version": "2.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz",
|
||||||
@@ -13685,7 +13709,7 @@
|
|||||||
"version": "7.2.3",
|
"version": "7.2.3",
|
||||||
"resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
|
"resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
|
||||||
"integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==",
|
"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",
|
"license": "ISC",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"fs.realpath": "^1.0.0",
|
"fs.realpath": "^1.0.0",
|
||||||
@@ -13703,9 +13727,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/eslint-plugin-testing-library/node_modules/semver": {
|
"node_modules/eslint-plugin-testing-library/node_modules/semver": {
|
||||||
"version": "7.7.2",
|
"version": "7.7.4",
|
||||||
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz",
|
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz",
|
||||||
"integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==",
|
"integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==",
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"bin": {
|
"bin": {
|
||||||
"semver": "bin/semver.js"
|
"semver": "bin/semver.js"
|
||||||
@@ -13949,9 +13973,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/eslint/node_modules/js-yaml": {
|
"node_modules/eslint/node_modules/js-yaml": {
|
||||||
"version": "3.14.1",
|
"version": "3.14.2",
|
||||||
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz",
|
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.2.tgz",
|
||||||
"integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==",
|
"integrity": "sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"argparse": "^1.0.7",
|
"argparse": "^1.0.7",
|
||||||
@@ -13968,9 +13992,9 @@
|
|||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/eslint/node_modules/semver": {
|
"node_modules/eslint/node_modules/semver": {
|
||||||
"version": "7.7.2",
|
"version": "7.7.4",
|
||||||
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz",
|
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz",
|
||||||
"integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==",
|
"integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==",
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"bin": {
|
"bin": {
|
||||||
"semver": "bin/semver.js"
|
"semver": "bin/semver.js"
|
||||||
@@ -14055,9 +14079,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/esquery": {
|
"node_modules/esquery": {
|
||||||
"version": "1.6.0",
|
"version": "1.7.0",
|
||||||
"resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz",
|
"resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz",
|
||||||
"integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==",
|
"integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==",
|
||||||
"license": "BSD-3-Clause",
|
"license": "BSD-3-Clause",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"estraverse": "^5.1.0"
|
"estraverse": "^5.1.0"
|
||||||
|
|||||||
@@ -44,7 +44,11 @@
|
|||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/core": "^7.20.0",
|
"@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
|
"private": true
|
||||||
}
|
}
|
||||||
|
|||||||
3
mobile/traque-app/util/colors.js
Normal file
3
mobile/traque-app/util/colors.js
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
export const Colors = {
|
||||||
|
background: '#f5f5f5'
|
||||||
|
};
|
||||||
13
mobile/traque-app/util/constants.js
Normal file
13
mobile/traque-app/util/constants.js
Normal 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"
|
||||||
|
};
|
||||||
16
mobile/traque-app/util/format.js
Normal file
16
mobile/traque-app/util/format.js
Normal 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");
|
||||||
|
};
|
||||||
@@ -3,4 +3,4 @@ export const GameState = {
|
|||||||
PLACEMENT: "placement",
|
PLACEMENT: "placement",
|
||||||
PLAYING: "playing",
|
PLAYING: "playing",
|
||||||
FINISHED: "finished"
|
FINISHED: "finished"
|
||||||
}
|
};
|
||||||
|
|||||||
@@ -1,3 +0,0 @@
|
|||||||
{
|
|
||||||
"extends": "next/core-web-vitals"
|
|
||||||
}
|
|
||||||
@@ -1,6 +1,4 @@
|
|||||||
module.exports = {
|
export const plugins = {
|
||||||
plugins: {
|
tailwindcss: {},
|
||||||
tailwindcss: {},
|
autoprefixer: {},
|
||||||
autoprefixer: {},
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,26 +1,29 @@
|
|||||||
/** @type {import('tailwindcss').Config} */
|
/** @type {import('tailwindcss').Config} */
|
||||||
module.exports = {
|
|
||||||
theme: {
|
export const theme = {
|
||||||
extend: {
|
extend: {
|
||||||
colors: {
|
colors: {
|
||||||
'custom-green': '#19e119',
|
'custom-green': '#19e119',
|
||||||
'custom-red': '#e11919',
|
'custom-red': '#e11919',
|
||||||
'custom-orange': '#fa6400',
|
'custom-orange': '#fa6400',
|
||||||
'custom-blue': '#1e90ff',
|
'custom-blue': '#1e90ff',
|
||||||
'custom-grey': '#808080',
|
'custom-grey': '#808080',
|
||||||
'custom-light-blue': '#80b3ff'
|
'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 = [];
|
||||||
|
|||||||
Reference in New Issue
Block a user