diff --git a/assets/img/favicon/android-icon-144x144.png b/assets/img/favicon/android-icon-144x144.png
deleted file mode 100644
index b4d43bb..0000000
Binary files a/assets/img/favicon/android-icon-144x144.png and /dev/null differ
diff --git a/assets/img/favicon/android-icon-192x192.png b/assets/img/favicon/android-icon-192x192.png
deleted file mode 100644
index 197cde4..0000000
Binary files a/assets/img/favicon/android-icon-192x192.png and /dev/null differ
diff --git a/assets/img/favicon/android-icon-36x36.png b/assets/img/favicon/android-icon-36x36.png
deleted file mode 100644
index d7b642a..0000000
Binary files a/assets/img/favicon/android-icon-36x36.png and /dev/null differ
diff --git a/assets/img/favicon/android-icon-48x48.png b/assets/img/favicon/android-icon-48x48.png
deleted file mode 100644
index f6bb35d..0000000
Binary files a/assets/img/favicon/android-icon-48x48.png and /dev/null differ
diff --git a/assets/img/favicon/android-icon-72x72.png b/assets/img/favicon/android-icon-72x72.png
deleted file mode 100644
index b0938fd..0000000
Binary files a/assets/img/favicon/android-icon-72x72.png and /dev/null differ
diff --git a/assets/img/favicon/android-icon-96x96.png b/assets/img/favicon/android-icon-96x96.png
deleted file mode 100644
index 4b8a353..0000000
Binary files a/assets/img/favicon/android-icon-96x96.png and /dev/null differ
diff --git a/assets/img/favicon/apple-icon-114x114.png b/assets/img/favicon/apple-icon-114x114.png
deleted file mode 100644
index d1a1194..0000000
Binary files a/assets/img/favicon/apple-icon-114x114.png and /dev/null differ
diff --git a/assets/img/favicon/apple-icon-120x120.png b/assets/img/favicon/apple-icon-120x120.png
deleted file mode 100644
index ad5eba2..0000000
Binary files a/assets/img/favicon/apple-icon-120x120.png and /dev/null differ
diff --git a/assets/img/favicon/apple-icon-144x144.png b/assets/img/favicon/apple-icon-144x144.png
deleted file mode 100644
index b4d43bb..0000000
Binary files a/assets/img/favicon/apple-icon-144x144.png and /dev/null differ
diff --git a/assets/img/favicon/apple-icon-152x152.png b/assets/img/favicon/apple-icon-152x152.png
deleted file mode 100644
index b61a663..0000000
Binary files a/assets/img/favicon/apple-icon-152x152.png and /dev/null differ
diff --git a/assets/img/favicon/apple-icon-180x180.png b/assets/img/favicon/apple-icon-180x180.png
deleted file mode 100644
index e2c73e8..0000000
Binary files a/assets/img/favicon/apple-icon-180x180.png and /dev/null differ
diff --git a/assets/img/favicon/apple-icon-57x57.png b/assets/img/favicon/apple-icon-57x57.png
deleted file mode 100644
index a64c182..0000000
Binary files a/assets/img/favicon/apple-icon-57x57.png and /dev/null differ
diff --git a/assets/img/favicon/apple-icon-60x60.png b/assets/img/favicon/apple-icon-60x60.png
deleted file mode 100644
index dacf326..0000000
Binary files a/assets/img/favicon/apple-icon-60x60.png and /dev/null differ
diff --git a/assets/img/favicon/apple-icon-72x72.png b/assets/img/favicon/apple-icon-72x72.png
deleted file mode 100644
index b0938fd..0000000
Binary files a/assets/img/favicon/apple-icon-72x72.png and /dev/null differ
diff --git a/assets/img/favicon/apple-icon-76x76.png b/assets/img/favicon/apple-icon-76x76.png
deleted file mode 100644
index 2170c4c..0000000
Binary files a/assets/img/favicon/apple-icon-76x76.png and /dev/null differ
diff --git a/assets/img/favicon/apple-icon-precomposed.png b/assets/img/favicon/apple-icon-precomposed.png
deleted file mode 100644
index a4809cf..0000000
Binary files a/assets/img/favicon/apple-icon-precomposed.png and /dev/null differ
diff --git a/assets/img/favicon/apple-icon.png b/assets/img/favicon/apple-icon.png
deleted file mode 100644
index a4809cf..0000000
Binary files a/assets/img/favicon/apple-icon.png and /dev/null differ
diff --git a/assets/img/favicon/favicon-16x16.png b/assets/img/favicon/favicon-16x16.png
deleted file mode 100644
index 1a58469..0000000
Binary files a/assets/img/favicon/favicon-16x16.png and /dev/null differ
diff --git a/assets/img/favicon/favicon-32x32.png b/assets/img/favicon/favicon-32x32.png
deleted file mode 100644
index cc8b788..0000000
Binary files a/assets/img/favicon/favicon-32x32.png and /dev/null differ
diff --git a/assets/img/favicon/favicon-96x96.png b/assets/img/favicon/favicon-96x96.png
deleted file mode 100644
index fc2267c..0000000
Binary files a/assets/img/favicon/favicon-96x96.png and /dev/null differ
diff --git a/assets/img/favicon/favicon.ico b/assets/img/favicon/favicon.ico
deleted file mode 100644
index c4b19d5..0000000
Binary files a/assets/img/favicon/favicon.ico and /dev/null differ
diff --git a/assets/img/favicon/ms-icon-144x144.png b/assets/img/favicon/ms-icon-144x144.png
deleted file mode 100644
index 43a5a9e..0000000
Binary files a/assets/img/favicon/ms-icon-144x144.png and /dev/null differ
diff --git a/assets/img/favicon/ms-icon-150x150.png b/assets/img/favicon/ms-icon-150x150.png
deleted file mode 100644
index 5a8d487..0000000
Binary files a/assets/img/favicon/ms-icon-150x150.png and /dev/null differ
diff --git a/assets/img/favicon/ms-icon-310x310.png b/assets/img/favicon/ms-icon-310x310.png
deleted file mode 100644
index f7a262a..0000000
Binary files a/assets/img/favicon/ms-icon-310x310.png and /dev/null differ
diff --git a/assets/img/favicon/ms-icon-70x70.png b/assets/img/favicon/ms-icon-70x70.png
deleted file mode 100644
index 8871919..0000000
Binary files a/assets/img/favicon/ms-icon-70x70.png and /dev/null differ
diff --git a/assets/img/logo-social.png b/assets/img/logo-social.png
deleted file mode 100644
index a97caba..0000000
Binary files a/assets/img/logo-social.png and /dev/null differ
diff --git a/assets/img/logo/center.svg b/assets/img/logo/center.svg
deleted file mode 100644
index dc28bac..0000000
--- a/assets/img/logo/center.svg
+++ /dev/null
@@ -1,88 +0,0 @@
-
-
-
diff --git a/assets/img/logo/circle.svg b/assets/img/logo/circle.svg
deleted file mode 100644
index fb67f48..0000000
--- a/assets/img/logo/circle.svg
+++ /dev/null
@@ -1,44 +0,0 @@
-
-
-
diff --git a/assets/img/logo/comment.svg b/assets/img/logo/comment.svg
deleted file mode 100644
index 8c3051a..0000000
--- a/assets/img/logo/comment.svg
+++ /dev/null
@@ -1 +0,0 @@
-
\ No newline at end of file
diff --git a/assets/img/logo/debug.svg b/assets/img/logo/debug.svg
deleted file mode 100644
index 9ea55ea..0000000
--- a/assets/img/logo/debug.svg
+++ /dev/null
@@ -1 +0,0 @@
-
\ No newline at end of file
diff --git a/assets/img/logo/delete.svg b/assets/img/logo/delete.svg
deleted file mode 100644
index b3f6f96..0000000
--- a/assets/img/logo/delete.svg
+++ /dev/null
@@ -1 +0,0 @@
-
\ No newline at end of file
diff --git a/assets/img/logo/dollar.svg b/assets/img/logo/dollar.svg
deleted file mode 100644
index 80c776e..0000000
--- a/assets/img/logo/dollar.svg
+++ /dev/null
@@ -1,42 +0,0 @@
-
-
-
diff --git a/assets/img/logo/edit.svg b/assets/img/logo/edit.svg
deleted file mode 100644
index 23f0650..0000000
--- a/assets/img/logo/edit.svg
+++ /dev/null
@@ -1,51 +0,0 @@
-
-
-
diff --git a/assets/img/logo/hide.svg b/assets/img/logo/hide.svg
deleted file mode 100644
index 7dda0aa..0000000
--- a/assets/img/logo/hide.svg
+++ /dev/null
@@ -1,75 +0,0 @@
-
-
diff --git a/assets/img/logo/info.svg b/assets/img/logo/info.svg
deleted file mode 100644
index 7cc245a..0000000
--- a/assets/img/logo/info.svg
+++ /dev/null
@@ -1,180 +0,0 @@
-
-
diff --git a/assets/img/logo/photo.svg b/assets/img/logo/photo.svg
deleted file mode 100644
index ebc0e4e..0000000
--- a/assets/img/logo/photo.svg
+++ /dev/null
@@ -1,47 +0,0 @@
-
-
-
diff --git a/assets/img/logo/precision.svg b/assets/img/logo/precision.svg
deleted file mode 100644
index 1114125..0000000
--- a/assets/img/logo/precision.svg
+++ /dev/null
@@ -1 +0,0 @@
-
\ No newline at end of file
diff --git a/assets/img/logo/star.svg b/assets/img/logo/star.svg
deleted file mode 100644
index bdca924..0000000
--- a/assets/img/logo/star.svg
+++ /dev/null
@@ -1 +0,0 @@
-
\ No newline at end of file
diff --git a/assets/img/marker/cluster-icon-blue.png b/assets/img/marker/cluster-icon-blue.png
deleted file mode 100644
index 06a9a60..0000000
Binary files a/assets/img/marker/cluster-icon-blue.png and /dev/null differ
diff --git a/assets/img/marker/cluster-icon-green.png b/assets/img/marker/cluster-icon-green.png
deleted file mode 100644
index 82f84e4..0000000
Binary files a/assets/img/marker/cluster-icon-green.png and /dev/null differ
diff --git a/assets/img/marker/cluster-icon-red.png b/assets/img/marker/cluster-icon-red.png
deleted file mode 100644
index 405c7e1..0000000
Binary files a/assets/img/marker/cluster-icon-red.png and /dev/null differ
diff --git a/assets/img/marker/marker-icon-black.png b/assets/img/marker/marker-icon-black.png
deleted file mode 100644
index 23c94cf..0000000
Binary files a/assets/img/marker/marker-icon-black.png and /dev/null differ
diff --git a/assets/img/marker/marker-icon-blue.png b/assets/img/marker/marker-icon-blue.png
deleted file mode 100644
index 0015b64..0000000
Binary files a/assets/img/marker/marker-icon-blue.png and /dev/null differ
diff --git a/assets/img/marker/marker-icon-gold.png b/assets/img/marker/marker-icon-gold.png
deleted file mode 100644
index 6992d65..0000000
Binary files a/assets/img/marker/marker-icon-gold.png and /dev/null differ
diff --git a/assets/img/marker/marker-icon-green.png b/assets/img/marker/marker-icon-green.png
deleted file mode 100644
index c359abb..0000000
Binary files a/assets/img/marker/marker-icon-green.png and /dev/null differ
diff --git a/assets/img/marker/marker-icon-grey.png b/assets/img/marker/marker-icon-grey.png
deleted file mode 100644
index 43b3eb4..0000000
Binary files a/assets/img/marker/marker-icon-grey.png and /dev/null differ
diff --git a/assets/img/marker/marker-icon-orange.png b/assets/img/marker/marker-icon-orange.png
deleted file mode 100644
index c3c8632..0000000
Binary files a/assets/img/marker/marker-icon-orange.png and /dev/null differ
diff --git a/assets/img/marker/marker-icon-red.png b/assets/img/marker/marker-icon-red.png
deleted file mode 100644
index 1c26e9f..0000000
Binary files a/assets/img/marker/marker-icon-red.png and /dev/null differ
diff --git a/assets/img/marker/marker-icon-violet.png b/assets/img/marker/marker-icon-violet.png
deleted file mode 100644
index ea748aa..0000000
Binary files a/assets/img/marker/marker-icon-violet.png and /dev/null differ
diff --git a/assets/img/marker/marker-icon-yellow.png b/assets/img/marker/marker-icon-yellow.png
deleted file mode 100644
index 8b677d9..0000000
Binary files a/assets/img/marker/marker-icon-yellow.png and /dev/null differ
diff --git a/assets/img/marker/marker-shadow.png b/assets/img/marker/marker-shadow.png
deleted file mode 100644
index 84c5808..0000000
Binary files a/assets/img/marker/marker-shadow.png and /dev/null differ
diff --git a/assets/nls/de.json b/assets/nls/de.json
deleted file mode 100644
index 29c0ea2..0000000
--- a/assets/nls/de.json
+++ /dev/null
@@ -1,121 +0,0 @@
-{
- "debug": {
- "lat": "Latitude",
- "lng": "Longitude",
- "updates": "Updates",
- "accuracy": "Accuracy",
- "highAccuracy": "High accuracy",
- "posAge": "Position max age",
- "posTimeout": "Position timeout",
- "zoom": "Zoom level",
- "enabled": "Enabled",
- "disabled": "Disabled",
- "marks": "Marks",
- "export": "Export data"
- },
- "notif": {
- "geolocationError": "Your browser doesn't implement the geolocation API",
- "newMarkerOutside": "New marker out of your range",
- "spotAdded": "New spot saved to map",
- "storeAdded": "New store saved to map",
- "barAdded": "New bar saved to map",
- "spotDeleted": "Spot has been successfully deleted from map",
- "storeDeleted": "Store has been successfully deleted from map",
- "barDeleted": "Bar has been successfully deleted from map",
- "markNameEmpty": "You didn't specified a name for the mark",
- "lockFocusOn": "Centering and locking on your position",
- "unlockFocusOn": "Position locking ended"
- },
- "nav": {
- "add": "Add",
- "cancel": "Cancel",
- "close": "Close",
- "delete": "Delete"
- },
- "map": {
- "newTitle": "New marker",
- "newSpot": "Add a spot",
- "newStore": "Add a shop",
- "newBar": "Add a bar",
- "planLayerOSM": "Plan OSM",
- "planLayerGeo": "Plan GeoPortail",
- "satLayerEsri": "Satellite ESRI",
- "satLayerGeo": "Satellite GeoPortail"
- },
- "spot": {
- "title": "New spot",
- "subtitle": "A spot is a remarkable place to crack a beer ! Share it with the community, wether it is for the astonishing view of for whatever it is enjoyable to drink a beer!",
- "nameLabel": "Name that spot",
- "descLabel": "Why don't you describe it",
- "rateLabel": "Give it a note"
- },
- "store": {
- "title": "New store",
- "subtitle": "The must have place to refill your beer stock. The more info you provide, the better you help your fellow beer crackerz!",
- "nameLabel": "Store name",
- "descLabel": "Why don't you describe it",
- "rateLabel": "Give it a note"
- },
- "bar": {
- "title": "New bar",
- "subtitle": "A bar is a holly place where you can get some nicely colded draft beers!",
- "nameLabel": "Bar name",
- "descLabel": "Why don't you describe it",
- "rateLabel": "Give it a note"
- },
- "modal": {
- "userTitle": "User account",
- "userAccuracyPref": "High precision",
- "userDebugPref": "Debug interface",
- "aboutTitle": "About BeerCrackerz",
- "aboutDesc": "A brilliant idea from David Béché! BeerCrackerz is the beer lovers comunity, filled with pint slayers and cereals lovers",
- "hideShowTitle": "Map options",
- "hideShowLabels": "Labels",
- "hideShowCircles": "Circles",
- "hideShowSpots": "Spots",
- "hideShowStores": "Stores",
- "hideShowBars": "Bars",
- "deleteMarkTitle": "Delete mark",
- "deleteMarkDesc": "Are you sure you want to delete this mark? This action is permanent and can not be reverted.",
- "spotEditTitle": "Edit spot",
- "storeEditTitle": "Edit store",
- "barEditTitle": "Edit bar"
- },
- "popup": {
- "spotFoundBy": "A spot discovered by",
- "storeFoundBy": "A store added by",
- "barFoundBy": "A bar added by",
- "spotNoDesc": "No description available for this spot",
- "storeNoDesc": "No description available for this store",
- "barNoDesc": "No description available for this bar"
- },
- "auth": {
- "login": {
- "headTitle": "Connexion | BeerCrackerz",
- "subtitle": "Se connecter",
- "hiddenError": "Oh! Un texte caché!",
- "username": "Nom d'utilisateur ou Email",
- "password": "Mot de passe",
- "login": "Se connecter",
- "notRegistered": "Pas encore inscrit?",
- "register": "Créer un compte",
- "bothEmpty": "Veuillez saisir un nom d'utilisateur et un mot de passe",
- "usernameEmpty": "Veuillez saisir un nom d'utilisateur",
- "passwordEmpty": "Veuillez saisir votre mot de passe",
- "serverError": "Une erreur serveur est survenue, contactez le support"
- },
- "register": {
- "headTitle": "S'inscrire | BeerCrackerz",
- "subtitle": "S'inscrire",
- "hiddenError": "Oh! Un texte caché!",
- "username": "Nom d'utilisateur ou Email",
- "password1": "Mot de passe",
- "password2": "Confirmer le mot de passe",
- "register": "S'inscrire",
- "notRegistered": "Déjà inscrit?",
- "login": "Se connecter",
- "fieldEmpty": "Veuillez remplir tous les champs du formulaire",
- "notMatchingPassword": "Les deux mots de passe ne correspondent pas"
- }
- }
-}
diff --git a/assets/nls/en.json b/assets/nls/en.json
deleted file mode 100644
index 848c375..0000000
--- a/assets/nls/en.json
+++ /dev/null
@@ -1,121 +0,0 @@
-{
- "debug": {
- "lat": "Latitude",
- "lng": "Longitude",
- "updates": "Updates",
- "accuracy": "Accuracy",
- "highAccuracy": "High accuracy",
- "posAge": "Position max age",
- "posTimeout": "Position timeout",
- "zoom": "Zoom level",
- "enabled": "Enabled",
- "disabled": "Disabled",
- "marks": "Marks",
- "export": "Export data"
- },
- "notif": {
- "geolocationError": "Your browser doesn't implement the geolocation API",
- "newMarkerOutside": "New marker out of your range",
- "spotAdded": "New spot saved to map",
- "storeAdded": "New store saved to map",
- "barAdded": "New bar saved to map",
- "spotDeleted": "Spot has been successfully deleted from map",
- "storeDeleted": "Store has been successfully deleted from map",
- "barDeleted": "Bar has been successfully deleted from map",
- "markNameEmpty": "You didn't specified a name for the mark",
- "lockFocusOn": "Centering and locking view on your position",
- "unlockFocusOn": "Position locking ended"
- },
- "nav": {
- "add": "Add",
- "cancel": "Cancel",
- "close": "Close",
- "delete": "Delete"
- },
- "map": {
- "newTitle": "New marker",
- "newSpot": "Add a spot",
- "newStore": "Add a shop",
- "newBar": "Add a bar",
- "planLayerOSM": "Plan OSM",
- "planLayerGeo": "Plan GeoPortail",
- "satLayerEsri": "Satellite ESRI",
- "satLayerGeo": "Satellite GeoPortail"
- },
- "spot": {
- "title": "New spot",
- "subtitle": "A spot is a remarkable place to crack a beer ! Share it with the community, wether it is for the astonishing view of for whatever it is enjoyable to drink a beer!",
- "nameLabel": "Name that spot",
- "descLabel": "Why don't you describe it",
- "rateLabel": "Give it a note"
- },
- "store": {
- "title": "New store",
- "subtitle": "The must have place to refill your beer stock. The more info you provide, the better you help your fellow beer crackerz!",
- "nameLabel": "Store name",
- "descLabel": "Why don't you describe it",
- "rateLabel": "Give it a note"
- },
- "bar": {
- "title": "New bar",
- "subtitle": "A bar is a holly place where you can get some nicely colded draft beers!",
- "nameLabel": "Bar name",
- "descLabel": "Why don't you describe it",
- "rateLabel": "Give it a note"
- },
- "modal": {
- "userTitle": "User account",
- "userAccuracyPref": "High precision",
- "userDebugPref": "Debug interface",
- "aboutTitle": "About BeerCrackerz",
- "aboutDesc": "A brilliant idea from David Béché! BeerCrackerz is the beer lovers comunity, filled with pint slayers and cereals lovers",
- "hideShowTitle": "Map options",
- "hideShowLabels": "Labels",
- "hideShowCircles": "Circles",
- "hideShowSpots": "Spots",
- "hideShowStores": "Stores",
- "hideShowBars": "Bars",
- "deleteMarkTitle": "Delete mark",
- "deleteMarkDesc": "Are you sure you want to delete this mark? This action is permanent and can not be reverted.",
- "spotEditTitle": "Edit spot",
- "storeEditTitle": "Edit store",
- "barEditTitle": "Edit bar"
- },
- "popup": {
- "spotFoundBy": "A spot discovered by",
- "storeFoundBy": "A store added by",
- "barFoundBy": "A bar added by",
- "spotNoDesc": "No description available for this spot",
- "storeNoDesc": "No description available for this store",
- "barNoDesc": "No description available for this bar"
- },
- "auth": {
- "login": {
- "headTitle": "Connexion | BeerCrackerz",
- "subtitle": "Se connecter",
- "hiddenError": "Oh! Un texte caché!",
- "username": "Nom d'utilisateur ou Email",
- "password": "Mot de passe",
- "login": "Se connecter",
- "notRegistered": "Pas encore inscrit?",
- "register": "Créer un compte",
- "bothEmpty": "Veuillez saisir un nom d'utilisateur et un mot de passe",
- "usernameEmpty": "Veuillez saisir un nom d'utilisateur",
- "passwordEmpty": "Veuillez saisir votre mot de passe",
- "serverError": "Une erreur serveur est survenue, contactez le support"
- },
- "register": {
- "headTitle": "S'inscrire | BeerCrackerz",
- "subtitle": "S'inscrire",
- "hiddenError": "Oh! Un texte caché!",
- "username": "Nom d'utilisateur ou Email",
- "password1": "Mot de passe",
- "password2": "Confirmer le mot de passe",
- "register": "S'inscrire",
- "notRegistered": "Déjà inscrit?",
- "login": "Se connecter",
- "fieldEmpty": "Veuillez remplir tous les champs du formulaire",
- "notMatchingPassword": "Les deux mots de passe ne correspondent pas"
- }
- }
-}
diff --git a/assets/nls/es.json b/assets/nls/es.json
deleted file mode 100644
index c7a8342..0000000
--- a/assets/nls/es.json
+++ /dev/null
@@ -1,121 +0,0 @@
-{
- "debug": {
- "lat": "Latitude",
- "lng": "Longitude",
- "updates": "Updates",
- "accuracy": "Accuracy",
- "highAccuracy": "High accuracy",
- "posAge": "Position max age",
- "posTimeout": "Position timeout",
- "zoom": "Zoom level",
- "enabled": "Enabled",
- "disabled": "Disabled",
- "marks": "Marks",
- "export": "Export data"
- },
- "notif": {
- "geolocationError": "Your browser doesn't implement the geolocation API",
- "newMarkerOutside": "New marker out of range",
- "spotAdded": "New spot saved to map",
- "storeAdded": "New store saved to map",
- "barAdded": "New bar saved to map",
- "spotDeleted": "Spot has been successfully deleted from map",
- "storeDeleted": "Store has been successfully deleted from map",
- "barDeleted": "Bar has been successfully deleted from map",
- "markNameEmpty": "You didn't specified a name for the mark",
- "lockFocusOn": "Centering and locking on your position",
- "unlockFocusOn": "Position locking ended"
- },
- "nav": {
- "add": "Add",
- "cancel": "Cancel",
- "close": "Close",
- "delete": "Delete"
- },
- "map": {
- "newTitle": "New marker",
- "newSpot": "Add a spot",
- "newStore": "Add a shop",
- "newBar": "Add a bar",
- "planLayerOSM": "Plan OSM",
- "planLayerGeo": "Plan GeoPortail",
- "satLayerEsri": "Satellite ESRI",
- "satLayerGeo": "Satellite GeoPortail"
- },
- "spot": {
- "title": "New spot",
- "subtitle": "A spot is a remarkable place to crack a beer ! Share it with the community, wether it is for the astonishing view of for whatever it is enjoyable to drink a beer!",
- "nameLabel": "Name that spot",
- "descLabel": "Why don't you describe it",
- "rateLabel": "Give it a note"
- },
- "store": {
- "title": "New store",
- "subtitle": "The must have place to refill your beer stock. The more info you provide, the better you help your fellow beer crackerz!",
- "nameLabel": "Store name",
- "descLabel": "Why don't you describe it",
- "rateLabel": "Give it a note"
- },
- "bar": {
- "title": "New bar",
- "subtitle": "A bar is a holly place where you can get some nicely colded draft beers!",
- "nameLabel": "Bar name",
- "descLabel": "Why don't you describe it",
- "rateLabel": "Give it a note"
- },
- "modal": {
- "userTitle": "User account",
- "userAccuracyPref": "High precision",
- "userDebugPref": "Debug interface",
- "aboutTitle": "About BeerCrackerz",
- "aboutDesc": "A brilliant idea from David Béché! BeerCrackerz is the beer lovers comunity, filled with pint slayers and cereals lovers",
- "hideShowTitle": "Map options",
- "hideShowLabels": "Labels",
- "hideShowCircles": "Circles",
- "hideShowSpots": "Spots",
- "hideShowStores": "Stores",
- "hideShowBars": "Bars",
- "deleteMarkTitle": "Delete mark",
- "deleteMarkDesc": "Are you sure you want to delete this mark? This action is permanent and can not be reverted.",
- "spotEditTitle": "Edit spot",
- "storeEditTitle": "Edit store",
- "barEditTitle": "Edit bar"
- },
- "popup": {
- "spotFoundBy": "A spot discovered by",
- "storeFoundBy": "A store added by",
- "barFoundBy": "A bar added by",
- "spotNoDesc": "No description available for this spot",
- "storeNoDesc": "No description available for this store",
- "barNoDesc": "No description available for this bar"
- },
- "auth": {
- "login": {
- "headTitle": "Connexion | BeerCrackerz",
- "subtitle": "Se connecter",
- "hiddenError": "Oh! Un texte caché!",
- "username": "Nom d'utilisateur ou Email",
- "password": "Mot de passe",
- "login": "Se connecter",
- "notRegistered": "Pas encore inscrit?",
- "register": "Créer un compte",
- "bothEmpty": "Veuillez saisir un nom d'utilisateur et un mot de passe",
- "usernameEmpty": "Veuillez saisir un nom d'utilisateur",
- "passwordEmpty": "Veuillez saisir votre mot de passe",
- "serverError": "Une erreur serveur est survenue, contactez le support"
- },
- "register": {
- "headTitle": "S'inscrire | BeerCrackerz",
- "subtitle": "S'inscrire",
- "hiddenError": "Oh! Un texte caché!",
- "username": "Nom d'utilisateur ou Email",
- "password1": "Mot de passe",
- "password2": "Confirmer le mot de passe",
- "register": "S'inscrire",
- "notRegistered": "Déjà inscrit?",
- "login": "Se connecter",
- "fieldEmpty": "Veuillez remplir tous les champs du formulaire",
- "notMatchingPassword": "Les deux mots de passe ne correspondent pas"
- }
- }
-}
diff --git a/assets/nls/fr.json b/assets/nls/fr.json
deleted file mode 100644
index 571ad49..0000000
--- a/assets/nls/fr.json
+++ /dev/null
@@ -1,121 +0,0 @@
-{
- "debug": {
- "lat": "Latitude",
- "lng": "Longitude",
- "updates": "Mises à jour",
- "accuracy": "Précision",
- "highAccuracy": "Haute précision",
- "posAge": "Fréquence de rafraichissement",
- "posTimeout": "Validité de la position",
- "zoom": "Niveau de zoom",
- "enabled": "Activée",
- "disabled": "Désactivée",
- "marks": "Marqueurs",
- "export": "Exporter les données"
- },
- "notif": {
- "geolocationError": "Votre navigateur ne peux utiliser votre localisation",
- "newMarkerOutside": "Nouveau marqueur hors de votre portée",
- "spotAdded": "Nouveau spot ajouté à la carte",
- "storeAdded": "Nouveau magasin ajouté à la carte",
- "barAdded": "Nouveau bar ajouté à la carte",
- "spotDeleted": "Le spot a été supprimé de la carte avec succès",
- "storeDeleted": "Le magasin a été supprimé de la carte avec succès",
- "barDeleted": "Le bar a été supprimé de la carte avec succès",
- "markNameEmpty": "Vous devez specifier un nom pour le marqueur",
- "lockFocusOn": "Suivre et recentrer sur votre position",
- "unlockFocusOn": "Fin du suivi de position"
- },
- "nav": {
- "add": "Ajouter",
- "cancel": "Annuler",
- "close": "Fermer",
- "delete": "Supprimer"
- },
- "map": {
- "newTitle": "Nouveau marqueur",
- "newSpot": "Ajouter un spot",
- "newStore": "Ajouter un magasin",
- "newBar": "Ajouter un bar",
- "planLayerOSM": "Plan OSM",
- "planLayerGeo": "Plan GeoPortail",
- "satLayerEsri": "Satellite ESRI",
- "satLayerGeo": "Satellite GeoPortail"
- },
- "spot": {
- "title": "Nouveau spot",
- "subtitle": "Un spot est un endroit remarquable pour cracker une bière en tout quiétude! Faites en profiter la communauté, que ce soit pour le calme exceptionnel, pour la vue incroyable ou pour tout autre source de ravissement houblonné.",
- "nameLabel": "Nommer ce spot",
- "descLabel": "Pourquoi ne pas le décrire",
- "rateLabel": "Lui attribuer une note"
- },
- "store": {
- "title": "Nouveau magasin",
- "subtitle": "C'est un indispensable pour se ravitailler des meilleurs breuvages houblonnés. Rensigner la gamme de prix et surtout si fraicheur il y a!",
- "nameLabel": "Nom du magasin",
- "descLabel": "Pourquoi ne pas le décrire",
- "rateLabel": "Lui attribuer une note"
- },
- "bar": {
- "title": "Nouveau bar",
- "subtitle": "Un bar est un endroit convivial ou le houblons coule des saintes tireuses à pression.",
- "nameLabel": "Nom du bar",
- "descLabel": "Pourquoi ne pas le décrire",
- "rateLabel": "Lui attribuer une note"
- },
- "modal": {
- "userTitle": "Compte utilisateur",
- "userAccuracyPref": "Haute précision",
- "userDebugPref": "Interface de debug",
- "aboutTitle": "À propos de BeerCrackerz",
- "aboutDesc": "Une idée brillante de David Béché! BeerCrackerz, c'est la communauté incontournable d'amoureux du houblons, de pourfendeurs de pintes, d'aficionados de céréales!",
- "hideShowTitle": "Options de carte",
- "hideShowLabels": "Étiquettes",
- "hideShowCircles": "Cercles",
- "hideShowSpots": "Spots",
- "hideShowStores": "Magasins",
- "hideShowBars": "Bars",
- "deleteMarkTitle": "Supprimer le marqueur",
- "deleteMarkDesc": "Êtes-vous sûr de vouloir supprimer ce marqueur? Cette action est permanente et irréversible.",
- "spotEditTitle": "Éditer le spot",
- "storeEditTitle": "Éditer le magasin",
- "barEditTitle": "Éditer le bar"
- },
- "popup": {
- "spotFoundBy": "Un spot découvert par",
- "storeFoundBy": "Un magasin ajouté par",
- "barFoundBy": "Un bar ajouté par",
- "spotNoDesc": "Pas de description disponible pour ce spot",
- "storeNoDesc": "Pas de description disponible pour ce magasin",
- "barNoDesc": "Pas de description disponible pour ce bar"
- },
- "auth": {
- "login": {
- "headTitle": "Connexion | BeerCrackerz",
- "subtitle": "Se connecter",
- "hiddenError": "Oh! Un texte caché!",
- "username": "Nom d'utilisateur ou Email",
- "password": "Mot de passe",
- "login": "Se connecter",
- "notRegistered": "Pas encore inscrit?",
- "register": "Créer un compte",
- "bothEmpty": "Veuillez saisir un nom d'utilisateur et un mot de passe",
- "usernameEmpty": "Veuillez saisir un nom d'utilisateur",
- "passwordEmpty": "Veuillez saisir votre mot de passe",
- "serverError": "Une erreur serveur est survenue, contactez le support"
- },
- "register": {
- "headTitle": "S'inscrire | BeerCrackerz",
- "subtitle": "S'inscrire",
- "hiddenError": "Oh! Un texte caché!",
- "username": "Nom d'utilisateur ou Email",
- "password1": "Mot de passe",
- "password2": "Confirmer le mot de passe",
- "register": "S'inscrire",
- "notRegistered": "Déjà inscrit?",
- "login": "Se connecter",
- "fieldEmpty": "Veuillez remplir tous les champs du formulaire",
- "notMatchingPassword": "Les deux mots de passe ne correspondent pas"
- }
- }
-}
diff --git a/authindex.html b/authindex.html
deleted file mode 100644
index 6f859be..0000000
--- a/authindex.html
+++ /dev/null
@@ -1,86 +0,0 @@
-
-
-
-
-
- Beer Crackerz
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
diff --git a/back/app/templates/email/html/password-reset.html b/back/app/templates/email/html/password-reset.html
new file mode 100644
index 0000000..a6bbac4
--- /dev/null
+++ b/back/app/templates/email/html/password-reset.html
@@ -0,0 +1,24 @@
+{% autoescape off %}
+
+
+ Bonjour {{ user.username | capfirst }},
+
+
+
+
+
+ Une demande de réinitialisation de votre mot de passe viens d'être effectuée. Pour changer votre mot de passe, veuillez cliquer sur le lien suivant :
+
+ Si vous n'ètes pas à l'origine de cette demande, merci d'ignorer ce mail.
+
+
+
+ L'Équipe BeerCrackerz
+
+{% endautoescape %}
diff --git a/back/app/templates/email/html/user-creation.html b/back/app/templates/email/html/user-creation.html
new file mode 100644
index 0000000..af84fe1
--- /dev/null
+++ b/back/app/templates/email/html/user-creation.html
@@ -0,0 +1,25 @@
+{% autoescape off %}
+
+
+ Bonjour et surtout bienvenu sur BeerCrackerz, {{ user.username | capfirst }},
+
+
+
+
+
+ Nous vous remercions de votre inscription et espérons que BeerCrackerz saura combler vos meilleures virées houblonées. Cette carte communautaire recense les meilleurs endroits pour se délecter d'une bière, mais également pour trouver de quoi se ravitailler!
+
+
+ Tout ce qu'il vous reste à faire pour utiliser BeerCrackerz, c'est de suivre le lien suivant pour valider votre adresse mail (et ainsi vérifier votre compte).
+
+{% endautoescape %}
diff --git a/back/app/templates/email/text/password-reset.txt b/back/app/templates/email/text/password-reset.txt
new file mode 100644
index 0000000..08955c8
--- /dev/null
+++ b/back/app/templates/email/text/password-reset.txt
@@ -0,0 +1,10 @@
+Bonjour {{ user.username | capfirst }},
+
+Une demande de réinitialisation de votre mot de passe viens d'être effectuée. Pour changer votre mot de passe, veuillez cliquer sur le lien suivant :
+
+{{ link }}
+
+Si vous n'êtes pas à l'origine de cette demande, merci d'ignorer ce mail.
+
+L'Équipe BeerCrackerz
+
diff --git a/back/app/templates/email/text/user-creation.txt b/back/app/templates/email/text/user-creation.txt
new file mode 100644
index 0000000..adebb52
--- /dev/null
+++ b/back/app/templates/email/text/user-creation.txt
@@ -0,0 +1,10 @@
+Bonjour et surtout bienvenu sur BeerCrackerz, {{ user.username | capfirst }},
+
+Nous vous remercions de votre inscription et espérons que BeerCrackerz saura combler vos meilleures virées houblonées. Cette carte communautaire recense les meilleurs endroits pour se délecter d'une bière, mais également pour trouver de quoi se ravitailler!
+
+Tout ce qu'il vous reste à faire pour utiliser BeerCrackerz, c'est de suivre le lien suivant pour valider votre adresse mail (et ainsi vérifier votre compte).
+
+{{ link }}
+
+À vos, spots!
+L'Équipe BeerCrackerz
\ No newline at end of file
diff --git a/back/app/templates/error.html b/back/app/templates/error.html
new file mode 100644
index 0000000..4e48541
--- /dev/null
+++ b/back/app/templates/error.html
@@ -0,0 +1,77 @@
+{% load static %}
+
+
+
+
+ Beer Crackerz
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-This component handles the whole BeerCrackerz app. It includes the map manipulation,
-the geolocation API to update the user position and process any map events that are
-relevant to an UX stand point. For more information, please consult the application
-description page at https://about.beercrackerz.org/
-
-The _initEvents() method will initialize all saved marker into the map.
+The _initEvents() method will initialize all saved marker into the map.
Markers must be retrieved from server with a specific format to ensure it works
-The _markerSaved() method is the callback used when a marker is created and added
-to the map. It is the last method of a new marker proccess.
+The addDebugUI() method appends the debug DOM element to the document body
-The aboutModal() method will request the about modal, which contains
-information about BeerCrackerz, cookies/tracking policies and links
+This method will delete a mark after prompting the user if he trully wants to
-The closeModal() method will close any opened modal if the click event is
-targeted on the modal overlay or on close buttons
+This method formats a mark so it can be parsed using JSON.parse
+in order to be later stored in database.
-The hidShowModal() method will request the hide show modal, which all
-toggles for map elements ; labels/circles/spots/stores/bars
+The hidShowMenu() method will request the hide show modal, which all
+toggles for map elements ; labels/circles/spots/shops/bars
The toggleCircle() method will, depending on user preference, display or not
-the circles around the spots/stores/bars marks. This circle indicates the minimal
+the circles around the spots/shops/bars marks. This circle indicates the minimal
distance which allow the user to make updates on the mark information
(static)
The toggleFocusLock() method will, depending on user preference, lock or unlock
the map centering around the user marker at each position refresh. This way the user
-can roam while the map is following its position.
+can roam while the map is following its position.
@@ -3975,7 +3507,7 @@
The toggleLabel() method will, depending on user preference, display or not
-the labels attached to spots/stores/bars marks. This label is basically the
+the labels attached to spots/shops/bars marks. This label is basically the
mark name given by its creator.
(static)
The toggleMarkers() method will, depending on user preference, display or not
a given mark type. This way, the user can fine tune what is displayed on the map.
-A mark type in spots/stores/bars must be given as an argument
+A mark type in spots/shops/bars must be given as an argument
@@ -4332,7 +3864,7 @@
-The userProfileModal() method will request the user modal, which contains
+The userProfile() method will request the user modal, which contains
the user preferences, and the user profile information
+This component handles all the authentication pages for BeerCrackerz. It provides the login, the
+register and the forgot password process. It also provides a public map so unauthenticated user
+can still browse the best BeerCrackerz spots. For more information, please consult the application
+description page at https://about.beercrackerz.org/
+
+The _createMarker() method will create all BeerCrackerz kind of markers (spot/shop/bar/user),
+will create if needed its popup (if provided in options) and will make it interactive to click.
+
The Leaflet marker extended with option properties
+
+
+
+
+
+
+ Type
+
+
+
+HTMLElement
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
(private, static) _drawUserMarker()
+
+
+
+
+
+
+
+
+The _drawUserMarker() method will draw the user marker to the position received
+from the geolocation API. If the marker doesn't exist yet, it will create it and
+place it to its default position (see constructor/this._user).
+
+The _fatalError() method will handle all fatal errors from which the app
+can't recover. It redirects to the error page and send info through the referrer
+so the error page can properly displays it to the user
+
+The _handleForgotPasswordAside() method will replace the aside content with the fogot password
+template, then it will handle its i18n, and all of its interactivity to submit forgot password
+form to the server.
+
+The _handleLoginAside() method will replace the aside content with the login template,
+then it will handle its i18n, and all of its interactivity to submit login form to the server.
+
+The _handleRegisterAside() method will replace the aside content with the register template,
+then it will handle its i18n, and all of its interactivity to submit register form to the server.
+
+The _init() method handle the whole app initialization sequence. It first
+set the aside content to login (as it comes with the base welcome.html template),
+then initialize the communication and notification handler, and will finally
+initialize the whole map, markers and interactivity.
+
+The _initEvents() method will listen to all required events to manipulate the map. Those events
+are both for commands and for map events (click, drag, zoom and layer change).
+
+The _initGeolocation() method will request from browser the location authorization.
+Once granted, an event listener is set on any position update, so it can update the
+map state and the markers position. This method can be called again, only if the
+geolocation watch has been cleared ; for example when updating the accuracy options.
+
+The _initMap() method will create the Leaflet.js map with two base layers (plan/satellite),
+add scale control, remove zoom control and set map bounds.
+
+The _initEvents() method will initialize all saved marker into the map.
+Markers must be retrieved from server with a specific format to ensure it works
+
+The _loadAside() method is a generic method to load an HTML template and replace
+the aside DOM content with that template, aswell as updating the document's class.
+
+
+
+
+
+
+
+
+
+
+
+
Parameters:
+
+
+
+
+
+
+
Name
+
+
+
Type
+
+
+
+
+
+
Description
+
+
+
+
+
+
+
+
+
type
+
+
+
+
+
+String
+
+
+
+
+
+
+
+
+
+
The aside to load in login/register/forgot-password
+The _markPopupFactory() method will create the associated popup DOM for a given mark. It will
+fetch the popup template, replace its content with its i18n and provide its tooltip.
+
+The _toggleAside() method will expand or collapse the aside, depending on the
+`this._isAsideExpanded` flag state. To be used as a callba, adding useful parameters to url before redirectck on aside expander.
+
The CustomEvents class provides an abstraction of JavaScript event listener, to allow
+easy binding and removing those events. It also provides an interface to register custom events. This class is
+meant to be used on all scopes you need ; module or global. Refer to each public method for detailed features.
+For source code, please go to
+https://github.com/ArthurBeaulieu/CustomEvents.js
+
+
+
+
+
+
+
+
+
+
+
Parameters:
+
+
+
+
+
+
+
Name
+
+
+
Type
+
+
+
Attributes
+
+
+
+
Default
+
+
+
Description
+
+
+
+
+
+
+
+
+
debug
+
+
+
+
+
+boolean
+
+
+
+
+
+
+
+
+ <optional>
+
+
+
+
+
+
+
+
+
+
+
+ false
+
+
+
+
+
Debug flag ; when true, logs will be output in JavaScript console at each event
The method status ; true for success, false for non-existing event
+
+
+
+
+
+
+
+ Type
+
+
+
+boolean
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/front/doc/DropElement.html b/front/doc/DropElement.html
new file mode 100644
index 0000000..cccebc2
--- /dev/null
+++ b/front/doc/DropElement.html
@@ -0,0 +1,1856 @@
+
+
+
+
+ JSDoc: Class: DropElement
+
+
+
+
+
+
+
+
+
+
+
+
+
Class: DropElement
+
+
+
+
+
+
+
+
+
+
+
DropElement(options)
+
+
+
+
+
+
+
+
+
+
+
+
+
new DropElement(options)
+
+
+
+
Make any DOM element drop friendly
+
+
+
+
+
+
This class will make any DOM element able to receive drop event. It propose an overlay
+when the target is hovered with a draggable element. It handle both the desktop and the mobile behavior. It must be
+used with a DragElement class for perfect compatibility!
This method will handle the entering of a dragged div over the target DOM element. When
+the target DOM element is hovered, a dashed border is made visible, replacing the transparent one to notify the
+user that the dragged div can be dropped.
This method will handle the event that is fired when the hovered div leaves the target
+DOM element. It require the movement counter to be equal to zero to restore the transparent border of the target
+DOM element.
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/front/doc/Kom.html b/front/doc/Kom.html
new file mode 100644
index 0000000..4aa73e7
--- /dev/null
+++ b/front/doc/Kom.html
@@ -0,0 +1,2384 @@
+
+
+
+
+ JSDoc: Class: Kom
+
+
+
+
+
+
+
+
+
+
+
+
+
Class: Kom
+
+
+
+
+
+
+
+
+
+
+
Kom()
+
+
+
+
+
+
+
+
+
+
+
+
+
new Kom()
+
+
+
+
Server communication abstraction
+
+
+
+
+
+
+This class is the main object to deal with when requesting something from the server.
+It handle all urls calls (GET, POST), treat responses or handle errors using
+Promise. Because it uses Promise, success and errors are to be handled in the caller
+function, using .then() and .catch(). To properly deal with POST request,
+the session must contain a csrf token in cookies. Otherwise, those POST call may fail.
+
The headers array, length 3, to be used in HTTP requests
+
+
+
+
+
+
+
+ Type
+
+
+
+Array.<Array>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
(private, static) _getCsrfCookie() → {String}
+
+
+
+
+
+
+
+
+Extract CSRF token value from client cookies and returns it as a string. Returns an empty
+string by default. This method is required to be called on construction.
+
+Generic tool method used by private methods on fetch responses to format output in the provided
+format. It must be either `json`, `text`, `raw` or `dom`.
+
+
+
+
+
+
+
+
+
+
+
+
Parameters:
+
+
+
+
+
+
+
Name
+
+
+
Type
+
+
+
+
+
+
Description
+
+
+
+
+
+
+
+
+
type
+
+
+
+
+
+String
+
+
+
+
+
+
+
+
+
+
The type of resolution, can be json, text, raw or dom
+Tool method used by public methods on fetch responses to format output data as text to be
+read in JavaScript code as string (mostly to parse HTML templates).
+
The request Promise, format response as a string on resolve, as error code string on reject
+
+
+
+
+
+
+ Type
+
+
+
+Promise
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
(async, static) get(url) → {Promise}
+
+
+
+
+
+
+
+
+GET HTTP request using the fetch API. resolve returns the
+response as an Object. reject returns an error key as a String.
+It is meant to perform API call to access database through the user interface.
+
+
+
+
+
+
+
+
+
+
+
+
Parameters:
+
+
+
+
+
+
+
Name
+
+
+
Type
+
+
+
+
+
+
Description
+
+
+
+
+
+
+
+
+
url
+
+
+
+
+
+String
+
+
+
+
+
+
+
+
+
+
The GET url to fetch data from, in supported back URLs
+GET HTTP request using the fetch API. resolve returns the
+response as a String. reject returns an error key as a String. It is
+meant to perform API call to get HTML templates as string to be parsed as documents/documents fragments.
+
+
+
+
+
+
+
+
+
+
+
+
Parameters:
+
+
+
+
+
+
+
Name
+
+
+
Type
+
+
+
+
+
+
Description
+
+
+
+
+
+
+
+
+
url
+
+
+
+
+
+String
+
+
+
+
+
+
+
+
+
+
The GET url to fetch data from, in supported back URLs
+GET HTTP request using the fetch API. resolve returns the
+response as a String. reject returns an error key as a String. It is
+meant to perform API call to get HTML templates as string to be parsed as documents/documents fragments.
+
+
+
+
+
+
+
+
+
+
+
+
Parameters:
+
+
+
+
+
+
+
Name
+
+
+
Type
+
+
+
+
+
+
Description
+
+
+
+
+
+
+
+
+
url
+
+
+
+
+
+String
+
+
+
+
+
+
+
+
+
+
The GET url to fetch data from, in supported back URLs
+POST HTTP request using the fetch API. Beware that the given options
+object match the url expectations. resolve
+returns the response as an Object. reject returns an error key as a String.
+
+POST HTTP request using the fetch API. Beware that the given options
+object match the url expectations. resolve
+returns the response as a String. reject returns an error key as a String.
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/doc/beercrackerz/0.0.1/fonts/OpenSans-Bold-webfont.eot b/front/doc/fonts/OpenSans-Bold-webfont.eot
similarity index 100%
rename from doc/beercrackerz/0.0.1/fonts/OpenSans-Bold-webfont.eot
rename to front/doc/fonts/OpenSans-Bold-webfont.eot
diff --git a/doc/beercrackerz/0.0.1/fonts/OpenSans-Bold-webfont.svg b/front/doc/fonts/OpenSans-Bold-webfont.svg
similarity index 100%
rename from doc/beercrackerz/0.0.1/fonts/OpenSans-Bold-webfont.svg
rename to front/doc/fonts/OpenSans-Bold-webfont.svg
diff --git a/doc/beercrackerz/0.0.1/fonts/OpenSans-Bold-webfont.woff b/front/doc/fonts/OpenSans-Bold-webfont.woff
similarity index 100%
rename from doc/beercrackerz/0.0.1/fonts/OpenSans-Bold-webfont.woff
rename to front/doc/fonts/OpenSans-Bold-webfont.woff
diff --git a/doc/beercrackerz/0.0.1/fonts/OpenSans-BoldItalic-webfont.eot b/front/doc/fonts/OpenSans-BoldItalic-webfont.eot
similarity index 100%
rename from doc/beercrackerz/0.0.1/fonts/OpenSans-BoldItalic-webfont.eot
rename to front/doc/fonts/OpenSans-BoldItalic-webfont.eot
diff --git a/doc/beercrackerz/0.0.1/fonts/OpenSans-BoldItalic-webfont.svg b/front/doc/fonts/OpenSans-BoldItalic-webfont.svg
similarity index 100%
rename from doc/beercrackerz/0.0.1/fonts/OpenSans-BoldItalic-webfont.svg
rename to front/doc/fonts/OpenSans-BoldItalic-webfont.svg
diff --git a/doc/beercrackerz/0.0.1/fonts/OpenSans-BoldItalic-webfont.woff b/front/doc/fonts/OpenSans-BoldItalic-webfont.woff
similarity index 100%
rename from doc/beercrackerz/0.0.1/fonts/OpenSans-BoldItalic-webfont.woff
rename to front/doc/fonts/OpenSans-BoldItalic-webfont.woff
diff --git a/doc/beercrackerz/0.0.1/fonts/OpenSans-Italic-webfont.eot b/front/doc/fonts/OpenSans-Italic-webfont.eot
similarity index 100%
rename from doc/beercrackerz/0.0.1/fonts/OpenSans-Italic-webfont.eot
rename to front/doc/fonts/OpenSans-Italic-webfont.eot
diff --git a/doc/beercrackerz/0.0.1/fonts/OpenSans-Italic-webfont.svg b/front/doc/fonts/OpenSans-Italic-webfont.svg
similarity index 100%
rename from doc/beercrackerz/0.0.1/fonts/OpenSans-Italic-webfont.svg
rename to front/doc/fonts/OpenSans-Italic-webfont.svg
diff --git a/doc/beercrackerz/0.0.1/fonts/OpenSans-Italic-webfont.woff b/front/doc/fonts/OpenSans-Italic-webfont.woff
similarity index 100%
rename from doc/beercrackerz/0.0.1/fonts/OpenSans-Italic-webfont.woff
rename to front/doc/fonts/OpenSans-Italic-webfont.woff
diff --git a/doc/beercrackerz/0.0.1/fonts/OpenSans-Light-webfont.eot b/front/doc/fonts/OpenSans-Light-webfont.eot
similarity index 100%
rename from doc/beercrackerz/0.0.1/fonts/OpenSans-Light-webfont.eot
rename to front/doc/fonts/OpenSans-Light-webfont.eot
diff --git a/doc/beercrackerz/0.0.1/fonts/OpenSans-Light-webfont.svg b/front/doc/fonts/OpenSans-Light-webfont.svg
similarity index 100%
rename from doc/beercrackerz/0.0.1/fonts/OpenSans-Light-webfont.svg
rename to front/doc/fonts/OpenSans-Light-webfont.svg
diff --git a/doc/beercrackerz/0.0.1/fonts/OpenSans-Light-webfont.woff b/front/doc/fonts/OpenSans-Light-webfont.woff
similarity index 100%
rename from doc/beercrackerz/0.0.1/fonts/OpenSans-Light-webfont.woff
rename to front/doc/fonts/OpenSans-Light-webfont.woff
diff --git a/doc/beercrackerz/0.0.1/fonts/OpenSans-LightItalic-webfont.eot b/front/doc/fonts/OpenSans-LightItalic-webfont.eot
similarity index 100%
rename from doc/beercrackerz/0.0.1/fonts/OpenSans-LightItalic-webfont.eot
rename to front/doc/fonts/OpenSans-LightItalic-webfont.eot
diff --git a/doc/beercrackerz/0.0.1/fonts/OpenSans-LightItalic-webfont.svg b/front/doc/fonts/OpenSans-LightItalic-webfont.svg
similarity index 100%
rename from doc/beercrackerz/0.0.1/fonts/OpenSans-LightItalic-webfont.svg
rename to front/doc/fonts/OpenSans-LightItalic-webfont.svg
diff --git a/doc/beercrackerz/0.0.1/fonts/OpenSans-LightItalic-webfont.woff b/front/doc/fonts/OpenSans-LightItalic-webfont.woff
similarity index 100%
rename from doc/beercrackerz/0.0.1/fonts/OpenSans-LightItalic-webfont.woff
rename to front/doc/fonts/OpenSans-LightItalic-webfont.woff
diff --git a/doc/beercrackerz/0.0.1/fonts/OpenSans-Regular-webfont.eot b/front/doc/fonts/OpenSans-Regular-webfont.eot
similarity index 100%
rename from doc/beercrackerz/0.0.1/fonts/OpenSans-Regular-webfont.eot
rename to front/doc/fonts/OpenSans-Regular-webfont.eot
diff --git a/doc/beercrackerz/0.0.1/fonts/OpenSans-Regular-webfont.svg b/front/doc/fonts/OpenSans-Regular-webfont.svg
similarity index 100%
rename from doc/beercrackerz/0.0.1/fonts/OpenSans-Regular-webfont.svg
rename to front/doc/fonts/OpenSans-Regular-webfont.svg
diff --git a/doc/beercrackerz/0.0.1/fonts/OpenSans-Regular-webfont.woff b/front/doc/fonts/OpenSans-Regular-webfont.woff
similarity index 100%
rename from doc/beercrackerz/0.0.1/fonts/OpenSans-Regular-webfont.woff
rename to front/doc/fonts/OpenSans-Regular-webfont.woff
diff --git a/front/doc/index.html b/front/doc/index.html
new file mode 100644
index 0000000..750f5e2
--- /dev/null
+++ b/front/doc/index.html
@@ -0,0 +1,86 @@
+
+
+
+
+ JSDoc: Home
+
+
+
+
+
+
+
+
+
+
+
+
+
Home
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
BeerCrackerz
+
Welcome, fellow beer lovers. BeerCrackerz is a community web app to list the best spots to drink a fresh one while you're outside. It provides a well-known map interface so it is really easy to browse, find or add unique spots!
+
You want to try it ? We are currently running an instance just so you can try (and add your personnal best places) :
class BaseModal {
+
+
+ constructor(type) {
+ /** @private
+ * @member {string} - The modal type */
+ this._type = type;
+ /** @private
+ * @member {string} - The HTML template url to fetch */
+ this._url = `/modal/${this._type}`;
+ /** @private
+ * @member {object} - The template root DOM element */
+ this._rootElement = null;
+ /** @private
+ * @member {object} - The overlay that contains the modal, full viewport size and close modal on click */
+ this._modalOverlay = null;
+ /** @private
+ * @member {object} - The close button, in the modal header */
+ this._closeButton = null;
+ /** @private
+ * @member {array} - The event IDs */
+ this._evtIds = [];
+
+ // Modal building sequence:
+ // - get HTML template from server;
+ // - parse template response to become DOM object;
+ // - append DOM element to global overlay;
+ // - open modal by adding overlay to the body;
+ // - let child class fill attributes and register its events.
+ this._loadTemplate();
+ }
+
+
+ /** @method
+ * @name destroy
+ * @public
+ * @memberof Modal
+ * @author Arthur Beaulieu
+ * @since November 2020
+ * @description <blockquote>This method must be overridden in child class. It only destroys the <code>Modal.js</code>
+ * properties and close event subscription. The developer must remove its abstracted properties and events after
+ * calling this method, to make the destruction process complete.</blockquote> **/
+ destroy() {
+ for (let i = 0; i < this._evtIds.length; ++i) {
+ window.Evts.removeEvent(this._evtIds[i]);
+ }
+ Object.keys(this).forEach(key => {
+ delete this[key];
+ });
+ }
+
+
+ /* --------------------------------------------------------------------------------------------------------------- */
+ /* ------------------------------------ MODAL INSTANTIATION SEQUENCE ------------------------------------------ */
+ /* --------------------------------------------------------------------------------------------------------------- */
+
+
+ /** @method
+ * @name _loadTemplate
+ * @private
+ * @async
+ * @memberof Modal
+ * @author Arthur Beaulieu
+ * @since November 2020
+ * @description <blockquote>This method creates the modal overlay, fetch the HTML template using the <code>Kom.js
+ * </code> component, it then build the modal DOM, append it to the overlay, open the modal and call <code>
+ * _fillAttributes()</code> that must be overridden in the child class. It is asynchronous because of the fetch call,
+ * so the child class constructor can be fully executed.</blockquote> **/
+ _loadTemplate() {
+ window.BeerCrackerz.kom.getTemplate(this._url).then(response => {
+ // Create DOM from fragment and tweak url to only keep modal type as css class
+ this._rootElement = response.firstElementChild;
+ this._rootElement.classList.add(`${this._type}-modal`);
+ // Create overlay modal container
+ this._modalOverlay = document.createElement('DIV');
+ this._modalOverlay.id = 'overlay';
+ this._modalOverlay.classList.add('overlay');
+ document.body.appendChild(this._modalOverlay);
+ // Get close button from template
+ this._closeButton = this._rootElement.querySelector('#modal-close');
+ this.open();
+ this._fillAttributes(); // Override in child class to process modal UI
+ }).catch(error => {
+ console.error(error);
+ });
+ }
+
+
+ /** @method
+ * @name _fillAttributes
+ * @private
+ * @memberof Modal
+ * @author Arthur Beaulieu
+ * @since November 2020
+ * @description <blockquote>This method doesn't implement anything. It must be overridden in child class, to use the
+ * template DOM elements to build its interactions. It is called once the template is successfully fetched from the
+ * server.</blockquote> **/
+ _fillAttributes() {
+ // Must be overridden in child class to build modal with HTML template attributes
+ }
+
+
+ /* --------------------------------------------------------------------------------------------------------------- */
+ /* ------------------------------------ MODAL VISIBILITY MANIPULATION ----------------------------------------- */
+ /* --------------------------------------------------------------------------------------------------------------- */
+
+
+ open() {
+ this._evtIds.push(window.Evts.addEvent('click', this._modalOverlay, this.close, this));
+ this._evtIds.push(window.Evts.addEvent('touchend', this._modalOverlay, this.close, this));
+ this._evtIds.push(window.Evts.addEvent('click', this._closeButton, this.close, this));
+ this._evtIds.push(window.Evts.addEvent('touchend', this._closeButton, this.close, this));
+ this._modalOverlay.appendChild(this._rootElement);
+ this._modalOverlay.style.display = 'flex';
+ setTimeout(() => this._modalOverlay.style.opacity = 1, 50);
+ }
+
+
+
+ close(event, force) {
+ if (event && event.stopPropagation) {
+ event.stopPropagation();
+ }
+
+ if (force === true || event.target.id === 'overlay' || event.target.id.indexOf('close') !== -1) {
+ if (event && event.type === 'touchend' && event.preventDefault) {
+ event.preventDefault();
+ }
+
+ this._modalOverlay.style.opacity = 0;
+ setTimeout(() => {
+ document.body.removeChild(this._modalOverlay);
+ this.destroy();
+ }, 200);
+ }
+ }
+
+
+}
+
+
+export default BaseModal;
+
import Utils from './Utils.js';
+
+
+class CustomEvents {
+
+
+ /** @summary <h1>JavaScript regular and custom events abstraction</h1>
+ * @author Arthur Beaulieu
+ * @since June 2020
+ * @description <blockquote>The CustomEvents class provides an abstraction of JavaScript event listener, to allow
+ * easy binding and removing those events. It also provides an interface to register custom events. This class is
+ * meant to be used on all scopes you need ; module or global. Refer to each public method for detailed features.
+ * For source code, please go to <a href="https://github.com/ArthurBeaulieu/CustomEvents.js" alt="custom-events-js">
+ * https://github.com/ArthurBeaulieu/CustomEvents.js</a></blockquote>
+ * @param {boolean} [debug=false] - Debug flag ; when true, logs will be output in JavaScript console at each event */
+ constructor(debug = false) {
+ // Prevent wrong type for debug
+ if (typeof debug !== 'boolean') {
+ debug = false;
+ }
+ /** @private
+ * @member {boolean} - Internal logging flag from constructor options, allow to output each event action */
+ this._debug = debug;
+ /** @private
+ * @member {number} - Start the ID incrementer at pseudo random value, used for both regular and custom events */
+ this._idIncrementor = (Math.floor(Math.random() * Math.floor(256)) * 5678);
+ /** @private
+ * @member {any[]} - We store classical event listeners in array of objects containing all their information */
+ this._regularEvents = [];
+ /** @private
+ * @member {object} - We store custom events by name as key, each key stores an Array of subscribed events */
+ this._customEvents = {};
+ /** @public
+ * @member {string} - Component version */
+ this.version = '1.2.1';
+ }
+
+
+ /** @method
+ * @name destroy
+ * @public
+ * @memberof CustomEvents
+ * @description <blockquote>CustomEvents destructor. Will remove all event listeners and keys in instance.</blockquote> */
+ destroy() {
+ // Debug logging
+ this._raise('log', 'CustomEvents.destroy');
+ // Remove all existing eventListener
+ this.removeAllEvents();
+ // Delete object attributes
+ Utils.removeAllObjectKeys(this);
+ }
+
+
+ /* --------------------------------------------------------------------------------------------------------------- */
+ /* -------------------------------------- CLASSIC JS EVENTS OVERRIDE ------------------------------------------ */
+ /* */
+ /* The following methods are made to abstract the event listeners from the JavaScript layer, so you can easily */
+ /* remove them when done using, without bothering with binding usual business for them. 'addEvent/removeEvent' */
+ /* method replace the initial ones. 'removeAllEvents' clears all instance event listeners ; nice for destroy */
+ /* --------------------------------------------------------------------------------------------------------------- */
+
+
+ /** @method
+ * @name addEvent
+ * @public
+ * @memberof CustomEvents
+ * @description <blockquote><code>addEvent</code> method abstracts the <code>addEventListener</code> method to easily
+ * remove it when needed, also to set a custom scope on callback.</blockquote>
+ * @param {string} eventName - The event name to fire (mousemove, click, context etc.)
+ * @param {object} element - The DOM element to attach the listener to
+ * @param {function} callback - The callback function to execute when event is realised
+ * @param {object} [scope=element] - The event scope to apply to the callback (optional, default to DOM element)
+ * @param {object|boolean} [options=false] - The event options (useCapture and else)
+ * @returns {number|boolean} - The event ID to use to manually remove an event, false if arguments are invalid */
+ addEvent(eventName, element, callback, scope = element, options = false) {
+ // Debug logging
+ this._raise('log', `CustomEvents.addEvent: ${eventName} ${element} ${callback} ${scope} ${options}`);
+ // Missing mandatory arguments
+ if (eventName === null || eventName === undefined ||
+ element === null || element === undefined ||
+ callback === null || callback === undefined) {
+ this._raise('error', 'CustomEvents.addEvent: Missing mandatory arguments');
+ return false;
+ }
+ // Prevent wrong type for arguments (mandatory and optional)
+ const err = () => {
+ this._raise('error', 'CustomEvents.addEvent: Wrong type for argument');
+ };
+ // Test argument validity for further process
+ if (typeof eventName !== 'string' || typeof element !== 'object' || typeof callback !== 'function') {
+ err();
+ return false;
+ }
+ if ((scope !== null && scope !== undefined) && typeof scope !== 'object' && typeof scope !== 'function') {
+ err();
+ return false;
+ }
+ if ((options !== null && options !== undefined) && (typeof options !== 'object' && typeof options !== 'boolean' && typeof options !== 'string' && typeof options !== 'number')) {
+ err();
+ return false;
+ }
+ // Save scope to callback function, default scope is DOM target object
+ callback = callback.bind(scope);
+ // Add event to internal array and keep all its data
+ this._regularEvents.push({
+ id: this._idIncrementor,
+ element: element,
+ eventName: eventName,
+ scope: scope,
+ callback: callback,
+ options: options
+ });
+ // Add event listener with options
+ element.addEventListener(eventName, callback, options);
+ // Post increment to return the true event entry id, then update the incrementer
+ return this._idIncrementor++;
+ }
+
+
+ /** @method
+ * @name removeEvent
+ * @public
+ * @memberof CustomEvents
+ * @description <blockquote><code>removeEvent</code> method abstracts the <code>removeEventListener</code> method to
+ * really remove event listeners.</blockquote>
+ * @param {number} eventId - The event ID to remove listener from. Returned when addEvent is called
+ * @returns {boolean} - The method status ; true for success, false for non-existing event */
+ removeEvent(eventId) {
+ // Debug logging
+ this._raise('log', `Events.removeEvent: ${eventId}`);
+ // Missing mandatory arguments
+ if (eventId === null || eventId === undefined) {
+ this._raise('error', 'CustomEvents.removeEvent: Missing mandatory arguments');
+ return false;
+ }
+ // Prevent wrong type for arguments (mandatory)
+ if (typeof eventId !== 'number') {
+ this._raise('error', 'CustomEvents.removeEvent: Wrong type for argument');
+ return false;
+ }
+ // Returned value
+ let statusCode = false; // Not found status code by default (false)
+ // Iterate over saved listeners, reverse order for proper splicing
+ for (let i = (this._regularEvents.length - 1); i >= 0 ; --i) {
+ // If an event ID match in saved ones, we remove it and update saved listeners
+ if (this._regularEvents[i].id === eventId) {
+ // Update status code
+ statusCode = true; // Found and removed event listener status code (true)
+ this._clearRegularEvent(i);
+ }
+ }
+ // Return with status code
+ return statusCode;
+ }
+
+
+ /** @method
+ * @name removeAllEvents
+ * @public
+ * @memberof CustomEvents
+ * @description <blockquote>Clear all event listener registered through this class object.</blockquote>
+ * @returns {boolean} - The method status ; true for success, false for not removed any event */
+ removeAllEvents() {
+ // Debug logging
+ this._raise('log', 'CustomEvents.removeAllEvents');
+ // Returned value
+ let statusCode = false; // Didn't removed any status code by default (false)
+ // Flag to know if there was any previously stored event listeners
+ const hadEvents = (this._regularEvents.length > 0);
+ // Iterate over saved listeners, reverse order for proper splicing
+ for (let i = (this._regularEvents.length - 1); i >= 0; --i) {
+ this._clearRegularEvent(i);
+ }
+ // If all events where removed, update statusCode to success
+ if (this._regularEvents.length === 0 && hadEvents) {
+ // Update status code
+ statusCode = true; // Found and removed all events listener status code (true)
+ }
+ // Return with status code
+ return statusCode;
+ }
+
+
+ /** @method
+ * @name _clearRegularEvent
+ * @private
+ * @memberof CustomEvents
+ * @description <blockquote><code>_clearRegularEvent</code> method remove the saved event listener for a
+ * given index in regularEvents array range.</blockquote>
+ * @param {number} index - The regular event index to remove from class attributes
+ * @return {boolean} - The method status ; true for success, false for not cleared any event */
+ _clearRegularEvent(index) {
+ // Debug logging
+ this._raise('log', `CustomEvents._clearRegularEvent: ${index}`);
+ // Missing mandatory arguments
+ if (index === null || index === undefined) {
+ this._raise('error', 'CustomEvents._clearRegularEvent: Missing mandatory argument');
+ return false;
+ }
+ // Prevent wrong type for arguments (mandatory)
+ if (typeof index !== 'number') {
+ this._raise('error', 'CustomEvents._clearRegularEvent: Wrong type for argument');
+ return false;
+ }
+ // Check if index match an existing event in attributes
+ if (this._regularEvents[index]) {
+ // Remove its event listener and update regularEvents array
+ const evt = this._regularEvents[index];
+ evt.element.removeEventListener(evt.eventName, evt.callback, evt.options);
+ this._regularEvents.splice(index, 1);
+ return true;
+ }
+
+ return false;
+ }
+
+
+ /* --------------------------------------------------------------------------------------------------------------- */
+ /* ------------------------------------------- CUSTOM JS EVENTS ----------------------------------------------- */
+ /* */
+ /* The three following methods (subscribe, unsubscribe, publish) are designed to reference an event by its name */
+ /* and handle as many subscriptions as you want. When subscribing, you get an ID you can use to unsubscribe your */
+ /* event later. Just publish with the event name to callback all its registered subscriptions. */
+ /* --------------------------------------------------------------------------------------------------------------- */
+
+
+ /** @method
+ * @name subscribe
+ * @public
+ * @memberof CustomEvents
+ * @description <blockquote>Subscribe method allow you to listen to an event and react when it occurs.</blockquote>
+ * @param {string} eventName - Event name (the one to use to publish)
+ * @param {function} callback - The callback to execute when event is published
+ * @param {boolean} [oneShot=false] - One shot : to remove subscription the first time callback is fired
+ * @returns {number|boolean} - The event id, to be used when manually unsubscribing */
+ subscribe(eventName, callback, oneShot = false) {
+ // Debug logging
+ this._raise('log', `CustomEvents.subscribe: ${eventName} ${callback} ${oneShot}`);
+ // Missing mandatory arguments
+ if (eventName === null || eventName === undefined ||
+ callback === null || callback === undefined) {
+ this._raise('error', 'CustomEvents.subscribe', 'Missing mandatory arguments');
+ return false;
+ }
+ // Prevent wrong type for arguments (mandatory and optional)
+ const err = () => {
+ this._raise('error', 'CustomEvents.subscribe: Wrong type for argument');
+ };
+ if (typeof eventName !== 'string' || typeof callback !== 'function') {
+ err();
+ return false;
+ }
+ if ((oneShot !== null && oneShot !== undefined) && typeof oneShot !== 'boolean') {
+ err();
+ return false;
+ }
+ // Create event entry if not already existing in the registered events
+ if (!this._customEvents[eventName]) {
+ this._customEvents[eventName] = []; // Set empty array for new event subscriptions
+ }
+ // Push new subscription for event name
+ this._customEvents[eventName].push({
+ id: this._idIncrementor,
+ name: eventName,
+ os: oneShot,
+ callback: callback
+ });
+ // Post increment to return the true event entry id, then update the incrementer
+ return this._idIncrementor++;
+ }
+
+
+ /** @method
+ * @name unsubscribe
+ * @public
+ * @memberof CustomEvents
+ * @description <blockquote>Unsubscribe method allow you to revoke an event subscription from its string name.</blockquote>
+ * @param {number} eventId - The subscription id returned when subscribing to an event name
+ * @returns {boolean} - The method status ; true for success, false for non-existing subscription **/
+ unsubscribe(eventId) {
+ // Debug logging
+ this._raise('log', `CustomEvents.unsubscribe: ${eventId}`);
+ // Missing mandatory arguments
+ if (eventId === null || eventId === undefined) {
+ this._raise('error', 'CustomEvents.unsubscribe: Missing mandatory arguments');
+ return false;
+ }
+ // Prevent wrong type for arguments (mandatory)
+ if (typeof eventId !== 'number') {
+ this._raise('error', 'CustomEvents.unsubscribe: Wrong type for argument');
+ return false;
+ }
+ // Returned value
+ let statusCode = false; // Not found status code by default (false)
+ // Save event keys to iterate properly on this._events Object
+ const keys = Object.keys(this._customEvents);
+ // Reverse events iteration to properly splice without messing with iteration order
+ for (let i = (keys.length - 1); i >= 0; --i) {
+ // Get event subscriptions
+ const subs = this._customEvents[keys[i]];
+ // Iterate over events subscriptions to find the one with given id
+ for (let j = 0; j < subs.length; ++j) {
+ // In case we got a subscription for this events
+ if (subs[j].id === eventId) {
+ // Debug logging
+ this._raise('log', `CustomEvents.unsubscribe: subscription found\n`, subs[j], `\nSubscription n°${eventId} for ${subs.name} has been removed`);
+ // Update status code
+ statusCode = true; // Found and unsubscribed status code (true)
+ // Remove subscription from event Array
+ subs.splice(j, 1);
+ // Remove event name if no remaining subscriptions
+ if (subs.length === 0) {
+ delete this._customEvents[keys[i]];
+ }
+ // Break since id are unique and no other subscription can be found after
+ break;
+ }
+ }
+ }
+ // Return with status code
+ return statusCode;
+ }
+
+
+ /** @method
+ * @name unsubscribeAllFor
+ * @public
+ * @memberof CustomEvents
+ * @description <blockquote><code>unsubscribeAllFor</code> method clear all subscriptions registered for given event name.</blockquote>
+ * @param {string} eventName - The event to clear subscription from
+ * @returns {boolean} - The method status ; true for success, false for non-existing event **/
+ unsubscribeAllFor(eventName) {
+ // Debug logging
+ this._raise('log', `CustomEvents.unsubscribeAllFor: ${eventName}`);
+ // Missing mandatory arguments
+ if (eventName === null || eventName === undefined) {
+ this._raise('error', 'CustomEvents.unsubscribeAllFor: Missing mandatory arguments');
+ return false;
+ }
+ // Prevent wrong type for arguments (mandatory and optional)
+ if (typeof eventName !== 'string') {
+ this._raise('error', 'CustomEvents.unsubscribeAllFor: Wrong type for argument');
+ return false;
+ }
+ // Returned value
+ let statusCode = false; // Not found status code by default (false)
+ // Save event keys to iterate properly on this._events Object
+ const keys = Object.keys(this._customEvents);
+ // Iterate through custom event keys to find matching event to remove
+ for (let i = 0; i < keys.length; ++i) {
+ if (keys[i] === eventName) {
+ // Get event subscriptions
+ const subs = this._customEvents[keys[i]];
+ // Iterate over events subscriptions to find the one with given id, reverse iteration to properly splice without messing with iteration order
+ for (let j = (subs.length - 1); j >= 0; --j) {
+ // Update status code
+ statusCode = true; // Found and unsubscribed all status code (true)
+ // Remove subscription from event Array
+ subs.splice(j, 1);
+ // Remove event name if no remaining subscriptions
+ if (subs.length === 0) {
+ delete this._customEvents[keys[i]];
+ }
+ }
+ }
+ }
+ // Return with status code
+ return statusCode;
+ }
+
+
+ /** @method
+ * @name publish
+ * @public
+ * @memberof CustomEvents
+ * @description <blockquote><code>Publish</code> method allow you to fire an event by name and trigger all its subscription by callbacks./blockquote>
+ * @param {string} eventName - Event name (the one to use to publish)
+ * @param {object} [data=undefined] - The data object to sent through the custom event
+ * @returns {boolean} - The method status ; true for success, false for non-existing event **/
+ publish(eventName, data = null) {
+ // Debug logging
+ this._raise('log', `CustomEvents.publish: ${eventName} ${data}`);
+ // Missing mandatory arguments
+ if (eventName === null || eventName === undefined) {
+ this._raise('error', 'CustomEvents.publish: Missing mandatory arguments');
+ return false;
+ }
+ // Prevent wrong type for arguments (mandatory and optional)
+ if (typeof eventName !== 'string' || (data !== undefined && typeof data !== 'object')) {
+ this._raise('error', 'CustomEvents.publish: Wrong type for argument');
+ return false;
+ }
+ // Returned value
+ let statusCode = false; // Not found status code by default (false)
+ // Save event keys to iterate properly on this._events Object
+ const keys = Object.keys(this._customEvents);
+ // Iterate over saved custom events
+ for (let i = 0; i < keys.length; ++i) {
+ // If published name match an existing events, we iterate its subscriptions. First subscribed, first served
+ if (keys[i] === eventName) {
+ // Update status code
+ statusCode = true; // Found and published status code (true)
+ // Get event subscriptions
+ const subs = this._customEvents[keys[i]];
+ // Iterate over events subscriptions to find the one with given id
+ // Reverse subscriptions iteration to properly splice without messing with iteration order
+ for (let j = (subs.length - 1); j >= 0; --j) {
+ // Debug logging
+ this._raise('log', `CustomEvents.publish: fire callback for ${eventName}, subscription n°${subs[j].id}`, subs[j]);
+ // Fire saved callback
+ subs[j].callback(data);
+ // Remove oneShot listener from event entry
+ if (subs[j].os) {
+ // Debug logging
+ this._raise('log', 'CustomEvents.publish: remove subscription because one shot usage is done');
+ subs.splice(j, 1);
+ // Remove event name if no remaining subscriptions
+ if (subs.length === 0) {
+ delete this._customEvents[keys[i]];
+ }
+ }
+ }
+ }
+ }
+ // Return with status code
+ return statusCode;
+ }
+
+
+ /* --------------------------------------------------------------------------------------------------------------- */
+ /* -------------------------------------------- COMPONENT UTILS ----------------------------------------------- */
+ /* --------------------------------------------------------------------------------------------------------------- */
+
+
+ /** @method
+ * @name _raise
+ * @private
+ * @memberof CustomEvents
+ * @description <blockquote>Internal method to abstract console wrapped in debug flag.</blockquote>
+ * @param {string} level - The console method to call
+ * @param {string} errorValue - The error value to display in console method **/
+ _raise(level, errorValue) {
+ if (this._debug) {
+ console[level](errorValue);
+ }
+ }
+
+
+}
+
+
+export default CustomEvents;
+
import Utils from './Utils.js';
+
+
+class DropElement {
+
+
+ /** @summary <h1>Make any DOM element drop friendly</h1>
+ * @author Arthur Beaulieu
+ * @since December 2020
+ * @description <blockquote>This class will make any DOM element able to receive drop event. It propose an overlay
+ * when the target is hovered with a draggable element. It handle both the desktop and the mobile behavior. It must be
+ * used with a DragElement class for perfect compatibility!</blockquote>
+ * @param {object} options - The element to drop options
+ * @param {object} options.target - The element to allow dropping in **/
+ constructor(options) {
+ /** @private
+ * @member {object} - The element to make allow dropping in */
+ this._target = options.target; // Get given target from the DOM
+ /** @private
+ * @member {function} - The callback function to call on each drop event */
+ this._onDropCB = options.onDrop;
+ /** @private
+ * @member {number[]} - The event IDs for all mobile and desktop dropping events */
+ this._evtIds = [];
+ /** @private
+ * @member {number} - This counter helps to avoid enter/leave events to overlap when target has children */
+ this._movementCounter = 0;
+ /** @private
+ * @member {string} - The transparent border that must be added to avoid weird target resize on hover */
+ this._transparentBorder = '';
+ // Build DOM elements and subscribe to drag events
+ this._buildElements();
+ this._events();
+ }
+
+
+ /** @method
+ * @name destroy
+ * @public
+ * @memberof DropElement
+ * @author Arthur Beaulieu
+ * @since December 2020
+ * @description <blockquote>This method will unsubscribe all drop events and remove all properties.</blockquote> **/
+ destroy() {
+// Utils.clearAllEvents(this._evtIds);
+ Utils.removeAllObjectKeys(this);
+ }
+
+
+ /* --------------------------------------------------------------------------------------------------------------- */
+ /* --------------------------------- DROPELEMENT INSTANTIATION SEQUENCE --------------------------------------- */
+ /* --------------------------------------------------------------------------------------------------------------- */
+
+
+ /** @method
+ * @name _buildElements
+ * @private
+ * @memberof DropElement
+ * @author Arthur Beaulieu
+ * @since December 2020
+ * @description <blockquote>This method will define the transparent border style and append this virtual border to the
+ * target DOM element.</blockquote> **/
+ _buildElements() {
+ this._transparentBorder = 'dashed 3px transparent';
+ this._target.style.border = this._transparentBorder;
+ }
+
+
+ /** @method
+ * @name _events
+ * @private
+ * @memberof DropElement
+ * @author Arthur Beaulieu
+ * @since December 2020
+ * @description <blockquote>This method will subscribe to drop events, both for desktop and mobile.</blockquote> **/
+ _events() {
+ this._target.addEventListener('dragenter', this._dragEnter.bind(this));
+ this._target.addEventListener('dragover', this._dragOver.bind(this));
+ this._target.addEventListener('dragleave', this._dragLeave.bind(this));
+ this._target.addEventListener('drop', this._drop.bind(this));
+ }
+
+
+ /* --------------------------------------------------------------------------------------------------------------- */
+ /* ----------------------------------- DESKTOP DROP EVENTS METHODS -------------------------------------------- */
+ /* --------------------------------------------------------------------------------------------------------------- */
+
+
+ /** @method
+ * @name _dragEnter
+ * @private
+ * @memberof DropElement
+ * @author Arthur Beaulieu
+ * @since December 2020
+ * @description <blockquote>This method will handle the entering of a dragged div over the target DOM element. When
+ * the target DOM element is hovered, a dashed border is made visible, replacing the transparent one to notify the
+ * user that the dragged div can be dropped.</blockquote>
+ * @param {object} event - The mouse event **/
+ _dragEnter(event) {
+ this._eventBehavior(event);
+ ++this._movementCounter;
+ this._target.style.border = 'dashed 3px #ffa800';
+ }
+
+
+ /** @method
+ * @name _dragOver
+ * @private
+ * @memberof DropElement
+ * @author Arthur Beaulieu
+ * @since December 2020
+ * @description <blockquote>This method will handle the dragged div hovering the target DOM element.</blockquote>
+ * @param {object} event - The mouse event **/
+ _dragOver(event) {
+ this._eventBehavior(event);
+ event.dataTransfer.dropEffect = 'copy';
+ }
+
+
+ /** @method
+ * @name _dragLeave
+ * @private
+ * @memberof DropElement
+ * @author Arthur Beaulieu
+ * @since December 2020
+ * @description <blockquote>This method will handle the event that is fired when the hovered div leaves the target
+ * DOM element. It require the movement counter to be equal to zero to restore the transparent border of the target
+ * DOM element.</blockquote>
+ * @param {object} event - The mouse event **/
+ _dragLeave(event) {
+ this._eventBehavior(event);
+ --this._movementCounter;
+ if (this._movementCounter === 0) {
+ this._target.style.border = this._transparentBorder;
+ }
+ }
+
+
+ /* --------------------------------------------------------------------------------------------------------------- */
+ /* ------------------------------ MOBILE AND DESKTOP DROP EVENTS METHODS -------------------------------------- */
+ /* --------------------------------------------------------------------------------------------------------------- */
+
+
+ /** @method
+ * @name _drop
+ * @private
+ * @memberof DropElement
+ * @author Arthur Beaulieu
+ * @since December 2020
+ * @description <blockquote>This method will handle the dropping of a DragElement, to properly read the data it holds
+ * and send it to the drop callback provided in constructor.</blockquote>
+ * @param {object} event - The mouse or touch event **/
+ _drop(event) {
+ this._eventBehavior(event);
+ this._target.style.border = this._transparentBorder;
+ this._onDropCB(event.dataTransfer);
+ }
+
+
+ /* --------------------------------------------------------------------------------------------------------------- */
+ /* ------------------------------------------- UTILS METHODS -------------------------------------------------- */
+ /* --------------------------------------------------------------------------------------------------------------- */
+
+
+ /** @method
+ * @name _eventBehavior
+ * @private
+ * @memberof DropElement
+ * @author Arthur Beaulieu
+ * @since December 2020
+ * @description <blockquote>This method will prevent the default behavior of given event, and will stop its
+ * propagation.</blockquote>
+ * @param {object} event - The mouse or touch event **/
+ _eventBehavior(event) {
+ event.preventDefault();
+ event.stopPropagation();
+ }
+
+
+ /** @method
+ * @name _isTouchEventInTarget
+ * @private
+ * @memberof DropElement
+ * @author Arthur Beaulieu
+ * @since December 2020
+ * @description <blockquote>This method will compare a touch point to the target position and return true if the
+ * touch point is inside the target DOM element.</blockquote>
+ * @param {object} touchPosition - The touch event
+ * @return {boolean} Do the touch point is included in the target DOM element **/
+ _isTouchEventInTarget(touchPosition) {
+ const rect = this._target.getBoundingClientRect();
+ const inAxisX = touchPosition.pageX >= rect.x && (touchPosition.pageX <= rect.x + rect.width);
+ const inAxisY = touchPosition.pageY >= rect.y && (touchPosition.pageY <= rect.y + rect.height);
+ return (inAxisX && inAxisY);
+ }
+
+
+ }
+
+
+ export default DropElement;
+
/**
+ * @class
+ * @static
+ * @public
+**/
+class Utils {
+
+
+ /**
+ * @method
+ * @name stripDom
+ * @public
+ * @static
+ * @memberof Utils
+ * @author Arthur Beaulieu
+ * @since November 2022
+ * @description
+ * <blockquote>
+ * From a given string/number input, this method will strip all unnecessary
+ * characters and will only retrun the text content as a string.
+ * </blockquote>
+ * @param {String|Number} html - The html string to strip
+ * @return {String} The stripped text content, empty string on error
+ **/
+ static stripDom(html) {
+ // Not accepting empty or not string/number
+ if (!html || (typeof html !== 'string' && typeof html !== 'number')) {
+ return '';
+ }
+
+ const doc = new DOMParser().parseFromString(html, 'text/html');
+ return doc.body.textContent || '';
+ }
+
+
+ /**
+ * @method
+ * @name replaceString
+ * @public
+ * @static
+ * @memberof Utils
+ * @author Arthur Beaulieu
+ * @since November 2022
+ * @description
+ * <blockquote>
+ * Replace a given string in an HTML element with another.
+ * </blockquote>
+ * @param {Element} element - The DOM element to replace string in
+ * @param {String} string - The string to be replaced
+ * @param {String} value - The value to apply to the replaced string
+ * @return {Boolean} The success status of the replace action
+ **/
+ static replaceString(element, string, value) {
+ if (!element || !element.innerHTML || !string || typeof string !== 'string' || !value || typeof value !== 'string') {
+ return false;
+ }
+
+ element.innerHTML = element.innerHTML.replace(string, value);
+ return true;
+ }
+
+
+ /**
+ * @method
+ * @name getDistanceBetweenCoords
+ * @public
+ * @static
+ * @memberof Utils
+ * @author Arthur Beaulieu
+ * @since November 2022
+ * @description
+ * <blockquote>
+ * Compute the distance in meters between two points given in [Lat, Lng] arrays.
+ * </blockquote>
+ * @param {Array} from - The first point lat and lng array
+ * @param {Array} to - The second point lat and lng array
+ * @return {Number} A floating number, the distance between two points given in meters
+ **/
+ static getDistanceBetweenCoords(from, to) {
+ // Generic argument testing
+ if (!from || !to || !Array.isArray(from) || !Array.isArray(to)) {
+ return -1;
+ }
+ // From input array testing
+ if (from.length !== 2 || typeof from[0] !== 'number' || typeof from[1] !== 'number') {
+ return -1;
+ }
+ // To input array testing
+ if (to.length !== 2 || typeof to[0] !== 'number' || typeof to[1] !== 'number') {
+ return -1;
+ }
+ // Return distance in meters
+ const lat1 = (from[0] * Math.PI) / 180;
+ const lon1 = (from[1] * Math.PI) / 180;
+ const lat2 = (to[0] * Math.PI) / 180;
+ const lon2 = (to[1] * Math.PI) / 180;
+ // Delta between coords to compute output distance
+ const deltaLat = lat2 - lat1;
+ const deltaLon = lon2 - lon1;
+ const a = Math.pow(Math.sin(deltaLat / 2), 2) + Math.cos(lat1) * Math.cos(lat2) * Math.pow(Math.sin(deltaLon / 2), 2);
+ const c = 2 * Math.asin(Math.sqrt(a));
+ return (c * 6371000); // Earth radius in meters
+ }
+
+
+ /**
+ * @method
+ * @name precisionRound
+ * @public
+ * @memberof Utils
+ * @author Arthur Beaulieu
+ * @since September 2018
+ * @description
+ * <blockquote>
+ * Do a Math.round with a given precision (ie amount of integers after the coma).
+ * </blockquote>
+ * @param {Nunmber} value - The value to precisely round (> 0)
+ * @param {Number} precision - The number of integers after the coma (> 0)
+ * @return {Number} - The rounded value
+ **/
+ static precisionRound(value, precision) {
+ if (typeof value !== 'number' || typeof precision !== 'number') {
+ return -1;
+ }
+ const multiplier = Math.pow(10, precision || 0);
+ return Math.round(value * multiplier) / multiplier;
+ }
+
+
+ /**
+ * @method
+ * @name setDefaultPreferences
+ * @public
+ * @static
+ * @memberof Utils
+ * @author Arthur Beaulieu
+ * @since November 2022
+ * @description
+ * <blockquote>
+ * Analyze preferences and fallback to default values if preferences doesn't exists.
+ * </blockquote>
+ **/
+ static setDefaultPreferences() {
+ if (Utils.getPreference('poi-show-spot') === null) {
+ Utils.setPreference('poi-show-spot', true);
+ }
+
+ if (Utils.getPreference('poi-show-shop') === null) {
+ Utils.setPreference('poi-show-shop', true);
+ }
+
+ if (Utils.getPreference('poi-show-bar') === null) {
+ Utils.setPreference('poi-show-bar', true);
+ }
+
+ if (Utils.getPreference('poi-show-circle') === null) {
+ Utils.setPreference('poi-show-circle', true);
+ }
+
+ if (Utils.getPreference('poi-show-label') === null) {
+ Utils.setPreference('poi-show-label', true);
+ }
+
+ if (Utils.getPreference('map-plan-layer') === null) {
+ Utils.setPreference('map-plan-layer', 'Plan OSM');
+ }
+
+ if (Utils.getPreference('selected-lang') === null) {
+ Utils.setPreference('selected-lang', 'en');
+ }
+
+ if (Utils.getPreference('app-debug') === null) {
+ Utils.setPreference('app-debug', false);
+ }
+
+ if (Utils.getPreference('map-high-accuracy') === null) {
+ Utils.setPreference('map-high-accuracy', false);
+ }
+
+ if (Utils.getPreference('map-center-on-user') === null) {
+ Utils.setPreference('map-center-on-user', false);
+ }
+
+ if (Utils.getPreference('dark-theme') === null) {
+ Utils.setPreference('dark-theme', true);
+ }
+
+ if (Utils.getPreference('startup-help') === null) {
+ Utils.setPreference('startup-help', true);
+ }
+ }
+
+
+ /**
+ * @method
+ * @name formatMarker
+ * @public
+ * @memberof BeerCrackerz
+ * @author Arthur Beaulieu
+ * @since February 2022
+ * @description
+ * <blockquote>
+ * This method formats a mark so it can be parsed using JSON.parse
+ * in order to be later stored in database.
+ * </blockquote>
+ * @param {Object} mark - The mark options to format for server communication
+ * @return {Object} The formatted mark
+ **/
+ static formatMarker(mark) {
+ // Mandatory arguments
+ if (!mark || !mark.name || !mark.types || !mark.lat || !mark.lng) {
+ return null;
+ }
+ // Mandatory arguments proper types
+ if (typeof mark.name !== 'string' || !Array.isArray(mark.types) || typeof mark.lat !== 'number' || typeof mark.lng !== 'number') {
+ return null;
+ }
+ // Only return if types aren't all strings
+ for (let i = 0; i < mark.types.length; ++i) {
+ if (typeof mark.types[i] !== 'string') {
+ return null;
+ }
+ }
+ // Only return if description is not properly formated
+ if (mark.description && typeof mark.description !== 'string') {
+ return null;
+ }
+ // Only return if modifiers are not properly formated
+ if (mark.modifiers) {
+ if (!Array.isArray(mark.modifiers)) {
+ return null;
+ }
+
+ for (let i = 0; i < mark.modifiers.length; ++i) {
+ if (typeof mark.modifiers[i] !== 'string') {
+ return null;
+ }
+ }
+ }
+ // Only return if rate is not a number or not between 0 and 4
+ if (mark.rate && typeof mark.rate !== 'number' || mark.rate < 0 || mark.rate > 4) {
+ return null;
+ }
+ // Only return if price is not a number or not between 0 and 2
+ if (mark.price && typeof mark.price !== 'number' || mark.price < 0 || mark.price > 2) {
+ return null;
+ }
+ // Finally return formatted mark
+ return {
+ name: mark.name,
+ types: mark.types,
+ lat: mark.lat,
+ lng: mark.lng,
+ description: mark.description,
+ modifiers: mark.modifiers,
+ rate: mark.rate,
+ price: mark.price
+ };
+ }
+
+
+ static removeAllObjectKeys(obj) {
+ if (!obj || typeof obj !== 'object' || Array.isArray(obj)) {
+ return false;
+ }
+
+ Object.keys(obj).forEach(key => {
+ delete obj[key];
+ });
+
+ return true;
+ }
+
+
+ /* Preference get set (DEPRECATED, will be mgrated with user pref when ready) */
+
+
+ static getPreference(pref) {
+ return localStorage.getItem(pref) || null;
+ }
+
+
+ static setPreference(pref, value) {
+ localStorage.setItem(pref, value);
+ }
+
+
+}
+
+
+export default Utils;
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/doc/beercrackerz/0.0.1/scripts/linenumber.js b/front/doc/scripts/linenumber.js
similarity index 100%
rename from doc/beercrackerz/0.0.1/scripts/linenumber.js
rename to front/doc/scripts/linenumber.js
diff --git a/doc/beercrackerz/0.0.1/scripts/prettify/Apache-License-2.0.txt b/front/doc/scripts/prettify/Apache-License-2.0.txt
similarity index 100%
rename from doc/beercrackerz/0.0.1/scripts/prettify/Apache-License-2.0.txt
rename to front/doc/scripts/prettify/Apache-License-2.0.txt
diff --git a/doc/beercrackerz/0.0.1/scripts/prettify/lang-css.js b/front/doc/scripts/prettify/lang-css.js
similarity index 100%
rename from doc/beercrackerz/0.0.1/scripts/prettify/lang-css.js
rename to front/doc/scripts/prettify/lang-css.js
diff --git a/doc/beercrackerz/0.0.1/scripts/prettify/prettify.js b/front/doc/scripts/prettify/prettify.js
similarity index 100%
rename from doc/beercrackerz/0.0.1/scripts/prettify/prettify.js
rename to front/doc/scripts/prettify/prettify.js
diff --git a/doc/beercrackerz/0.0.1/styles/jsdoc-default.css b/front/doc/styles/jsdoc-default.css
similarity index 100%
rename from doc/beercrackerz/0.0.1/styles/jsdoc-default.css
rename to front/doc/styles/jsdoc-default.css
diff --git a/doc/beercrackerz/0.0.1/styles/prettify-jsdoc.css b/front/doc/styles/prettify-jsdoc.css
similarity index 100%
rename from doc/beercrackerz/0.0.1/styles/prettify-jsdoc.css
rename to front/doc/styles/prettify-jsdoc.css
diff --git a/doc/beercrackerz/0.0.1/styles/prettify-tomorrow.css b/front/doc/styles/prettify-tomorrow.css
similarity index 100%
rename from doc/beercrackerz/0.0.1/styles/prettify-tomorrow.css
rename to front/doc/styles/prettify-tomorrow.css
diff --git a/front/src/BeerCrackerz.js b/front/src/BeerCrackerz.js
new file mode 100644
index 0000000..6258d7a
--- /dev/null
+++ b/front/src/BeerCrackerz.js
@@ -0,0 +1,939 @@
+import './BeerCrackerz.scss';
+import Kom from './js/core/Kom.js';
+import LangManager from './js/core/LangManager.js';
+
+import ZoomSlider from './js/ui/component/ZoomSlider.js';
+import Notification from './js/ui/component/Notification.js';
+import VisuHelper from './js/ui/VisuHelper.js';
+import MarkPopup from './js/ui/MarkPopup';
+import ModalFactory from './js/ui/ModalFactory';
+
+import CustomEvents from './js/utils/CustomEvents.js';
+import Utils from './js/utils/Utils.js';
+import AccuracyEnum from './js/utils/enums/AccuracyEnum.js';
+import ClustersEnum from './js/utils/enums/ClusterEnum.js';
+import ColorEnum from './js/utils/enums/ColorEnum.js';
+import ProvidersEnum from './js/utils/enums/ProviderEnum.js';
+import MapEnum from './js/utils/enums/MapEnum.js';
+
+
+window.VERSION = '0.1.0';
+window.Evts = new CustomEvents();
+
+
+/**
+ * @class
+ * @constructor
+ * @public
+**/
+class BeerCrackerz {
+
+
+ /**
+ * @summary The BeerCrackerz main component
+ * @author Arthur Beaulieu
+ * @since January 2022
+ * @description
+ *
+ * This component handles the whole BeerCrackerz app. It includes the map manipulation,
+ * the geolocation API to update the user position and process any map events that are
+ * relevant to an UX stand point. For more information, please consult the application
+ * description page at https://about.beercrackerz.org/
+ *
+ **/
+ constructor() {
+ /**
+ * The core Leaflet.js map
+ * @type {Object}
+ * @private
+ **/
+ this._map = null;
+ /**
+ * The zoom slider handler
+ * @type {Object}
+ * @private
+ **/
+ this._zoomSlider = null;
+ /**
+ * The notification handler
+ * @type {Object}
+ * @private
+ **/
+ this._notification = null;
+ /**
+ * The user object holds everything useful to ensure a proper session
+ * @type {Object}
+ * @private
+ **/
+ this._user = {
+ lat: 48.853121540141096, // Default lat to Paris Notre-Dame latitude
+ lng: 2.3498955769881156, // Default lng to Paris Notre-Dame longitude
+ accuracy: 0, // Accuracy in meter given by geolocation API
+ marker: null, // The user marker on map
+ circle: null, // The accuracy circle around the user marker
+ range: null, // The range in which user can add a new marker
+ color: ColorEnum.user, // The color to use for circle (match the user marker color)
+ id: -1,
+ username: ''
+ };
+ /**
+ * The stored marks for spots, shops and bars
+ * @type {Object}
+ * @private
+ **/
+ this._marks = {
+ spot: [],
+ shop: [],
+ bar: []
+ };
+ /**
+ * The stored clusters for markers, see Leaflet.markercluster plugin
+ * @type {Object}
+ * @private
+ **/
+ this._clusters = {
+ spot: {},
+ shop: {},
+ bar: {}
+ };
+ /**
+ * The temporary marker for new marks only
+ * @type {Object}
+ * @private
+ **/
+ this._newMarker = null;
+
+ this._popupOpened = false;
+ /**
+ * The debug DOM object
+ * @type {Object}
+ * @private
+ **/
+ this.debugElement = null;
+ /**
+ * ID for geolocation watch callback
+ * @type {Number}
+ * @private
+ **/
+ this._watchId = null;
+ /**
+ * Flag to know if a zoom action is occuring on map
+ * @type {Boolean}
+ * @private
+ **/
+ this._isZooming = false;
+ /**
+ * Whether the user is double clicking the map
+ * @type {Boolean}
+ * @private
+ **/
+ this._dbClick = false;
+ /**
+ * The timeout id for double click detection
+ * @type {Number}
+ * @private
+ **/
+ this._dbClickTimeoutId = -1;
+ /**
+ * The communication manager to process all server call
+ * @type {Object}
+ * @private
+ **/
+ this._kom = new Kom();
+ /**
+ * The LangManager must be instantiated to handle nls accross the app
+ * @type {Object}
+ * @private
+ **/
+ // The BeerCrackerz app is only initialized once nls are set up
+ this._lang = new LangManager();
+ // Start app initialization
+ this._init();
+ }
+
+
+ // ======================================================================== //
+ // ----------------- Application initialization sequence ------------------ //
+ // ======================================================================== //
+
+
+ /**
+ * @method
+ * @name _init
+ * @private
+ * @memberof BeerCrackerz
+ * @author Arthur Beaulieu
+ * @since January 2022
+ * @description
+ *
+ * The _init() method is designed to properly configure the user session, according
+ * to its saved preferences and its position. It first build the debug interface,
+ * then loads the user preferences, then create the map and finally, events are listened.
+ *
+ * The _initPreferences() will initialize user preference if they are not set yet,
+ * it will also update the UI according to user preferences ; debug DOM visible,
+ * update the command classList for selected ones.
+ *
+ * @returns {Promise} A Promise resolved when preferences are set
+ **/
+ _initPreferences() {
+ return new Promise((resolve, reject) => {
+ // If no pref, set fallbacks
+ Utils.setDefaultPreferences();
+ // Update icon class if center on preference is set to true
+ if (Utils.getPreference('map-center-on-user') === 'true') {
+ document.getElementById('center-on').classList.add('lock-center-on');
+ }
+ // Replace dark-theme class with light-theme class on body
+ if (Utils.getPreference('dark-theme') === 'false') {
+ document.body.classList.remove('dark-theme');
+ document.body.classList.add('light-theme');
+ }
+ // Update LangManager with pref language
+ this.nls.updateLang(Utils.getPreference('selected-lang')).then(() => {
+ this.debugElement = VisuHelper.initDebugUI();
+ // Create and append debug UI with proper nls settings
+ if (window.DEBUG === true || (Utils.getPreference('app-debug') === 'true')) {
+ window.DEBUG = true; // Ensure to set global flag if preference comes from local storage
+ Utils.setPreference('app-debug', true); // Ensure to set local storage preference if debug flag was added to the url
+ VisuHelper.addDebugUI();
+ }
+ resolve();
+ }).catch(reject);
+ });
+ }
+
+
+ /**
+ * @method
+ * @name _initGeolocation
+ * @private
+ * @memberof BeerCrackerz
+ * @author Arthur Beaulieu
+ * @since January 2022
+ * @description
+ *
+ * The _initGeolocation() method will request from browser the location authorization.
+ * Once granted, an event listener is set on any position update, so it can update the
+ * map state and the markers position. This method can be called again, only if the
+ * geolocation watch has been cleared ; for example when updating the accuracy options.
+ *
+ * @returns {Promise} A Promise resolved when preferences are set
+ **/
+ _initGeolocation() {
+ return new Promise(resolve => {
+ if ('geolocation' in navigator) {
+ const options = (Utils.getPreference('map-high-accuracy') === 'true') ? AccuracyEnum.high : AccuracyEnum.optimized;
+ this._watchId = navigator.geolocation.watchPosition(position => {
+ // Update saved user position
+ this._user.lat = position.coords.latitude;
+ this._user.lng = position.coords.longitude;
+ this._user.accuracy = position.coords.accuracy;
+ // Only draw marker if map is already created
+ if (this._map) {
+ VisuHelper.drawUserMarker();
+ // Update map position if focus lock is active
+ if (Utils.getPreference('map-center-on-user') === 'true' && !this._isZooming) {
+ this._map.setView(this._user);
+ }
+ // Updating debug info
+ VisuHelper.updateDebugUI();
+ }
+ resolve();
+ }, resolve, options);
+ } else {
+ this.notification.raise(this.nls.notif('geolocationError'));
+ resolve();
+ }
+ });
+ }
+
+
+ /**
+ * @method
+ * @name _initMap
+ * @private
+ * @memberof BeerCrackerz
+ * @author Arthur Beaulieu
+ * @since January 2022
+ * @description
+ *
+ * The _initMap() method will create the Leaflet.js map with two base layers (plan/satellite),
+ * add scale control, remove zoom control and set map bounds.
+ *
+ * @returns {Promise} A Promise resolved when preferences are set
+ **/
+ _initMap() {
+ return new Promise(resolve => {
+ // Use main div to inject OSM into
+ this._map = window.L.map('beer-crakerz-map', {
+ zoomControl: false,
+ }).setView([this._user.lat, this._user.lng], 18);
+ // Add meter and feet scale on map
+ window.L.control.scale().addTo(this._map);
+ // Place user marker on the map
+ VisuHelper.drawUserMarker();
+ // Add OSM credits to the map next to leaflet credits
+ const osm = ProvidersEnum.planOsm;
+ const esri = ProvidersEnum.satEsri;
+ // Prevent panning outside of the world's edge
+ this._map.setMaxBounds(MapEnum.mapBounds);
+ // Add layer group to interface
+ const baseMaps = {};
+ baseMaps[`
${this.nls.map('planLayerOSM')}
`] = osm;
+ baseMaps[`
${this.nls.map('satLayerEsri')}
`] = esri;
+ // Append layer depending on user preference
+ if (Utils.getPreference('map-plan-layer')) {
+ switch (Utils.getPreference('map-plan-layer')) {
+ case this.nls.map('planLayerOSM'):
+ osm.addTo(this._map);
+ break;
+ case this.nls.map('satLayerEsri'):
+ esri.addTo(this._map);
+ break;
+ default:
+ osm.addTo(this._map);
+ break;
+ }
+ } else { // No saved pref, fallback on OSM base map
+ osm.addTo(this._map);
+ }
+ // Add layer switch radio on bottom right of the map
+ window.L.control.layers(baseMaps, {}, { position: 'bottomright' }).addTo(this._map);
+ // Init zoom slider when map has been created
+ this._zoomSlider = new ZoomSlider(this._map);
+ resolve();
+ });
+ }
+
+
+ /**
+ * @method
+ * @name _initMarkers
+ * @private
+ * @memberof BeerCrackerz
+ * @author Arthur Beaulieu
+ * @since January 2022
+ * @description
+ *
+ * The _initEvents() method will initialize all saved marker into the map.
+ * Markers must be retrieved from server with a specific format to ensure it works
+ *
+ * @returns {Promise} A Promise resolved when preferences are set
+ **/
+ _initMarkers() {
+ return new Promise(resolve => {
+ // Init map clusters for marks to be displayed (disable clustering at opened popup zoom level)
+ this._clusters.spot = ClustersEnum.spot;
+ this._clusters.shop = ClustersEnum.shop;
+ this._clusters.bar = ClustersEnum.bar;
+
+ if (Utils.getPreference(`poi-show-spot`) === 'true') {
+ this._map.addLayer(this._clusters.spot);
+ }
+ if (Utils.getPreference(`poi-show-shop`) === 'true') {
+ this._map.addLayer(this._clusters.shop);
+ }
+ if (Utils.getPreference(`poi-show-bar`) === 'true') {
+ this._map.addLayer(this._clusters.bar);
+ }
+
+ const iterateMarkers = mark => {
+ const popup = new MarkPopup(mark, dom => {
+ mark.dom = dom;
+ mark.marker = VisuHelper.addMark(mark);
+ mark.popup = popup;
+ this._marks[mark.type].push(mark);
+ this._clusters[mark.type].addLayer(mark.marker);
+ });
+ };
+
+ this._kom.getSpots().then(spots => {
+ for (let i = 0; i < spots.length; ++i) {
+ iterateMarkers(spots[i]);
+ }
+ });
+
+ this._kom.getShops().then(shops => {
+ for (let i = 0; i < shops.length; ++i) {
+ iterateMarkers(shops[i]);
+ }
+ });
+
+ this._kom.getBars().then(bars => {
+ for (let i = 0; i < bars.length; ++i) {
+ iterateMarkers(bars[i]);
+ }
+ });
+
+ resolve();
+ });
+ }
+
+
+ /**
+ * @method
+ * @name _initEvents
+ * @private
+ * @memberof BeerCrackerz
+ * @author Arthur Beaulieu
+ * @since January 2022
+ * @description
+ *
+ * The _initEvents() method will listen to all required events to manipulate the map. Those events
+ * are both for commands and for map events (click, drag, zoom and layer change).
+ *
+ * @returns {Promise} A Promise resolved when preferences are set
+ **/
+ _initEvents() {
+ return new Promise(resolve => {
+ // Subscribe to click event on map to react
+ this._map.on('click', this.mapClicked.bind(this));
+ // Map is dragged by user mouse/finger
+ this._map.on('drag', () => {
+ // Constrain pan to the map bounds
+ this._map.panInsideBounds(MapEnum.mapBounds, { animate: true });
+ // Disable lock focus if user drags the map
+ if (Utils.getPreference('map-center-on-user') === 'true') {
+ VisuHelper.toggleFocusLock();
+ }
+ });
+ // Map events
+ this._map.on('zoomstart', () => {
+ this._isZooming = true;
+ });
+ this._map.on('zoomend', () => {
+ this._isZooming = false;
+ // Updating debug info
+ VisuHelper.updateDebugUI();
+ });
+ this._map.on('baselayerchange', event => {
+ Utils.setPreference('map-plan-layer', Utils.stripDom(event.name));
+ });
+
+ // Update map view for big popups
+ this._map.on('popupopen', event => {
+ const px = this._map.project(event.target._popup._latlng);
+ px.y -= event.target._popup._container.clientHeight / 2;
+ this._map.flyTo(this._map.unproject(px), this._map.getZoom());
+ this._popupOpened = true;
+ this._zoomSlider.hide();
+ });
+
+ // Clustering events
+ this._clusters.spot.on('clusterclick', this._zoomSlider.hide.bind(this._zoomSlider));
+ this._clusters.shop.on('clusterclick', this._zoomSlider.hide.bind(this._zoomSlider));
+ this._clusters.bar.on('clusterclick', this._zoomSlider.hide.bind(this._zoomSlider));
+ this._clusters.spot.on('animationend', VisuHelper.checkClusteredMark.bind(this, 'spot'));
+ this._clusters.shop.on('animationend', VisuHelper.checkClusteredMark.bind(this, 'shop'));
+ this._clusters.bar.on('animationend', VisuHelper.checkClusteredMark.bind(this, 'bar'));
+ // Command events
+ window.Evts.addEvent('click', document.getElementById('user-profile'), this.userProfile, this);
+ window.Evts.addEvent('click', document.getElementById('hide-show'), this.hidShowMenu, this);
+ window.Evts.addEvent('click', document.getElementById('center-on'), VisuHelper.toggleFocusLock, this);
+ // Bus events
+ window.Evts.subscribe('addMark', this.addMark.bind(this)); // Event from addMarkPopup
+ window.Evts.subscribe('onMarkAdded', this._onMarkAdded.bind(this)); // Event from MarkPopup
+ window.Evts.subscribe('deleteMark', this.deleteMark.bind(this)); // Event from MarkPopup
+ window.Evts.subscribe('onMarkDeleted', this._onMarkDeleted.bind(this)); // User confirmed the mark deletion
+ window.Evts.subscribe('editMark', this.editMark.bind(this)); // Event from MarkPopup
+ window.Evts.subscribe('onMarkEdited', this._onMarkEdited.bind(this)); // User confirmed the mark edition
+ window.Evts.subscribe('updateProfile', this.updateProfilePicture.bind(this)); // Event from user modal
+ window.Evts.subscribe('onProfilePictureUpdated', this._onProfilePictureUpdated.bind(this)); // Event from update pp modal
+
+ window.Evts.subscribe('centerOn', VisuHelper.centerOn.bind(VisuHelper));
+
+ resolve();
+ });
+ }
+
+
+ _startSession() {
+ if (Utils.getPreference(`startup-help`) === 'true') {
+ this._modal = ModalFactory.build('StartupHelp');
+ } else {
+ this.notification.raise(this.nls.notif('welcomeBack'));
+ }
+ }
+
+
+ // ======================================================================== //
+ // ------------------------- Toggle for map items ------------------------- //
+ // ======================================================================== //
+
+
+ /**
+ * @method
+ * @name toggleHighAccuracy
+ * @public
+ * @memberof BeerCrackerz
+ * @author Arthur Beaulieu
+ * @since January 2022
+ * @description
+ *
+ * The toggleHighAccuracy() method will, depending on user preference, update the
+ * geolocation accuracy between optimized and high. The high settings might cause
+ * more memory and processing consumption, but gives better results. It will clear
+ * any previous position watch on the geolocation API so it can subscribe a new one
+ * with the new accuracy parameters (see Utils for values)
+ *
+ * The mapClicked() method is the callback used when the user clicked on the Leaflet.js map
+ *
+ * @param {Event} event The click event
+ **/
+ mapClicked(event) {
+ if (!this._dbClick) {
+ // On first click, we activate double click detection
+ // If no registered click in 300mx, handle standard click
+ this._dbClick = true;
+ this._dbClickTimeoutId = setTimeout(() => {
+ this._dbClick = false;
+ if (this._newMarker && this._newMarker.popupClosed) { // Avoid to open new marker right after popup closing
+ this._newMarker = null;
+ this._popupOpened = false;
+ } else if (this._popupOpened === true) { // Do not open new mark if popup previously opened
+ this._popupOpened = false;
+ } else if (this._newMarker === null || !this._newMarker.isBeingDefined) { // Check for new mark
+ // Only create new marker if click is in range to add a mark
+ const distance = Utils.getDistanceBetweenCoords([this._user.lat, this._user.lng], [event.latlng.lat, event.latlng.lng]);
+ if (distance < MapEnum.newMarkRange) { // In range to create new mark
+ this.addMarkPopup(event.latlng);
+ this._newMarker.openPopup();
+ } else if (this._map.getZoom() >= 10) { // Avoid poluting UI when strong dezoom applied
+ this.notification.raise(this.nls.notif('newMarkerOutside'));
+ }
+ }
+ }, 200);
+ } else {
+ // Second click close enough to the previous one to be qualified double click
+ this._dbClick = false;
+ clearTimeout(this._dbClickTimeoutId);
+ }
+ }
+
+
+ // ======================================================================== //
+ // ----------------------- Marker manipulation ---------------------------- //
+ // ======================================================================== //
+
+
+ addMarkPopup(options) {
+ const dom = {
+ wrapper: document.createElement('DIV'),
+ title: document.createElement('P'),
+ spot: document.createElement('BUTTON'),
+ shop: document.createElement('BUTTON'),
+ bar: document.createElement('BUTTON'),
+ };
+ // Update class and inner HTMl content according to user's nls
+ dom.wrapper.className = 'new-poi';
+ dom.title.innerHTML = this.nls.map('newTitle');
+ dom.spot.innerHTML = this.nls.map('newSpot');
+ dom.shop.innerHTML = this.nls.map('newShop');
+ dom.bar.innerHTML = this.nls.map('newBar');
+ // Atach data type to each button (to be used in clicked callback)
+ dom.spot.dataset.type = 'spot';
+ dom.shop.dataset.type = 'shop';
+ dom.bar.dataset.type = 'bar';
+ // DOM chaining
+ dom.wrapper.appendChild(dom.title);
+ dom.wrapper.appendChild(dom.spot);
+ dom.wrapper.appendChild(dom.shop);
+ dom.wrapper.appendChild(dom.bar);
+ // Update popup content with DOM elements
+ options.dom = dom.wrapper;
+ // Create temporary mark with wrapper content and open it to offer user the creation menu
+ this._newMarker = VisuHelper.addMark(options);
+ options.marker = this._newMarker; // Attach marker to option so it can be manipulated in clicked callbacks
+ // Callback on button clicked (to open modal and define a new mark)
+ const _prepareNewMark = e => {
+ this._newMarker.isBeingDefined = true;
+ this._newMarker.closePopup();
+ options.type = e.target.dataset.type;
+ window.Evts.publish('addMark', options);
+ };
+ // Buttons click events
+ dom.spot.addEventListener('click', _prepareNewMark);
+ dom.shop.addEventListener('click', _prepareNewMark);
+ dom.bar.addEventListener('click', _prepareNewMark);
+ // Listen to clicks outside of popup to close new mark
+ this._newMarker.on('popupclose', () => {
+ if (!this._newMarker.isBeingDefined) {
+ this._newMarker.popupClosed = true;
+ this._newMarker.removeFrom(this.map);
+ }
+ });
+ }
+
+
+ addMark(options) {
+ this._modal = ModalFactory.build('AddMark', options);
+ }
+
+
+ _onMarkAdded(options) {
+ const popup = new MarkPopup(options, dom => {
+ options.dom = dom;
+ options.marker = VisuHelper.addMark(options); // Create final marker
+ options.popup = popup;
+ // Save new marker in local storage, later to be sent to the server
+ this._kom[`${options.type}Created`](Utils.formatMarker(options)).then(data => {
+ // Update marker data to session memory
+ options.id = data.id;
+ options.name = data.name;
+ options.description = data.description;
+ options.lat = data.lat;
+ options.lng = data.lng;
+ // Save marke in marks and clusters for the map
+ this._marks[options.type].push(options);
+ this._clusters[options.type].addLayer(options.marker);
+ // Notify user that new marker has been saved
+ this.notification.raise(this.nls.notif(`${options.type}Added`));
+ // Clear new marker to let user add other stuff
+ this._newMarker = null;
+ }).catch(() => {
+ this.notification.raise(this.nls.notif(`${options.type}NotAdded`));
+ });
+ });
+ }
+
+
+ /**
+ * @method
+ * @name deleteMark
+ * @public
+ * @memberof BeerCrackerz
+ * @author Arthur Beaulieu
+ * @since February 2022
+ * @description
+ *
+ * This method will delete a mark after prompting the user if he trully wants to
+ *
+ * @param {Object} options The mark options to delete
+ **/
+ deleteMark(options) {
+ this._map.closePopup();
+ this._modal = ModalFactory.build('DeleteMark', options);
+ }
+
+
+ _onMarkDeleted(options) {
+ // Iterate through marks to find matching one (by coord as marks coordinates are unique)
+ const marks = this._marks[options.type];
+ for (let i = 0; i < marks.length; ++i) {
+ // We found, remove circle, label and marker from map/clusters
+ if (options.lat === marks[i].lat && options.lng === marks[i].lng) {
+ // Send data to server
+ this._kom[`${options.type}Deleted`](marks[i].id, Utils.formatMarker(marks[i])).then(() => {
+ VisuHelper.removeMarkDecoration(marks[i]);
+ this._clusters[options.type].removeLayer(marks[i].marker);
+ marks.splice(i, 1);
+ // Update internal marks array
+ this._marks[options.type] = marks;
+ // Notify user through UI that marker has been successfully deleted
+ this.notification.raise(this.nls.notif(`${options.type}Deleted`));
+ }).catch(() => {
+ this.notification.raise(this.nls.notif(`${options.type}NotDeleted`));
+ });
+ break;
+ }
+ }
+ }
+
+
+ /**
+ * @method
+ * @name editMark
+ * @public
+ * @memberof BeerCrackerz
+ * @author Arthur Beaulieu
+ * @since February 2022
+ * @description
+ *
+ * This method will open a mark edition modal
+ *
+ * @param {Object} options The mark options to edit
+ **/
+ editMark(options) {
+ this._map.closePopup();
+ this._modal = ModalFactory.build('EditMark', options);
+ }
+
+
+ _onMarkEdited(options) {
+ // Iterate through marks to find matching one (by coord as marks coordinates are unique)
+ for (let i = 0; i < this._marks[options.type].length; ++i) {
+ // We found, remove circle, label and marker from map/clusters
+ if (options.lat === this._marks[options.type][i].lat && options.lng === this._marks[options.type][i].lng) {
+ this._marks[options.type][i].name = options.name.value;
+ this._marks[options.type][i].description = options.description.value;
+ this._marks[options.type][i].rate = options.rating;
+ if (options.price) {
+ this._marks[options.type][i].price = options.price;
+ }
+ const popup = new MarkPopup(options, dom => {
+ options.dom = dom;
+ options.marker.setPopupContent(popup.dom);
+ options.popup = popup;
+ // Send data to server
+ this._kom[`${options.type}Edited`](options.id, Utils.formatMarker(options)).then(data => {
+ // Update marker data to session memory
+ options.id = data.id;
+ options.name = data.name;
+ options.description = data.description;
+ options.lat = data.lat;
+ options.lng = data.lng;
+ // Notify user through UI that marker has been successfully deleted
+ this.notification.raise(this.nls.notif(`${options.type}Edited`));
+ }).catch(() => {
+ this.notification.raise(this.nls.notif(`${options.type}NotEdited`));
+ }).finally(() => {
+ this._modal.close(null, true);
+ });
+ });
+ break;
+ }
+ }
+ }
+
+
+ // ======================================================================== //
+ // --------------------------- User profile ------------------------------- //
+ // ======================================================================== //
+
+
+
+ /**
+ * @method
+ * @name userProfile
+ * @public
+ * @memberof BeerCrackerz
+ * @author Arthur Beaulieu
+ * @since January 2022
+ * @description
+ *
+ * The userProfile() method will request the user modal, which contains
+ * the user preferences, and the user profile information
+ *
+ * This component handles all the authentication pages for BeerCrackerz. It provides the login, the
+ * register and the forgot password process. It also provides a public map so unauthenticated user
+ * can still browse the best BeerCrackerz spots. For more information, please consult the application
+ * description page at https://about.beercrackerz.org/
+ *
+ **/
+ constructor() {
+ /**
+ * The minimal user object holds position and accuracy
+ * @type {Object}
+ * @private
+ **/
+ this._user = {
+ lat: 48.853121540141096, // Default lat to Paris Notre-Dame latitude
+ lng: 2.3498955769881156, // Default lng to Paris Notre-Dame longitude
+ accuracy: 0 // Accuracy in meter given by geolocation API
+ };
+ /**
+ * The stored marks for spots, shops and bars
+ * @type {Object}
+ * @private
+ **/
+ this._marks = {
+ spot: [],
+ shop: [],
+ bar: []
+ };
+ /**
+ * The stored clusters for markers, see Leaflet.markercluster plugin
+ * @type {Object}
+ * @private
+ **/
+ this._clusters = {
+ spot: {},
+ shop: {},
+ bar: {}
+ };
+ /**
+ * The Aside DOM container
+ * @type {Object}
+ * @private
+ **/
+ this._aside = null;
+ /**
+ * The Aside expand status
+ * @type {Boolean}
+ * @private
+ **/
+ this._isAsideExpanded = true;
+ /**
+ * The server communication class
+ * @type {Object}
+ * @private
+ **/
+ this._kom = null;
+ /**
+ * The frontend i18n manager
+ * @type {Object}
+ * @private
+ **/
+ this._lang = new LangManager();
+
+ this._init();
+ }
+
+
+ // ======================================================================== //
+ // -------------------------- App initialization -------------------------- //
+ // ======================================================================== //
+
+
+ /**
+ * @method
+ * @name _init
+ * @private
+ * @memberof BeerCrackerzAuth
+ * @author Arthur Beaulieu
+ * @since September 2022
+ * @description
+ *
+ * The _init() method handle the whole app initialization sequence. It first
+ * set the aside content to login (as it comes with the base welcome.html template),
+ * then initialize the communication and notification handler, and will finally
+ * initialize the whole map, markers and interactivity.
+ *
+ **/
+ _init() {
+ // Force default language to english for first page loading
+ if (Utils.getPreference('selected-lang') === null) {
+ Utils.setPreference('selected-lang', 'en');
+ }
+ this.nls.updateLang(Utils.getPreference('selected-lang')).then(() => {
+ // By default, the template contains the login aside, no need to fetch it
+ this._handleLoginAside();
+ this._kom = new Kom();
+ // We ensure the Kom layer is valid and ready to go any further
+ if (this._kom.isValid === true) {
+ const urlSearchParams = new URLSearchParams(window.location.search);
+ const params = Object.fromEntries(urlSearchParams.entries());
+ if (params.activate) {
+ const error = document.getElementById('login-error');
+ error.classList.add('visible');
+ if (params.activate === 'True') {
+ error.classList.add('success');
+ error.innerHTML = this.nls.register('activationSuccess');
+ } else {
+ error.innerHTML = this.nls.register('activationError');
+ }
+ } else if (params.uidb64 && params.token) {
+ this._loadForgotPasswordAside(params);
+ }
+
+ this._initMap()
+ .then(this._initGeolocation.bind(this))
+ .then(this._initMarkers.bind(this))
+ .then(this._initEvents.bind(this))
+ .catch(this._fatalError.bind(this));
+ } else {
+ this._fatalError({
+ file: 'Kom.js',
+ msg: (this._kom.csrf === null) ? `The CSRF token doesn't exists in cookies` : `The headers amount is invalid`
+ });
+ }
+ });
+ }
+
+
+ /**
+ * @method
+ * @async
+ * @name _initMap
+ * @private
+ * @memberof BeerCrackerzAuth
+ * @author Arthur Beaulieu
+ * @since September 2022
+ * @description
+ *
+ * The _initMap() method will create the Leaflet.js map with two base layers (plan/satellite),
+ * add scale control, remove zoom control and set map bounds.
+ *
+ * @returns {Promise} A Promise resolved when preferences are set
+ **/
+ _initMap() {
+ return new Promise(resolve => {
+ // Use main div to inject OSM into
+ this._map = window.L.map('beer-crakerz-map', {
+ zoomControl: false,
+ }).setView([48.853121540141096, 2.3498955769881156], 12);
+ // Add meter and feet scale on map
+ window.L.control.scale().addTo(this._map);
+ // Place user marker on the map
+ this._drawUserMarker();
+ // Prevent panning outside of the world's edge
+ this._map.setMaxBounds(MapEnum.mapBounds);
+ // Add layer group to interface
+ const baseMaps = {};
+ baseMaps[`
${this.nls.map('planLayerOSM')}
`] = ProvidersEnum.planOsm;
+ baseMaps[`
${this.nls.map('satLayerEsri')}
`] = ProvidersEnum.satEsri;
+ // Append layer depending on user preference
+ ProvidersEnum.planOsm.addTo(this._map);
+ // Add layer switch radio on bottom right of the map
+ window.L.control.layers(baseMaps, {}, { position: 'bottomright' }).addTo(this._map);
+ // Init zoom slider when map has been created
+ this._zoomSlider = new ZoomSlider(this._map);
+ resolve();
+ });
+ }
+
+
+ /**
+ * @method
+ * @async
+ * @name _initGeolocation
+ * @private
+ * @memberof BeerCrackerzAuth
+ * @author Arthur Beaulieu
+ * @since September 2022
+ * @description
+ *
+ * The _initGeolocation() method will request from browser the location authorization.
+ * Once granted, an event listener is set on any position update, so it can update the
+ * map state and the markers position. This method can be called again, only if the
+ * geolocation watch has been cleared ; for example when updating the accuracy options.
+ *
+ * @returns {Promise} A Promise resolved when preferences are set
+ **/
+ _initGeolocation() {
+ return new Promise(resolve => {
+ if ('geolocation' in navigator) {
+ this._watchId = navigator.geolocation.watchPosition(position => {
+ // Update saved user position
+ this._user.lat = position.coords.latitude;
+ this._user.lng = position.coords.longitude;
+ this._user.accuracy = position.coords.accuracy;
+ // Only draw marker if map is already created
+ if (this._map) {
+ this._drawUserMarker();
+ }
+ }, null, AccuracyEnum.high);
+ resolve();
+ } else {
+ resolve();
+ }
+ });
+ }
+
+
+ /**
+ * @method
+ * @async
+ * @name _initMarkers
+ * @private
+ * @memberof BeerCrackerzAuth
+ * @author Arthur Beaulieu
+ * @since September 2022
+ * @description
+ *
+ * The _initEvents() method will initialize all saved marker into the map.
+ * Markers must be retrieved from server with a specific format to ensure it works
+ *
+ * @returns {Promise} A Promise resolved when preferences are set
+ **/
+ _initMarkers() {
+ return new Promise(resolve => {
+ // Init map clusters for marks to be displayed (disable clustering at opened popup zoom level)
+ this._clusters.spot = ClustersEnum.spot;
+ this._clusters.shop = ClustersEnum.shop;
+ this._clusters.bar = ClustersEnum.bar;
+
+ this._map.addLayer(this._clusters.spot);
+ this._map.addLayer(this._clusters.shop);
+ this._map.addLayer(this._clusters.bar);
+
+ const iterateMarkers = mark => {
+ const popup = new MarkPopup(mark, dom => {
+ mark.dom = dom;
+ mark.marker = VisuHelper.addMark(mark);
+ mark.popup = popup;
+ this._marks[mark.type].push(mark);
+ this._clusters[mark.type].addLayer(mark.marker);
+ });
+ };
+
+ this._kom.getSpots().then(spots => {
+ for (let i = 0; i < spots.length; ++i) {
+ iterateMarkers(spots[i]);
+ }
+ });
+
+ this._kom.getShops().then(shops => {
+ for (let i = 0; i < shops.length; ++i) {
+ iterateMarkers(shops[i]);
+ }
+ });
+
+ this._kom.getBars().then(bars => {
+ for (let i = 0; i < bars.length; ++i) {
+ iterateMarkers(bars[i]);
+ }
+ });
+
+ resolve();
+ });
+ }
+
+
+ /**
+ * @method
+ * @async
+ * @name _initEvents
+ * @private
+ * @memberof BeerCrackerzAuth
+ * @author Arthur Beaulieu
+ * @since September 2022
+ * @description
+ *
+ * The _initEvents() method will listen to all required events to manipulate the map. Those events
+ * are both for commands and for map events (click, drag, zoom and layer change).
+ *
+ * @returns {Promise} A Promise resolved when preferences are set
+ **/
+ _initEvents() {
+ return new Promise(resolve => {
+ // Map is dragged by user mouse/finger
+ this._map.on('drag', () => {
+ // Constrain pan to the map bounds
+ this._map.panInsideBounds(MapEnum.mapBounds, { animate: true });
+ });
+ // Update map view for big popups
+ this._map.on('popupopen', event => {
+ const px = this._map.project(event.target._popup._latlng);
+ px.y -= event.target._popup._container.clientHeight / 2;
+ this._map.panTo(this._map.unproject(px), { animate: true });
+ });
+ // Center on command
+ document.getElementById('center-on').addEventListener('click', () => {
+ this._map.flyTo([this._user.lat, this._user.lng], 18);
+ });
+
+ window.Evts.subscribe('centerOn', VisuHelper.centerOn.bind(VisuHelper));
+ resolve();
+ });
+ }
+
+
+ /**
+ * @method
+ * @name _fatalError
+ * @private
+ * @memberof BeerCrackerzAuth
+ * @author Arthur Beaulieu
+ * @since September 2022
+ * @description
+ *
+ * The _fatalError() method will handle all fatal errors from which the app
+ * can't recover. It redirects to the error page and send info through the referrer
+ * so the error page can properly displays it to the user
+ *
+ * @param {Object} err - The error object with its info
+ * @param {Number} [err.status] - The HTTP error code
+ * @param {String} [err.url] - The URL that generated the HTTP error
+ * @param {String} [err.file] - The file in which the fatal error happened
+ * @param {String} [err.msg] - The custom error message
+ **/
+ _fatalError(err) {
+ if (window.DEBUG === false) { // In production, do the actual redirection
+ // We add params to referrer then redirect to error page so the information can be displayed
+ if (err && err.status) { // HTTP or related error
+ window.history.pushState('', '', `/welcome?&page=welcome&code=${err.status}&url=${err.url}&msg=${err.msg}`);
+ } else if (err && err.file && err.msg) { // File or process error
+ window.history.pushState('', '', `/welcome?&page=welcome&file=${err.file}&msg=${err.msg}`);
+ } else { // Generic error fallback
+ window.history.pushState('', '', `/welcome?&page=welcome&file=BeerCrackerzAuth.js&msg=An unknown error occured`);
+ }
+ // Now redirect the user to error page
+ window.location.href = '/error';
+ } else {
+ console.error(err);
+ }
+ }
+
+
+ // ======================================================================== //
+ // -------------------------- Aside interactivity ------------------------- //
+ // ======================================================================== //
+
+
+ /**
+ * @method
+ * @name _toggleAside
+ * @private
+ * @memberof BeerCrackerzAuth
+ * @author Arthur Beaulieu
+ * @since September 2022
+ * @description
+ *
+ * The _toggleAside() method will expand or collapse the aside, depending on the
+ * `this._isAsideExpanded` flag state. To be used as a callba, adding useful parameters to url before redirectck on aside expander.
+ *
+ * The _loadAside() method is a generic method to load an HTML template and replace
+ * the aside DOM content with that template, aswell as updating the document's class.
+ *
+ * @param {String} type - The aside to load in login/register/forgot-password
+ * @returns {Promise} A Promise resolved when template is loaded and in DOM
+ **/
+ _loadAside(type) {
+ return new Promise((resolve, reject) => {
+ this._kom.getTemplate(`/aside/${type}`).then(dom => {
+ //document.body.className = 'login dark-theme'; // Clear previous css class
+ document.body.classList.add(type); // Update body class with current aside view
+ // We need to get aside at the last moment because of nls that changed HTML content
+ this._aside = document.getElementById('aside');
+ this._aside.innerHTML = ''; // Clear HTML content
+ this._aside.appendChild(dom); // Replace with current aside dom
+ resolve();
+ }).catch(reject);
+ });
+ }
+
+
+ /**
+ * @method
+ * @name _loadLoginAside
+ * @private
+ * @memberof BeerCrackerzAuth
+ * @author Arthur Beaulieu
+ * @since September 2022
+ * @description
+ *
+ * The _loadLoginAside() method will load the login content into the aside
+ *
+ * The _handleLoginAside() method will replace the aside content with the login template,
+ * then it will handle its i18n, and all of its interactivity to submit login form to the server.
+ *
+ **/
+ _handleLoginAside(checkMail = false) {
+ // Update page nls according to browser language
+ document.title = this.nls.login('headTitle');
+ this.nls.handleLoginAside(document.getElementById('aside'));
+
+ const error = document.getElementById('login-error');
+ const username = document.getElementById('username');
+ const password = document.getElementById('password');
+
+ if (checkMail === true) {
+ error.classList.add('visible');
+ error.innerHTML = this.nls.login('checkMail');
+ }
+
+ // useful login method for field check and server response check
+ const _frontFieldValidation = () => {
+ error.className = 'error';
+ // Handling empty error cases
+ if (username.value === '' && password.value === '') {
+ error.classList.add('visible');
+ error.innerHTML = this.nls.login('bothEmpty');
+ username.classList.add('error');
+ password.classList.add('error');
+ return false;
+ } else if (username.value === '') {
+ error.classList.add('visible');
+ error.innerHTML = this.nls.login('usernameEmpty');
+ username.classList.add('error');
+ return false;
+ } else if (password.value === '') {
+ error.classList.add('visible');
+ error.innerHTML = this.nls.login('passwordEmpty');
+ password.classList.add('error');
+ return false;
+ }
+ return true;
+ };
+ const _backValidation = (e) => {
+ // Check response and handle status codes
+ // If all front and back tests are ok, redirect to auth
+ // If the user manually force redirection to authindex,
+ // the server should reject the request as the user is not authenticated
+ window.location = '/';
+ };
+ const _submit = () => {
+ // Reset error css classes
+ error.classList.remove('visible');
+ username.classList.remove('error');
+ password.classList.remove('error');
+ if (_frontFieldValidation()) {
+ this._kom.post('/api/auth/login/', {
+ username: username.value,
+ password: password.value
+ }).then(_backValidation).catch(e => {
+ error.classList.add('visible');
+ if (e.status === 401) {
+ error.innerHTML = this.nls.login('credsInvalid');
+ } else {
+ error.innerHTML = this.nls.login('serverError');
+ }
+ });
+ }
+ };
+ // Submit click event
+ document.getElementById('login-submit').addEventListener('click', _submit.bind(this), false);
+ password.addEventListener('keydown', e => { if (e.key === 'Enter') { _submit(); } });
+ // Register event
+ document.getElementById('register-aside').addEventListener('click', this._loadRegisterAside.bind(this), false);
+ document.getElementById('forgot-password').addEventListener('click', this._loadForgotPasswordAside.bind(this), false);
+ document.getElementById('aside-expander').addEventListener('click', this._toggleAside.bind(this), false);
+ }
+
+
+ /**
+ * @method
+ * @name _handleRegisterAside
+ * @private
+ * @memberof BeerCrackerzAuth
+ * @author Arthur Beaulieu
+ * @since September 2022
+ * @description
+ *
+ * The _handleRegisterAside() method will replace the aside content with the register template,
+ * then it will handle its i18n, and all of its interactivity to submit register form to the server.
+ *
+ * The _handleForgotPasswordAside() method will replace the aside content with the fogot password
+ * template, then it will handle its i18n, and all of its interactivity to submit forgot password
+ * form to the server.
+ *
+ * The _drawUserMarker() method will draw the user marker to the position received
+ * from the geolocation API. If the marker doesn't exist yet, it will create it and
+ * place it to its default position (see constructor/this._user).
+ *
+ * This class is the main object to deal with when requesting something from the server.
+ * It handle all urls calls (GET, POST), treat responses or handle errors using
+ * Promise. Because it uses Promise, success and errors are to be handled in the caller
+ * function, using .then() and .catch(). To properly deal with POST request,
+ * the session must contain a csrf token in cookies. Otherwise, those POST call may fail.
+ *
+ **/
+ constructor() {
+ /**
+ * User session CSRF token to use in POST request
+ * @type {string}
+ * @private
+ **/
+ this._csrfToken = this._getCsrfCookie();
+ /**
+ * Array of HTTP headers to be used in HTTP calls
+ * @type {Array[]}
+ * @private
+ **/
+ this._headers = this._createRequestHeaders();
+ /**
+ * Wether the Kom class headers are properly built
+ * @type {Boolean}
+ * @public
+ **/
+ this.isValid = this._checkValidity(); // Check that CSRF token exists and that headers are properly created
+ }
+
+
+ // ======================================================================== //
+ // ------------------------- Class initialization ------------------------- //
+ // ======================================================================== //
+
+
+ /**
+ * @method
+ * @name _getCsrfCookie
+ * @private
+ * @memberof Kom
+ * @author Arthur Beaulieu
+ * @since September 2022
+ * @description
+ *
+ * Extract CSRF token value from client cookies and returns it as a string. Returns an empty
+ * string by default. This method is required to be called on construction.
+ *
+ * @return {String} - The CSRF token string
+ **/
+ _getCsrfCookie() {
+ if (document.cookie && document.cookie !== '') {
+ const cookies = document.cookie.split(';');
+ for (let i = 0; i < cookies.length; ++i) {
+ // Parse current cookie to extract its properties
+ const cookie = cookies[i].split('=');
+ if (cookie !== undefined && cookie[0].toLowerCase().includes('srf')) {
+ // Found a matching cookie for csrftoken value, return as decoded string
+ return decodeURIComponent(cookie[1]);
+ }
+ }
+ }
+ // Return empty string by default, POST calls may fail
+ return null;
+ }
+
+
+ /**
+ * @method
+ * @name _createRequestHeaders
+ * @private
+ * @memberof Kom
+ * @author Arthur Beaulieu
+ * @since September 2022
+ * @description
+ *
+ * Fills Kom _headers private member array, to use in HTTP requests later on.
+ * This method is required to be called on construction.
+ *
+ * @return {Array[]} - The headers array, length 3, to be used in HTTP requests
+ **/
+ _createRequestHeaders() {
+ return [
+ ['Content-Type', 'application/json; charset=UTF-8'],
+ ['Accept', 'application/json'],
+ ['X-CSRFToken', this._csrfToken]
+ ];
+ }
+
+
+ /**
+ * @method
+ * @name _checkValidity
+ * @private
+ * @memberof Kom
+ * @author Arthur Beaulieu
+ * @since September 2022
+ * @description
+ *
+ * Check the Kom instance validity (eough headers) to ensure its properties validity.
+ *
+ * Generic tool method used by private methods on fetch responses to format output in the provided
+ * format. It must be either `json`, `text`, `raw` or `dom`.
+ *
+ * @param {String} type - The type of resolution, can be `json`, `text`, `raw` or `dom`
+ * @param {Object} response - The fetch response object
+ * @returns {Promise} The request Promise, format response as an object on resolve, as error code string on reject
+ **/
+ _resolveAs(type, response) {
+ return new Promise((resolve, reject) => {
+ if (response) {
+ if (type === 'raw') { // Raw are made in XMLHttpRequest and need special handling
+ if (response.status === 200) {
+ resolve(response.responseText);
+ } else {
+ reject(response.status);
+ }
+ } else if (type === 'json' || type === 'text') { // Call are made using fetch API
+ if (response[type]) {
+ resolve(response[type]());
+ } else { // Fallback on standard error handling
+ reject(response.status);
+ }
+ } else if (type === 'dom') {
+ response.text().then(html => {
+ resolve(document.createRange().createContextualFragment(html));
+ }).catch(reject);
+ } else { // Resolution type doesn't exists, resolving empty
+ resolve();
+ }
+ } else {
+ reject('F_KOM_MISSING_ARGUMENT');
+ }
+ });
+ }
+
+
+ /**
+ * @method
+ * @async
+ * @name _resolveAsJSON
+ * @private
+ * @memberof Kom
+ * @author Arthur Beaulieu
+ * @since September 2022
+ * @description
+ *
+ * Tool method used by public methods on fetch responses to format output data as JSON to be
+ * read in JavaScript code as objects.
+ *
+ * @param {Object} response - The fetch response object
+ * @returns {Promise} The request Promise, format response as an object on resolve, as error code string on reject
+ **/
+ _resolveAsJSON(response) {
+ return this._resolveAs('json', response);
+ }
+
+
+ /**
+ * @method
+ * @async
+ * @name _resolveAsText
+ * @private
+ * @memberof Kom
+ * @author Arthur Beaulieu
+ * @since September 2022
+ * @description
+ *
+ * Tool method used by public methods on fetch responses to format output data as text to be
+ * read in JavaScript code as string (mostly to parse HTML templates).
+ *
+ * @param {Object} response - The fetch response object
+ * @returns {Promise} The request Promise, format response as a string on resolve, as error code string on reject
+ **/
+ _resolveAsText(response) {
+ return this._resolveAs('text', response);
+ }
+
+
+ /**
+ * @method
+ * @async
+ * @name _resolveAsDom
+ * @private
+ * @memberof Kom
+ * @author Arthur Beaulieu
+ * @since September 2022
+ * @description
+ *
+ * Tool method used by public methods on fetch responses to format output data as DOM fragment to be
+ * read in JavaScript code as HTML template.
+ *
+ * @param {Object} response - The fetch response object
+ * @returns {Promise} The request Promise, format response as a string on resolve, as error code string on reject
+ **/
+ _resolveAsDom(response) {
+ return this._resolveAs('dom', response);
+ }
+
+
+
+ // ======================================================================== //
+ // --------------------------- GET server calls --------------------------- //
+ // ======================================================================== //
+
+
+ /**
+ * @method
+ * @async
+ * @name get
+ * @public
+ * @memberof Kom
+ * @author Arthur Beaulieu
+ * @since September 2022
+ * @description
+ *
+ * GET HTTP request using the fetch API. resolve returns the
+ * response as an Object. reject returns an error key as a String.
+ * It is meant to perform API call to access database through the user interface.
+ *
+ * @param {String} url - The GET url to fetch data from, in supported back URLs
+ * @returns {Promise} The request Promise
+ **/
+ get(url, resolution = this._resolveAsJSON.bind(this)) {
+ return new Promise((resolve, reject) => {
+ const options = {
+ method: 'GET',
+ headers: new Headers([this._headers[0]]) // Content type to JSON
+ };
+
+ fetch(url, options)
+ .then(data => {
+ // In case the request wen well but didn't gave the expected 200 status
+ if (data.status !== 200) {
+ reject(data);
+ }
+ return resolution(data);
+ })
+ .then(resolve)
+ .catch(reject);
+ });
+ }
+
+
+ /**
+ * @method
+ * @async
+ * @name getText
+ * @public
+ * @memberof Kom
+ * @author Arthur Beaulieu
+ * @since September 2022
+ * @description
+ *
+ * GET HTTP request using the fetch API. resolve returns the
+ * response as a String. reject returns an error key as a String. It is
+ * meant to perform API call to get HTML templates as string to be parsed as documents/documents fragments.
+ *
+ * @param {String} url - The GET url to fetch data from, in supported back URLs
+ * @returns {Promise} The request Promise
+ **/
+ getText(url) {
+ return this.get(url, this._resolveAsText.bind(this));
+ }
+
+
+ /**
+ * @method
+ * @async
+ * @name getText
+ * @public
+ * @memberof Kom
+ * @author Arthur Beaulieu
+ * @since September 2022
+ * @description
+ *
+ * GET HTTP request using the fetch API. resolve returns the
+ * response as a String. reject returns an error key as a String. It is
+ * meant to perform API call to get HTML templates as string to be parsed as documents/documents fragments.
+ *
+ * @param {String} url - The GET url to fetch data from, in supported back URLs
+ * @returns {Promise} The request Promise
+ **/
+ getTemplate(url) {
+ return this.get(url, this._resolveAsDom.bind(this));
+ }
+
+
+ // ======================================================================== //
+ // -------------------------- POST server calls --------------------------- //
+ // ======================================================================== //
+
+
+ /**
+ * @method
+ * @async
+ * @name post
+ * @public
+ * @memberof Kom
+ * @author Arthur Beaulieu
+ * @since September 2022
+ * @description
+ *
+ * POST HTTP request using the fetch API. Beware that the given options
+ * object match the url expectations. resolve
+ * returns the response as an Object. reject returns an error key as a String.
+ *
+ * @param {String} url - The POST url to fetch data from
+ * @param {Object} data - The JSON object that contains POST parameters
+ * @returns {Promise} The request Promise
+ **/
+ post(url, data, resolution = this._resolveAsJSON.bind(this)) {
+ return new Promise((resolve, reject) => {
+ const options = {
+ method: 'POST',
+ headers: new Headers(this._headers), // POST needs all previously defined headers
+ body: JSON.stringify(data)
+ };
+
+ fetch(url, options)
+ .then(data => {
+ // In case the request wen well but didn't gave the expected 200 status
+ if (data.status >= 400) {
+ reject(data);
+ }
+
+ if (resolution !== undefined && resolution !== null) {
+ return resolution(data);
+ }
+
+ return data;
+ })
+ .then(resolve)
+ .catch(reject);
+ });
+ }
+
+
+ /**
+ * @method
+ * @async
+ * @name postText
+ * @public
+ * @memberof Kom
+ * @author Arthur Beaulieu
+ * @since September 2022
+ * @description
+ *
+ * POST HTTP request using the fetch API. Beware that the given options
+ * object match the url expectations. resolve
+ * returns the response as a String. reject returns an error key as a String.
+ *
+ * The updateDebugUI() method will update informations held in the debug DOM
+ *
+ **/
+ static updateDebugUI() {
+ if (window.DEBUG === true) {
+ const options = (Utils.getPreference('map-high-accuracy') === 'true') ? AccuracyEnum.high : AccuracyEnum.optimized;
+ const element = window.BeerCrackerz.debugElement;
+ const user = window.BeerCrackerz.user;
+ const bc = window.BeerCrackerz;
+ const lang = bc.nls.debug.bind(bc.nls);
+ const updateSplittedString = element.querySelector('.debug-updates-amount').innerHTML.split(' ');
+ const updates = parseInt(updateSplittedString[updateSplittedString.length - 1]) + 1;
+ const marks = bc.marks.spot.length + bc.marks.shop.length + bc.marks.bar.length;
+ element.querySelector('.debug-user-lat').innerHTML = `${lang('lat')} ${user.lat}`;
+ element.querySelector('.debug-user-lng').innerHTML = `${lang('lng')} ${user.lng}`;
+ element.querySelector('.debug-updates-amount').innerHTML = `${lang('updates')} ${updates}`;
+ element.querySelector('.debug-user-accuracy').innerHTML = `${lang('accuracy')} ${Utils.precisionRound(user.accuracy, 2)}m`;
+ element.querySelector('.debug-high-accuracy').innerHTML = `${lang('highAccuracy')} ${options.enableHighAccuracy === true ? lang('enabled') : lang('disabled')}`;
+ element.querySelector('.debug-pos-max-age').innerHTML = `${lang('posAge')} ${options.maximumAge / 1000}s`;
+ element.querySelector('.debug-pos-timeout').innerHTML = `${lang('posTimeout')} ${options.timeout / 1000}s`;
+ element.querySelector('.debug-zoom-level').innerHTML = `${lang('zoom')} ${bc.map.getZoom()}`;
+ element.querySelector('.debug-marks-amount').innerHTML = `${lang('marks')} ${marks}`;
+ }
+ }
+
+
+ static drawUserMarker() {
+ if (!window.BeerCrackerz.user.marker) { // Create user marker if not existing
+ window.BeerCrackerz.user.type = 'user';
+ window.BeerCrackerz.user.marker = VisuHelper.addMark(window.BeerCrackerz.user);
+ // Append circle around marker for accuracy and range for new marker
+ window.BeerCrackerz.user.radius = window.BeerCrackerz.user.accuracy;
+ window.BeerCrackerz.user.circle = VisuHelper.drawCircle(window.BeerCrackerz.user);
+
+ window.BeerCrackerz.user.circle.addTo(window.BeerCrackerz.map);
+ window.BeerCrackerz.user.circle.setStyle({
+ opacity: 1,
+ fillOpacity: 0.1
+ });
+ // Callback on marker clicked to add marker on user position
+ window.BeerCrackerz.user.marker.on('click', window.BeerCrackerz.mapClicked.bind(window.BeerCrackerz));
+ } else { // Update user marker position, range, and accuracy circle
+ window.BeerCrackerz.user.marker.setLatLng(window.BeerCrackerz.user);
+ window.BeerCrackerz.user.circle.setLatLng(window.BeerCrackerz.user);
+ window.BeerCrackerz.user.circle.setRadius(window.BeerCrackerz.user.accuracy);
+ }
+ }
+
+
+ static drawCircle(options) {
+ return window.L.circle(options, {
+ color: options.color,
+ fillColor: options.color,
+ opacity: 0, // This needs to be updated according to user proximity
+ fillOpacity: 0, // Same for this parameter
+ radius: options.radius ? options.radius : MapEnum.socialMarkRange,
+ });
+ }
+
+
+ static removeMarkDecoration(mark) {
+ if (mark.popup) {
+ mark.popup.destroy();
+ }
+ }
+
+
+ static addMark(mark) {
+ let icon = MarkersEnum.black;
+ if (mark.type === 'shop') {
+ icon = MarkersEnum.blue;
+ } else if (mark.type === 'spot') {
+ icon = MarkersEnum.green;
+ } else if (mark.type === 'bar') {
+ icon = MarkersEnum.red;
+ } else if (mark.type === 'user') {
+ icon = MarkersEnum.user;
+ }
+
+ const marker = window.L.marker([mark.lat, mark.lng], { icon: icon }).on('click', VisuHelper.centerOn.bind(VisuHelper, mark));
+ if (mark.dom) {
+ marker.bindPopup(mark.dom);
+ }
+ // All markers that are not spot/shop/bar should be appended to the map
+ if (['spot', 'shop', 'bar'].indexOf(mark.type) === -1) {
+ marker.addTo(window.BeerCrackerz.map);
+ }
+
+ return marker;
+ }
+
+
+ /**
+ * @method
+ * @name toggleMarkers
+ * @public
+ * @memberof BeerCrackerz
+ * @author Arthur Beaulieu
+ * @since January 2022
+ * @description
+ *
+ * The toggleMarkers() method will, depending on user preference, display or not
+ * a given mark type. This way, the user can fine tune what is displayed on the map.
+ * A mark type in spots/shops/bars must be given as an argument
+ *
+ * @param {String} type - The mark type in spots/tores/bars
+ **/
+ static toggleMarkers(event) {
+ const type = event.target.dataset.type;
+ const visible = !(Utils.getPreference(`poi-show-${type}`) === 'true');
+ if (visible === true) {
+ for (let i = 0; i < window.BeerCrackerz.marks[type].length; ++i) {
+ window.BeerCrackerz.marks[type][i].visible = true;
+ }
+ window.BeerCrackerz.map.addLayer(window.BeerCrackerz.clusters[type]);
+ } else {
+ for (let i = 0; i < window.BeerCrackerz.marks[type].length; ++i) {
+ window.BeerCrackerz.marks[type][i].visible = false;
+ }
+ window.BeerCrackerz.map.removeLayer(window.BeerCrackerz.clusters[type]);
+ }
+ Utils.setPreference(`poi-show-${type}`, visible);
+ }
+
+
+ /**
+ * @method
+ * @name toggleDebug
+ * @public
+ * @memberof BeerCrackerz
+ * @author Arthur Beaulieu
+ * @since January 2022
+ * @description
+ *
+ * The toggleDebug() method will, depending on user preference, add or remove
+ * the debug DOM element to the user interface. The debug DOM display several
+ * useful information to identify an issue with the geolocation API
+ *
+ * The toggleFocusLock() method will, depending on user preference, lock or unlock
+ * the map centering around the user marker at each position refresh. This way the user
+ * can roam while the map is following its position.
+ *
This method must be overridden in child class. It only destroys the Modal.js
+ * properties and close event subscription. The developer must remove its abstracted properties and events after
+ * calling this method, to make the destruction process complete.
This method creates the modal overlay, fetch the HTML template using the Kom.js
+ * component, it then build the modal DOM, append it to the overlay, open the modal and call
+ * _fillAttributes() that must be overridden in the child class. It is asynchronous because of the fetch call,
+ * so the child class constructor can be fully executed.
**/
+ _loadTemplate() {
+ window.BeerCrackerz.kom.getTemplate(this._url).then(response => {
+ // Create DOM from fragment and tweak url to only keep modal type as css class
+ this._rootElement = response.firstElementChild;
+ this._rootElement.classList.add(`${this._type}-modal`);
+ // Create overlay modal container
+ this._modalOverlay = document.createElement('DIV');
+ this._modalOverlay.id = 'overlay';
+ this._modalOverlay.classList.add('overlay');
+ document.body.appendChild(this._modalOverlay);
+ // Get close button from template
+ this._closeButton = this._rootElement.querySelector('#modal-close');
+ this.open();
+ this._fillAttributes(); // Override in child class to process modal UI
+ }).catch(error => {
+ console.error(error);
+ });
+ }
+
+
+ /** @method
+ * @name _fillAttributes
+ * @private
+ * @memberof Modal
+ * @author Arthur Beaulieu
+ * @since November 2020
+ * @description
This method doesn't implement anything. It must be overridden in child class, to use the
+ * template DOM elements to build its interactions. It is called once the template is successfully fetched from the
+ * server.
+ * @author Arthur Beaulieu
+ * @since June 2020
+ * @description
The CustomEvents class provides an abstraction of JavaScript event listener, to allow
+ * easy binding and removing those events. It also provides an interface to register custom events. This class is
+ * meant to be used on all scopes you need ; module or global. Refer to each public method for detailed features.
+ * For source code, please go to
+ * https://github.com/ArthurBeaulieu/CustomEvents.js
+ * @param {boolean} [debug=false] - Debug flag ; when true, logs will be output in JavaScript console at each event */
+ constructor(debug = false) {
+ // Prevent wrong type for debug
+ if (typeof debug !== 'boolean') {
+ debug = false;
+ }
+ /** @private
+ * @member {boolean} - Internal logging flag from constructor options, allow to output each event action */
+ this._debug = debug;
+ /** @private
+ * @member {number} - Start the ID incrementer at pseudo random value, used for both regular and custom events */
+ this._idIncrementor = (Math.floor(Math.random() * Math.floor(256)) * 5678);
+ /** @private
+ * @member {any[]} - We store classical event listeners in array of objects containing all their information */
+ this._regularEvents = [];
+ /** @private
+ * @member {object} - We store custom events by name as key, each key stores an Array of subscribed events */
+ this._customEvents = {};
+ /** @public
+ * @member {string} - Component version */
+ this.version = '1.2.1';
+ }
+
+
+ /** @method
+ * @name destroy
+ * @public
+ * @memberof CustomEvents
+ * @description
CustomEvents destructor. Will remove all event listeners and keys in instance.
*/
+ destroy() {
+ // Debug logging
+ this._raise('log', 'CustomEvents.destroy');
+ // Remove all existing eventListener
+ this.removeAllEvents();
+ // Delete object attributes
+ Utils.removeAllObjectKeys(this);
+ }
+
+
+ /* --------------------------------------------------------------------------------------------------------------- */
+ /* -------------------------------------- CLASSIC JS EVENTS OVERRIDE ------------------------------------------ */
+ /* */
+ /* The following methods are made to abstract the event listeners from the JavaScript layer, so you can easily */
+ /* remove them when done using, without bothering with binding usual business for them. 'addEvent/removeEvent' */
+ /* method replace the initial ones. 'removeAllEvents' clears all instance event listeners ; nice for destroy */
+ /* --------------------------------------------------------------------------------------------------------------- */
+
+
+ /** @method
+ * @name addEvent
+ * @public
+ * @memberof CustomEvents
+ * @description
addEvent method abstracts the addEventListener method to easily
+ * remove it when needed, also to set a custom scope on callback.
+ * @param {string} eventName - The event name to fire (mousemove, click, context etc.)
+ * @param {object} element - The DOM element to attach the listener to
+ * @param {function} callback - The callback function to execute when event is realised
+ * @param {object} [scope=element] - The event scope to apply to the callback (optional, default to DOM element)
+ * @param {object|boolean} [options=false] - The event options (useCapture and else)
+ * @returns {number|boolean} - The event ID to use to manually remove an event, false if arguments are invalid */
+ addEvent(eventName, element, callback, scope = element, options = false) {
+ // Debug logging
+ this._raise('log', `CustomEvents.addEvent: ${eventName} ${element} ${callback} ${scope} ${options}`);
+ // Missing mandatory arguments
+ if (eventName === null || eventName === undefined ||
+ element === null || element === undefined ||
+ callback === null || callback === undefined) {
+ this._raise('error', 'CustomEvents.addEvent: Missing mandatory arguments');
+ return false;
+ }
+ // Prevent wrong type for arguments (mandatory and optional)
+ const err = () => {
+ this._raise('error', 'CustomEvents.addEvent: Wrong type for argument');
+ };
+ // Test argument validity for further process
+ if (typeof eventName !== 'string' || typeof element !== 'object' || typeof callback !== 'function') {
+ err();
+ return false;
+ }
+ if ((scope !== null && scope !== undefined) && typeof scope !== 'object' && typeof scope !== 'function') {
+ err();
+ return false;
+ }
+ if ((options !== null && options !== undefined) && (typeof options !== 'object' && typeof options !== 'boolean' && typeof options !== 'string' && typeof options !== 'number')) {
+ err();
+ return false;
+ }
+ // Save scope to callback function, default scope is DOM target object
+ callback = callback.bind(scope);
+ // Add event to internal array and keep all its data
+ this._regularEvents.push({
+ id: this._idIncrementor,
+ element: element,
+ eventName: eventName,
+ scope: scope,
+ callback: callback,
+ options: options
+ });
+ // Add event listener with options
+ element.addEventListener(eventName, callback, options);
+ // Post increment to return the true event entry id, then update the incrementer
+ return this._idIncrementor++;
+ }
+
+
+ /** @method
+ * @name removeEvent
+ * @public
+ * @memberof CustomEvents
+ * @description
removeEvent method abstracts the removeEventListener method to
+ * really remove event listeners.
+ * @param {number} eventId - The event ID to remove listener from. Returned when addEvent is called
+ * @returns {boolean} - The method status ; true for success, false for non-existing event */
+ removeEvent(eventId) {
+ // Debug logging
+ this._raise('log', `Events.removeEvent: ${eventId}`);
+ // Missing mandatory arguments
+ if (eventId === null || eventId === undefined) {
+ this._raise('error', 'CustomEvents.removeEvent: Missing mandatory arguments');
+ return false;
+ }
+ // Prevent wrong type for arguments (mandatory)
+ if (typeof eventId !== 'number') {
+ this._raise('error', 'CustomEvents.removeEvent: Wrong type for argument');
+ return false;
+ }
+ // Returned value
+ let statusCode = false; // Not found status code by default (false)
+ // Iterate over saved listeners, reverse order for proper splicing
+ for (let i = (this._regularEvents.length - 1); i >= 0 ; --i) {
+ // If an event ID match in saved ones, we remove it and update saved listeners
+ if (this._regularEvents[i].id === eventId) {
+ // Update status code
+ statusCode = true; // Found and removed event listener status code (true)
+ this._clearRegularEvent(i);
+ }
+ }
+ // Return with status code
+ return statusCode;
+ }
+
+
+ /** @method
+ * @name removeAllEvents
+ * @public
+ * @memberof CustomEvents
+ * @description
Clear all event listener registered through this class object.
+ * @returns {boolean} - The method status ; true for success, false for not removed any event */
+ removeAllEvents() {
+ // Debug logging
+ this._raise('log', 'CustomEvents.removeAllEvents');
+ // Returned value
+ let statusCode = false; // Didn't removed any status code by default (false)
+ // Flag to know if there was any previously stored event listeners
+ const hadEvents = (this._regularEvents.length > 0);
+ // Iterate over saved listeners, reverse order for proper splicing
+ for (let i = (this._regularEvents.length - 1); i >= 0; --i) {
+ this._clearRegularEvent(i);
+ }
+ // If all events where removed, update statusCode to success
+ if (this._regularEvents.length === 0 && hadEvents) {
+ // Update status code
+ statusCode = true; // Found and removed all events listener status code (true)
+ }
+ // Return with status code
+ return statusCode;
+ }
+
+
+ /** @method
+ * @name _clearRegularEvent
+ * @private
+ * @memberof CustomEvents
+ * @description
_clearRegularEvent method remove the saved event listener for a
+ * given index in regularEvents array range.
+ * @param {number} index - The regular event index to remove from class attributes
+ * @return {boolean} - The method status ; true for success, false for not cleared any event */
+ _clearRegularEvent(index) {
+ // Debug logging
+ this._raise('log', `CustomEvents._clearRegularEvent: ${index}`);
+ // Missing mandatory arguments
+ if (index === null || index === undefined) {
+ this._raise('error', 'CustomEvents._clearRegularEvent: Missing mandatory argument');
+ return false;
+ }
+ // Prevent wrong type for arguments (mandatory)
+ if (typeof index !== 'number') {
+ this._raise('error', 'CustomEvents._clearRegularEvent: Wrong type for argument');
+ return false;
+ }
+ // Check if index match an existing event in attributes
+ if (this._regularEvents[index]) {
+ // Remove its event listener and update regularEvents array
+ const evt = this._regularEvents[index];
+ evt.element.removeEventListener(evt.eventName, evt.callback, evt.options);
+ this._regularEvents.splice(index, 1);
+ return true;
+ }
+
+ return false;
+ }
+
+
+ /* --------------------------------------------------------------------------------------------------------------- */
+ /* ------------------------------------------- CUSTOM JS EVENTS ----------------------------------------------- */
+ /* */
+ /* The three following methods (subscribe, unsubscribe, publish) are designed to reference an event by its name */
+ /* and handle as many subscriptions as you want. When subscribing, you get an ID you can use to unsubscribe your */
+ /* event later. Just publish with the event name to callback all its registered subscriptions. */
+ /* --------------------------------------------------------------------------------------------------------------- */
+
+
+ /** @method
+ * @name subscribe
+ * @public
+ * @memberof CustomEvents
+ * @description
Subscribe method allow you to listen to an event and react when it occurs.
+ * @param {string} eventName - Event name (the one to use to publish)
+ * @param {function} callback - The callback to execute when event is published
+ * @param {boolean} [oneShot=false] - One shot : to remove subscription the first time callback is fired
+ * @returns {number|boolean} - The event id, to be used when manually unsubscribing */
+ subscribe(eventName, callback, oneShot = false) {
+ // Debug logging
+ this._raise('log', `CustomEvents.subscribe: ${eventName} ${callback} ${oneShot}`);
+ // Missing mandatory arguments
+ if (eventName === null || eventName === undefined ||
+ callback === null || callback === undefined) {
+ this._raise('error', 'CustomEvents.subscribe', 'Missing mandatory arguments');
+ return false;
+ }
+ // Prevent wrong type for arguments (mandatory and optional)
+ const err = () => {
+ this._raise('error', 'CustomEvents.subscribe: Wrong type for argument');
+ };
+ if (typeof eventName !== 'string' || typeof callback !== 'function') {
+ err();
+ return false;
+ }
+ if ((oneShot !== null && oneShot !== undefined) && typeof oneShot !== 'boolean') {
+ err();
+ return false;
+ }
+ // Create event entry if not already existing in the registered events
+ if (!this._customEvents[eventName]) {
+ this._customEvents[eventName] = []; // Set empty array for new event subscriptions
+ }
+ // Push new subscription for event name
+ this._customEvents[eventName].push({
+ id: this._idIncrementor,
+ name: eventName,
+ os: oneShot,
+ callback: callback
+ });
+ // Post increment to return the true event entry id, then update the incrementer
+ return this._idIncrementor++;
+ }
+
+
+ /** @method
+ * @name unsubscribe
+ * @public
+ * @memberof CustomEvents
+ * @description
Unsubscribe method allow you to revoke an event subscription from its string name.
+ * @param {number} eventId - The subscription id returned when subscribing to an event name
+ * @returns {boolean} - The method status ; true for success, false for non-existing subscription **/
+ unsubscribe(eventId) {
+ // Debug logging
+ this._raise('log', `CustomEvents.unsubscribe: ${eventId}`);
+ // Missing mandatory arguments
+ if (eventId === null || eventId === undefined) {
+ this._raise('error', 'CustomEvents.unsubscribe: Missing mandatory arguments');
+ return false;
+ }
+ // Prevent wrong type for arguments (mandatory)
+ if (typeof eventId !== 'number') {
+ this._raise('error', 'CustomEvents.unsubscribe: Wrong type for argument');
+ return false;
+ }
+ // Returned value
+ let statusCode = false; // Not found status code by default (false)
+ // Save event keys to iterate properly on this._events Object
+ const keys = Object.keys(this._customEvents);
+ // Reverse events iteration to properly splice without messing with iteration order
+ for (let i = (keys.length - 1); i >= 0; --i) {
+ // Get event subscriptions
+ const subs = this._customEvents[keys[i]];
+ // Iterate over events subscriptions to find the one with given id
+ for (let j = 0; j < subs.length; ++j) {
+ // In case we got a subscription for this events
+ if (subs[j].id === eventId) {
+ // Debug logging
+ this._raise('log', `CustomEvents.unsubscribe: subscription found\n`, subs[j], `\nSubscription n°${eventId} for ${subs.name} has been removed`);
+ // Update status code
+ statusCode = true; // Found and unsubscribed status code (true)
+ // Remove subscription from event Array
+ subs.splice(j, 1);
+ // Remove event name if no remaining subscriptions
+ if (subs.length === 0) {
+ delete this._customEvents[keys[i]];
+ }
+ // Break since id are unique and no other subscription can be found after
+ break;
+ }
+ }
+ }
+ // Return with status code
+ return statusCode;
+ }
+
+
+ /** @method
+ * @name unsubscribeAllFor
+ * @public
+ * @memberof CustomEvents
+ * @description
unsubscribeAllFor method clear all subscriptions registered for given event name.
+ * @param {string} eventName - The event to clear subscription from
+ * @returns {boolean} - The method status ; true for success, false for non-existing event **/
+ unsubscribeAllFor(eventName) {
+ // Debug logging
+ this._raise('log', `CustomEvents.unsubscribeAllFor: ${eventName}`);
+ // Missing mandatory arguments
+ if (eventName === null || eventName === undefined) {
+ this._raise('error', 'CustomEvents.unsubscribeAllFor: Missing mandatory arguments');
+ return false;
+ }
+ // Prevent wrong type for arguments (mandatory and optional)
+ if (typeof eventName !== 'string') {
+ this._raise('error', 'CustomEvents.unsubscribeAllFor: Wrong type for argument');
+ return false;
+ }
+ // Returned value
+ let statusCode = false; // Not found status code by default (false)
+ // Save event keys to iterate properly on this._events Object
+ const keys = Object.keys(this._customEvents);
+ // Iterate through custom event keys to find matching event to remove
+ for (let i = 0; i < keys.length; ++i) {
+ if (keys[i] === eventName) {
+ // Get event subscriptions
+ const subs = this._customEvents[keys[i]];
+ // Iterate over events subscriptions to find the one with given id, reverse iteration to properly splice without messing with iteration order
+ for (let j = (subs.length - 1); j >= 0; --j) {
+ // Update status code
+ statusCode = true; // Found and unsubscribed all status code (true)
+ // Remove subscription from event Array
+ subs.splice(j, 1);
+ // Remove event name if no remaining subscriptions
+ if (subs.length === 0) {
+ delete this._customEvents[keys[i]];
+ }
+ }
+ }
+ }
+ // Return with status code
+ return statusCode;
+ }
+
+
+ /** @method
+ * @name publish
+ * @public
+ * @memberof CustomEvents
+ * @description
Publish method allow you to fire an event by name and trigger all its subscription by callbacks./blockquote>
+ * @param {string} eventName - Event name (the one to use to publish)
+ * @param {object} [data=undefined] - The data object to sent through the custom event
+ * @returns {boolean} - The method status ; true for success, false for non-existing event **/
+ publish(eventName, data = null) {
+ // Debug logging
+ this._raise('log', `CustomEvents.publish: ${eventName} ${data}`);
+ // Missing mandatory arguments
+ if (eventName === null || eventName === undefined) {
+ this._raise('error', 'CustomEvents.publish: Missing mandatory arguments');
+ return false;
+ }
+ // Prevent wrong type for arguments (mandatory and optional)
+ if (typeof eventName !== 'string' || (data !== undefined && typeof data !== 'object')) {
+ this._raise('error', 'CustomEvents.publish: Wrong type for argument');
+ return false;
+ }
+ // Returned value
+ let statusCode = false; // Not found status code by default (false)
+ // Save event keys to iterate properly on this._events Object
+ const keys = Object.keys(this._customEvents);
+ // Iterate over saved custom events
+ for (let i = 0; i < keys.length; ++i) {
+ // If published name match an existing events, we iterate its subscriptions. First subscribed, first served
+ if (keys[i] === eventName) {
+ // Update status code
+ statusCode = true; // Found and published status code (true)
+ // Get event subscriptions
+ const subs = this._customEvents[keys[i]];
+ // Iterate over events subscriptions to find the one with given id
+ // Reverse subscriptions iteration to properly splice without messing with iteration order
+ for (let j = (subs.length - 1); j >= 0; --j) {
+ // Debug logging
+ this._raise('log', `CustomEvents.publish: fire callback for ${eventName}, subscription n°${subs[j].id}`, subs[j]);
+ // Fire saved callback
+ subs[j].callback(data);
+ // Remove oneShot listener from event entry
+ if (subs[j].os) {
+ // Debug logging
+ this._raise('log', 'CustomEvents.publish: remove subscription because one shot usage is done');
+ subs.splice(j, 1);
+ // Remove event name if no remaining subscriptions
+ if (subs.length === 0) {
+ delete this._customEvents[keys[i]];
+ }
+ }
+ }
+ }
+ }
+ // Return with status code
+ return statusCode;
+ }
+
+
+ /* --------------------------------------------------------------------------------------------------------------- */
+ /* -------------------------------------------- COMPONENT UTILS ----------------------------------------------- */
+ /* --------------------------------------------------------------------------------------------------------------- */
+
+
+ /** @method
+ * @name _raise
+ * @private
+ * @memberof CustomEvents
+ * @description
Internal method to abstract console wrapped in debug flag.
+ * @param {string} level - The console method to call
+ * @param {string} errorValue - The error value to display in console method **/
+ _raise(level, errorValue) {
+ if (this._debug) {
+ console[level](errorValue);
+ }
+ }
+
+
+}
+
+
+export default CustomEvents;
diff --git a/front/src/js/utils/DropElement.js b/front/src/js/utils/DropElement.js
new file mode 100644
index 0000000..a6af52e
--- /dev/null
+++ b/front/src/js/utils/DropElement.js
@@ -0,0 +1,202 @@
+import Utils from './Utils.js';
+
+
+class DropElement {
+
+
+ /** @summary
Make any DOM element drop friendly
+ * @author Arthur Beaulieu
+ * @since December 2020
+ * @description
This class will make any DOM element able to receive drop event. It propose an overlay
+ * when the target is hovered with a draggable element. It handle both the desktop and the mobile behavior. It must be
+ * used with a DragElement class for perfect compatibility!
+ * @param {object} options - The element to drop options
+ * @param {object} options.target - The element to allow dropping in **/
+ constructor(options) {
+ /** @private
+ * @member {object} - The element to make allow dropping in */
+ this._target = options.target; // Get given target from the DOM
+ /** @private
+ * @member {function} - The callback function to call on each drop event */
+ this._onDropCB = options.onDrop;
+ /** @private
+ * @member {number[]} - The event IDs for all mobile and desktop dropping events */
+ this._evtIds = [];
+ /** @private
+ * @member {number} - This counter helps to avoid enter/leave events to overlap when target has children */
+ this._movementCounter = 0;
+ /** @private
+ * @member {string} - The transparent border that must be added to avoid weird target resize on hover */
+ this._transparentBorder = '';
+ // Build DOM elements and subscribe to drag events
+ this._buildElements();
+ this._events();
+ }
+
+
+ /** @method
+ * @name destroy
+ * @public
+ * @memberof DropElement
+ * @author Arthur Beaulieu
+ * @since December 2020
+ * @description
This method will unsubscribe all drop events and remove all properties.
This method will handle the entering of a dragged div over the target DOM element. When
+ * the target DOM element is hovered, a dashed border is made visible, replacing the transparent one to notify the
+ * user that the dragged div can be dropped.
This method will handle the event that is fired when the hovered div leaves the target
+ * DOM element. It require the movement counter to be equal to zero to restore the transparent border of the target
+ * DOM element.
This method will handle the dropping of a DragElement, to properly read the data it holds
+ * and send it to the drop callback provided in constructor.
- * This component handles the whole BeerCrackerz app. It includes the map manipulation,
- * the geolocation API to update the user position and process any map events that are
- * relevant to an UX stand point. For more information, please consult the application
- * description page at https://about.beercrackerz.org/
- *
- **/
- constructor() {
- super();
- /**
- * The core Leaflet.js map
- * @type {Object}
- * @private
- **/
- this._map = null;
- /**
- * The zoom slider handler
- * @type {Object}
- * @private
- **/
- this._zoomSlider = null;
- /**
- * The notification handler
- * @type {Object}
- * @private
- **/
- this._notification = null;
- /**
- * The user object holds everything useful to ensure a proper session
- * @type {Object}
- * @private
- **/
- this._user = {
- lat: 48.853121540141096, // Default lat to Paris Notre-Dame latitude
- lng: 2.3498955769881156, // Default lng to Paris Notre-Dame longitude
- accuracy: 0, // Accuracy in meter given by geolocation API
- marker: null, // The user marker on map
- circle: null, // The accuracy circle around the user marker
- range: null, // The range in which user can add a new marker
- color: Utils.USER_COLOR, // The color to use for circle (match the user marker color)
- id: -1,
- username: ''
- };
- /**
- * The stored marks for spots, stores and bars
- * @type {Object}
- * @private
- **/
- this._marks = {
- spot: [],
- store: [],
- bar: [],
- };
- /**
- * The stored clusters for markers, see Leaflet.markercluster plugin
- * @type {Object}
- * @private
- **/
- this._clusters = {
- spot: {},
- store: {},
- bar: {},
- };
- /**
- * The temporary marker for new marks only
- * @type {Object}
- * @private
- **/
- this._newMarker = null;
- /**
- * The debug DOM object
- * @type {Object}
- * @private
- **/
- this._debugElement = null;
- /**
- * ID for geolocation watch callback
- * @type {Number}
- * @private
- **/
- this._watchId = null;
- /**
- * Flag to know if a zoom action is occuring on map
- * @type {Boolean}
- * @private
- **/
- this._isZooming = false;
- /**
- * The LangManager must be instantiated to handle nls accross the app
- * @type {Boolean}
- * @private
- **/
- // The BeerCrackerz app is only initialized once nls are set up
- this._lang = new LangManager(
- window.navigator.language.substring(0, 2),
- this._init.bind(this)
- );
- }
-
-
- // ======================================================================== //
- // ----------------- Application initialization sequence ------------------ //
- // ======================================================================== //
-
-
- /**
- * @method
- * @name _init
- * @private
- * @memberof BeerCrackerz
- * @author Arthur Beaulieu
- * @since January 2022
- * @description
- *
- * The _init() method is designed to properly configure the user session, according
- * to its saved preferences and its position. It first build the debug interface,
- * then loads the user preferences, then create the map and finally, events are listened.
- *
- * The _init() method initialize the user object according to its information
- * and statistic so the UI can be properly built.
- *
- * @returns {Promise} A Promise resolved when preferences are set
- **/
- _initUser() {
- return new Promise(resolve => {
- // TODO fill user information from server
- this._user.id = 42;
- this._user.username = 'messmaker';
- resolve();
- });
- }
-
-
- /**
- * @method
- * @name _initPreferences
- * @private
- * @memberof BeerCrackerz
- * @author Arthur Beaulieu
- * @since January 2022
- * @description
- *
- * The _initPreferences() will initialize user preference if they are not set yet,
- * it will also update the UI according to user preferences ; debug DOM visible,
- * update the command classList for selected ones.
- *
- * @returns {Promise} A Promise resolved when preferences are set
- **/
- _initPreferences() {
- return new Promise(resolve => {
- if (Utils.getPreference('poi-show-spot') === null) {
- Utils.setPreference('poi-show-spot', true);
- }
-
- if (Utils.getPreference('poi-show-store') === null) {
- Utils.setPreference('poi-show-store', true);
- }
-
- if (Utils.getPreference('poi-show-bar') === null) {
- Utils.setPreference('poi-show-bar', true);
- }
-
- if (Utils.getPreference('map-plan-layer') === null) {
- Utils.setPreference('map-plan-layer', true);
- }
-
- if (window.DEBUG === true || (Utils.getPreference('app-debug') === 'true')) {
- window.DEBUG = true; // Ensure to set global flag if preference comes from local storage
- Utils.setPreference('app-debug', true); // Ensure to set local storage preference if debug flag was added to the url
- this.addDebugUI();
- }
- // Update icon class if center on preference is set to true
- if (Utils.getPreference('map-center-on-user') === 'true') {
- document.getElementById('center-on').classList.add('lock-center-on');
- }
-
- resolve();
- });
- }
-
-
- /**
- * @method
- * @name _initGeolocation
- * @private
- * @memberof BeerCrackerz
- * @author Arthur Beaulieu
- * @since January 2022
- * @description
- *
- * The _initGeolocation() method will request from browser the location authorization.
- * Once granted, an event listener is set on any position update, so it can update the
- * map state and the markers position. This method can be called again, only if the
- * geolocation watch has been cleared ; for example when updating the accuracy options.
- *
- * @returns {Promise} A Promise resolved when preferences are set
- **/
- _initGeolocation() {
- return new Promise(resolve => {
- if ('geolocation' in navigator) {
- const options = (Utils.getPreference('map-high-accuracy') === 'true') ? Utils.HIGH_ACCURACY : Utils.OPTIMIZED_ACCURACY;
- this._watchId = navigator.geolocation.watchPosition(position => {
- // Update saved user position
- this._user.lat = position.coords.latitude;
- this._user.lng = position.coords.longitude;
- this._user.accuracy = position.coords.accuracy;
- // Only draw marker if map is already created
- if (this._map) {
- this.drawUserMarker();
- this.updateMarkerCirclesVisibility();
- // Update map position if focus lock is active
- if (Utils.getPreference('map-center-on-user') === 'true' && !this._isZooming) {
- this._map.setView(this._user);
- }
- // Updating debug info
- this.updateDebugUI();
- }
- resolve();
- }, resolve, options);
- } else {
- this._notification.raise(this.nls.notif('geolocationError'));
- resolve();
- }
- });
- }
-
-
- /**
- * @method
- * @name _initMap
- * @private
- * @memberof BeerCrackerz
- * @author Arthur Beaulieu
- * @since January 2022
- * @description
- *
- * The _initMap() method will create the Leaflet.js map with two base layers (plan/satellite),
- * add scale control, remove zoom control and set map bounds.
- *
- * @returns {Promise} A Promise resolved when preferences are set
- **/
- _initMap() {
- return new Promise(resolve => {
- // Use main div to inject OSM into
- this._map = window.L.map('beer-crakerz-map', {
- zoomControl: false,
- }).setView([this._user.lat, this._user.lng], 18);
- // Add meter and feet scale on map
- window.L.control.scale().addTo(this._map);
- // Place user marker on the map
- this.drawUserMarker();
- // Add OSM credits to the map next to leaflet credits
- const osm = Providers.planOsm;
- //const plan = Providers.planGeo;
- const esri = Providers.satEsri;
- //const geo = Providers.satGeo;
- // Prevent panning outside of the world's edge
- this._map.setMaxBounds(Utils.MAP_BOUNDS);
- // Add layer group to interface
- const baseMaps = {};
- baseMaps[`
${this.nls.map('planLayerOSM')}
`] = osm;
- //baseMaps[`
${this.nls.map('planLayerGeo')}
`] = plan;
- baseMaps[`
${this.nls.map('satLayerEsri')}
`] = esri;
- //baseMaps[`
${this.nls.map('satLayerGeo')}
`] = geo;
- // Append layer depending on user preference
- if (Utils.getPreference('map-plan-layer')) {
- switch (Utils.getPreference('map-plan-layer')) {
- case this.nls.map('planLayerOSM'):
- osm.addTo(this._map);
- break;
- /*case this.nls.map('planLayerGeo'):
- plan.addTo(this._map);
- break;*/
- case this.nls.map('satLayerEsri'):
- esri.addTo(this._map);
- break;
- /*case this.nls.map('satLayerGeo'):
- geo.addTo(this._map);
- break;*/
- default:
- osm.addTo(this._map);
- break;
- }
- } else { // No saved pref, fallback on OSM base map
- osm.addTo(this._map);
- }
- // Add layer switch radio on bottom right of the map
- window.L.control.layers(baseMaps, {}, { position: 'bottomright' }).addTo(this._map);
- // Init zoom slider when map has been created
- this._zoomSlider = new ZoomSlider(this._map);
- resolve();
- });
- }
-
-
- /**
- * @method
- * @name _initEvents
- * @private
- * @memberof BeerCrackerz
- * @author Arthur Beaulieu
- * @since January 2022
- * @description
- *
- * The _initEvents() method will listen to all required events to manipulate the map. Those events
- * are both for commands and for map events (click, drag, zoom and layer change).
- *
- * @returns {Promise} A Promise resolved when preferences are set
- **/
- _initEvents() {
- return new Promise(resolve => {
- // Command events
- document.getElementById('user-profile').addEventListener('click', this.userProfileModal.bind(this));
- document.getElementById('hide-show').addEventListener('click', this.hidShowModal.bind(this));
- document.getElementById('center-on').addEventListener('click', this.toggleFocusLock.bind(this));
- document.getElementById('overlay').addEventListener('click', this.closeModal.bind(this));
- // Subscribe to click event on map to react
- this._map.on('click', this.mapClicked.bind(this));
- // Map is dragged by user mouse/finger
- this._map.on('drag', () => {
- // Constrain pan to the map bounds
- this._map.panInsideBounds(Utils.MAP_BOUNDS, { animate: true });
- // Disable lock focus if user drags the map
- if (Utils.getPreference('map-center-on-user') === 'true') {
- this.toggleFocusLock();
- }
- });
- // Map events
- this._map.on('zoomstart', () => {
- this._isZooming = true;
- if (Utils.getPreference('poi-show-circle') === 'true') {
- this.setMarkerCircles(this._marks.spot, false);
- this.setMarkerCircles(this._marks.store, false);
- this.setMarkerCircles(this._marks.bar, false);
- this.setMarkerCircles([this._user], false);
- this.setMarkerCircles([{ circle: this._user.range }], false);
- }
- });
- this._map.on('zoomend', () => {
- this._isZooming = false;
- if (Utils.getPreference('poi-show-circle') === 'true') {
- if (this._map.getZoom() >= 15) {
- this.setMarkerCircles(this._marks.spot, true);
- this.setMarkerCircles(this._marks.store, true);
- this.setMarkerCircles(this._marks.bar, true);
- this.setMarkerCircles([this._user], true);
- this.setMarkerCircles([{ circle: this._user.range }], true);
- }
- }
- // Auto hide labels if zoom level is too high (and restore it when needed)
- if (Utils.getPreference('poi-marker-label') === 'true') {
- if (this._map.getZoom() < 15) {
- this.setMarkerLabels(this._marks.spot, false);
- this.setMarkerLabels(this._marks.store, false);
- this.setMarkerLabels(this._marks.bar, false);
- } else {
- this.setMarkerLabels(this._marks.spot, true);
- this.setMarkerLabels(this._marks.store, true);
- this.setMarkerLabels(this._marks.bar, true);
- }
- }
- // Updating debug info
- this.updateDebugUI();
- });
- this._map.on('baselayerchange', event => {
- Utils.setPreference('map-plan-layer', Utils.stripDom(event.name));
- });
- resolve();
- });
- }
-
-
- /**
- * @method
- * @name _initMarkers
- * @private
- * @memberof BeerCrackerz
- * @author Arthur Beaulieu
- * @since January 2022
- * @description
- *
- * The _initEvents() method will initialize all saved marker into the map.
- * Markers must be retrieved from server with a specific format to ensure it works
- *
- * @returns {Promise} A Promise resolved when preferences are set
- **/
- _initMarkers() {
- return new Promise(resolve => {
- // Init map clusters for marks to be displayed (disable clustering at opened popup zoom level)
- const clusterOptions = {
- animateAddingMarkers: true,
- disableClusteringAtZoom: 18,
- spiderfyOnMaxZoom: false
- };
- this._clusters.spot = new window.L.MarkerClusterGroup(Object.assign(clusterOptions, {
- iconCreateFunction: cluster => {
- return window.L.divIcon({
- className: 'cluster-icon-wrapper',
- html: `
-
- ${cluster.getChildCount()}
- `
- });
- }
- }));
- this._clusters.store = new window.L.MarkerClusterGroup(Object.assign(clusterOptions, {
- iconCreateFunction: cluster => {
- return window.L.divIcon({
- className: 'cluster-icon-wrapper',
- html: `
-
- ${cluster.getChildCount()}
- `
- });
- }
- }));
- this._clusters.bar = new window.L.MarkerClusterGroup(Object.assign(clusterOptions, {
- iconCreateFunction: cluster => {
- return window.L.divIcon({
- className: 'cluster-icon-wrapper',
- html: `
-
- ${cluster.getChildCount()}
- `
- });
- }
- }));
- // Append clusters to the map depending on user preferences
- if (Utils.getPreference(`poi-show-spot`) === 'true') {
- this._map.addLayer(this._clusters.spot);
- }
- if (Utils.getPreference(`poi-show-store`) === 'true') {
- this._map.addLayer(this._clusters.store);
- }
- if (Utils.getPreference(`poi-show-bar`) === 'true') {
- this._map.addLayer(this._clusters.bar);
- }
- // Load data from local storage, later to be fetched from server
- const iterateMarkers = mark => {
- this.markPopupFactory(mark).then(dom => {
- mark.dom = dom;
- mark.marker = this.placeMarker(mark);
- this._marks[mark.type].push(mark);
- this._clusters[mark.type].addLayer(mark.marker);
- });
- };
- let marks = JSON.parse(Utils.getPreference('saved-spot')) || [];
- for (let i = 0; i < marks.length; ++i) {
- iterateMarkers(marks[i]);
- }
- marks = JSON.parse(Utils.getPreference('saved-store')) || [];
- for (let i = 0; i < marks.length; ++i) {
- iterateMarkers(marks[i]);
- }
- marks = JSON.parse(Utils.getPreference('saved-bar')) || [];
- for (let i = 0; i < marks.length; ++i) {
- iterateMarkers(marks[i]);
- }
-
- resolve();
- });
- }
-
-
- // ======================================================================== //
- // ------------------------- Toggle for map items ------------------------- //
- // ======================================================================== //
-
-
- /**
- * @method
- * @name toggleFocusLock
- * @public
- * @memberof BeerCrackerz
- * @author Arthur Beaulieu
- * @since January 2022
- * @description
- *
- * The toggleFocusLock() method will, depending on user preference, lock or unlock
- * the map centering around the user marker at each position refresh. This way the user
- * can roam while the map is following its position.
- *
- * The toggleLabel() method will, depending on user preference, display or not
- * the labels attached to spots/stores/bars marks. This label is basically the
- * mark name given by its creator.
- *
- * The toggleCircle() method will, depending on user preference, display or not
- * the circles around the spots/stores/bars marks. This circle indicates the minimal
- * distance which allow the user to make updates on the mark information
- *
- * The toggleMarkers() method will, depending on user preference, display or not
- * a given mark type. This way, the user can fine tune what is displayed on the map.
- * A mark type in spots/stores/bars must be given as an argument
- *
- * @param {String} type The mark type in spots/tores/bars
- **/
- toggleMarkers(type) {
- const visible = !(Utils.getPreference(`poi-show-${type}`) === 'true');
- if (visible === true) {
- this._map.addLayer(this._clusters[type]);
- } else {
- this._map.removeLayer(this._clusters[type]);
- }
- Utils.setPreference(`poi-show-${type}`, visible);
- }
-
-
- /**
- * @method
- * @name toggleHighAccuracy
- * @public
- * @memberof BeerCrackerz
- * @author Arthur Beaulieu
- * @since January 2022
- * @description
- *
- * The toggleHighAccuracy() method will, depending on user preference, update the
- * geolocation accuracy between optimized and high. The high settings might cause
- * more memory and processing consumption, but gives better results. It will clear
- * any previous position watch on the geolocation API so it can subscribe a new one
- * with the new accuracy parameters (see Utils for values)
- *
- * The toggleDebug() method will, depending on user preference, add or remove
- * the debug DOM element to the user interface. The debug DOM display several
- * useful information to identify an issue with the geolocation API
- *
- **/
- toggleDebug() {
- const visible = !window.DEBUG;
- window.DEBUG = visible;
- Utils.setPreference('app-debug', visible);
- if (visible) {
- this.addDebugUI();
- } else {
- this.removeDebugUI();
- }
- }
-
-
- // ======================================================================== //
- // ----------------- App modals display and interaction ------------------- //
- // ======================================================================== //
-
-
- newMarkModal(dom) {
- document.getElementById('overlay').appendChild(dom);
- document.getElementById('overlay').style.display = 'flex';
- setTimeout(() => document.getElementById('overlay').style.opacity = 1, 50);
- }
-
-
- editMarkModal(options) {
- Utils.fetchTemplate(`assets/html/modal/edit${options.type}.html`).then(dom => {
- const name = dom.querySelector(`#${options.type}-name`);
- const description = dom.querySelector(`#${options.type}-desc`);
- const submit = dom.querySelector(`#${options.type}-submit`);
- const cancel = dom.querySelector(`#${options.type}-cancel`);
- const rate = dom.querySelector(`#${options.type}-rating`);
- const rating = new Rating(rate, options.rate);
- // Update nls for template
- Utils.replaceString(dom.querySelector(`#nls-modal-title`), `{{MODAL_TITLE}}`, this.nls.modal(`${options.type}EditTitle`));
- Utils.replaceString(dom.querySelector(`#nls-${options.type}-name`), `{{${options.type.toUpperCase()}_NAME}}`, this.nls[options.type]('nameLabel'));
- Utils.replaceString(dom.querySelector(`#nls-${options.type}-desc`), `{{${options.type.toUpperCase()}_DESC}}`, this.nls[options.type]('descLabel'));
- Utils.replaceString(dom.querySelector(`#nls-${options.type}-rate`), `{{${options.type.toUpperCase()}_RATE}}`, this.nls[options.type]('rateLabel'));
- Utils.replaceString(submit, `{{${options.type.toUpperCase()}_SUBMIT}}`, this.nls.nav('add'));
- Utils.replaceString(cancel, `{{${options.type.toUpperCase()}_CANCEL}}`, this.nls.nav('cancel'));
- name.value = options.name;
- description.value = options.description;
- submit.addEventListener('click', () => {
- // Iterate through marks to find matching one (by coord as marks coordinates are unique)
- for (let i = 0; i < this._marks[options.type].length; ++i) {
- // We found, remove circle, label and marker from map/clusters
- if (options.lat === this._marks[options.type][i].lat && options.lng === this._marks[options.type][i].lng) {
- this._marks[options.type][i].name = name.value;
- this._marks[options.type][i].description = description.value;
- this._marks[options.type][i].rate = rating.currentRate;
- options.tooltip.removeFrom(this.map);
- this.markPopupFactory(options).then(dom => {
- options.dom = dom;
- options.marker.setPopupContent(options.dom);
- });
- break;
- }
- }
- // Format marks to be saved and then update user preference with
- const formattedMarks = [];
- for (let i = 0; i < this._marks[options.type].length; ++i) {
- formattedMarks.push(this.formatSavedMarker(this._marks[options.type][i]));
- }
- Utils.setPreference(`saved-${options.type}`, JSON.stringify(formattedMarks));
- // Notify user through UI that marker has been successfully deleted
- this._notification.raise(this.nls.notif(`${options.type}Deleted`));
- this.closeModal(null, true);
- });
-
- cancel.addEventListener('click', this.closeModal.bind(this, null, true));
- document.getElementById('overlay').appendChild(dom);
- document.getElementById('overlay').style.display = 'flex';
- setTimeout(() => document.getElementById('overlay').style.opacity = 1, 50);
- });
- }
-
-
- /**
- * @method
- * @name deleteMarkModal
- * @public
- * @memberof BeerCrackerz
- * @author Arthur Beaulieu
- * @since February 2022
- * @description
- *
- * The deleteMarkModal() method will request the mark delete modal, which prompts
- * the user a confirmation to actually delete the mark
- *
- * The mapClicked() method is the callback used when the user clicked on the Leaflet.js map
- *
- * @param {Event} event The click event
- **/
- mapClicked(event) {
- if (this._newMarker && this._newMarker.popupClosed) {
- // Avoid to open new marker right after popup closing
- this._newMarker = null;
- } else if (this._newMarker === null || !this._newMarker.isBeingDefined) {
- // Only create new marker if none is in progress, and that click is max range to add a marker
- const distance = Utils.getDistanceBetweenCoords([this._user.lat, this._user.lng], [event.latlng.lat, event.latlng.lng]);
- if (distance < Utils.NEW_MARKER_RANGE) {
- this._newMarker = this.definePOI(event.latlng, this._markerSaved.bind(this));
- } else {
- this._notification.raise(this.nls.notif('newMarkerOutside'));
- }
- }
- }
-
-
- /**
- * @method
- * @name _markerSaved
- * @private
- * @memberof BeerCrackerz
- * @author Arthur Beaulieu
- * @since January 2022
- * @description
- *
- * The _markerSaved() method is the callback used when a marker is created and added
- * to the map. It is the last method of a new marker proccess.
- *
- * @param {Object} options The new marker options
- **/
- _markerSaved(options) {
- // Save marke in marks and clusters for the map
- this._marks[options.type].push(options);
- this._clusters[options.type].addLayer(options.marker);
- // Notify user that new marker has been saved
- this._notification.raise(this.nls.notif(`${options.type}Added`));
- // Update marker circles visibility according to user position
- this.updateMarkerCirclesVisibility();
- // Clear new marker to let user add other stuff
- this._newMarker = null;
- // Save new marker in local storage, later to be sent to the server
- const marks = JSON.parse(Utils.getPreference(`saved-${options.type}`)) || [];
- marks.push(this.formatSavedMarker(options));
- Utils.setPreference(`saved-${options.type}`, JSON.stringify(marks));
- }
-
-
- /**
- * @method
- * @name updateMarkerCirclesVisibility
- * @public
- * @memberof BeerCrackerz
- * @author Arthur Beaulieu
- * @since January 2022
- * @description
- *
- * The updateMarkerCirclesVisibility() method will update the circle visibility for
- * all mark types (spots/stores/bars) and for the user marker
- *
- **/
- updateMarkerCirclesVisibility() {
- const _updateByType = data => {
- // Check spots in user's proximity
- for (let i = 0; i < data.length; ++i) {
- // Only update circles that are in user view
- if (this._map.getBounds().contains(data[i].marker.getLatLng())) {
- const marker = data[i].marker;
- const distance = Utils.getDistanceBetweenCoords([this._user.lat, this._user.lng], [marker.getLatLng().lat, marker.getLatLng().lng]);
- // Only show if user distance to marker is under circle radius
- if (distance < Utils.CIRCLE_RADIUS && !data[i].circle.visible) {
- data[i].circle.visible = true;
- data[i].circle.setStyle({
- opacity: 1,
- fillOpacity: 0.1
- });
- } else if (distance >= Utils.CIRCLE_RADIUS && data[i].circle.visible) {
- data[i].circle.visible = false;
- data[i].circle.setStyle({
- opacity: 0,
- fillOpacity: 0
- });
- }
- }
- }
- };
-
- if (Utils.getPreference('poi-show-circle') === 'true') {
- _updateByType(this._marks.spot);
- _updateByType(this._marks.store);
- _updateByType(this._marks.bar);
- _updateByType([this._user]);
- }
- }
-
-
- // ======================================================================== //
- // -------------------------- Marker edition ------------------------------ //
- // ======================================================================== //
-
-
- /**
- * @method
- * @name formatSavedMarker
- * @public
- * @memberof BeerCrackerz
- * @author Arthur Beaulieu
- * @since February 2022
- * @description
- *
- * This method formats a mark returned from MapHelper so it can be parsed
- * using JSON.parse (in order to store it in local storage/database)
- *
Beercrackerz – v0.1.0 – MBP 2022/2023",
+ "updatePP": "Aktualisieren sie ihr profilbild",
+ "updatePPSizeError": "Bitte wählen sie ein bild unter 2.5 Mo aus",
+ "updatePPDimensionError": "Die minimale bildgröße beträgt 512x512",
+ "updatePPTitle": "Neues profilbild",
+ "updatePPDesc": "Schneiden Sie das bild bei bedarf zu und legen sie los!",
+ "logout": "Ausloggen",
+ "hideShowTitle": "Kartenoptionen",
+ "hideShowSpots": "Spots",
+ "hideShowShops": "Läden",
+ "hideShowBars": "Bars",
+ "hideShowHelperLabel": "Klicken sie auf ein symbol, um seine beschreibung anzuzeigen",
+ "spotHelperHideShow": "Zum ausblenden oder anzeigen aller spots auf der karte.",
+ "shopHelperHideShow": "Zum ausblenden oder anzeigen aller läden auf der karte.",
+ "barHelperHideShow": "Zum ausblenden oder anzeigen aller bar auf der karte.",
+ "deleteMarkTitle": "Markierung löschen",
+ "deleteMarkDesc": "Möchten sie diese markierung wirklich löschen? Diese aktion ist dauerhaft und kann nicht rückgängig gemacht werden.",
+ "spotEditTitle": "Spot bearbeiten",
+ "shopEditTitle": "Laden bearbeiten",
+ "barEditTitle": "Bar bearbeiten",
+ "helpTitle": "Entdecken Sie BeerCrackerz",
+ "helpNavNext": "Navigieren sie zur nächsten seite oder verlassen sie dieses hilfemenü!",
+ "helpNavNextPrev": "Navigieren sie zur nächsten oder vorherigen seite oder verlassen sie dieses hilfemenü!",
+ "helpNavEnd": "Sie können diesen assistenten jetzt schließen!",
+ "helpPage1": "Willkommen bei BeerCrackerz! Mal sehen, wie die Karte funktioniert; Ihre Position wird durch das Symbol gekennzeichnet. Um ihre position herum zeigt ein blauer kreis die aktuelle Genauigkeit Ihrer Position an. Um sie herum gibt es auch einen goldenen kreis ; Dies ist die maximale entfernung, für die sie einen neuen spot, ein laden oder eine bar deklarieren können.",
+ "helpPage2": "Ein spot wird durch das symbol dargestellt. Spot sind perfekte orte, um eine bierdose zu genießen. Jeder spot hat seine besonderheiten, es liegt an ihnen, sie zu entdecken.",
+ "helpPage3": "Ein laden wird durch das symbol dargestellt. Laden sind der ideale ort, um sich mit bierdosen einzudecken. Jeder laden hat seine besonderheiten, es liegt an ihnen, sie zu entdecken.",
+ "helpPage4": "Ein bar wird durch das symbol dargestellt. Bars sind freundliche orte, die frische und gute biere servieren. Jeder bar hat seine besonderheiten, es liegt an ihnen, sie zu entdecken.",
+ "helpPage5": "Es stehen mehrere befehle zur verfügung: 1. auswahl der basiskarte, 2. zentrieren und sperren der Karte auf ihrer position, 3. ein oder ausblenden von kartenelementen und 4. ihr konto.",
+ "helpQuit": "Aufhören",
+ "helpQuitNoSee": "Aufhören und nie wieder sehen"
+ },
+ "popup": {
+ "spotFoundBy": "Eine spot, die von entdeckt wurde",
+ "spotFoundWhen": "Seit der",
+ "shopFoundBy": "Ein laden hinzugefügt von",
+ "shopFoundWhen": "Seit der",
+ "barFoundBy": "Ein bar hinzugefügt von",
+ "barFoundWhen": "Seit der",
+ "spotNoDesc": "Für diesen spot ist keine beschreibung verfügbar",
+ "shopNoDesc": "Für diesen laden ist keine beschreibung verfügbar",
+ "barNoDesc": "Für diesen bar ist keine beschreibung verfügbar"
+ },
+ "auth": {
+ "login": {
+ "headTitle": "Verbindung | BeerCrackerz",
+ "subtitle": "Siehe anschluss",
+ "hiddenError": "Oh! Ein versteckter text!",
+ "username": "Benutzername",
+ "password": "Passwort",
+ "login": "Siehe anschluss",
+ "notRegistered": "Nicht registriert?",
+ "register": "Ein konto erstellen",
+ "forgot": "Passwort vergessen?",
+ "reset": "Setzen sie es zurück",
+ "bothEmpty": "Bitte geben sie einen benutzernamen und ein passwort ein",
+ "usernameEmpty": "Bitte geben sie einen benutzernamen ein",
+ "passwordEmpty": "Bitte geben sie ihr passwort ein",
+ "credsInvalid": "Falscher Benutzername oder falsches Passwort",
+ "serverError": "Ein serverfehler ist aufgetreten, wenden sie sich an den support",
+ "checkMail": "Eine E-Mail wurde gesendet, damit sie fortfahren können"
+ },
+ "register": {
+ "headTitle": "Registrieren | BeerCrackerz",
+ "subtitle": "Registrieren",
+ "hiddenError": "Oh! Ein versteckter text!",
+ "username": "Nutzername",
+ "mail": "E-Mail-Addresse",
+ "password1": "Passwort",
+ "password2": "Kennwort bestätigen",
+ "register": "Registrieren",
+ "notRegistered": "Bereits registriert?",
+ "login": "Siehe anschluss",
+ "fieldEmpty": "Bitte füllen sie alle felder des formulars aus",
+ "notMatchingPassword": "Die zwei passwörter stimmen nicht überein",
+ "activationSuccess": "Ihr konto wurde erfolgreich aktiviert!",
+ "activationError": "Fehler beim aktivieren ihres kontos"
+ },
+ "forgotPassword": {
+ "headTitle": "Passwort vergessen | BeerCrackerz",
+ "subtitle": "Passwort vergessen",
+ "hiddenError": "Oh! Ein versteckter text!",
+ "mail": "E-Mail-Addresse",
+ "submit": "Setze dein passwort zurück",
+ "loginLabel": "Du erinnerst dich?",
+ "login": "Siehe anschluss",
+ "fieldEmpty": "Geben sie ihre e-mail-adresse ein",
+ "serverError": "Ein serverfehler ist aufgetreten, wenden sie sich an den support"
+ },
+ "resetPassword": {
+ "headTitle": "Passwort zurücksetzen | BeerCrackerz",
+ "subtitle": "Passwort zurücksetzen",
+ "hiddenError": "Oh! Ein versteckter text!",
+ "password1": "Passwort",
+ "password2": "Kennwort bestätigen",
+ "reset": "Setze dein passwort zurück",
+ "loginLabel": "Du erinnerst dich?",
+ "login": "Siehe anschluss",
+ "fieldEmpty": "Bitte füllen sie alle felder des formulars aus",
+ "notMatchingPassword": "Die zwei passwörter stimmen nicht überein",
+ "serverError": "Ein serverfehler ist aufgetreten, wenden sie sich an den support"
+ }
+ }
+}
diff --git a/static/nls/en.json b/static/nls/en.json
new file mode 100644
index 0000000..14f81d3
--- /dev/null
+++ b/static/nls/en.json
@@ -0,0 +1,244 @@
+{
+ "debug": {
+ "lat": "Latitude",
+ "lng": "Longitude",
+ "updates": "Updates",
+ "accuracy": "Accuracy",
+ "highAccuracy": "High accuracy",
+ "posAge": "Position max age",
+ "posTimeout": "Position timeout",
+ "zoom": "Zoom level",
+ "enabled": "Enabled",
+ "disabled": "Disabled",
+ "marks": "Marks"
+ },
+ "notif": {
+ "geolocationError": "Your browser doesn't implement the geolocation API",
+ "newMarkerOutside": "New marker out of your range",
+ "spotAdded": "New spot saved to map",
+ "spotNotAdded": "Couldn't add spot",
+ "shopAdded": "New shop saved to map",
+ "shopNotAdded": "Couldn't add shop",
+ "barAdded": "New bar saved to map",
+ "barNotAdded": "Couldn't add bar",
+ "spotEdited": "Spot edited! Thanks",
+ "spotNotEdited": "Couldn't edit spot",
+ "shopEdited": "Shop edited! Thanks",
+ "shopNotEdited": "Couldn't edit shop",
+ "barEdited": "Bar edited! Thanks",
+ "barNotEdited": "Couldn't edit bar",
+ "spotDeleted": "Spot deleted",
+ "spotNotDeleted": "Couldn't delete spot",
+ "shopDeleted": "Shop deleted",
+ "shopNotDeleted": "Couldn't delete shop",
+ "barDeleted": "Bar deleted",
+ "barNotDeleted": "Couldn't delete bar",
+ "markNameEmpty": "You didn't specified a name for the mark",
+ "markTypeEmpty": "You didn't specified a type for the mark",
+ "markRateEmpty": "You didn't specified a rate for the mark",
+ "markPriceEmpty": "You didn't specified a price for the mark",
+ "lockFocusOn": "Centering and locking view on your position",
+ "unlockFocusOn": "Position locking ended",
+ "uploadPPSuccess": "Profile picture updated",
+ "uploadPPFailed": "Failed to upload profile picture",
+ "welcomeBack": "Glad to see you back on BeerCrackerz!"
+ },
+ "nav": {
+ "add": "Add",
+ "edit": "Edit",
+ "upload": "Upload",
+ "cancel": "Cancel",
+ "close": "Close",
+ "delete": "Delete"
+ },
+ "map": {
+ "newTitle": "New marker",
+ "newSpot": "Add a spot",
+ "newShop": "Add a shop",
+ "newBar": "Add a bar",
+ "planLayerOSM": "Plan OSM",
+ "satLayerEsri": "Satellite ESRI"
+ },
+ "spot": {
+ "addTitle": "New spot",
+ "editTitle": "Edit spot",
+ "subtitle": "A spot is a remarkable place to crack a beer ! Share it with the community, wether it is for the astonishing view or for whatever it is enjoyable to drink a beer!",
+ "nameLabel": "Name that spot",
+ "descLabel": "Why don't you describe it",
+ "rateLabel": "Give it a note",
+ "typeLabel": "What kind of spot it is",
+ "forestType": "Forest",
+ "riverType": "River",
+ "lakeType": "Lake",
+ "cliffType": "Cliff",
+ "mountainType": "Mountain",
+ "beachType": "Beach",
+ "seaType": "Sea",
+ "cityType": "City",
+ "povType": "Point of View",
+ "modifiersLabel": "Has it any specific features",
+ "benchModifier": "Bench",
+ "coveredModifier": "Covered",
+ "toiletModifier": "Toilet",
+ "storeModifier": "Store nearby",
+ "trashModifier": "Trash",
+ "parkingModifier": "Parking"
+ },
+ "shop": {
+ "addTitle": "New shop",
+ "editTitle": "Edit shop",
+ "subtitle": "The must have place to refill your beer stock. The more info you provide, the better you help your fellow beer crackerz!",
+ "nameLabel": "Shop name",
+ "descLabel": "Why don't you describe it",
+ "rateLabel": "Give it a note",
+ "priceLabel": "How expensive it is",
+ "typeLabel": "What kind of shop it is",
+ "storeType": "Grocery store",
+ "superType": "Supermarket",
+ "hyperType": "Hypermarket",
+ "cellarType": "Cellar",
+ "modifiersLabel": "Has it any specific features",
+ "bioModifier": "Bio",
+ "craftModifier": "Craft",
+ "freshModifier": "Refrigerated",
+ "cardModifier": "Credit card",
+ "choiceModifier": "Wide choice"
+ },
+ "bar": {
+ "addTitle": "New bar",
+ "editTitle": "Edit bar",
+ "subtitle": "A bar is a holly place where you can get some nicely colded draft beers!",
+ "nameLabel": "Bar name",
+ "descLabel": "Why don't you describe it",
+ "rateLabel": "Give it a note",
+ "priceLabel": "How expensive it is",
+ "typeLabel": "What kind of bar it is",
+ "regularType": "Regular",
+ "snackType": "Snack",
+ "cellarType": "Brewery",
+ "rooftopType": "Rooftop",
+ "modifiersLabel": "Has it any specific features",
+ "tobaccoModifier": "Cigarets",
+ "foodModifier": "Food",
+ "cardModifier": "Credit card",
+ "choiceModifier": "Wide choice",
+ "outdoorModifier": "Outdoor"
+ },
+ "modal": {
+ "userTitle": "User account",
+ "userAccuracyPref": "High precision",
+ "darkThemePref": "Dark theme",
+ "userDebugPref": "Debug interface",
+ "startupHelp": "Starting help",
+ "langPref": "Interface language",
+ "langFr": "🇫🇷 French",
+ "langEn": "🇬🇧 English",
+ "langEs": "🇪🇸 Spanish",
+ "langDe": "🇩🇪 German",
+ "langIt": "🇩🇪 Italian",
+ "langPt": "🇵🇹 Portuguese",
+ "aboutTitle": "About BeerCrackerz",
+ "aboutDesc": "A brilliant idea from David Béché! BeerCrackerz is the beer lovers comunity, filled with pint slayers and cereals lovers.
Beercrackerz – v0.1.0 – MBP 2022/2023",
+ "updatePP": "Update your profile pic",
+ "updatePPSizeError": "Please select an image below 2.5 Mo",
+ "updatePPDimensionError": "The minimum image size is 512x512",
+ "updatePPTitle": "New profile picture pic",
+ "updatePPDesc": "Crop the picture if needed and go!",
+ "logout": "Sign out",
+ "hideShowTitle": "Map options",
+ "hideShowSpots": "Spots",
+ "hideShowShops": "Shops",
+ "hideShowBars": "Bars",
+ "hideShowHelperLabel": "Click on an icon to display its description",
+ "spotHelperHideShow": "To hide or show all spots on the map.",
+ "shopHelperHideShow": "To hide or show all shops on the map.",
+ "barHelperHideShow": "To hide or show all bars on the map.",
+ "deleteMarkTitle": "Delete mark",
+ "deleteMarkDesc": "Are you sure you want to delete this mark? This action is permanent and can not be reverted.",
+ "spotEditTitle": "Edit spot",
+ "shopEditTitle": "Edit shop",
+ "barEditTitle": "Edit bar",
+ "helpTitle": "Discover BeerCrackerz",
+ "helpNavNext": "Navigate to the next page, or exit this help menu!",
+ "helpNavNextPrev": "Navigate to the next or previous page, or exit this help menu!",
+ "helpNavEnd": "You can now close this wizard!",
+ "helpPage1": "Welcome to BeerCrackerz! Let's see how the map works; your position is marked by the symbol . Around your position, a blue circle represents the current accuracy of your position. Around you, there is also a golden circle ; this is the maximum distance for which you can declare a new spot, a store or a bar.",
+ "helpPage2": "A spot is represented by the symbol . Spots are perfect places to enjoy a beer can. Each spot has its specificities, it's up to you to discover them.",
+ "helpPage3": "A shop is represented by the symbol . Shops are the ideal places to stock up on beer cans. Each shop has its specificities, it's up to you to discover them.",
+ "helpPage4": "A bar is represented by the symbol . Bars are friendly places that serve fresh and good beers. Each bar has its specificities, it's up to you to discover them.",
+ "helpPage5": "Several commands are available : 1. base map choice, 2. recenter and lock the map on your position, 3. show or hide map items and 4. your account.",
+ "helpQuit": "Quit",
+ "helpQuitNoSee": "Quit and don't see again"
+ },
+ "popup": {
+ "spotFoundBy": "A spot discovered by",
+ "spotFoundWhen": "Since the",
+ "shopFoundBy": "A shop added by",
+ "shopFoundWhen": "Since the",
+ "barFoundBy": "A bar added by",
+ "barFoundWhen": "Since the",
+ "spotNoDesc": "No description available for this spot",
+ "shopNoDesc": "No description available for this shop",
+ "barNoDesc": "No description available for this bar"
+ },
+ "auth": {
+ "login": {
+ "headTitle": "Login | BeerCrackerz",
+ "subtitle": "Login",
+ "hiddenError": "Oh! A hidden text!",
+ "username": "Username",
+ "password": "Password",
+ "login": "Login",
+ "notRegistered": "Not registered yet?",
+ "register": "Create an account",
+ "forgot": "Forgot password?",
+ "reset": "Reset it",
+ "bothEmpty": "Please fill the username and password fields",
+ "usernameEmpty": "Please fill the username field",
+ "passwordEmpty": "Please fill your password field",
+ "credsInvalid": "Invalid credentials",
+ "serverError": "Something wrong happened, contact support",
+ "checkMail": "An email has been sent so you can proceed"
+ },
+ "register": {
+ "headTitle": "Register | BeerCrackerz",
+ "subtitle": "Create an account",
+ "hiddenError": "Oh! A hidden text!",
+ "username": "Username",
+ "mail": "Email address",
+ "password1": "Password",
+ "password2": "Confirm password",
+ "register": "Register",
+ "notRegistered": "Already registered?",
+ "login": "Login",
+ "fieldEmpty": "Please fill all required fields",
+ "notMatchingPassword": "Passwords doesn't match",
+ "activationSuccess": "Your account has been successfully created!",
+ "activationError": "Something wrong happened while creating your acocunt, contact support"
+ },
+ "forgotPassword": {
+ "headTitle": "Forgot password | BeerCrackerz",
+ "subtitle": "Forgot password",
+ "hiddenError": "Oh! A hidden text!",
+ "mail": "Email",
+ "submit": "Reset password",
+ "loginLabel": "You remember it?",
+ "login": "Login",
+ "fieldEmpty": "Please fill al required fields",
+ "serverError": "Something wrong happened, contact support"
+ },
+ "resetPassword": {
+ "headTitle": "Reset password | BeerCrackerz",
+ "subtitle": "Reset password",
+ "hiddenError": "Oh! A hidden text!",
+ "password1": "New password",
+ "password2": "Confirm password",
+ "reset": "Reset password",
+ "loginLabel": "You remember it?",
+ "login": "Login",
+ "fieldEmpty": "Please fill all fields",
+ "notMatchingPassword": "Passwords doesn't match",
+ "serverError": "Something wrong happened, contact support"
+ }
+ }
+}
diff --git a/static/nls/es.json b/static/nls/es.json
new file mode 100644
index 0000000..d82cf50
--- /dev/null
+++ b/static/nls/es.json
@@ -0,0 +1,244 @@
+{
+ "debug": {
+ "lat": "Latitud",
+ "lng": "Longitud",
+ "updates": "Actualizaciones",
+ "accuracy": "Precisión",
+ "highAccuracy": "Alta precisión",
+ "posAge": "Posición edad máxima",
+ "posTimeout": "Tiempo de espera de posición",
+ "zoom": "Zoom",
+ "enabled": "Activado",
+ "disabled": "Desactivado",
+ "marks": "Marcas"
+ },
+ "notif": {
+ "geolocationError": "Tu navegador no implementa la API de geolocalización",
+ "newMarkerOutside": "Nuevo marcador fuera de tu rango",
+ "spotAdded": "Nuevo spot guardado en el mapa",
+ "spotNotAdded": "No se pudo agregar el spot",
+ "shopAdded": "Nueva tienda guardada en el mapa",
+ "shopNotAdded": "No se pudo agregar la tienda",
+ "barAdded": "Nueva barra guardada en el mapa",
+ "barNotAdded": "No se pudo agregar la barra",
+ "spotEdited": "¡Spot editado! Gracias",
+ "spotNotEdited": "No se pudo editar el spot",
+ "shopEdited": "¡Tienda editada! Gracias",
+ "shopNotEdited": "No se pudo editar la tienda",
+ "barEdited": "¡Barra editada! Gracias",
+ "barNotEdited": "No se pudo editar la tienda",
+ "spotDeleted": "Spot eliminado",
+ "spotNotDeleted": "No se pudo eliminar el spot",
+ "shopDeleted": "Tienda eliminada",
+ "shopNotDeleted": "No se pudo eliminar la tienda",
+ "barDeleted": "Barra eliminada",
+ "barNotDeleted": "Barra eliminada",
+ "markNameEmpty": "No especificó un nombre para la marca",
+ "markTypeEmpty": "No especificó un tipo para la marca",
+ "markRateEmpty": "No especificaste una nota para la marca",
+ "markPriceEmpty": "No especificaste un precio para la marca",
+ "lockFocusOn": "Centrar y bloquear la vista en su posición",
+ "unlockFocusOn": "Finalizó el bloqueo de posición",
+ "uploadPPSuccess": "Imagen de perfil actualizada",
+ "uploadPPFailed": "No se pudo cargar la foto de perfil",
+ "welcomeBack": "Encantado de verte de nuevo en BeerCrackerz!"
+ },
+ "nav": {
+ "add": "Agregar",
+ "edit": "Editar",
+ "upload": "Subir",
+ "cancel": "Cancelar",
+ "close": "Cerca",
+ "delete": "Borrar"
+ },
+ "map": {
+ "newTitle": "Nuevo marcador",
+ "newSpot": "Agregar un spot",
+ "newShop": "Añadir una tienda",
+ "newBar": "Añadir una barra",
+ "planLayerOSM": "Mapa OSM",
+ "satLayerEsri": "Satélite ESRI"
+ },
+ "spot": {
+ "addTitle": "Nuevo spot",
+ "editTitle": "Editar spot",
+ "subtitle": "¡Un spot es un lugar extraordinario para tomar una cerveza! ¡Compártalo con la comunidad, ya sea por la vista asombrosa o por lo que sea que sea agradable para beber una cerveza!",
+ "nameLabel": "Nombra ese spot",
+ "descLabel": "¿Por qué no lo describe?",
+ "rateLabel": "Dale una nota",
+ "typeLabel": "Que tipo de spot es",
+ "forestType": "Bosque",
+ "riverType": "Río",
+ "lakeType": "Lago",
+ "cliffType": "Acantilado",
+ "mountainType": "Montaña",
+ "beachType": "Playa",
+ "seaType": "Mar",
+ "cityType": "Ciudad",
+ "povType": "Mirador",
+ "modifiersLabel": "Tiene alguna caracteristica especifica",
+ "benchModifier": "Banco",
+ "coveredModifier": "Cubierto",
+ "toiletModifier": "Inodoro",
+ "storeModifier": "Tienda cercana",
+ "trashModifier": "Basura",
+ "parkingModifier": "Estacionamiento"
+ },
+ "shop": {
+ "addTitle": "Nueva tienda",
+ "editTitle": "Editar tienda",
+ "subtitle": "El lugar imprescindible para rellenar tu stock de cerveza. ¡Cuanta más información proporciones, mejor ayudarás a tus compañeros BeerCrackerz!",
+ "nameLabel": "Nombre de tienda",
+ "descLabel": "¿Por qué no lo describe?",
+ "rateLabel": "Dale una nota",
+ "priceLabel": "Que caro es",
+ "typeLabel": "Que tipo de tienda es",
+ "storeType": "Tienda de comestibles",
+ "superType": "Supermercado",
+ "hyperType": "Hipermercado",
+ "cellarType": "Cava",
+ "modifiersLabel": "Tiene alguna caracteristica especifica",
+ "bioModifier": "Bio",
+ "craftModifier": "Artesanal",
+ "freshModifier": "Refrigerado",
+ "cardModifier": "Tarjeta de crédito",
+ "choiceModifier": "Amplia selección"
+ },
+ "bar": {
+ "addTitle": "Nueva barra",
+ "editTitle": "Editar barra",
+ "subtitle": "¡Un bar es un lugar sagrado donde puedes tomar unas cervezas de barril bien frías!",
+ "nameLabel": "Nombre de la barra",
+ "descLabel": "¿Por qué no lo describe?",
+ "rateLabel": "Dale una nota",
+ "priceLabel": "Que caro es",
+ "typeLabel": "Que tipo de barra es",
+ "regularType": "Regular",
+ "snackType": "Merienda",
+ "cellarType": "Cervecería",
+ "rooftopType": "Techo",
+ "modifiersLabel": "Tiene alguna caracteristica especifica",
+ "tobaccoModifier": "Cigarrillos",
+ "foodModifier": "Alimento",
+ "cardModifier": "Tarjeta de crédito",
+ "choiceModifier": "Amplia selección",
+ "outdoorModifier": "Exterior"
+ },
+ "modal": {
+ "userTitle": "Cuenta de usuario",
+ "userAccuracyPref": "Alta precisión",
+ "darkThemePref": "Tema oscuro",
+ "userDebugPref": "Interfaz de depuración",
+ "startupHelp": "Empezando",
+ "langPref": "Lenguaje de interfaz",
+ "langFr": "🇫🇷 Francés",
+ "langEn": "🇬🇧 Inglés",
+ "langEs": "🇪🇸 Español",
+ "langDe": "🇩🇪 Alemán",
+ "langIt": "🇩🇪 Italiano",
+ "langPt": "🇵🇹 Portugués",
+ "aboutTitle": "Acerca de BeerCrackerz",
+ "aboutDesc": "¡Una idea brillante de David Béché! BeerCrackerz es la comunidad de amantes de la cerveza, llena de cazadores de pintas y amantes de los cereales.
Beercrackerz – v0.1.0 – MBP 2022/2023",
+ "updatePP": "Actualiza tu foto de perfil",
+ "updatePPSizeError": "Seleccione una imagen debajo de 2.5 Mo",
+ "updatePPDimensionError": "El tamaño mínimo de la imagen es 512x512",
+ "updatePPTitle": "Nueva foto de perfil foto",
+ "updatePPDesc": "¡Recorta la imagen si es necesario y listo!",
+ "logout": "Desconectarse",
+ "hideShowTitle": "Opciones de mapa",
+ "hideShowSpots": "Spots",
+ "hideShowShops": "Tiendas",
+ "hideShowBars": "Barras",
+ "hideShowHelperLabel": "Haga clic en un icono para mostrar su descripción",
+ "spotHelperHideShow": "Para ocultar o mostrar todos los spots del mapa.",
+ "shopHelperHideShow": "Para ocultar o mostrar todas las tiendas en el mapa.",
+ "barHelperHideShow": "Para ocultar o mostrar todas las barras del mapa.",
+ "deleteMarkTitle": "Borrar marca",
+ "deleteMarkDesc": "¿Está seguro de que desea eliminar esta marca? Esta acción es permanente y no se puede revertir.",
+ "spotEditTitle": "Editar spot",
+ "shopEditTitle": "Editar tienda",
+ "barEditTitle": "Editar barra",
+ "helpTitle": "Descubre BeerCrackerz",
+ "helpNavNext": "¡Navegue a la página siguiente o salga de este menú de ayuda!",
+ "helpNavNextPrev": "¡Navegue a la página siguiente o anterior, o salga de este menú de ayuda!",
+ "helpNavEnd": "¡Ya puede cerrar este asistente!",
+ "helpPage1": "¡Bienvenido a BeerCrackerz! Veamos cómo funciona el mapa; su posición está marcada por el símbolo . Alrededor de su posición, un círculo azul representa la precisión actual de su posición. A tu alrededor, también hay un círculo dorado ; esta es la distancia máxima por la que puedes declarar un nuevo lugar, una tienda o un bar.",
+ "helpPage2": "Un spot está representado por el símbolo . Los spots son lugares perfectos para disfrutar de una lata de cerveza. Cada spot tiene sus especificidades, depende de ti descubrirlas.",
+ "helpPage3": "Una tienda está representada por el símbolo . Las tiendas son los lugares ideales para abastecerse de latas de cerveza. Cada tienda tiene sus especificidades, depende de ti descubrirlas.",
+ "helpPage4": "Una tienda barra representada por el símbolo . Los bares son lugares acogedores que sirven cervezas frescas y buenas. Cada barra tiene sus especificidades, depende de ti descubrirlas.",
+ "helpPage5": "Hay varios comandos disponibles: 1. elección del mapa base, 2. volver a centrar y bloquear el mapa en su posición, 3. mostrar u ocultar elementos del mapa y 4. su cuenta.",
+ "helpQuit": "Sal",
+ "helpQuitNoSee": "Sal y no vuelvas a ver"
+ },
+ "popup": {
+ "spotFoundBy": "Un spot descubierto por",
+ "spotFoundWhen": "Desde el",
+ "shopFoundBy": "Una tienda añadida por",
+ "shopFoundWhen": "Desde el",
+ "barFoundBy": "Una barra añadida por",
+ "barFoundWhen": "Desde el",
+ "spotNoDesc": "No hay descripción disponible para este spot",
+ "shopNoDesc": "No hay descripción disponible para esta tienda",
+ "barNoDesc": "No hay descripción disponible para este bar"
+ },
+ "auth": {
+ "login": {
+ "headTitle": "Conexión | BeerCrackerz",
+ "subtitle": "Conectarse",
+ "hiddenError": "¡Vaya! ¡Un texto oculto!",
+ "username": "Nombre de usuario",
+ "password": "Contraseña",
+ "login": "Conectarse",
+ "notRegistered": "¿Aún no estás registrado?",
+ "register": "Crear una cuenta",
+ "forgot": "¿Contraseña olvidada?",
+ "reset": "Reinicialo",
+ "bothEmpty": "Por favor ingrese un nombre de usuario y contraseña",
+ "usernameEmpty": "Por favor, ingrese un nombre de usuario",
+ "passwordEmpty": "Por favor inserte su contraseña",
+ "credsInvalid": "Nombre de usuario o contraseña incorrectos",
+ "serverError": "Se ha producido un error en el servidor, póngase en contacto con el soporte",
+ "checkMail": "Se ha enviado un correo electrónico para que pueda continuar"
+ },
+ "register": {
+ "headTitle": "Inscribirse | BeerCrackerz",
+ "subtitle": "Inscribirse",
+ "hiddenError": "¡Vaya! ¡Un texto oculto!",
+ "username": "Nombre del usuario",
+ "mail": "Correo electrónico",
+ "password1": "Contraseña",
+ "password2": "Confirmar la contraseña",
+ "register": "Inscribirse",
+ "notRegistered": "¿Ya inscrito?",
+ "login": "Conectarse",
+ "fieldEmpty": "Por favor complete todos los campos del formulario",
+ "notMatchingPassword": "Las dos contraseñas no coinciden",
+ "activationSuccess": "¡Su cuenta ha sido activada exitosamente!",
+ "activationError": "Error al activar tu cuenta"
+ },
+ "forgotPassword": {
+ "headTitle": "Contraseña olvidada | BeerCrackerz",
+ "subtitle": "Contraseña olvidada",
+ "hiddenError": "¡Vaya! ¡Un texto oculto!",
+ "mail": "Correo electrónico",
+ "submit": "Restablecer su contraseña",
+ "loginLabel": "¿Tu recuerdas?",
+ "login": "Conectarse",
+ "fieldEmpty": "Ingrese su dirección de correo electrónico",
+ "serverError": "Se ha producido un error en el servidor, póngase en contacto con el soporte"
+ },
+ "resetPassword": {
+ "headTitle": "Restablecer la contraseña | BeerCrackerz",
+ "subtitle": "Restablecer la contraseña",
+ "hiddenError": "¡Vaya! ¡Un texto oculto!",
+ "password1": "Contraseña",
+ "password2": "Confirmar la contraseña",
+ "reset": "Restablecer su contraseña",
+ "loginLabel": "¿Tu recuerdas?",
+ "login": "Conectarse",
+ "fieldEmpty": "Por favor complete todos los campos del formulario",
+ "notMatchingPassword": "Las dos contraseñas no coinciden",
+ "serverError": "Se ha producido un error en el servidor, póngase en contacto con el soporte"
+ }
+ }
+}
diff --git a/static/nls/fr.json b/static/nls/fr.json
new file mode 100644
index 0000000..ee4c221
--- /dev/null
+++ b/static/nls/fr.json
@@ -0,0 +1,244 @@
+{
+ "debug": {
+ "lat": "Latitude",
+ "lng": "Longitude",
+ "updates": "Mises à jours",
+ "accuracy": "Précision",
+ "highAccuracy": "Haute précision",
+ "posAge": "Durée maximale de la position",
+ "posTimeout": "Durée d'échance de la position",
+ "zoom": "Zoom",
+ "enabled": "Activé",
+ "disabled": "Désactivé",
+ "marks": "Points"
+ },
+ "notif": {
+ "geolocationError": "Votre navigateur ne suporte pas l'API de geolocalisation",
+ "newMarkerOutside": "Nouveau point hors de votre porté",
+ "spotAdded": "Nouveau spot ajouté à la carte",
+ "spotNotAdded": "Impossible d'ajouter le spot",
+ "shopAdded": "Nouveau magasin ajouté à la carte",
+ "shopNotAdded": "Impossible d'ajouter le magasin",
+ "barAdded": "Nouveau bar ajouté à la carte",
+ "barNotAdded": "Impossible d'ajouter le bar",
+ "spotEdited": "Spot édité! Merci",
+ "spotNotEdited": "Impossible d'éditer le spot",
+ "shopEdited": "Magasin édité! Merci",
+ "shopNotEdited": "Impossible d'éditer le magasin",
+ "barEdited": "Bar édité! Merci",
+ "barNotEdited": "Impossible d'éditer le bar",
+ "spotDeleted": "Spot supprimé",
+ "spotNotDeleted": "Impossible de supprimer le spot",
+ "shopDeleted": "Magasin supprimé",
+ "shopNotDeleted": "Impossible de supprimer le magasin",
+ "barDeleted": "Bar supprimé",
+ "barNotDeleted": "Impossible de supprimer le bar",
+ "markNameEmpty": "Préciser un nom pour le point",
+ "markTypeEmpty": "Préciser un type pour le point",
+ "markRateEmpty": "Donnez une note pour le point",
+ "markPriceEmpty": "Donnez un prix pour le point",
+ "lockFocusOn": "Centrer et verrouiller sur la position",
+ "unlockFocusOn": "Fin du verrouillage de la position",
+ "uploadPPSuccess": "Photo de profil téléversée avec succès",
+ "uploadPPFailed": "Impossible de téléverser la photo de profil",
+ "welcomeBack": "Content de vous revoir sur BeerCrackerz!"
+ },
+ "nav": {
+ "add": "Ajouter",
+ "edit": "Éditer",
+ "upload": "Téléverser",
+ "cancel": "Annuler",
+ "close": "Fermer",
+ "delete": "Supprimer"
+ },
+ "map": {
+ "newTitle": "Nouveau point",
+ "newSpot": "Ajouter un spot",
+ "newShop": "Ajouter un magasin",
+ "newBar": "Ajouter un bar",
+ "planLayerOSM": "Plan OSM",
+ "satLayerEsri": "Satellite ESRI"
+ },
+ "spot": {
+ "addTitle": "Nouveau spot",
+ "editTitle": "Éditer le spot",
+ "subtitle": "Un spot est un endroit remarquable pour se craquer une bière ! Partager le avec la communauté, que ce soit pour sa vue ou pour n'importe quelle autre raison, en faisant l'endroit parfait pour y boire une bière!",
+ "nameLabel": "Nommer ce spot",
+ "descLabel": "Pourquoi ne pas le décrire une peu",
+ "rateLabel": "Lui attribuer une note",
+ "typeLabel": "Quel est le type du spot",
+ "forestType": "Fôret",
+ "riverType": "Rivière",
+ "lakeType": "Lac",
+ "cliffType": "Falaise",
+ "mountainType": "Montagne",
+ "beachType": "Plage",
+ "seaType": "Mer",
+ "cityType": "Ville",
+ "povType": "Point de vue",
+ "modifiersLabel": "Caractéristiques particulières au spot",
+ "benchModifier": "Banc",
+ "coveredModifier": "Couvert",
+ "toiletModifier": "Toilette",
+ "storeModifier": "Magasin à proximité",
+ "trashModifier": "Poubelle",
+ "parkingModifier": "Parking"
+ },
+ "shop": {
+ "addTitle": "Nouveau magasin",
+ "editTitle": "Éditer le magasin",
+ "subtitle": "L'endroit parfait pour refaire le stock de bière. Le plus d'information vous fournissez, le plus d'amateurs du craquage de bière seront satisfaits!",
+ "nameLabel": "Nom du magasin",
+ "descLabel": "Pourquoi ne pas le décrire une peu",
+ "rateLabel": "Lui attribuer une note",
+ "priceLabel": "Est-il cher",
+ "typeLabel": "Quel est le type du magasin",
+ "storeType": "Épicerie",
+ "superType": "Supermarché",
+ "hyperType": "Hypermarché",
+ "cellarType": "Cave",
+ "modifiersLabel": "Caractéristiques particulières au magasin",
+ "bioModifier": "Bio",
+ "craftModifier": "Artisanal",
+ "freshModifier": "Réfrigéré",
+ "cardModifier": "Carte de crédit",
+ "choiceModifier": "Large choix"
+ },
+ "bar": {
+ "addTitle": "Nouveau bar",
+ "editTitle": "Éditer le bar",
+ "subtitle": "Un bar est le sain endroit ou vous pouvez obtenir le breuvage des dieux à la pression!",
+ "nameLabel": "Nom du bar",
+ "descLabel": "Pourquoi ne pas le décrire une peu",
+ "rateLabel": "Lui attribuer une note",
+ "priceLabel": "Est-il cher",
+ "typeLabel": "Quel est le type du bar",
+ "regularType": "Classique",
+ "snackType": "Snack",
+ "cellarType": "Brasserie",
+ "rooftopType": "Rooftop",
+ "modifiersLabel": "Caractéristiques particulières au bar",
+ "tobaccoModifier": "Cigarettes",
+ "foodModifier": "Nourriture",
+ "cardModifier": "Carte de crédit",
+ "choiceModifier": "Large choix",
+ "outdoorModifier": "Extérieur"
+ },
+ "modal": {
+ "userTitle": "Compte utilisateur",
+ "userAccuracyPref": "Haute précision",
+ "darkThemePref": "Theme sombre",
+ "userDebugPref": "Interface de debug",
+ "startupHelp": "Aide au démarrage",
+ "langPref": "Langue de l'interface",
+ "langFr": "🇫🇷 Français",
+ "langEn": "🇬🇧 Anglais",
+ "langEs": "🇪🇸 Espagnol",
+ "langDe": "🇩🇪 Allemand",
+ "langIt": "🇩🇪 Italien",
+ "langPt": "🇵🇹 Portuguais",
+ "aboutTitle": "À propos de BeerCrackerz",
+ "aboutDesc": "Un idée brillant du grand David Béché! BeerCrackerz, c'est la communauté des amoureux de la bière et du plein air, des pourfendeurs de pinte, des déglingos de la céréale.
Beercrackerz – v0.1.0 – MBP 2022/2023",
+ "updatePP": "Mettre à jour la photo de profil",
+ "updatePPSizeError": "Le poid de l'image doit être inférieur à 2.5 Mo",
+ "updatePPDimensionError": "La taille minimale de l'image est de 512x512",
+ "updatePPTitle": "Nouvelle photo de profil",
+ "updatePPDesc": "Rogner l'image selon vos besoin et c'est parti!",
+ "logout": "Se déconnecter",
+ "hideShowTitle": "Options de la carte",
+ "hideShowSpots": "Spots",
+ "hideShowShops": "Magasins",
+ "hideShowBars": "Bars",
+ "hideShowHelperLabel": "Cliquer sur un icône pour en afficher la description",
+ "spotHelperHideShow": "Pour cacher ou aficher tout les spots de la carte.",
+ "shopHelperHideShow": "Pour cacher ou aficher tout les magasins de la carte.",
+ "barHelperHideShow": "Pour cacher ou aficher tout les bars de la carte.",
+ "deleteMarkTitle": "Supprimer le point",
+ "deleteMarkDesc": "Ètes vous sûr de vouloir supprimer ce point? Cette action est permanente et irréversible.",
+ "spotEditTitle": "Éditer le spot",
+ "shopEditTitle": "Éditer le magasin",
+ "barEditTitle": "Éditer le bar",
+ "helpTitle": "Découvrir BeerCrackerz",
+ "helpNavNext": "Naviguez jusqu'à la page suivante, ou quittez ce menu d'aide!",
+ "helpNavNextPrev": "Naviguez jusqu'à la page suivante ou précèdante, ou quittez ce menu d'aide!",
+ "helpNavEnd": "Vous pouvez maintenant fermer cet assistant!",
+ "helpPage1": "Bienvenue sur BeerCrackerz! Voyons comment marche la carte ; votre position est marquée par le symbole . Autour de votre position, un cercle bleu ; ce dernier représente la précision actuelle de votre position. Autour de vous, il y a également un cercle doré ; c'est la distance maximale pour laquelle vous pouvez déclarer un nouveau spot, un magasin ou un bar.",
+ "helpPage2": "Un spot est représenté par le symbole . Les spots sont les endroit ideaux pour deguster une cannette. Chaque spot à ses specificité, a vous de les découvrir.",
+ "helpPage3": "Un magasin est représenté par le symbole . Les spots sont les endroit ideaux pour se ravitailler en cannettes. Chaque magasin à ses specificités, à vous de les découvrir.",
+ "helpPage4": "Un bar est représenté par le symbole . Les bars sont des endroit conviviaux qui servent de fraîche et bonnes bières. Chaque bar à ses specificités, à vous de les découvrir.",
+ "helpPage5": "Plusieurs commandes sont à votre disposition : 1. choix du fond de carte, 2. recentrer et vérouiller la carte sur votre position, 3. afficher ou cacher les éléments de la carte et 4. votre compte.",
+ "helpQuit": "Quitter",
+ "helpQuitNoSee": "Quitter et ne plus revoir"
+ },
+ "popup": {
+ "spotFoundBy": "Un spot découvert par",
+ "spotFoundWhen": "Depuis le",
+ "shopFoundBy": "Un magasin ajouté par",
+ "shopFoundWhen": "Depuis le",
+ "barFoundBy": "Un bar ajouté par",
+ "barFoundWhen": "Depuis le",
+ "spotNoDesc": "Pas de description disponible pour ce spot",
+ "shopNoDesc": "Pas de description disponible pour ce magasin",
+ "barNoDesc": "Pas de description disponible pour ce bar"
+ },
+ "auth": {
+ "login": {
+ "headTitle": "Connexion | BeerCrackerz",
+ "subtitle": "Se connecter",
+ "hiddenError": "Oh! Un texte caché!",
+ "username": "Nom d'utilisateur",
+ "password": "Mot de passe",
+ "login": "Se connecter",
+ "notRegistered": "Pas encore inscrit?",
+ "register": "Créer un compte",
+ "forgot": "Mot de passe oublié?",
+ "reset": "Le réinitialiser",
+ "bothEmpty": "Veuillez saisir un nom d'utilisateur et un mot de passe",
+ "usernameEmpty": "Veuillez saisir un nom d'utilisateur",
+ "passwordEmpty": "Veuillez saisir votre mot de passe",
+ "credsInvalid": "Nom d'utilisateur ou mot de passe incorrect",
+ "serverError": "Une erreur serveur est survenue, contactez le support",
+ "checkMail": "Consultez vos email avant de continuer"
+ },
+ "register": {
+ "headTitle": "S'inscrire | BeerCrackerz",
+ "subtitle": "S'inscrire",
+ "hiddenError": "Oh! Un texte caché!",
+ "username": "Nom d'utilisateur",
+ "mail": "Adresse email",
+ "password1": "Mot de passe",
+ "password2": "Confirmer le mot de passe",
+ "register": "S'inscrire",
+ "notRegistered": "Déjà inscrit?",
+ "login": "Se connecter",
+ "fieldEmpty": "Veuillez remplir tous les champs du formulaire",
+ "notMatchingPassword": "Les deux mots de passe ne correspondent pas",
+ "activationSuccess": "Votre compte à été activé avec succès!",
+ "activationError": "Erreur lors de l'activation de votre compte"
+ },
+ "forgotPassword": {
+ "headTitle": "Mot de passe oublié | BeerCrackerz",
+ "subtitle": "Mot de passe oublié",
+ "hiddenError": "Oh! Un texte caché!",
+ "mail": "Mail",
+ "submit": "Réinitialiser votre mot de passe",
+ "loginLabel": "Vous vous en souvenez?",
+ "login": "Se connecter",
+ "fieldEmpty": "Renseignez votre adresse mail",
+ "serverError": "Une erreur serveur est survenue, contactez le support"
+ },
+ "resetPassword": {
+ "headTitle": "Réinitialiser le mot de passe | BeerCrackerz",
+ "subtitle": "Réinitialiser le mot de passe",
+ "hiddenError": "Oh! Un texte caché!",
+ "password1": "Mot de passe",
+ "password2": "Confirmer le mot de passe",
+ "reset": "Réinitialiser votre mot de passe",
+ "loginLabel": "Vous vous en souvenez?",
+ "login": "Se connecter",
+ "fieldEmpty": "Veuillez remplir tous les champs du formulaire",
+ "notMatchingPassword": "Les deux mots de passe ne correspondent pas",
+ "serverError": "Une erreur serveur est survenue, contactez le support"
+ }
+ }
+}
diff --git a/static/nls/it.json b/static/nls/it.json
new file mode 100644
index 0000000..5a360ce
--- /dev/null
+++ b/static/nls/it.json
@@ -0,0 +1,244 @@
+{
+ "debug": {
+ "lat": "Latitudine",
+ "lng": "Longitudine",
+ "updates": "Aggiornamenti",
+ "accuracy": "Precisione",
+ "highAccuracy": "Alta precisione",
+ "posAge": "Posizione età massima",
+ "posTimeout": "Scadenza posizione",
+ "zoom": "Ingrandisci",
+ "enabled": "Abilitato",
+ "disabled": "Disabilitato",
+ "marks": "Marcatore"
+ },
+ "notif": {
+ "geolocationError": "Il tuo browser non implementa l'API di geolocalizzazione",
+ "newMarkerOutside": "Nuovo marcatore fuori dalla tua portata",
+ "spotAdded": "Nuovo spot salvato sulla mappa",
+ "spotNotAdded": "Impossibile aggiungere lo spot",
+ "shopAdded": "Nuovo negozio salvato sulla mappa",
+ "shopNotAdded": "Impossibile aggiungere il negozio",
+ "barAdded": "Nuova barra salvata sulla mappa",
+ "barNotAdded": "Impossibile aggiungere la barra",
+ "spotEdited": "Spot modificato! Grazie",
+ "spotNotEdited": "Impossibile modificare il spot",
+ "shopEdited": "Negozio modificato! Grazie",
+ "shopNotEdited": "Impossibile modificare il negozio",
+ "barEdited": "Barra modificata! Grazie",
+ "barNotEdited": "Impossibile modificare la barra",
+ "spotDeleted": "Spot eliminato",
+ "spotNotDeleted": "Impossibile eliminare lo spot",
+ "shopDeleted": "Negozio eliminato",
+ "shopNotDeleted": "Impossibile eliminare il negozio",
+ "barDeleted": "Barra eliminata",
+ "barNotDeleted": "Impossibile eliminare la barra",
+ "markNameEmpty": "Non hai specificato un nome per il marcatore",
+ "markTypeEmpty": "Non hai specificato un tipo per il marcatore",
+ "markRateEmpty": "Non hai specificato una valutazione per il marcatore",
+ "markPriceEmpty": "Non hai specificato un prezzo per il marcatore",
+ "lockFocusOn": "Centratura e blocco vista sulla tua posizione",
+ "unlockFocusOn": "Blocco della posizione terminato",
+ "uploadPPSuccess": "Immagine del profilo aggiornata",
+ "uploadPPFailed": "Impossibile caricare l'immagine del profilo",
+ "welcomeBack": "Felice di rivederti su BeerCrackerz!"
+ },
+ "nav": {
+ "add": "Aggiungere",
+ "edit": "Modificare",
+ "upload": "Caricamento",
+ "cancel": "Annulla",
+ "close": "Vicino",
+ "delete": "Eliminare"
+ },
+ "map": {
+ "newTitle": "Nuovo marcatore",
+ "newSpot": "Aggiungi un spot",
+ "newShop": "Aggiungi un negozio",
+ "newBar": "Aggiungi una barra",
+ "planLayerOSM": "Carte OSM",
+ "satLayerEsri": "Satellitare ESRI"
+ },
+ "spot": {
+ "addTitle": "Nuovo spot",
+ "editTitle": "Modifica spot",
+ "subtitle": "Un spot è un posto straordinario per rompere una birra! Condividilo con la community, che sia per la vista mozzafiato o per quello che è piacevole bere una birra!",
+ "nameLabel": "Dai un nome a quel spot",
+ "descLabel": "Perché non lo descrivi",
+ "rateLabel": "Dagli una nota",
+ "typeLabel": "Che tipo di spot è",
+ "forestType": "Foresta",
+ "riverType": "Fiume",
+ "lakeType": "Lago",
+ "cliffType": "Scogliera",
+ "mountainType": "Montagna",
+ "beachType": "Spiaggia",
+ "seaType": "Mare",
+ "cityType": "Città",
+ "povType": "Punto di vista",
+ "modifiersLabel": "Ha delle caratteristiche specifiche",
+ "benchModifier": "Panca",
+ "coveredModifier": "Coperto",
+ "toiletModifier": "Toilette",
+ "storeModifier": "Negozio nelle vicinanze",
+ "trashModifier": "Spazzatura",
+ "parkingModifier": "Parcheggio"
+ },
+ "shop": {
+ "addTitle": "Nuovo negozio",
+ "editTitle": "Modifica negozio",
+ "subtitle": "The must have place to refill your beer stock. The more info you provide, the better you help your fellow beercrackerz!",
+ "nameLabel": "Nome del negozio",
+ "descLabel": "Perché non lo descrivi",
+ "rateLabel": "Dagli una nota",
+ "priceLabel": "Quanto costa",
+ "typeLabel": "Che tipo di negozio è",
+ "storeType": "Negozio di alimentari",
+ "superType": "Supermercato",
+ "hyperType": "Ipermercato",
+ "cellarType": "Cantina",
+ "modifiersLabel": "Ha delle caratteristiche specifiche",
+ "bioModifier": "Bio",
+ "craftModifier": "Artigianale",
+ "freshModifier": "Refrigerato",
+ "cardModifier": "Carta di credito",
+ "choiceModifier": "Ampia scelta"
+ },
+ "bar": {
+ "addTitle": "Nuova barra",
+ "editTitle": "Modifica barra",
+ "subtitle": "Un bar è un luogo sacro dove puoi prendere delle birre alla spina ben fredde!",
+ "nameLabel": "Nome della barra",
+ "descLabel": "Perché non lo descrivi",
+ "rateLabel": "Dagli una nota",
+ "priceLabel": "Quanto costa",
+ "typeLabel": "Che tipo di bar è",
+ "regularType": "Regolare",
+ "snackType": "Merenda",
+ "cellarType": "Birrificio",
+ "rooftopType": "Tetto",
+ "modifiersLabel": "Ha delle caratteristiche specifiche",
+ "tobaccoModifier": "Sigarette",
+ "foodModifier": "Cibo",
+ "cardModifier": "Carta di credito",
+ "choiceModifier": "Ampia scelta",
+ "outdoorModifier": "All'aperto"
+ },
+ "modal": {
+ "userTitle": "Account utente",
+ "userAccuracyPref": "Alta precisione",
+ "darkThemePref": "Tema scuro",
+ "userDebugPref": "Interfaccia di debug",
+ "startupHelp": "Aiuto iniziale",
+ "langPref": "Linguaggio dell'interfaccia",
+ "langFr": "🇫🇷 Francese",
+ "langEn": "🇬🇧 Inglese",
+ "langEs": "🇪🇸 Spagnolo",
+ "langDe": "🇩🇪 Tedesco",
+ "langIt": "🇩🇪 Italiano",
+ "langPt": "🇵🇹 Portoghese",
+ "aboutTitle": "A proposito di BeerCrackerz",
+ "aboutDesc": "Un'idea geniale di David Béché! BeerCrackerz è la comunità degli amanti della birra, piena di cacciatori di pinte e amanti dei cereali.
Beercrackerz – v0.1.0 – MBP 2022/2023",
+ "updatePP": "Aggiorna la tua immagine del profilo",
+ "updatePPSizeError": "Seleziona un'immagine inferiore a 2.5Mo",
+ "updatePPDimensionError": "La dimensione minima dell'immagine è 512x512",
+ "updatePPTitle": "Nuova immagine dell'immagine del profilo",
+ "updatePPDesc": "Ritaglia l'immagine se necessario e vai!",
+ "logout": "Disconnessione",
+ "hideShowTitle": "Opzioni mappa",
+ "hideShowSpots": "Spots",
+ "hideShowShops": "Negozi",
+ "hideShowBars": "Barre",
+ "hideShowHelperLabel": "Fare clic su un'icona per visualizzarne la descrizione",
+ "spotHelperHideShow": "Per nascondere o mostrare tutti i spots sulla mappa.",
+ "shopHelperHideShow": "Per nascondere o mostrare tutti i negozzi sulla mappa.",
+ "barHelperHideShow": "Per nascondere o mostrare tutte le barre sulla mappa.",
+ "deleteMarkTitle": "Elimina marcatore",
+ "deleteMarkDesc": "Sei sicuro di voler eliminare questo marchio? Questa azione è permanente e non può essere annullata.",
+ "spotEditTitle": "Modifica spot",
+ "shopEditTitle": "Modifica negozio",
+ "barEditTitle": "Modifica barra",
+ "helpTitle": "Scopri BeerCrackerz",
+ "helpNavNext": "Vai alla pagina successiva o esci da questo menu di aiuto!",
+ "helpNavNextPrev": "Vai alla pagina successiva o precedente o esci da questo menu di aiuto!",
+ "helpNavEnd": "Ora puoi chiudere questa procedura guidata!",
+ "helpPage1": "Benvenuto in BeerCrackerz! Vediamo come funziona la mappa; la tua posizione è contrassegnata dal simbolo . Intorno alla tua posizione, un cerchio blu rappresenta la precisione attuale della tua posizione. Intorno a te c'è anche un cerchio d'oro ; questa è la distanza massima per la quale puoi dichiarare un nuovo spot, un negozio o un bar.",
+ "helpPage2": "Un spot è rappresentato dal simbolo . I punti sono luoghi perfetti per godersi una lattina di birra. Ogni spot ha le sue specificità, sta a te scoprirle.",
+ "helpPage3": "Un negozio è rappresentato dal simbolo . I negozi sono i luoghi ideali per fare scorta di lattine di birra. Ogni negozio ha le sue specificità, sta a te scoprirle.",
+ "helpPage4": "Una barra è rappresentata dal simbolo . I bar sono luoghi accoglienti che servono birre fresche e buone. Ogni bar ha le sue specificità, sta a te scoprirle.",
+ "helpPage5": "Sono disponibili diversi comandi: 1. scelta della mappa di base, 2. ricentra e blocca la mappa sulla tua posizione, 3. mostra o nascondi gli elementi della mappa e 4. il tuo account.",
+ "helpQuit": "Quit",
+ "helpQuitNoSee": "Esci e non vedere più"
+ },
+ "popup": {
+ "spotFoundBy": "Un spot scoperto da",
+ "spotFoundWhen": "Dal",
+ "shopFoundBy": "Un negozio aggiunto da",
+ "shopFoundWhen": "Dal",
+ "barFoundBy": "Una barra aggiunta da",
+ "barFoundWhen": "Dal",
+ "spotNoDesc": "Nessuna descrizione disponibile per questo spot",
+ "shopNoDesc": "Nessuna descrizione disponibile per questo negozio",
+ "barNoDesc": "Nessuna descrizione disponibile per questa barra"
+ },
+ "auth": {
+ "login": {
+ "headTitle": "Login | BeerCrackerz",
+ "subtitle": "Login",
+ "hiddenError": "Oh! Un testo nascosto!",
+ "username": "Nome utente",
+ "password": "Password",
+ "login": "Login",
+ "notRegistered": "Non sei ancora registrato?",
+ "register": "Creare un account",
+ "forgot": "Ha dimenticato la password",
+ "reset": "Ripristinalo",
+ "bothEmpty": "Si prega di compilare i campi nome utente e password",
+ "usernameEmpty": "Si prega di compilare il campo del nome utente",
+ "passwordEmpty": "Si prega di compilare il campo della password",
+ "credsInvalid": "Credenziali non valide",
+ "serverError": "È successo qualcosa di sbagliato, contatta l'assistenza",
+ "checkMail": "È stata inviata un'e-mail in modo da poter procedere"
+ },
+ "register": {
+ "headTitle": "Registrati | BeerCrackerz",
+ "subtitle": "Creare un account",
+ "hiddenError": "Oh! Un testo nascosto!",
+ "username": "Nome utente",
+ "mail": "Indirizzo e-mail",
+ "password1": "Password",
+ "password2": "Conferma password",
+ "register": "Registrati",
+ "notRegistered": "Già registrato??",
+ "login": "Login",
+ "fieldEmpty": "Si prega di compilare tutti i campi obbligatori",
+ "notMatchingPassword": "Le password non corrispondono",
+ "activationSuccess": "Il tuo account è stato creato con successo!",
+ "activationError": "È successo qualcosa di sbagliato durante la creazione del tuo account, contatta l'assistenza"
+ },
+ "forgotPassword": {
+ "headTitle": "Ha dimenticato la password | BeerCrackerz",
+ "subtitle": "Ha dimenticato la password",
+ "hiddenError": "Oh! Un testo nascosto!",
+ "mail": "Email",
+ "submit": "Resetta la password",
+ "loginLabel": "Te lo ricordi?",
+ "login": "Login",
+ "fieldEmpty": "Si prega di compilare tutti i campi obbligatori",
+ "serverError": "È successo qualcosa di sbagliato, contatta l'assistenza"
+ },
+ "resetPassword": {
+ "headTitle": "Resetta la password | BeerCrackerz",
+ "subtitle": "Resetta la password",
+ "hiddenError": "Oh! Un testo nascosto!",
+ "password1": "Nuova password",
+ "password2": "Conferma password",
+ "reset": "Resetta la password",
+ "loginLabel": "Te lo ricordi?",
+ "login": "Login",
+ "fieldEmpty": "Per favore compila tutti i campi",
+ "notMatchingPassword": "Le password non corrispondono",
+ "serverError": "È successo qualcosa di sbagliato, contatta l'assistenza"
+ }
+ }
+}
diff --git a/static/nls/pt.json b/static/nls/pt.json
new file mode 100644
index 0000000..d53dd30
--- /dev/null
+++ b/static/nls/pt.json
@@ -0,0 +1,244 @@
+{
+ "debug": {
+ "lat": "Latitude",
+ "lng": "Longitude",
+ "updates": "Atualizações",
+ "accuracy": "Precisão",
+ "highAccuracy": "Alta precisão",
+ "posAge": "Idade máxima da posição",
+ "posTimeout": "Tempo limite da posição",
+ "zoom": "Zoom",
+ "enabled": "Habilitado",
+ "disabled": "Desabilitado",
+ "marks": "Marcas"
+ },
+ "notif": {
+ "geolocationError": "Seu navegador não implementa a API de geolocalização",
+ "newMarkerOutside": "Novo marcador fora do seu alcance",
+ "spotAdded": "Novo spot salvo no mapa",
+ "spotNotAdded": "Não foi possível adicionar o spot",
+ "shopAdded": "Nova loja salva no mapa",
+ "shopNotAdded": "Não foi possível adicionar a loja",
+ "barAdded": "Nova barra salva no mapa",
+ "barNotAdded": "Não foi possível adicionar a barra",
+ "spotEdited": "Spot editado! Obrigado",
+ "spotNotEdited": "Não foi possível editar o spot",
+ "shopEdited": "Loja editada! Obrigado",
+ "shopNotEdited": "Não foi possível editar a loja",
+ "barEdited": "Barra editada! Obrigado",
+ "barNotEdited": "Não foi possível editar a barra",
+ "spotDeleted": "Spot excluído",
+ "spotNotDeleted": "Não foi possível excluir o spot",
+ "shopDeleted": "Loja excluído",
+ "shopNotDeleted": "Não foi possível excluir a loja",
+ "barDeleted": "Barra excluído",
+ "barNotDeleted": "Não foi possível excluir a barra",
+ "markNameEmpty": "Você não especificou um nome para a marca",
+ "markTypeEmpty": "Você não especificou um tipo para a marca",
+ "markRateEmpty": "Você não especificou uma taxa para a marca",
+ "markPriceEmpty": "Você não especificou um preço para a marca",
+ "lockFocusOn": "Centralizando e bloqueando a visualização em sua posição",
+ "unlockFocusOn": "Bloqueio de posição finalizado",
+ "uploadPPSuccess": "Foto do perfil atualizada",
+ "uploadPPFailed": "Falha ao carregar a foto do perfil",
+ "welcomeBack": "Fico feliz em vê-lo de volta no BeerCrackerz!"
+ },
+ "nav": {
+ "add": "Adicionar",
+ "edit": "Editar",
+ "upload": "Carregar",
+ "cancel": "Cancelar",
+ "close": "Perto",
+ "delete": "Excluir"
+ },
+ "map": {
+ "newTitle": "Novo marcador",
+ "newSpot": "Adicionar um spot",
+ "newShop": "Adicionar uma loja",
+ "newBar": "Adicionar uma bara",
+ "planLayerOSM": "Plano OSM",
+ "satLayerEsri": "Satélite ESRI"
+ },
+ "spot": {
+ "addTitle": "Novo spot",
+ "editTitle": "Editar spot",
+ "subtitle": "Um spot é um lugar notável para quebrar uma cerveja! Compartilhe com a comunidade, seja pela vista deslumbrante seja pelo prazer de beber uma cervejinha!",
+ "nameLabel": "Nomeie esse spot",
+ "descLabel": "Por que você não descreve",
+ "rateLabel": "Dê uma nota",
+ "typeLabel": "Que tipo de spot é",
+ "forestType": "Floresta",
+ "riverType": "Rio",
+ "cliffType": "Penhasco",
+ "lakeType": "Lago",
+ "mountainType": "Montanha",
+ "beachType": "Praia",
+ "seaType": "Mar",
+ "cityType": "Cidade",
+ "povType": "Ponto de vista",
+ "modifiersLabel": "Tem alguma característica específica",
+ "benchModifier": "Banco",
+ "coveredModifier": "Abordado",
+ "toiletModifier": "Toalete",
+ "storeModifier": "Loja próxima",
+ "trashModifier": "Lixo",
+ "parkingModifier": "Estacionamento"
+ },
+ "shop": {
+ "addTitle": "Nova loja",
+ "editTitle": "Editar loja",
+ "subtitle": "O deve ter lugar para reabastecer seu estoque de cerveja. Quanto mais informações você fornecer, melhor você ajudará seus colegas crackerz!",
+ "nameLabel": "Nome da loja",
+ "descLabel": "Por que você não descreve",
+ "rateLabel": "Dê uma nota",
+ "priceLabel": "Quão caro é",
+ "typeLabel": "Que tipo de loja é",
+ "storeType": "Bomboneria",
+ "superType": "Supermercado",
+ "hyperType": "Hipermercado",
+ "cellarType": "Porão",
+ "modifiersLabel": "Tem alguma característica específica",
+ "bioModifier": "Bio",
+ "craftModifier": "Artesanal",
+ "freshModifier": "Refrigerado",
+ "cardModifier": "Cartão de crédito",
+ "choiceModifier": "Ampla escolha"
+ },
+ "bar": {
+ "addTitle": "Nova barra",
+ "editTitle": "Editar barra",
+ "subtitle": "Um bar é um lugar sagrado onde você pode tomar alguns chopes bem gelados!",
+ "nameLabel": "Nome do bar",
+ "descLabel": "Por que você não descreve",
+ "rateLabel": "Dê uma nota",
+ "priceLabel": "Quão caro é",
+ "typeLabel": "Que tipo de bara é",
+ "regularType": "Regular",
+ "snackType": "Lanche",
+ "cellarType": "Cervejaria",
+ "rooftopType": "Cobertura",
+ "modifiersLabel": "Tem alguma característica específica",
+ "tobaccoModifier": "Cigarros",
+ "foodModifier": "Comida",
+ "cardModifier": "Cartão de crédito",
+ "choiceModifier": "Ampla escolha",
+ "outdoorModifier": "Terraço"
+ },
+ "modal": {
+ "userTitle": "Conta de usuário",
+ "userAccuracyPref": "Alta precisão",
+ "darkThemePref": "Tema escuro",
+ "userDebugPref": "Interface de depuração",
+ "startupHelp": "Começando",
+ "langPref": "Idioma da interface",
+ "langFr": "🇫🇷 Francês",
+ "langEn": "🇬🇧 Inglês",
+ "langEs": "🇪🇸 Espanhol",
+ "langDe": "🇩🇪 Alemão",
+ "langIt": "🇩🇪 Italiano",
+ "langPt": "🇵🇹 Português",
+ "aboutTitle": "Sobre BeerCrackerz",
+ "aboutDesc": "Uma ideia brilhante de David Béché! BeerCrackerz é a comunidade dos amantes da cerveja, cheia de caçadores de cerveja e amantes de cereais.
Beercrackerz – v0.1.0 – MBP 2022/2023",
+ "updatePP": "Atualize sua foto de perfil",
+ "updatePPSizeError": "Selecione uma imagem abaixo de 2,5 Mo",
+ "updatePPDimensionError": "O tamanho mínimo da imagem é 512x512",
+ "updatePPTitle": "Nova foto de perfil",
+ "updatePPDesc": "Recorte a imagem, se necessário, e pronto!",
+ "logout": "Sair",
+ "hideShowTitle": "Opções do mapa",
+ "hideShowSpots": "Spots",
+ "hideShowShops": "Lojas",
+ "hideShowBars": "Bares",
+ "hideShowHelperLabel": "Clique em um ícone para exibir sua descrição",
+ "spotHelperHideShow": "Para ocultar ou mostrar todos os spots no mapa.",
+ "shopHelperHideShow": "Para ocultar ou mostrar todas as lojas no mapa.",
+ "barHelperHideShow": "Para ocultar ou mostrar todas as barras no mapa.",
+ "deleteMarkTitle": "Excluir marca",
+ "deleteMarkDesc": "Tem certeza de que deseja excluir esta marca? Esta ação é permanente e não pode ser revertida.",
+ "spotEditTitle": "Editar spot",
+ "shopEditTitle": "Editar loja",
+ "barEditTitle": "Editar barra",
+ "helpTitle": "Descubra o BeerCrackerz",
+ "helpNavNext": "Navegue para a próxima página ou saia deste menu de ajuda!",
+ "helpNavNextPrev": "Navegue para a página seguinte ou anterior ou saia deste menu de ajuda!",
+ "helpNavEnd": "Agora você pode fechar este assistente!",
+ "helpPage1": "Bem-vindo ao BeerCrackerz! Vamos ver como o mapa funciona; sua posição é marcada pelo símbolo . Ao redor de sua posição, um círculo azul representa a precisão atual de sua posição. Ao seu redor, há também um círculo dourado ; esta é a distância máxima para a qual você pode declarar um novo spot, uma loja ou um bar.",
+ "helpPage2": "Um spot é representado pelo símbolo . Spots são lugares perfeitos para curtir uma lata de cerveja. Cada spot tem as suas especificidades, cabe-te a ti descobri-las.",
+ "helpPage3": "Uma loja é representada pelo símbolo . As lojas são os locais ideais para estocar latas de cerveja. Cada loja tem suas especificidades, cabe a você descobri-las.",
+ "helpPage4": "Uma barra é representada pelo símbolo . Bares são lugares amigáveis que servem cervejas frescas e boas. Cada barra tem suas especificidades, cabe a você descobri-las.",
+ "helpPage5": "Vários comandos estão disponíveis: 1. escolha do mapa base, 2. recentralizar e bloquear o mapa em sua posição, 3. mostrar ou ocultar itens do mapa e 4. sua conta.",
+ "helpQuit": "Saia",
+ "helpQuitNoSee": "Saia e não veja mais"
+ },
+ "popup": {
+ "spotFoundBy": "Um spot descoberto por",
+ "spotFoundWhen": "Desde o",
+ "shopFoundBy": "Uma loja adicionada por",
+ "shopFoundWhen": "Desde o",
+ "barFoundBy": "Uma barra adicionada por",
+ "barFoundWhen": "Desde o",
+ "spotNoDesc": "Nenhuma descrição disponível para este spot",
+ "shopNoDesc": "Nenhuma descrição disponível para esta loja",
+ "barNoDesc": "Nenhuma descrição disponível para esta barra"
+ },
+ "auth": {
+ "login": {
+ "headTitle": "Conexão | BeerCrackerz",
+ "subtitle": "Se conector",
+ "hiddenError": "Oh! Um texto oculto!",
+ "username": "Nome de usuário",
+ "password": "Senha",
+ "login": "Entrar",
+ "notRegistered": "Não registrado?",
+ "register": "Crie a sua conta aqui",
+ "forgot": "Esqueceu sua senha?",
+ "reset": "Reinicie",
+ "bothEmpty": "Por favor, insira um nome de usuário e senha",
+ "usernameEmpty": "Por favor coloque um nome de usuário",
+ "passwordEmpty": "Por favor, insira sua senha",
+ "credsInvalid": "Nome de utilizador ou palavra-passe incorrectos",
+ "serverError": "Ocorreu um erro no servidor, entre em contato com o suporte",
+ "checkMail": "Um e-mail foi enviado para que você possa prosseguir"
+ },
+ "register": {
+ "headTitle": "Registro | BeerCrackerz",
+ "subtitle": "Registro",
+ "hiddenError": "Oh! Um texto oculto!",
+ "username": "Nome do usuário",
+ "mail": "Endereço de e-mail",
+ "password1": "Senha",
+ "password2": "Confirme a Senha",
+ "register": "Registro",
+ "notRegistered": "Já registrado?",
+ "login": "Entrar",
+ "fieldEmpty": "Por favor, preencha todos os campos do formulário",
+ "notMatchingPassword": "As duas senhas não combinam",
+ "activationSuccess": "Sua conta foi ativada com sucesso!",
+ "activationError": "Erro ao ativar sua conta"
+ },
+ "forgotPassword": {
+ "headTitle": "Esqueceu sua senha | BeerCrackerz",
+ "subtitle": "Esqueceu sua senha",
+ "hiddenError": "Oh! Um texto oculto!",
+ "mail": "O email",
+ "submit": "Redefina sua senha",
+ "loginLabel": "Você lembra?",
+ "login": "Entrar",
+ "fieldEmpty": "Insira o seu endereço de email",
+ "serverError": "Ocorreu um erro no servidor, entre em contato com o suporte"
+ },
+ "resetPassword": {
+ "headTitle": "Redefinir senha | BeerCrackerz",
+ "subtitle": "Redefinir senha",
+ "hiddenError": "Oh! Um texto oculto!",
+ "password1": "Senha",
+ "password2": "Confirme a Senha",
+ "reset": "Redefina sua senha",
+ "loginLabel": "Você lembra?",
+ "login": "Entrar",
+ "fieldEmpty": "Por favor, preencha todos os campos do formulário",
+ "notMatchingPassword": "As duas senhas não combinam",
+ "serverError": "Ocorreu um erro no servidor, entre em contato com o suporte"
+ }
+ }
+}