Les Mille Vues

J’ai créé ce projet par moi même en tant que dernier projet de ma technique en intégration multimédia.
Le but de cette application web est de créer un outil pour permettre aux points de vue intéressants d’être affichés et découverts au Québec. La base de donnée utilisée pour ce projet est Firebase.
Les principales fonctionnalités de ce site web sont :
- Permettre la connexion de l’utilisateur.
- Permettre la recherche grâce à une carte google maps.
- Permettre de rechercher un endroit en particulier grâce à une barre de recherche.
- Permettre aux utilisateur de créer leurs propres points de vues (Avec images!).
- Traitement automatique des images ajoutées aux points de vues pour modifier leurs dimensions.
Exemple de code conçus lors de ce projet
Composant permettant la création d’un point de vue.
import './creationPoint.scss';
import GoogleMapProps from '../../GoogleMapProps/googleMapsProps';
import { useState } from 'react';
import { Marker } from '@react-google-maps/api';
import { useAuth } from '../../../Contexts/authContext';
import { addDoc, collection, setDoc } from 'firebase/firestore';
import { db } from '../../../Configs/firebase';
import axios from 'axios';
import { geohashForLocation } from 'geofire-common';
import { typesAccessibilite, typesPaysage } from '../RecherchePoint/RechercheFiltres/IndexFiltres/indexFiltres';
import PopupAttente from '../../UI/PopupAttente/popupAttente';
import { useNotifs } from '../../../Contexts/notificationsContext';
import Geocode from 'react-geocode';
import { useHistory } from 'react-router-dom';
// composant permettant la creation d'un nouveau point de vue
const CreationPoint = () => {
const history = useHistory();
const {user} = useAuth();
// fonction permettant d'envoyer des notifications
const {updateNotif} = useNotifs();
// clef d'api permettant d'usage de Geocode
// cette clef sera limitee par google cloud et deva etre ajoutee dans un .env eventuellement
Geocode.setApiKey("Ceci devrait être la clef (retirée)");
// change la lanque pour que les resultats soient en francais
Geocode.setLanguage("fr-CA");
// state de la position du marqueur du user
const [userMarkerPosition,setUserMarkerPosition] = useState(null);
// Initie le marqueur du user à l'endroit ou celui-ci ce trouve
const initUserMarker = () => {
navigator.geolocation.getCurrentPosition(function(position){
setUserMarkerPosition({lat : position.coords.latitude, lng : position.coords.longitude});
});
}
// position du marqueur cible sur la carte
const [markerPosition,setMarkerPosition] = useState(null);
// fonction lors d'un clic sur la carte pour choisir l'emplacement du point
const onClickHandler = (e) => {
setMarkerPosition(e.latLng);
updatePointHandler(e.latLng.lat(), "lat");
updatePointHandler(e.latLng.lng(), "lng");
updatePointHandler(geohashForLocation([e.latLng.lat(),e.latLng.lng()]), "geoHash");
}
// champs du point de vue en creation
const [pointVueEnCreation,setPointVueEnCreation] = useState(
{
id : null,
idCreateur : null,
nom : "",
description : "",
typePaysage: [],
accessibilite : [],
popularite : 0,
lat : null,
lng : null,
commentaires : [],
geoHash: null,
}
);
// verification instantanee de la connexion au charement de la composante, ne peut pas etre dans cette pas si non connecte
const VerificationConnexion = () => {
if(user === null){
updateNotif("Vous devez être connecté pour ajouter un point de vue.", true);
history.push("/points-de-vue");
}
}
VerificationConnexion();
// update des champs du state du point de vue
const updatePointHandler = (value, prop) => {
// si le champ modifie est un array, donc peut avoir plusieurs valeurs
// permet d'ajouter des valeurs dans le tableau du champ ou d'en retirer
if(Array.isArray(pointVueEnCreation[prop])){
if(pointVueEnCreation[prop].includes(value)){
// retourne toutes les valeur differentes de value
const arraySansLaValue = pointVueEnCreation[prop].filter(
(uneValeurDuArray) => uneValeurDuArray !== value
);
// retire la valeur du tableau
setPointVueEnCreation(
(current) => ({
...current,
[prop] : arraySansLaValue,
})
);
}
else{
// ajoute une valeur
let copieChamp = pointVueEnCreation[prop];
copieChamp.push(value);
setPointVueEnCreation(
(current) => ({
...current,
[prop] : copieChamp,
})
);
}
}
else{
// change la valeur d'un champ en fonction des props envoye dans cette fonction
setPointVueEnCreation(
(current) => ({
...current,
[prop] : value,
})
)
}
}
// etape de l'utilisateur dans le formulaire
const [etapeCreation,setEtapeCreation] = useState(1);
// change les etapes de creation d'une page a l'autre
const changeStepHandler = (valeur) => {
if(etapeCreation + valeur >= 1 && etapeCreation + valeur <= 4){
setEtapeCreation((current) => current += valeur);
}
}
// verification si la creation est possible (si des valeurs sont presentes)
const verificationCreation = () => {
if(
user !== null &&
pointVueEnCreation.nom !== (null || "") &&
pointVueEnCreation.description !== (null || "") &&
pointVueEnCreation.typePaysage.length > 0 &&
pointVueEnCreation.lat !== null &&
pointVueEnCreation.lng !== null &&
pointVueEnCreation.geoHash !== null &&
urlImagesAjoutees.length > 0
){
return true;
}
else {
return false;
}
}
// state necessaire a l'affichage du charement de l'envoi
const [envoiEnCours, setEnvoiEnCours] = useState(false);
// ajoute le point de vue a la liste des points de vue existants
const addPointDeVue = async() => {
// seulement si la verification passe
if(verificationCreation() === true){
// active le state d'affichage de chargement
setEnvoiEnCours(true);
/* ENVOI DES IMAGES AU SERVEUR */
// creations du form data contenant toutes les images ajoutees par le user
let formData = new FormData();
// Ajout du id dans l'envoi au cas ou necessaire
formData.append('userId', user.id);
// ajoute les donnes des images dans le form data
imagesAjoutees.forEach(uneImageAjoutee => {
formData.append('uneImage', uneImageAjoutee.file, uneImageAjoutee.fileName);
});
// post des images sur le serveur de traitement d'images par axios
axios.post("https://milles-vues-images.herokuapp.com/image-upload", formData)
// quand les images sont traites, obtient leur valeur (publicUrl, etc...)
.then(
async(res) => {
// Obtention de l'adrese du point de vue en creation
Geocode.fromLatLng(pointVueEnCreation.lat, pointVueEnCreation.lng).then(
// quand l'adresse est calculee par l'api de google
async (response) => {
// obtient la valeur affichable dans une interface
const adresse = response.results[0].formatted_address;
/* ENREGISTREMENT DU POINT DE VUE*/
await addDoc(collection(db, 'pointsVue'), {
// Ajout des valeurs entrees dans les champs
...pointVueEnCreation,
// Force le createur selon le user connecte
idCreateur : user.id,
createur : user,
// images: res.data,
momentCreation: Date.now(),
// ajoute l'adresse formatee
adresse: adresse,
})
/* ENREGISTREMENT DU POINT DE VUE ID DANS LE POINT DE VUE + LES IMAGES COMPLETES */
.then(
async(pointVueRef) => {
// images avec tous leurs champs
let imagesCompletes = [];
// tableau paralellele aux images ajoutees contenant les id de chaque "photographe"
// lors de la creation, seulement le use lui meme
let photographes = [];
// Modification des images pour les rendre completes
res.data.forEach(uneImage => {
// les valeurs de photographe doivent etre paraleles aux images
// un id de photographe par image
photographes.push(user.id);
imagesCompletes.push(
{
...uneImage,
idCreateur : user.id,
idPointVue : pointVueRef.id,
coeurs : [],
}
);
});
// ajoute les images completes / photographes et permet au point de vue de contenir son propre id
await setDoc(pointVueRef,
{
id: pointVueRef.id,
photographes: photographes,
images : imagesCompletes,
},
{ merge: true }
// quand l'envoi de toutes les infos est terminee
).then(
() => {
// desactive le state d'affichage de chargement (images recues)
setEnvoiEnCours(false);
// envoie un notif dans le contexte
updateNotif(`Point de vue ${pointVueEnCreation.nom} créé`);
// navigation vers le document cree
history.push(`/points-de-vue/rechercher/${pointVueRef.id}`);
}
)
}
);
},
// en cas d'erreur
(error) => {
console.error(error);
updateNotif(`Une erreur s'est produite lors de la creation d'un point de vue. Veuillez réessayer plus tard.`);
history.push(`/points-de-vue/rechercher/`);
}
);
}
)
.catch(error => {
// Message d'erreur de creation de point de vue pour le user
updateNotif(`Une erreur est survenue lors de la création de ce point de vue. Veuillez réessayer plus tard.`);
})
}
}
// tableau contenant les form data des images ajoutees
const [imagesAjoutees, setImagesAjoutees] = useState([]);
// tableau contenant les url locales des images ajoutees
const [urlImagesAjoutees, setUrlImagesAjoutes] = useState([]);
// fonction permettant l'ajout d'images par le formulaire (de facon local jusqu'a l'envoi du point de vue)
const ajouterImage = (e) => {
// Limitation du nombre d'images envoyees
if(imagesAjoutees.length <= 15) {
// Enregistrement des donnes de l'image dans un array (sera ajoute dans un form data lors)
// de l'envoi au serveur
setImagesAjoutees(
(current) => [...current, { file : e.target.files[0], fileName : e.target.files[0].name}]
);
// Ajout une URL blob pour l'affichage instantane dans le navigateur
setUrlImagesAjoutes(
(current) => [...current, URL.createObjectURL(e.target.files[0])]
);
}
else {
// si trop d'images, desactiver la possibilite d'en ajouter (hidden dans le bouton)
// console log pusiqu'un ajout d'une image apres que le bouton soit desactive provient d''un utilisateur modifiant la page
console.log("Trop d'images and you know it");
}
}
const retirerImage = (index) => {
// cree une copie des tableaux de state puisque ceux-ci ne sont pas directement
// modifiables
const copieArrayUrl = [...urlImagesAjoutees];
const copieArrayImagesAjoutees = [...imagesAjoutees];
// retire la valeur de chaque tableau
copieArrayUrl.splice(index,1);
copieArrayImagesAjoutees.splice(index,1);
// set les states a leur nouvelle valeur
setUrlImagesAjoutes(copieArrayUrl);
setImagesAjoutees(copieArrayImagesAjoutees);
}
return(
<>
{
// N'affiche le chargement que lorsque le point de vue est en creation
envoiEnCours ?
<PopupAttente texte={"Point de vue en création, veuillez patienter quelque instants."}/>
:
null
}
<div className="backdrop-creation-point">
<div className="creation-point">
{/* Retour en arriere a la dernier page vue si annuler */}
<div onClick={() => history.goBack()} className='btn-fermer-point'>Annuler</div>
<div className="etapes-creation-point">Étape {etapeCreation} sur 4</div>
<h2 className="titre-creation-point">Création d'un point de vue personnalisé</h2>
{/* ne reload pas la page */}
<form onSubmit={(e) => e.preventDefault()} className="creation-point-form">
{
// si l'index des etaps est celui de la page, l'affiche
etapeCreation === 1 ?
<>
{/* champs donnant chacun une valeur a un champ specifique */}
<div className="champ-creation-point">
<div className={`titre-champ-creation-point${pointVueEnCreation.nom !== "" ?" ok":""}`}>Nom</div>
<input
placeholder="Veuillez choisir un nom"
className="input-creation-point"
name="nom"
type="text"
onChange={ (e) => updatePointHandler(e.target.value, "nom")}
value={pointVueEnCreation.nom}
maxLength={60}
/>
<div className="input-underline"></div>
</div>
<div className="champ-creation-point">
<div className={`titre-champ-creation-point${pointVueEnCreation.description !== "" ?" ok":""}`}>Description</div>
<textarea
placeholder="Veuillez choisir une description pour ce point de vue"
className="input-creation-point"
name="description"
onChange={ (e) => updatePointHandler(e.target.value, "description")}
value={pointVueEnCreation.description}
maxLength={1400}
/>
<div className="input-underline"></div>
</div>
<div className="form-navigation-creation-point suivant-only">
<button onClick={() => changeStepHandler(+1)}>Suivant</button>
</div>
</>
:
null
}
{
etapeCreation === 2 ?
<>
<div className="champ-creation-point">
<div className={`titre-champ-creation-point${pointVueEnCreation.typePaysage.length > 0 ?" ok":""}`}>Type de point de vue</div>
<div className="input-creation-point type-ptn-vue">
{
typesPaysage.map(
(unTypePaysage) => {
return(
<div key={unTypePaysage} onClick={() => updatePointHandler(unTypePaysage,"typePaysage")} className={`pastille-filtre${pointVueEnCreation.typePaysage.includes(unTypePaysage)?" active":""}`}>{unTypePaysage}</div>
);
}
)
}
</div>
</div>
<div className="champ-creation-point">
<div className={`titre-champ-creation-point${pointVueEnCreation.accessibilite.length > 0 ?" ok":""}`}>Méthodes d'accès</div>
<div className="input-creation-point type-ptn-vue">
{
typesAccessibilite.map(
(unTypeAccessibilite) => {
return(
<div key={unTypeAccessibilite} onClick={() => updatePointHandler(unTypeAccessibilite,"accessibilite")} className={`pastille-filtre${pointVueEnCreation.accessibilite.includes(unTypeAccessibilite)?" active":""}`}>{unTypeAccessibilite}</div>
);
}
)
}
</div>
</div>
<div className="form-navigation-creation-point">
<button onClick={() => changeStepHandler(-1)}>Précédent</button>
<button onClick={() => changeStepHandler(+1)}>Suivant</button>
</div>
</>
:
null
}
{
etapeCreation === 3 ?
<>
<div className="champ-creation-point">
<div className={`titre-champ-creation-point${pointVueEnCreation.lat !== null && pointVueEnCreation.lng !== null ?" ok":""}`}>Emplacement</div>
<div className="infos-champ-creation-point">Choisir l'emplacement de ce point de vue</div>
{/* Mini google maps permettant d'affiche et de choisir un emplacement sur la carte */}
<GoogleMapProps
initMarkers={[]}
mapCenter={userMarkerPosition}
initUserMarker={initUserMarker}
userMarkerPosition={userMarkerPosition}
origineTrace={null}
finTrace={null}
// Ne rien faire apres le chargement dans ce script
onMapLoadCB={() => {}}
mapStyles = { { // style du container contenant la carte (par defaut hauteur 0, donc invisible)
height : "300px",
width : "100%",
borderRadius : "20px"
}}
onClickHandler={onClickHandler}
>
{
markerPosition !== null ?
<Marker
icon={{
url: "/icones/point-vue.svg",
scaledSize: new window.google.maps.Size(60, 60)
}}
position={markerPosition}
/>
:
null
}
</GoogleMapProps>
{/* Bouton permettant d'utiliser la position locale de l'utilisateur */}
<button
className="utiliser-position-locale"
onClick={
() => navigator.geolocation.getCurrentPosition(function(position){
updatePointHandler(position.coords.latitude, "lat");
updatePointHandler(position.coords.longitude, "lng");
updatePointHandler(geohashForLocation([position.coords.latitude,position.coords.longitude]), "geoHash");
setMarkerPosition({lat: position.coords.latitude,lng: position.coords.longitude});
})
}
>
Utiliser ma position
</button>
</div>
<div className="form-navigation-creation-point">
<button onClick={() => changeStepHandler(-1)}>Précédent</button>
<button onClick={() => changeStepHandler(+1)}>Suivant</button>
</div>
</>
:
null
}
{
etapeCreation === 4 ?
<>
<div className="champ-creation-point">
<div className={`titre-champ-creation-point${urlImagesAjoutees.length > 0 ?" ok":""}`}>Ajout d'images</div>
<div className="infos-champ-creation-point">Ajouter des images au point de vue (Min. 1)</div>
{/* Affiche une mini galerie des iamges ajoutees */}
<div className="ajout-images">
<div className="images-ajoutees">
{
urlImagesAjoutees.map(
(unUrlImage, index) => {
return(
<div key={`${unUrlImage}${index}`} style={{backgroundImage: `url(${unUrlImage})`}} className="image-ajoutee">
<div onClick={() => retirerImage(index)} className="btn-retirer">Retirer</div>
</div>
);
}
)
}
{/* Bouton d'ajout d'image, cache si le maximum d'images est atteint */}
<label style={imagesAjoutees.length >= 15?{display:"none"}:null} className="btn-ajout">
<input style={{display:"none"}} type="file" accept="image/*" onChange={(e) => ajouterImage(e)} value=""></input>
<div>Ajouter une image</div>
<div className="icone-ajout">+</div>
</label>
</div>
</div>
{/* si la verification est correcte, change son aspect, le disables est dans la fonction de creation */}
<div className={`btn-creer-ptn-vue${verificationCreation() === true ?" activable":""}`} onClick={() => addPointDeVue()}>Créer le point de vue</div>
</div>
<div className="form-navigation-creation-point">
<button onClick={() => changeStepHandler(-1)}>Précédent</button>
</div>
</>
:
null
}
</form>
</div>
</div>
</>
);
}
export default CreationPoint;