From b9d6ff877921b3930baf4ce627a750dab6f13fe0 Mon Sep 17 00:00:00 2001 From: Filip Czaplicki Date: Wed, 27 Dec 2023 19:18:22 +0100 Subject: [PATCH] Apply formatting by biome --- .github/workflows/ci_test.yml | 4 +- .github/workflows/deploy_development.yml | 2 + .github/workflows/deploy_production.yml | 2 + biome.json | 11 +- package.json | 2 + public/manifest.json | 117 +-- src/3rdparty/react-store-badges/index.tsx | 189 +++-- src/Main.tsx | 184 +++-- src/appContext.tsx | 54 +- src/backend.ts | 57 +- src/components/downloadCard.tsx | 146 ++-- src/components/footer.tsx | 179 +++-- src/components/languageSwitcher.tsx | 45 +- src/components/legend.tsx | 137 ++-- src/components/logInButton.tsx | 62 +- src/components/map.tsx | 737 +++++++++--------- src/components/map_style.ts | 415 +++++----- src/components/modal.tsx | 277 +++---- src/components/navbar.tsx | 268 ++++--- src/components/nominatimGeocoder.ts | 74 +- src/components/partnersModal.tsx | 196 ++--- src/components/sidebar-left.tsx | 108 +-- src/components/sidebar-right.tsx | 26 +- src/components/sidebar/access.tsx | 94 +-- src/components/sidebar/buttons.tsx | 236 +++--- src/components/sidebar/contactNumber.tsx | 60 +- .../sidebar/defibrillatorDetails.tsx | 419 +++++----- .../sidebar/defibrillatorEditor.tsx | 315 +++++--- src/components/sidebar/detailTextRow.tsx | 28 +- src/components/sidebar/indoor.tsx | 115 +-- src/components/sidebar/location.tsx | 49 +- src/components/sidebar/openingHours.tsx | 199 ++--- src/components/sidebar/photoReporter.tsx | 196 ++--- src/components/sidebar/photoUploader.tsx | 305 ++++---- src/components/sidebar/spanNoData.tsx | 10 +- src/components/sidebar/verificationDate.tsx | 97 +-- src/constants.ts | 2 +- src/i18n.ts | 121 +-- src/index.tsx | 11 +- src/model/auth.ts | 10 +- src/model/buttonsType.ts | 8 +- src/model/country.ts | 10 +- src/model/defibrillatorData.ts | 20 +- src/model/modal.ts | 40 +- src/model/sidebarAction.ts | 14 +- src/osm.ts | 346 ++++---- taginfo.json | 44 +- types/custom.d.ts | 9 +- vite.config.js | 36 +- 49 files changed, 3289 insertions(+), 2797 deletions(-) diff --git a/.github/workflows/ci_test.yml b/.github/workflows/ci_test.yml index 6f7fb58..a33fe5f 100644 --- a/.github/workflows/ci_test.yml +++ b/.github/workflows/ci_test.yml @@ -26,7 +26,7 @@ jobs: run: npm ci - name: Run TypeScript typecheck run: npm run typecheck - - name: Run linter - run: npm run lint + - name: Run linter & formatter + run: npm run check - name: Build run: npm run build diff --git a/.github/workflows/deploy_development.yml b/.github/workflows/deploy_development.yml index c1dc1d4..04605f2 100644 --- a/.github/workflows/deploy_development.yml +++ b/.github/workflows/deploy_development.yml @@ -30,6 +30,8 @@ jobs: cache: 'npm' - name: Install dependencies run: npm ci + - name: Run linter & formatter + run: npm run check - name: Run TypeScript typecheck run: npm run typecheck - name: Set git commit diff --git a/.github/workflows/deploy_production.yml b/.github/workflows/deploy_production.yml index 8188d35..e44de3e 100644 --- a/.github/workflows/deploy_production.yml +++ b/.github/workflows/deploy_production.yml @@ -24,6 +24,8 @@ jobs: cache: 'npm' - name: Install dependencies run: npm ci + - name: Run linter & formatter + run: npm run check - name: Run TypeScript typecheck run: npm run typecheck - name: Set git commit diff --git a/biome.json b/biome.json index 09af2ae..074a38d 100644 --- a/biome.json +++ b/biome.json @@ -1,6 +1,15 @@ { "$schema": "./node_modules/@biomejs/biome/configuration_schema.json", "files": { - "ignore": [".devcontainer", "build"] + "ignore": [".devcontainer", "build", "public/locales", "public/img"] + }, + "organizeImports": { + "enabled": true + }, + "linter": { + "enabled": true, + "rules": { + "recommended": true + } } } diff --git a/package.json b/package.json index 6025376..2b3ec34 100644 --- a/package.json +++ b/package.json @@ -36,6 +36,8 @@ "sprites": "spritezero ./public/img/sprite ./src/marker_icons; spritezero --ratio=2 ./public/img/sprite@2x ./src/marker_icons; spritezero --ratio=4 ./public/img/sprite@4x ./src/marker_icons", "lint": "biome lint .", "lint-fix": "biome lint . --apply", + "format": "biome format . --write", + "check": "biome ci .", "css-build": "sass sass/mystyles.scss src/mystyles.css", "css-watch": "npm run css-build -- --watch" }, diff --git a/public/manifest.json b/public/manifest.json index 68e70bb..69d6924 100644 --- a/public/manifest.json +++ b/public/manifest.json @@ -1,60 +1,61 @@ { - "short_name": "OpenAEDMap", - "name": "Open AED Map", - "icons": [ - { - "src": "favicon.ico", - "sizes": "64x64 32x32 24x24 16x16", - "type": "image/x-icon" - }, - { - "src": "img/logo-aed-120.png", - "type": "image/png", - "sizes": "120x120" - }, - { - "src": "img/logo-aed-144.png", - "type": "image/png", - "sizes": "144x144" - }, - { - "src": "img/logo-aed-152.png", - "type": "image/png", - "sizes": "152x152" - }, - { - "src": "img/logo-aed-180.png", - "type": "image/png", - "sizes": "180x180" - }, - { - "src": "img/logo-aed-192.png", - "type": "image/png", - "sizes": "192x192" - }, - { - "src": "img/logo-aed-384.png", - "type": "image/png", - "sizes": "384x384" - }, - { - "src": "img/logo-aed-512.png", - "type": "image/png", - "sizes": "512x512" - } - ], - "start_url": ".", - "display": "standalone", - "theme_color": "#000000", - "background_color": "#ffffff", - "related_applications": [ - { - "platform": "play", - "url": "https://play.google.com/store/apps/details?id=pl.enteam.aed_map", - "id": "pl.enteam.aed_map" - }, { - "platform": "itunes", - "url": "https://apps.apple.com/app/mapa-aed/id1638495701" - } - ] + "short_name": "OpenAEDMap", + "name": "Open AED Map", + "icons": [ + { + "src": "favicon.ico", + "sizes": "64x64 32x32 24x24 16x16", + "type": "image/x-icon" + }, + { + "src": "img/logo-aed-120.png", + "type": "image/png", + "sizes": "120x120" + }, + { + "src": "img/logo-aed-144.png", + "type": "image/png", + "sizes": "144x144" + }, + { + "src": "img/logo-aed-152.png", + "type": "image/png", + "sizes": "152x152" + }, + { + "src": "img/logo-aed-180.png", + "type": "image/png", + "sizes": "180x180" + }, + { + "src": "img/logo-aed-192.png", + "type": "image/png", + "sizes": "192x192" + }, + { + "src": "img/logo-aed-384.png", + "type": "image/png", + "sizes": "384x384" + }, + { + "src": "img/logo-aed-512.png", + "type": "image/png", + "sizes": "512x512" + } + ], + "start_url": ".", + "display": "standalone", + "theme_color": "#000000", + "background_color": "#ffffff", + "related_applications": [ + { + "platform": "play", + "url": "https://play.google.com/store/apps/details?id=pl.enteam.aed_map", + "id": "pl.enteam.aed_map" + }, + { + "platform": "itunes", + "url": "https://apps.apple.com/app/mapa-aed/id1638495701" + } + ] } diff --git a/src/3rdparty/react-store-badges/index.tsx b/src/3rdparty/react-store-badges/index.tsx index f018a34..fc3a878 100644 --- a/src/3rdparty/react-store-badges/index.tsx +++ b/src/3rdparty/react-store-badges/index.tsx @@ -4,110 +4,133 @@ import React, { FC, useLayoutEffect, useState } from "react"; const HEIGHT_RATIO = 3.375; const getImage = (locale: string, language: string) => ({ - ios: `https://apple-resources.s3.amazonaws.com/media-badges/download-on-the-app-store/black/${locale}.svg`, - android: - `https://raw.githubusercontent.com/yjb94/google-play-badge-svg/master/img/${language}_get.svg?sanitize=true`, + ios: `https://apple-resources.s3.amazonaws.com/media-badges/download-on-the-app-store/black/${locale}.svg`, + android: `https://raw.githubusercontent.com/yjb94/google-play-badge-svg/master/img/${language}_get.svg?sanitize=true`, }); export interface ReactStoreBadgesProps { - /** url of App Store and Play Store */ - url: string + /** url of App Store and Play Store */ + url: string; - /** platform name. 'ios' and 'android' only */ - platform: "ios" | "android" + /** platform name. 'ios' and 'android' only */ + platform: "ios" | "android"; - /** language name. such as en */ - language: string + /** language name. such as en */ + language: string; - /** width for badge size */ - width?: number + /** width for badge size */ + width?: number; - /** height for badge size */ - height?: number + /** height for badge size */ + height?: number; - /** target for url to be opened */ - target?: "_self" | "_blank" | "_parent" | "_top" + /** target for url to be opened */ + target?: "_self" | "_blank" | "_parent" | "_top"; } const defaultLocale = "en-us"; function shortCodeFromLanguage(language: string): string { - switch (language) { - case "uk": return "ua"; - case "zh-Hans": return "zh-cn"; - case "zh-Hant": return "zh-tw"; - default: return "en"; - } + switch (language) { + case "uk": + return "ua"; + case "zh-Hans": + return "zh-cn"; + case "zh-Hant": + return "zh-tw"; + default: + return "en"; + } } function localeFromLanguage(language: string): string { - // available badges for App Store - // https://developer.apple.com/app-store/marketing/guidelines/ - switch (language) { - case "ca": return "ca-es"; - case "cs": return "cs-cz"; - case "de": return "de-de"; - case "en": return "en-us"; - case "es": return "es-es"; - case "fi": return "fi-fi"; - case "fr": return "fr-fr"; - case "it": return "it-it"; - case "ja": return "ja-jp"; - case "ko": return "ko-kr"; - case "nl": return "nl-nl"; - case "pl": return "pl-pl"; - case "ru": return "ru-ru"; - case "sk": return "sk-sk"; - case "sl": return "sl-sl"; - case "sr": return "sr-cs"; - case "uk": return "uk-ua"; - case "zh-Hans": return "zh-cn"; - case "zh-Hant": return "zh-tw"; - default: return defaultLocale; - } + // available badges for App Store + // https://developer.apple.com/app-store/marketing/guidelines/ + switch (language) { + case "ca": + return "ca-es"; + case "cs": + return "cs-cz"; + case "de": + return "de-de"; + case "en": + return "en-us"; + case "es": + return "es-es"; + case "fi": + return "fi-fi"; + case "fr": + return "fr-fr"; + case "it": + return "it-it"; + case "ja": + return "ja-jp"; + case "ko": + return "ko-kr"; + case "nl": + return "nl-nl"; + case "pl": + return "pl-pl"; + case "ru": + return "ru-ru"; + case "sk": + return "sk-sk"; + case "sl": + return "sl-sl"; + case "sr": + return "sr-cs"; + case "uk": + return "uk-ua"; + case "zh-Hans": + return "zh-cn"; + case "zh-Hant": + return "zh-tw"; + default: + return defaultLocale; + } } const ReactStoreBadges: FC = ({ - url, - platform, - language, - width = 100, - height = width / HEIGHT_RATIO, - target = "_self", + url, + platform, + language, + width = 100, + height = width / HEIGHT_RATIO, + target = "_self", }) => { - const shortCode = shortCodeFromLanguage(language); - const locale = localeFromLanguage(language); - const [image, setImage] = useState(getImage(locale, shortCode)); + const shortCode = shortCodeFromLanguage(language); + const locale = localeFromLanguage(language); + const [image, setImage] = useState(getImage(locale, shortCode)); - const setDefaultImage = () => { - setImage(getImage(defaultLocale, language)); - }; + const setDefaultImage = () => { + setImage(getImage(defaultLocale, language)); + }; - useLayoutEffect(() => { - setImage(getImage(locale, shortCode)); - }, [locale, shortCode]); + useLayoutEffect(() => { + setImage(getImage(locale, shortCode)); + }, [locale, shortCode]); - return ( - - - - ); + return ( + + + + ); }; -export default ReactStoreBadges; \ No newline at end of file +export default ReactStoreBadges; diff --git a/src/Main.tsx b/src/Main.tsx index b11fd43..ab99d01 100644 --- a/src/Main.tsx +++ b/src/Main.tsx @@ -1,14 +1,12 @@ // @ts-ignore import { osmAuth } from "osm-auth"; -import React, {Suspense, - useEffect, useMemo,useState, -} from "react"; +import React, { Suspense, useEffect, useMemo, useState } from "react"; import { AppContext } from "~/appContext"; -import {fetchCountriesData} from "~/backend"; +import { fetchCountriesData } from "~/backend"; import CustomModal from "~/components/modal"; -import {useLanguage} from "~/i18n"; +import { useLanguage } from "~/i18n"; import { AuthState } from "~/model/auth"; -import {Country} from "~/model/country"; +import { Country } from "~/model/country"; import { DefibrillatorData } from "~/model/defibrillatorData"; import { ModalType, initialModalState } from "~/model/modal"; import SidebarAction from "~/model/sidebarAction"; @@ -18,91 +16,117 @@ import SiteNavbar from "./components/navbar"; import SidebarRight from "./components/sidebar-right"; function Main() { - // some ui elements might depend on window size i.e. we don't want some stuff open by default on mobile - const defaultRightSidebarState = window.innerWidth > 1024; + // some ui elements might depend on window size i.e. we don't want some stuff open by default on mobile + const defaultRightSidebarState = window.innerWidth > 1024; - const [modalState, setModalState] = useState(initialModalState); - const [sidebarAction, setSidebarAction] = useState(SidebarAction.init); - const [sidebarData, setSidebarData] = useState(null); - const [rightSidebarShown, setRightSidebarShown] = useState(defaultRightSidebarState); - const [countriesData, setCountriesData] = useState>([]); - const [countriesDataLanguage, setCountriesDataLanguage] = useState(""); + const [modalState, setModalState] = useState(initialModalState); + const [sidebarAction, setSidebarAction] = useState(SidebarAction.init); + const [sidebarData, setSidebarData] = useState( + null, + ); + const [rightSidebarShown, setRightSidebarShown] = useState( + defaultRightSidebarState, + ); + const [countriesData, setCountriesData] = useState>([]); + const [countriesDataLanguage, setCountriesDataLanguage] = + useState(""); - const toggleRightSidebarShown = () => setRightSidebarShown(!rightSidebarShown); - const closeRightSidebar = () => setRightSidebarShown(false); + const toggleRightSidebarShown = () => + setRightSidebarShown(!rightSidebarShown); + const closeRightSidebar = () => setRightSidebarShown(false); - const { VITE_OSM_API_URL, VITE_OSM_OAUTH2_CLIENT_ID, VITE_OSM_OAUTH2_CLIENT_SECRET } = import.meta.env; - const redirectPath = window.location.origin + window.location.pathname; - const [auth] = useState( - osmAuth({ - url: VITE_OSM_API_URL, - client_id: VITE_OSM_OAUTH2_CLIENT_ID ?? "", - client_secret: VITE_OSM_OAUTH2_CLIENT_SECRET ?? "", - redirect_uri: `${redirectPath}land.html`, - scope: "read_prefs write_api", - auto: false, - singlepage: false, - apiUrl: VITE_OSM_API_URL, - }), - ); - const [osmUsername, setOsmUsername] = useState(""); - const [openChangesetId, setOpenChangesetId] = useState(""); + const { + VITE_OSM_API_URL, + VITE_OSM_OAUTH2_CLIENT_ID, + VITE_OSM_OAUTH2_CLIENT_SECRET, + } = import.meta.env; + const redirectPath = window.location.origin + window.location.pathname; + const [auth] = useState( + osmAuth({ + url: VITE_OSM_API_URL, + client_id: VITE_OSM_OAUTH2_CLIENT_ID ?? "", + client_secret: VITE_OSM_OAUTH2_CLIENT_SECRET ?? "", + redirect_uri: `${redirectPath}land.html`, + scope: "read_prefs write_api", + auto: false, + singlepage: false, + apiUrl: VITE_OSM_API_URL, + }), + ); + const [osmUsername, setOsmUsername] = useState(""); + const [openChangesetId, setOpenChangesetId] = useState(""); - const handleLogIn = () => { - auth.authenticate(() => { - updateOsmUsernameState(auth, setOsmUsername); - if (modalState.type === ModalType.NeedToLogin) { - setModalState(initialModalState); - } - }); - }; + const handleLogIn = () => { + auth.authenticate(() => { + updateOsmUsernameState(auth, setOsmUsername); + if (modalState.type === ModalType.NeedToLogin) { + setModalState(initialModalState); + } + }); + }; - const handleLogOut = () => { - auth.logout(); - setOsmUsername(""); - }; + const handleLogOut = () => { + auth.logout(); + setOsmUsername(""); + }; - const authState: AuthState = { auth, osmUsername }; + const authState: AuthState = { auth, osmUsername }; - const appContext = useMemo( - () => ({ - authState, - modalState, - setModalState, - handleLogIn, - handleLogOut, - sidebarAction, - setSidebarAction, - sidebarData, - setSidebarData, - countriesData, - setCountriesData, - countriesDataLanguage, - setCountriesDataLanguage, - }), - [authState, sidebarData, sidebarAction, modalState, handleLogIn, handleLogOut, countriesDataLanguage, countriesData], - ); - useEffect(() => { - if (auth.authenticated()) updateOsmUsernameState(auth, setOsmUsername); - }, [auth]); - return ( - - - - { rightSidebarShown && } - - - ); + const appContext = useMemo( + () => ({ + authState, + modalState, + setModalState, + handleLogIn, + handleLogOut, + sidebarAction, + setSidebarAction, + sidebarData, + setSidebarData, + countriesData, + setCountriesData, + countriesDataLanguage, + setCountriesDataLanguage, + }), + [ + authState, + sidebarData, + sidebarAction, + modalState, + handleLogIn, + handleLogOut, + countriesDataLanguage, + countriesData, + ], + ); + useEffect(() => { + if (auth.authenticated()) updateOsmUsernameState(auth, setOsmUsername); + }, [auth]); + return ( + + + + {rightSidebarShown && } + + + ); } function Fallback() { - return
; + return ( +
+
+
+ ); } export default function WrappedApp() { - return ( - }> -
- - ); + return ( + }> +
+ + ); } diff --git a/src/appContext.tsx b/src/appContext.tsx index 778b1c2..57d51c2 100644 --- a/src/appContext.tsx +++ b/src/appContext.tsx @@ -7,34 +7,34 @@ import { ModalState, initialModalState } from "~/model/modal"; import SidebarAction from "./model/sidebarAction"; interface AppContextType { - authState: AuthState, - modalState: ModalState, - setModalState: (modalState: ModalState) => void, - sidebarAction: SidebarAction, - setSidebarAction: (sidebarAction: SidebarAction) => void, - sidebarData: DefibrillatorData | null, - setSidebarData: (sidebarData: DefibrillatorData | null) => void, - handleLogIn: () => void, - handleLogOut: () => void, - countriesData: Array, - setCountriesData: (countriesData: Array) => void, - countriesDataLanguage: string, - setCountriesDataLanguage: (language: string) => void, + authState: AuthState; + modalState: ModalState; + setModalState: (modalState: ModalState) => void; + sidebarAction: SidebarAction; + setSidebarAction: (sidebarAction: SidebarAction) => void; + sidebarData: DefibrillatorData | null; + setSidebarData: (sidebarData: DefibrillatorData | null) => void; + handleLogIn: () => void; + handleLogOut: () => void; + countriesData: Array; + setCountriesData: (countriesData: Array) => void; + countriesDataLanguage: string; + setCountriesDataLanguage: (language: string) => void; } const defaultAppContext: AppContextType = { - authState: initialAuthState, - modalState: initialModalState, - setModalState: () => {}, - sidebarAction: SidebarAction.init, - setSidebarAction: () => {}, - sidebarData: null, - setSidebarData: () => {}, - handleLogIn: () => {}, - handleLogOut: () => {}, - countriesData: [], - setCountriesData: () => {}, - countriesDataLanguage: "", - setCountriesDataLanguage: () => {}, + authState: initialAuthState, + modalState: initialModalState, + setModalState: () => {}, + sidebarAction: SidebarAction.init, + setSidebarAction: () => {}, + sidebarData: null, + setSidebarData: () => {}, + handleLogIn: () => {}, + handleLogOut: () => {}, + countriesData: [], + setCountriesData: () => {}, + countriesDataLanguage: "", + setCountriesDataLanguage: () => {}, }; export const AppContext = React.createContext(defaultAppContext); -export const useAppContext = () => useContext(AppContext); \ No newline at end of file +export const useAppContext = () => useContext(AppContext); diff --git a/src/backend.ts b/src/backend.ts index c6da7b9..8ae79fc 100644 --- a/src/backend.ts +++ b/src/backend.ts @@ -4,31 +4,42 @@ import { fetchNodeData } from "./osm"; export const backendBaseUrl = import.meta.env.VITE_BACKEND_API_URL; -export async function fetchNodeDataFromBackend(nodeId: string): Promise { - const url = `${backendBaseUrl}/api/v1/node/${nodeId}`; - console.log("Request object info for node with osm id:", nodeId, " via url: ", url); - return fetchNodeData(url); +export async function fetchNodeDataFromBackend( + nodeId: string, +): Promise { + const url = `${backendBaseUrl}/api/v1/node/${nodeId}`; + console.log( + "Request object info for node with osm id:", + nodeId, + " via url: ", + url, + ); + return fetchNodeData(url); } interface BackendCountry { - country_code: string; - country_names: Record; - feature_count: number; - data_path: string; + country_code: string; + country_names: Record; + feature_count: number; + data_path: string; } -export async function fetchCountriesData(language: string): Promise | null> { - const url = `${backendBaseUrl}/api/v1/countries/names?language=${language.toUpperCase()}`; - return fetch(url) - .then((response) => response.json()) - .then((response: Array) => response.map((country: BackendCountry) => ({ - code: country.country_code, - names: country.country_names, - featureCount: country.feature_count, - dataPath: country.data_path, - }))) - .catch((error) => { - console.error("Error:", error); - return null; - }); -} \ No newline at end of file +export async function fetchCountriesData( + language: string, +): Promise | null> { + const url = `${backendBaseUrl}/api/v1/countries/names?language=${language.toUpperCase()}`; + return fetch(url) + .then((response) => response.json()) + .then((response: Array) => + response.map((country: BackendCountry) => ({ + code: country.country_code, + names: country.country_names, + featureCount: country.feature_count, + dataPath: country.data_path, + })), + ) + .catch((error) => { + console.error("Error:", error); + return null; + }); +} diff --git a/src/components/downloadCard.tsx b/src/components/downloadCard.tsx index b8e4d60..f22a5ba 100644 --- a/src/components/downloadCard.tsx +++ b/src/components/downloadCard.tsx @@ -2,79 +2,87 @@ import { mdiDownload } from "@mdi/js"; import Icon from "@mdi/react"; import React, { useEffect, useState } from "react"; import { useTranslation } from "react-i18next"; -import {useAppContext} from "~/appContext"; +import { useAppContext } from "~/appContext"; import { backendBaseUrl, fetchCountriesData } from "~/backend"; -import {useLanguage} from "~/i18n"; +import { useLanguage } from "~/i18n"; import { Country } from "~/model/country"; const worldCountryCode = "WORLD"; export default function DownloadCard() { - const { t} = useTranslation(); - const language = useLanguage(); - const { countriesData} = useAppContext(); - function countryName(country: Country) { - if (country.code === worldCountryCode) return t("sidebar.world"); - const backendLanguageUppercase = language.toUpperCase(); - if (Object.hasOwn(country.names, backendLanguageUppercase)) { - return country.names[backendLanguageUppercase]; - } - if (backendLanguageUppercase.includes("-")) { - const basicLanguage = backendLanguageUppercase.split("-")[0]; - if (Object.hasOwn(country.names, basicLanguage)) { - return country.names[basicLanguage]; - } - } - return country.names.default; - } - const sortedCountriesByName = countriesData - .sort((a: Country, b: Country) => { - if (a.code === worldCountryCode) return -1; - if (b.code === worldCountryCode) return 1; - return countryName(a) < countryName(b) ? -1 : 1; - }); - const [selectedCountryCode, setSelectedCountryCode] = useState(worldCountryCode); - const selectedCountry = countriesData.find((country) => country.code === selectedCountryCode); + const { t } = useTranslation(); + const language = useLanguage(); + const { countriesData } = useAppContext(); + function countryName(country: Country) { + if (country.code === worldCountryCode) return t("sidebar.world"); + const backendLanguageUppercase = language.toUpperCase(); + if (Object.hasOwn(country.names, backendLanguageUppercase)) { + return country.names[backendLanguageUppercase]; + } + if (backendLanguageUppercase.includes("-")) { + const basicLanguage = backendLanguageUppercase.split("-")[0]; + if (Object.hasOwn(country.names, basicLanguage)) { + return country.names[basicLanguage]; + } + } + return country.names.default; + } + const sortedCountriesByName = countriesData.sort((a: Country, b: Country) => { + if (a.code === worldCountryCode) return -1; + if (b.code === worldCountryCode) return 1; + return countryName(a) < countryName(b) ? -1 : 1; + }); + const [selectedCountryCode, setSelectedCountryCode] = + useState(worldCountryCode); + const selectedCountry = countriesData.find( + (country) => country.code === selectedCountryCode, + ); - function countryLabel(country: Country) { - return `${countryName(country)} (${country.featureCount})`; - } - return ( -
-
-

- - {t("sidebar.download_title")} -

- - - - {t("sidebar.geojson")} - -
-
-
- ); -} \ No newline at end of file + function countryLabel(country: Country) { + return `${countryName(country)} (${country.featureCount})`; + } + return ( +
+
+

+ + {t("sidebar.download_title")} +

+ + + + {t("sidebar.geojson")} + +
+
+
+ ); +} diff --git a/src/components/footer.tsx b/src/components/footer.tsx index 5f53c49..6856e61 100644 --- a/src/components/footer.tsx +++ b/src/components/footer.tsx @@ -1,8 +1,8 @@ import { - mdiAccountGroup, - mdiArrowRightBold, - mdiCancel, - mdiMapMarkerPlus, + mdiAccountGroup, + mdiArrowRightBold, + mdiCancel, + mdiMapMarkerPlus, } from "@mdi/js"; import Icon from "@mdi/react"; import React, { FC } from "react"; @@ -14,84 +14,109 @@ import { ModalType, initialModalState } from "~/model/modal"; import "./footer.css"; const FooterDiv: FC = ({ - startAEDAdding, mobileCancel, showFormMobile, buttonsConfiguration, + startAEDAdding, + mobileCancel, + showFormMobile, + buttonsConfiguration, }) => { - const { t } = useTranslation(); - const { setModalState } = useAppContext(); - const basicButtons = ( -
- - - - - - - -
- ); - const mobileAddAedButtons = ( - <> - - - - ); - function getFooterButtons(buttonConfigurationType: ButtonsType) { - switch (buttonConfigurationType) { - case ButtonsType.None: return null; - case ButtonsType.Basic: return basicButtons; - case ButtonsType.MobileAddAed: return mobileAddAedButtons; - default: return null; - } - } + const { t } = useTranslation(); + const { setModalState } = useAppContext(); + const basicButtons = ( +
+ + + + + + + +
+ ); + const mobileAddAedButtons = ( + <> + + + + ); + function getFooterButtons(buttonConfigurationType: ButtonsType) { + switch (buttonConfigurationType) { + case ButtonsType.None: + return null; + case ButtonsType.Basic: + return basicButtons; + case ButtonsType.MobileAddAed: + return mobileAddAedButtons; + default: + return null; + } + } - if (buttonsConfiguration === ButtonsType.None) return null; - return ( -
-
- {getFooterButtons(buttonsConfiguration)} -
-
- ); + if (buttonsConfiguration === ButtonsType.None) return null; + return ( +
+
+ {getFooterButtons(buttonsConfiguration)} +
+
+ ); }; interface FooterDivProps { - startAEDAdding: (mobile: boolean) => void, - mobileCancel: () => void, - showFormMobile: () => void, - buttonsConfiguration: number, + startAEDAdding: (mobile: boolean) => void; + mobileCancel: () => void; + showFormMobile: () => void; + buttonsConfiguration: number; } export default FooterDiv; diff --git a/src/components/languageSwitcher.tsx b/src/components/languageSwitcher.tsx index 71fb4aa..a92697a 100644 --- a/src/components/languageSwitcher.tsx +++ b/src/components/languageSwitcher.tsx @@ -1,30 +1,27 @@ import i18n from "i18next"; import React from "react"; import { Navbar } from "react-bulma-components"; -import {languages, useLanguage} from "~/i18n"; +import { languages, useLanguage } from "~/i18n"; export default function LanguageSwitcher() { - const language = useLanguage(); - return ( - -
- -
-
- ); + const language = useLanguage(); + return ( + +
+ +
+
+ ); } diff --git a/src/components/legend.tsx b/src/components/legend.tsx index 5d7c551..9dc474f 100644 --- a/src/components/legend.tsx +++ b/src/components/legend.tsx @@ -10,65 +10,78 @@ import MarkerUnknown from "~/marker_icons/marker_unknown.svg"; import MarkerDefault from "~/marker_icons/marker_yes.svg"; export default function MapLegend() { - const { t } = useTranslation(); - const markers = [ - { - key: "default", - icon: MarkerDefault, - text: t("access.yes"), - tag: "access=yes", - }, - { - key: "permissive", - icon: MarkerPermissive, - text: t("access.permissive"), - tag: "access=permissive", - }, - { - key: "customers", - icon: MarkerCustomers, - text: t("access.customers"), - tag: "access=customers", - }, - { - key: "private", - icon: MarkerPrivate, - text: t("access.private"), - tag: "access=private", - }, - { - key: "no", - icon: MarkerNo, - text: t("access.no"), - tag: "access=no", - }, - { - key: "unknown", - icon: MarkerUnknown, - text: t("access.unknown"), - tag: t("access.unknownTag"), - }, - ]; - return ( -
-
- -

{t("sidebar.map_legend_title")}

-
- {markers.map(({ - icon, key, text, tag, - }) => ( -
- {text} -

{text}

-
- ))} -
- ); -} \ No newline at end of file + const { t } = useTranslation(); + const markers = [ + { + key: "default", + icon: MarkerDefault, + text: t("access.yes"), + tag: "access=yes", + }, + { + key: "permissive", + icon: MarkerPermissive, + text: t("access.permissive"), + tag: "access=permissive", + }, + { + key: "customers", + icon: MarkerCustomers, + text: t("access.customers"), + tag: "access=customers", + }, + { + key: "private", + icon: MarkerPrivate, + text: t("access.private"), + tag: "access=private", + }, + { + key: "no", + icon: MarkerNo, + text: t("access.no"), + tag: "access=no", + }, + { + key: "unknown", + icon: MarkerUnknown, + text: t("access.unknown"), + tag: t("access.unknownTag"), + }, + ]; + return ( +
+
+ +

+ {t("sidebar.map_legend_title")} +

+
+ {markers.map(({ icon, key, text, tag }) => ( +
+ {text} +

+ {text} +

+
+ ))} +
+ ); +} diff --git a/src/components/logInButton.tsx b/src/components/logInButton.tsx index 4ed5b11..bbfb7e2 100644 --- a/src/components/logInButton.tsx +++ b/src/components/logInButton.tsx @@ -6,36 +6,44 @@ import { useTranslation } from "react-i18next"; import { useAppContext } from "~/appContext"; interface LogInButtonProps { - inNavBar: boolean, + inNavBar: boolean; } const LogInButton: FC = ({ inNavBar }) => { - const { t } = useTranslation(); - const { authState: { auth, osmUsername }, handleLogIn, handleLogOut } = useAppContext(); + const { t } = useTranslation(); + const { + authState: { auth, osmUsername }, + handleLogIn, + handleLogOut, + } = useAppContext(); - if (auth?.authenticated()) { - return ( - - - - {osmUsername} - - - - - {t("navbar.logout")} - - - - ); - } - return ( - - - - ); + if (auth?.authenticated()) { + return ( + + + + {osmUsername} + + + + + {t("navbar.logout")} + + + + ); + } + return ( + + + + ); }; -export default LogInButton; \ No newline at end of file +export default LogInButton; diff --git a/src/components/map.tsx b/src/components/map.tsx index 799ab88..fe5f563 100644 --- a/src/components/map.tsx +++ b/src/components/map.tsx @@ -3,14 +3,12 @@ import MaplibreGeocoder from "@maplibre/maplibre-gl-geocoder"; import "@maplibre/maplibre-gl-geocoder/dist/maplibre-gl-geocoder.css"; import maplibregl from "maplibre-gl"; import "maplibre-gl/dist/maplibre-gl.css"; -import React, { - FC, useEffect, useRef, useState, -} from "react"; +import React, { FC, useEffect, useRef, useState } from "react"; import { useTranslation } from "react-i18next"; import { useAppContext } from "~/appContext"; -import {fetchCountriesData, fetchNodeDataFromBackend} from "~/backend"; +import { fetchCountriesData, fetchNodeDataFromBackend } from "~/backend"; import nominatimGeocoder from "~/components/nominatimGeocoder"; -import {useLanguage} from "~/i18n"; +import { useLanguage } from "~/i18n"; import ButtonsType from "~/model/buttonsType"; import { DefibrillatorData } from "~/model/defibrillatorData"; import { ModalType, initialModalState } from "~/model/modal"; @@ -21,366 +19,399 @@ import mapStyle from "./map_style"; import SidebarLeft from "./sidebar-left"; function fillSidebarWithOsmDataAndShow( - nodeId: string, - mapInstance: maplibregl.Map, - setSidebarAction: (action: SidebarAction) => void, - setSidebarData: (data: DefibrillatorData) => void, - setSidebarLeftShown: (sidebarLeftShown: boolean) => void, - jumpInsteadOfEaseTo: boolean, + nodeId: string, + mapInstance: maplibregl.Map, + setSidebarAction: (action: SidebarAction) => void, + setSidebarData: (data: DefibrillatorData) => void, + setSidebarLeftShown: (sidebarLeftShown: boolean) => void, + jumpInsteadOfEaseTo: boolean, ) { - const result = fetchNodeDataFromBackend(nodeId); - result.then((data) => { - if (data) { - const zoomLevelForDetailedView = 17; - const currentZoomLevel = mapInstance.getZoom(); - // todo: possibly add handling of request error which will cause lnglat to be NaN, NaN - if (currentZoomLevel < zoomLevelForDetailedView) { - if (jumpInsteadOfEaseTo) { - mapInstance.jumpTo({ - zoom: zoomLevelForDetailedView, - center: [data.lon, data.lat], - }); - } else { - mapInstance.easeTo({ - zoom: zoomLevelForDetailedView, - around: { lon: data.lon, lat: data.lat }, - }); - } - } else if (jumpInsteadOfEaseTo) { - mapInstance.jumpTo({ - zoom: currentZoomLevel, - center: [data.lon, data.lat], - }); - } else { - mapInstance.easeTo({ - zoom: currentZoomLevel, - around: { lon: data.lon, lat: data.lat }, - }); - } - setSidebarData(data); - setSidebarAction(SidebarAction.showDetails); - setSidebarLeftShown(true); - } - }); + const result = fetchNodeDataFromBackend(nodeId); + result.then((data) => { + if (data) { + const zoomLevelForDetailedView = 17; + const currentZoomLevel = mapInstance.getZoom(); + // todo: possibly add handling of request error which will cause lnglat to be NaN, NaN + if (currentZoomLevel < zoomLevelForDetailedView) { + if (jumpInsteadOfEaseTo) { + mapInstance.jumpTo({ + zoom: zoomLevelForDetailedView, + center: [data.lon, data.lat], + }); + } else { + mapInstance.easeTo({ + zoom: zoomLevelForDetailedView, + around: { lon: data.lon, lat: data.lat }, + }); + } + } else if (jumpInsteadOfEaseTo) { + mapInstance.jumpTo({ + zoom: currentZoomLevel, + center: [data.lon, data.lat], + }); + } else { + mapInstance.easeTo({ + zoom: currentZoomLevel, + around: { lon: data.lon, lat: data.lat }, + }); + } + setSidebarData(data); + setSidebarAction(SidebarAction.showDetails); + setSidebarLeftShown(true); + } + }); } function parseHash(): Record { - const parameters: Record = {}; - for (const part of window.location.hash.slice(1).split("&")) { - const [key, value] = part.split("=", 2); - parameters[key] = value; - }; - return parameters; + const parameters: Record = {}; + for (const part of window.location.hash.slice(1).split("&")) { + const [key, value] = part.split("=", 2); + parameters[key] = value; + } + return parameters; } function getNewHashString(parameters: Record) { - return Object - .entries(parameters) - .map(([key, value]) => `${key}=${value}`) - .join("&"); + return Object.entries(parameters) + .map(([key, value]) => `${key}=${value}`) + .join("&"); } const MapView: FC = ({ openChangesetId, setOpenChangesetId }) => { - const { - authState: { auth }, setModalState, sidebarAction, setSidebarAction, sidebarData, setSidebarData, countriesData, setCountriesData, countriesDataLanguage, setCountriesDataLanguage, - } = useAppContext(); - const { t} = useTranslation(); - const language = useLanguage(); - const [mapLanguage, setMapLanguage] = useState(""); - const [mapCountryLanguage, setMapCountryLanguage] = useState(""); - - const hash4MapName = "map"; - - const paramsFromHash = parseHash(); - - let initialLongitude = -8; - let initialLatitude = 47.74; - let initialZoom = 3; - - if (paramsFromHash[hash4MapName]) { - [initialZoom, initialLatitude, initialLongitude] = paramsFromHash[hash4MapName].split("/").map(Number); - } - - const mapContainer = useRef(null); - const mapRef = useRef(null); - const maplibreGeocoderRef = useRef(null); - const controlsLocation = "bottom-right"; - - const [marker, setMarker] = useState(null); - - const [sidebarLeftShown, setSidebarLeftShown] = useState(false); - - const [footerButtonType, setFooterButtonType] = useState(ButtonsType.Basic); - - const removeNodeIdFromHash = () => { - const hashParams = parseHash(); - // biome-ignore lint/performance/noDelete: using undefined assignment causes it to be part of url - delete hashParams.node_id; - window.location.hash = getNewHashString(hashParams); - }; - - const deleteMarker = () => { - if (marker !== null) { - marker.remove(); - setMarker(null); - } - }; - - const closeSidebarLeft = () => { - setSidebarLeftShown(false); - deleteMarker(); - removeNodeIdFromHash(); - setFooterButtonType(ButtonsType.Basic); - }; - - const checkConditionsThenCall = (callable: () => void) => { - if (mapRef.current === null) return; - const map = mapRef.current; - if (auth === null || !auth.authenticated()) { - setModalState({ ...initialModalState, visible: true, type: ModalType.NeedToLogin }); - } else if (map.getZoom() < 15) { - setModalState({ - ...initialModalState, visible: true, type: ModalType.NeedMoreZoom, currentZoom: map.getZoom(), - }); - } else callable(); - }; - - const mobileCancel = () => { - deleteMarker(); - setSidebarLeftShown(false); - setFooterButtonType(ButtonsType.Basic); - }; - - const showFormMobile = () => { - setSidebarLeftShown(true); - setFooterButtonType(ButtonsType.None); - }; - - const startAEDAdding = (mobile: boolean) => { - if (mapRef.current === null) return; - const map = mapRef.current; - deleteMarker(); - removeNodeIdFromHash(); - setSidebarData(null); - setSidebarAction(SidebarAction.addNode); - setSidebarLeftShown(!mobile); // for mobile hide sidebar so marker is visible - setFooterButtonType(mobile ? ButtonsType.MobileAddAed : ButtonsType.None); - // add marker - const markerColour = "#e81224"; - const mapCenter = map.getCenter(); - const initialCoordinates: [number, number] = [mapCenter.lng, mapCenter.lat]; - setMarker( - new maplibregl.Marker({ - draggable: true, - color: markerColour, - }) - .setLngLat(initialCoordinates) - .setPopup(new maplibregl.Popup().setHTML(t("form.marker_popup_text"))) - .addTo(mapRef.current) - .togglePopup(), - ); - }; - - useEffect(() => { - const fetchData = async () => { - const data = await fetchCountriesData(language); - if (data !== null) { - setCountriesData(data); - setCountriesDataLanguage(language); - } - }; - fetchData().catch(console.error); - }, [language, setCountriesDataLanguage, setCountriesData]); - - function addMaplibreGeocoder(map: maplibregl.Map) { - if (maplibreGeocoderRef.current !== null) { - map.removeControl(maplibreGeocoderRef.current); - } - const newMaplibreGeocoder = new MaplibreGeocoder(nominatimGeocoder, { - maplibregl, - placeholder: t("sidebar.find_location"), - }); - newMaplibreGeocoder.setLanguage(language); - map.addControl(newMaplibreGeocoder); - maplibreGeocoderRef.current = newMaplibreGeocoder; - } - - useEffect(() => { - if (mapContainer.current === null) return; - if (mapRef.current !== null) return; // stops map from initializing more than once - const map = new maplibregl.Map({ - container: mapContainer.current, - hash: hash4MapName, - // @ts-ignore - style: mapStyle(language.toUpperCase(), countriesData), - center: [initialLongitude, initialLatitude], - zoom: initialZoom, - minZoom: 3, - maxZoom: 19, - maplibreLogo: false, - }); - setMapLanguage(language); - setMapCountryLanguage(countriesDataLanguage); - - addMaplibreGeocoder(map); - mapRef.current = map; - - // how fast mouse scroll wheel zooms - map.scrollZoom.setWheelZoomRate(1); - - // disable map rotation using right click + drag - map.dragRotate.disable(); - - // disable map rotation using touch rotation gesture - map.touchZoomRotate.disableRotation(); - - const control = new maplibregl.NavigationControl({ - showCompass: false, - }); - - const geolocate = new maplibregl.GeolocateControl({ - positionOptions: { - enableHighAccuracy: true, - }, - }); - - // Map controls - map.addControl(control, controlsLocation); - map.addControl(geolocate, controlsLocation); - - // Map interaction - map.on("mouseenter", "clustered-circle", () => { - map.getCanvas().style.cursor = "pointer"; - }); - map.on("mouseleave", "clustered-circle", () => { - map.getCanvas().style.cursor = ""; - }); - map.on("mouseenter", "unclustered", () => { - map.getCanvas().style.cursor = "pointer"; - }); - map.on("mouseleave", "unclustered", () => { - map.getCanvas().style.cursor = ""; - }); - map.on("mouseenter", "clustered-circle-low-zoom", () => { - map.getCanvas().style.cursor = "pointer"; - }); - map.on("mouseleave", "clustered-circle-low-zoom", () => { - map.getCanvas().style.cursor = ""; - }); - map.on("mouseenter", "unclustered-low-zoom", () => { - map.getCanvas().style.cursor = "pointer"; - }); - map.on("mouseleave", "unclustered-low-zoom", () => { - map.getCanvas().style.cursor = ""; - }); - - // biome-ignore lint/suspicious/noExplicitAny: unknown type - type MapEventType = any; - // zoom to cluster on click - map.on("click", "clustered-circle", (e) => { - const features = map.queryRenderedFeatures(e.point, { - layers: ["clustered-circle"], - }); - const zoom = map.getZoom(); - map.easeTo({ - // @ts-ignore - center: features[0].geometry.coordinates, - zoom: zoom + 2, - }); - }); - map.on("click", "clustered-circle-low-zoom", (e: MapEventType) => { - const features = map.queryRenderedFeatures(e.point, { - layers: ["clustered-circle-low-zoom"], - }); - const zoom = map.getZoom(); - map.easeTo({ - // @ts-ignore - center: features[0].geometry.coordinates, - zoom: zoom + 2, - }); - }); - function showObjectWithProperties(e: MapEventType) { - console.log("Clicked on object with properties: ", e.features[0].properties); - if (e.features[0].properties !== undefined && mapRef.current !== null) { - const osmNodeId = e.features[0].properties.node_id; - console.log("Clicked on object with osm_id: ", osmNodeId); - // show sidebar - fillSidebarWithOsmDataAndShow( - osmNodeId, - mapRef.current, - setSidebarAction, - setSidebarData, - setSidebarLeftShown, - false, - ); - // update hash - const params = { - ...parseHash(), - node_id: osmNodeId, - }; - console.log("new hash params", params); - window.location.hash = getNewHashString(params); - } - } - - // show sidebar on single element click - map.on("click", "unclustered", showObjectWithProperties); - map.on("click", "unclustered-low-zoom", showObjectWithProperties); - - // if direct link to osm node then get its data and zoom in - const newParamsFromHash = parseHash(); - if (newParamsFromHash.node_id && mapRef.current !== null) { - fillSidebarWithOsmDataAndShow( - newParamsFromHash.node_id, - mapRef.current, - setSidebarAction, - setSidebarData, - setSidebarLeftShown, - true, - ); - } - }, [initialLatitude, initialLongitude, initialZoom, setSidebarAction, setSidebarData, language, countriesData, countriesDataLanguage]); - - useEffect(() => { - if (mapRef.current === null) return; - const map = mapRef.current; - addMaplibreGeocoder(map); - if (countriesDataLanguage !== language) return; // wait for countries data to be loaded - setMapLanguage(language); - setMapCountryLanguage(countriesDataLanguage); - // @ts-ignore - map.setStyle(mapStyle(language.toUpperCase(), countriesData)); - }, [countriesData, countriesDataLanguage, language]); - - return ( - <> - { sidebarLeftShown && ( - - )} -
-
-
- checkConditionsThenCall(() => startAEDAdding(mobile))} - mobileCancel={mobileCancel} - showFormMobile={showFormMobile} - buttonsConfiguration={footerButtonType} - /> - - ); + const { + authState: { auth }, + setModalState, + sidebarAction, + setSidebarAction, + sidebarData, + setSidebarData, + countriesData, + setCountriesData, + countriesDataLanguage, + setCountriesDataLanguage, + } = useAppContext(); + const { t } = useTranslation(); + const language = useLanguage(); + const [mapLanguage, setMapLanguage] = useState(""); + const [mapCountryLanguage, setMapCountryLanguage] = useState(""); + + const hash4MapName = "map"; + + const paramsFromHash = parseHash(); + + let initialLongitude = -8; + let initialLatitude = 47.74; + let initialZoom = 3; + + if (paramsFromHash[hash4MapName]) { + [initialZoom, initialLatitude, initialLongitude] = paramsFromHash[ + hash4MapName + ] + .split("/") + .map(Number); + } + + const mapContainer = useRef(null); + const mapRef = useRef(null); + const maplibreGeocoderRef = useRef(null); + const controlsLocation = "bottom-right"; + + const [marker, setMarker] = useState(null); + + const [sidebarLeftShown, setSidebarLeftShown] = useState(false); + + const [footerButtonType, setFooterButtonType] = useState(ButtonsType.Basic); + + const removeNodeIdFromHash = () => { + const hashParams = parseHash(); + // biome-ignore lint/performance/noDelete: using undefined assignment causes it to be part of url + delete hashParams.node_id; + window.location.hash = getNewHashString(hashParams); + }; + + const deleteMarker = () => { + if (marker !== null) { + marker.remove(); + setMarker(null); + } + }; + + const closeSidebarLeft = () => { + setSidebarLeftShown(false); + deleteMarker(); + removeNodeIdFromHash(); + setFooterButtonType(ButtonsType.Basic); + }; + + const checkConditionsThenCall = (callable: () => void) => { + if (mapRef.current === null) return; + const map = mapRef.current; + if (auth === null || !auth.authenticated()) { + setModalState({ + ...initialModalState, + visible: true, + type: ModalType.NeedToLogin, + }); + } else if (map.getZoom() < 15) { + setModalState({ + ...initialModalState, + visible: true, + type: ModalType.NeedMoreZoom, + currentZoom: map.getZoom(), + }); + } else callable(); + }; + + const mobileCancel = () => { + deleteMarker(); + setSidebarLeftShown(false); + setFooterButtonType(ButtonsType.Basic); + }; + + const showFormMobile = () => { + setSidebarLeftShown(true); + setFooterButtonType(ButtonsType.None); + }; + + const startAEDAdding = (mobile: boolean) => { + if (mapRef.current === null) return; + const map = mapRef.current; + deleteMarker(); + removeNodeIdFromHash(); + setSidebarData(null); + setSidebarAction(SidebarAction.addNode); + setSidebarLeftShown(!mobile); // for mobile hide sidebar so marker is visible + setFooterButtonType(mobile ? ButtonsType.MobileAddAed : ButtonsType.None); + // add marker + const markerColour = "#e81224"; + const mapCenter = map.getCenter(); + const initialCoordinates: [number, number] = [mapCenter.lng, mapCenter.lat]; + setMarker( + new maplibregl.Marker({ + draggable: true, + color: markerColour, + }) + .setLngLat(initialCoordinates) + .setPopup(new maplibregl.Popup().setHTML(t("form.marker_popup_text"))) + .addTo(mapRef.current) + .togglePopup(), + ); + }; + + useEffect(() => { + const fetchData = async () => { + const data = await fetchCountriesData(language); + if (data !== null) { + setCountriesData(data); + setCountriesDataLanguage(language); + } + }; + fetchData().catch(console.error); + }, [language, setCountriesDataLanguage, setCountriesData]); + + function addMaplibreGeocoder(map: maplibregl.Map) { + if (maplibreGeocoderRef.current !== null) { + map.removeControl(maplibreGeocoderRef.current); + } + const newMaplibreGeocoder = new MaplibreGeocoder(nominatimGeocoder, { + maplibregl, + placeholder: t("sidebar.find_location"), + }); + newMaplibreGeocoder.setLanguage(language); + map.addControl(newMaplibreGeocoder); + maplibreGeocoderRef.current = newMaplibreGeocoder; + } + + useEffect(() => { + if (mapContainer.current === null) return; + if (mapRef.current !== null) return; // stops map from initializing more than once + const map = new maplibregl.Map({ + container: mapContainer.current, + hash: hash4MapName, + // @ts-ignore + style: mapStyle(language.toUpperCase(), countriesData), + center: [initialLongitude, initialLatitude], + zoom: initialZoom, + minZoom: 3, + maxZoom: 19, + maplibreLogo: false, + }); + setMapLanguage(language); + setMapCountryLanguage(countriesDataLanguage); + + addMaplibreGeocoder(map); + mapRef.current = map; + + // how fast mouse scroll wheel zooms + map.scrollZoom.setWheelZoomRate(1); + + // disable map rotation using right click + drag + map.dragRotate.disable(); + + // disable map rotation using touch rotation gesture + map.touchZoomRotate.disableRotation(); + + const control = new maplibregl.NavigationControl({ + showCompass: false, + }); + + const geolocate = new maplibregl.GeolocateControl({ + positionOptions: { + enableHighAccuracy: true, + }, + }); + + // Map controls + map.addControl(control, controlsLocation); + map.addControl(geolocate, controlsLocation); + + // Map interaction + map.on("mouseenter", "clustered-circle", () => { + map.getCanvas().style.cursor = "pointer"; + }); + map.on("mouseleave", "clustered-circle", () => { + map.getCanvas().style.cursor = ""; + }); + map.on("mouseenter", "unclustered", () => { + map.getCanvas().style.cursor = "pointer"; + }); + map.on("mouseleave", "unclustered", () => { + map.getCanvas().style.cursor = ""; + }); + map.on("mouseenter", "clustered-circle-low-zoom", () => { + map.getCanvas().style.cursor = "pointer"; + }); + map.on("mouseleave", "clustered-circle-low-zoom", () => { + map.getCanvas().style.cursor = ""; + }); + map.on("mouseenter", "unclustered-low-zoom", () => { + map.getCanvas().style.cursor = "pointer"; + }); + map.on("mouseleave", "unclustered-low-zoom", () => { + map.getCanvas().style.cursor = ""; + }); + + // biome-ignore lint/suspicious/noExplicitAny: unknown type + type MapEventType = any; + // zoom to cluster on click + map.on("click", "clustered-circle", (e) => { + const features = map.queryRenderedFeatures(e.point, { + layers: ["clustered-circle"], + }); + const zoom = map.getZoom(); + map.easeTo({ + // @ts-ignore + center: features[0].geometry.coordinates, + zoom: zoom + 2, + }); + }); + map.on("click", "clustered-circle-low-zoom", (e: MapEventType) => { + const features = map.queryRenderedFeatures(e.point, { + layers: ["clustered-circle-low-zoom"], + }); + const zoom = map.getZoom(); + map.easeTo({ + // @ts-ignore + center: features[0].geometry.coordinates, + zoom: zoom + 2, + }); + }); + function showObjectWithProperties(e: MapEventType) { + console.log( + "Clicked on object with properties: ", + e.features[0].properties, + ); + if (e.features[0].properties !== undefined && mapRef.current !== null) { + const osmNodeId = e.features[0].properties.node_id; + console.log("Clicked on object with osm_id: ", osmNodeId); + // show sidebar + fillSidebarWithOsmDataAndShow( + osmNodeId, + mapRef.current, + setSidebarAction, + setSidebarData, + setSidebarLeftShown, + false, + ); + // update hash + const params = { + ...parseHash(), + node_id: osmNodeId, + }; + console.log("new hash params", params); + window.location.hash = getNewHashString(params); + } + } + + // show sidebar on single element click + map.on("click", "unclustered", showObjectWithProperties); + map.on("click", "unclustered-low-zoom", showObjectWithProperties); + + // if direct link to osm node then get its data and zoom in + const newParamsFromHash = parseHash(); + if (newParamsFromHash.node_id && mapRef.current !== null) { + fillSidebarWithOsmDataAndShow( + newParamsFromHash.node_id, + mapRef.current, + setSidebarAction, + setSidebarData, + setSidebarLeftShown, + true, + ); + } + }, [ + initialLatitude, + initialLongitude, + initialZoom, + setSidebarAction, + setSidebarData, + language, + countriesData, + countriesDataLanguage, + ]); + + useEffect(() => { + if (mapRef.current === null) return; + const map = mapRef.current; + addMaplibreGeocoder(map); + if (countriesDataLanguage !== language) return; // wait for countries data to be loaded + setMapLanguage(language); + setMapCountryLanguage(countriesDataLanguage); + // @ts-ignore + map.setStyle(mapStyle(language.toUpperCase(), countriesData)); + }, [countriesData, countriesDataLanguage, language]); + + return ( + <> + {sidebarLeftShown && ( + + )} +
+
+
+ + checkConditionsThenCall(() => startAEDAdding(mobile)) + } + mobileCancel={mobileCancel} + showFormMobile={showFormMobile} + buttonsConfiguration={footerButtonType} + /> + + ); }; interface MapViewProps { - openChangesetId: string, - setOpenChangesetId: (openChangesetId: string) => void, + openChangesetId: string; + setOpenChangesetId: (openChangesetId: string) => void; } -export default MapView; \ No newline at end of file +export default MapView; diff --git a/src/components/map_style.ts b/src/components/map_style.ts index 03040ca..4c7bfdc 100644 --- a/src/components/map_style.ts +++ b/src/components/map_style.ts @@ -1,215 +1,224 @@ import { backendBaseUrl } from "~/backend"; -import {Country} from "~/model/country"; +import { Country } from "~/model/country"; const getUrl = window.location; const baseUrl = `${getUrl.protocol}//${getUrl.host}${getUrl.pathname}`; -const spriteUrl = (new URL("img/sprite", baseUrl)).href; +const spriteUrl = new URL("img/sprite", baseUrl).href; // can't use URL class since this is a template not literal url const tilesUrl = `${backendBaseUrl}/api/v1/tile/{z}/{x}/{y}.mvt`; const TILE_COUNTRIES_MAX_ZOOM = 6; const mapStyle = (lang: string, countriesData: Array) => { - const countryCodeToName: Record = countriesData.reduce((map: Record, country) => { - if (country.names[lang.toUpperCase()] !== undefined) { - map[country.code] = country.names[lang]; - } else if (country.names.default !== undefined) { - map[country.code] = country.names.default; - } - return map; - }, {}); - return { - version: 8, - name: "Map style", - sources: { - osm: { - type: "raster", - tiles: ["https://tile.openstreetmap.org/{z}/{x}/{y}.png"], - tileSize: 256, - minzoom: 0, - maxzoom: 19, - attribution: "© " - + "OpenStreetMap contributors", - }, - countries: { - type: "vector", - tiles: [tilesUrl], - minzoom: 3, - maxzoom: TILE_COUNTRIES_MAX_ZOOM, - }, - "aed-locations": { - type: "vector", - tiles: [tilesUrl], - minzoom: TILE_COUNTRIES_MAX_ZOOM, - maxzoom: 16, - }, - }, - sprite: spriteUrl, - glyphs: "https://orangemug.github.io/font-glyphs/glyphs/{fontstack}/{range}.pbf", - layers: [ - { - id: "background", - type: "raster", - source: "osm", - layout: { - visibility: "visible", - }, - }, - { - id: "borders-fill", - type: "fill", - source: "countries", - "source-layer": "countries", - paint: { - "fill-color": "#7a7a7a", - "fill-opacity": 0.5, - }, - maxzoom: TILE_COUNTRIES_MAX_ZOOM, - }, - { - id: "borders", - type: "line", - source: "countries", - "source-layer": "countries", - paint: { - "line-color": "#ff3333", - "line-width": 2, - "line-blur": 1, - }, - maxzoom: TILE_COUNTRIES_MAX_ZOOM, - }, - { - id: "unclustered", - type: "symbol", - source: "aed-locations", - "source-layer": "defibrillators", - minzoom: 9, - filter: ["!has", "point_count"], - layout: { - "icon-image": [ - "coalesce", - ["image", ["concat", "marker_", ["get", "access"]]], - ["image", "marker_unknown"], - ], - "icon-size": 0.5, - "icon-allow-overlap": true, - }, - }, - { - id: "unclustered-low-zoom", - type: "symbol", - source: "aed-locations", - "source-layer": "defibrillators", - minzoom: TILE_COUNTRIES_MAX_ZOOM, - maxzoom: 9, - filter: ["!has", "point_count"], - layout: { - "icon-image": [ - "coalesce", - ["image", ["concat", "marker_", ["get", "access"]]], - ["image", "marker_unknown"], - ], - "icon-size": 0.3, - "icon-allow-overlap": true, - }, - }, - { - id: "clustered-circle", - type: "circle", - source: "aed-locations", - "source-layer": "defibrillators", - minzoom: 8, - filter: [">", "point_count", 0], - layout: { visibility: "visible" }, - paint: { - "circle-color": "rgba(0,145,64, 0.85)", - "circle-radius": [ - "step", - ["get", "point_count"], - 12, - 100, - 16, - 999, - 20, - ], - "circle-stroke-color": "rgba(245, 245, 245, 0.88)", - "circle-stroke-width": 2, - }, - }, - { - id: "clustered-circle-low-zoom", - type: "circle", - source: "aed-locations", - "source-layer": "defibrillators", - minzoom: TILE_COUNTRIES_MAX_ZOOM, - maxzoom: 8, - filter: [">", "point_count", 0], - layout: { visibility: "visible" }, - paint: { - "circle-color": "rgba(0,145,64, 0.85)", - "circle-radius": [ - "step", - ["get", "point_count"], - 8, - 100, - 11, - 999, - 14, - ], - "circle-stroke-color": "rgba(245, 245, 245, 0.88)", - "circle-stroke-width": 2, - }, - }, - { - id: "clustered-label", - type: "symbol", - source: "aed-locations", - "source-layer": "defibrillators", - minzoom: TILE_COUNTRIES_MAX_ZOOM, - filter: [">", "point_count", 0], - layout: { - "text-allow-overlap": true, - "text-field": "{point_count_abbreviated}", - "text-font": ["Open Sans Bold"], - "text-size": 10, - "text-letter-spacing": 0.05, - visibility: "visible", - }, - paint: { "text-color": "#f5f5f5" }, - }, - { - id: "countries-label", - type: "symbol", - source: "countries", - "source-layer": "defibrillators", - maxzoom: TILE_COUNTRIES_MAX_ZOOM, - layout: { - "text-allow-overlap": false, - "text-field": [ - "concat", - [ - "coalesce", - ["get", ["get", "country_code", ["properties"]], ["literal", countryCodeToName]], - ["get", "country_name", ["properties"]], - ], - "\n", - ["get", "point_count_abbreviated"], - ], - "text-font": ["Open Sans Bold"], - "text-size": 14, - "text-letter-spacing": 0.05, - visibility: "visible", - "symbol-sort-key": ["*", ["get", "point_count"], -1], - }, - paint: { - "text-halo-width": 3, - "text-halo-color": "#f5f5f5", - "text-halo-blur": 1, - // "text-color": "#f5f5f5" - }, - }, - ], - id: "style", + const countryCodeToName: Record = countriesData.reduce( + (map: Record, country) => { + if (country.names[lang.toUpperCase()] !== undefined) { + map[country.code] = country.names[lang]; + } else if (country.names.default !== undefined) { + map[country.code] = country.names.default; + } + return map; + }, + {}, + ); + return { + version: 8, + name: "Map style", + sources: { + osm: { + type: "raster", + tiles: ["https://tile.openstreetmap.org/{z}/{x}/{y}.png"], + tileSize: 256, + minzoom: 0, + maxzoom: 19, + attribution: + '© ' + + "OpenStreetMap contributors", + }, + countries: { + type: "vector", + tiles: [tilesUrl], + minzoom: 3, + maxzoom: TILE_COUNTRIES_MAX_ZOOM, + }, + "aed-locations": { + type: "vector", + tiles: [tilesUrl], + minzoom: TILE_COUNTRIES_MAX_ZOOM, + maxzoom: 16, + }, + }, + sprite: spriteUrl, + glyphs: + "https://orangemug.github.io/font-glyphs/glyphs/{fontstack}/{range}.pbf", + layers: [ + { + id: "background", + type: "raster", + source: "osm", + layout: { + visibility: "visible", + }, + }, + { + id: "borders-fill", + type: "fill", + source: "countries", + "source-layer": "countries", + paint: { + "fill-color": "#7a7a7a", + "fill-opacity": 0.5, + }, + maxzoom: TILE_COUNTRIES_MAX_ZOOM, + }, + { + id: "borders", + type: "line", + source: "countries", + "source-layer": "countries", + paint: { + "line-color": "#ff3333", + "line-width": 2, + "line-blur": 1, + }, + maxzoom: TILE_COUNTRIES_MAX_ZOOM, + }, + { + id: "unclustered", + type: "symbol", + source: "aed-locations", + "source-layer": "defibrillators", + minzoom: 9, + filter: ["!has", "point_count"], + layout: { + "icon-image": [ + "coalesce", + ["image", ["concat", "marker_", ["get", "access"]]], + ["image", "marker_unknown"], + ], + "icon-size": 0.5, + "icon-allow-overlap": true, + }, + }, + { + id: "unclustered-low-zoom", + type: "symbol", + source: "aed-locations", + "source-layer": "defibrillators", + minzoom: TILE_COUNTRIES_MAX_ZOOM, + maxzoom: 9, + filter: ["!has", "point_count"], + layout: { + "icon-image": [ + "coalesce", + ["image", ["concat", "marker_", ["get", "access"]]], + ["image", "marker_unknown"], + ], + "icon-size": 0.3, + "icon-allow-overlap": true, + }, + }, + { + id: "clustered-circle", + type: "circle", + source: "aed-locations", + "source-layer": "defibrillators", + minzoom: 8, + filter: [">", "point_count", 0], + layout: { visibility: "visible" }, + paint: { + "circle-color": "rgba(0,145,64, 0.85)", + "circle-radius": [ + "step", + ["get", "point_count"], + 12, + 100, + 16, + 999, + 20, + ], + "circle-stroke-color": "rgba(245, 245, 245, 0.88)", + "circle-stroke-width": 2, + }, + }, + { + id: "clustered-circle-low-zoom", + type: "circle", + source: "aed-locations", + "source-layer": "defibrillators", + minzoom: TILE_COUNTRIES_MAX_ZOOM, + maxzoom: 8, + filter: [">", "point_count", 0], + layout: { visibility: "visible" }, + paint: { + "circle-color": "rgba(0,145,64, 0.85)", + "circle-radius": [ + "step", + ["get", "point_count"], + 8, + 100, + 11, + 999, + 14, + ], + "circle-stroke-color": "rgba(245, 245, 245, 0.88)", + "circle-stroke-width": 2, + }, + }, + { + id: "clustered-label", + type: "symbol", + source: "aed-locations", + "source-layer": "defibrillators", + minzoom: TILE_COUNTRIES_MAX_ZOOM, + filter: [">", "point_count", 0], + layout: { + "text-allow-overlap": true, + "text-field": "{point_count_abbreviated}", + "text-font": ["Open Sans Bold"], + "text-size": 10, + "text-letter-spacing": 0.05, + visibility: "visible", + }, + paint: { "text-color": "#f5f5f5" }, + }, + { + id: "countries-label", + type: "symbol", + source: "countries", + "source-layer": "defibrillators", + maxzoom: TILE_COUNTRIES_MAX_ZOOM, + layout: { + "text-allow-overlap": false, + "text-field": [ + "concat", + [ + "coalesce", + [ + "get", + ["get", "country_code", ["properties"]], + ["literal", countryCodeToName], + ], + ["get", "country_name", ["properties"]], + ], + "\n", + ["get", "point_count_abbreviated"], + ], + "text-font": ["Open Sans Bold"], + "text-size": 14, + "text-letter-spacing": 0.05, + visibility: "visible", + "symbol-sort-key": ["*", ["get", "point_count"], -1], + }, + paint: { + "text-halo-width": 3, + "text-halo-color": "#f5f5f5", + "text-halo-blur": 1, + // "text-color": "#f5f5f5" + }, + }, + ], + id: "style", + }; }; -} export default mapStyle; diff --git a/src/components/modal.tsx b/src/components/modal.tsx index 4415b95..8578a0a 100644 --- a/src/components/modal.tsx +++ b/src/components/modal.tsx @@ -9,144 +9,153 @@ import LogInButton from "./logInButton"; import PartnersModal from "./partnersModal"; const ModalContent: FC = () => { - const { t } = useTranslation(); - const { - modalState: { - type, currentZoom, errorMessage, nodeId, - }, - } = useAppContext(); - const helpTranslationText = `🖋 ${t("navbar.help_translating")}`; + const { t } = useTranslation(); + const { + modalState: { type, currentZoom, errorMessage, nodeId }, + } = useAppContext(); + const helpTranslationText = `🖋 ${t("navbar.help_translating")}`; - switch (type) { - case ModalType.NodeAddedSuccessfully: { - const nodeUrl = `${import.meta.env.VITE_OSM_API_URL}/node/${nodeId}/`; - return ( -
-

{t("modal.aed_added_successfully")}

-

- {t("modal.available_in_osm")} -   - {nodeUrl} -

-

{t("modal.should_appear_soon")}

-
- ); - } - case ModalType.NodeUpdatedSuccessfully: { - const nodeUrl = `${import.meta.env.VITE_OSM_API_URL}/node/${nodeId}/`; - return ( -
-

{t("modal.aed_updated_successfully")}

-

- {t("modal.available_in_osm")} -   - {nodeUrl} -

-
- ); - } - case ModalType.NeedToLogin: - return ( -
-

{t("modal.need_to_login")}

- -
- ); - case ModalType.NeedMoreZoom: - return ( -
-

{t("modal.need_more_zoom")}

-

- {t("modal.current_zoom")} - {" "} - {currentZoom.toFixed(0)} - . -

-

{t("modal.zoom_need_to_be")}

-
- ); - case ModalType.About: - return ( -
-

{t("modal.about_project")}

-

- - -

-

{t("modal.about_osm")}

-

- {t("modal.create_account")} - osm.org -

-
- ); - case ModalType.Error: { - const errorText = `${t("modal.error_occurred")}: $${errorMessage}`; - return

{errorText}

; - } - case ModalType.Partners: return ; - case ModalType.ThanksForPhoto: - return ( -
-

- {t("modal.thanks_for_photo")} -

-
- ); - case ModalType.ThanksForReport: - return ( -
-

- {t("modal.thanks_for_report")} -

-
- ); - default: - return null; - } + switch (type) { + case ModalType.NodeAddedSuccessfully: { + const nodeUrl = `${import.meta.env.VITE_OSM_API_URL}/node/${nodeId}/`; + return ( +
+

{t("modal.aed_added_successfully")}

+

+ {t("modal.available_in_osm")} +   + + {nodeUrl} + +

+

{t("modal.should_appear_soon")}

+
+ ); + } + case ModalType.NodeUpdatedSuccessfully: { + const nodeUrl = `${import.meta.env.VITE_OSM_API_URL}/node/${nodeId}/`; + return ( +
+

{t("modal.aed_updated_successfully")}

+

+ {t("modal.available_in_osm")} +   + + {nodeUrl} + +

+
+ ); + } + case ModalType.NeedToLogin: + return ( +
+

{t("modal.need_to_login")}

+ +
+ ); + case ModalType.NeedMoreZoom: + return ( +
+

{t("modal.need_more_zoom")}

+

+ {t("modal.current_zoom")}{" "} + + {currentZoom.toFixed(0)} + + . +

+

{t("modal.zoom_need_to_be")}

+
+ ); + case ModalType.About: + return ( +
+

{t("modal.about_project")}

+

+ + +

+

{t("modal.about_osm")}

+

+ {t("modal.create_account")} + + osm.org + +

+
+ ); + case ModalType.Error: { + const errorText = `${t("modal.error_occurred")}: $${errorMessage}`; + return

{errorText}

; + } + case ModalType.Partners: + return ; + case ModalType.ThanksForPhoto: + return ( +
+

{t("modal.thanks_for_photo")}

+
+ ); + case ModalType.ThanksForReport: + return ( +
+

{t("modal.thanks_for_report")}

+
+ ); + default: + return null; + } }; const CustomModal: FC = () => { - const { t } = useTranslation(); - const { modalState, setModalState } = useAppContext(); + const { t } = useTranslation(); + const { modalState, setModalState } = useAppContext(); - return ( - { setModalState({ ...modalState, visible: false }); }} - closeOnEsc - closeOnBlur - > - - - - - {modalState.type === ModalType.Partners ? t("partners.honorary_patronage") : t("modal.title")} - - - - { modalState.visible && } - - - - ); + return ( + { + setModalState({ ...modalState, visible: false }); + }} + closeOnEsc + closeOnBlur + > + + + + + {modalState.type === ModalType.Partners + ? t("partners.honorary_patronage") + : t("modal.title")} + + + + {modalState.visible && } + + + + ); }; -export default CustomModal; \ No newline at end of file +export default CustomModal; diff --git a/src/components/navbar.tsx b/src/components/navbar.tsx index dea04e3..accd667 100644 --- a/src/components/navbar.tsx +++ b/src/components/navbar.tsx @@ -5,139 +5,159 @@ import { Button, Navbar } from "react-bulma-components"; import { useTranslation } from "react-i18next"; import ReactStoreBadges from "~/3rdparty/react-store-badges"; import { useAppContext } from "~/appContext"; -import {useLanguage} from "~/i18n"; +import { useLanguage } from "~/i18n"; import { ModalType, initialModalState } from "~/model/modal"; import LanguageSwitcher from "./languageSwitcher"; import LogInButton from "./logInButton"; import "./navbar.css"; const SiteNavbar: FC = ({ toggleSidebarShown }) => { - const { setModalState } = useAppContext(); - const [isActive, setIsActive] = React.useState(false); - const { t} = useTranslation(); - const language = useLanguage(); - return ( - - - - - {/* TODO: extract svg logo */} - - - - Open - AED - Map - - - - - {t("navbar.created_with_<3_by")} -   - - {t("osmp")} - - - - - - {t("navbar.hosted_by")} - {" "} - - - CloudFerro - - - - { - setIsActive(!isActive); - }} - className={`${isActive ? "is-active" : ""}`} - aria-label="menu" - aria-expanded="false" - data-target="navbarMenu" - > - - - - - - - - - - - - - - - - - - - - {t("navbar.created_with_<3_by")} -   - - {t("osmp")} - - - - - - {t("navbar.hosted_by")} - {" "} - - CloudFerro - - - - - ); + const { setModalState } = useAppContext(); + const [isActive, setIsActive] = React.useState(false); + const { t } = useTranslation(); + const language = useLanguage(); + return ( + + + + + {/* TODO: extract svg logo */} + + + + Open + AED + Map + + + + + {t("navbar.created_with_<3_by")} +   + + {t("osmp")} + + + + + + {t("navbar.hosted_by")}{" "} + + + CloudFerro + + + + { + setIsActive(!isActive); + }} + className={`${isActive ? "is-active" : ""}`} + aria-label="menu" + aria-expanded="false" + data-target="navbarMenu" + > + + + + + + + + + + + + + + + + + + + + {t("navbar.created_with_<3_by")} +   + + {t("osmp")} + + + + + + {t("navbar.hosted_by")}{" "} + + CloudFerro + + + + + ); }; interface SiteNavbarProps { - toggleSidebarShown: () => void + toggleSidebarShown: () => void; } export default SiteNavbar; diff --git a/src/components/nominatimGeocoder.ts b/src/components/nominatimGeocoder.ts index cf0dde4..5bfd4e2 100644 --- a/src/components/nominatimGeocoder.ts +++ b/src/components/nominatimGeocoder.ts @@ -1,46 +1,42 @@ interface GeocoderConfig { - query: string, + query: string; } const nominatimGeocoder = { - forwardGeocode: async (config: GeocoderConfig) => { - const features = []; - try { - const request = `https://nominatim.openstreetmap.org/search?q=${ - config.query - }&format=geojson&polygon_geojson=1&addressdetails=1`; - const response = await fetch(request); - const geojson = await response.json(); - for (let i = 0; i < geojson.features.length; i += 1) { - const feature = geojson.features[i]; - const center = [ - feature.bbox[0] - + (feature.bbox[2] - feature.bbox[0]) / 2, - feature.bbox[1] - + (feature.bbox[3] - feature.bbox[1]) / 2, - ]; - const point = { - type: "Feature", - geometry: { - type: "Point", - coordinates: center, - }, - place_name: feature.properties.display_name, - properties: feature.properties, - text: feature.properties.display_name, - place_type: ["place"], - center, - }; - features.push(point); - } - } catch (e) { - console.error(`Failed to forwardGeocode with error: ${e}`); - } + forwardGeocode: async (config: GeocoderConfig) => { + const features = []; + try { + const request = `https://nominatim.openstreetmap.org/search?q=${config.query}&format=geojson&polygon_geojson=1&addressdetails=1`; + const response = await fetch(request); + const geojson = await response.json(); + for (let i = 0; i < geojson.features.length; i += 1) { + const feature = geojson.features[i]; + const center = [ + feature.bbox[0] + (feature.bbox[2] - feature.bbox[0]) / 2, + feature.bbox[1] + (feature.bbox[3] - feature.bbox[1]) / 2, + ]; + const point = { + type: "Feature", + geometry: { + type: "Point", + coordinates: center, + }, + place_name: feature.properties.display_name, + properties: feature.properties, + text: feature.properties.display_name, + place_type: ["place"], + center, + }; + features.push(point); + } + } catch (e) { + console.error(`Failed to forwardGeocode with error: ${e}`); + } - return { - features, - }; - }, + return { + features, + }; + }, }; -export default nominatimGeocoder; \ No newline at end of file +export default nominatimGeocoder; diff --git a/src/components/partnersModal.tsx b/src/components/partnersModal.tsx index 63c254d..a06af7a 100644 --- a/src/components/partnersModal.tsx +++ b/src/components/partnersModal.tsx @@ -2,100 +2,112 @@ import React, { FC } from "react"; import { useTranslation } from "react-i18next"; interface Partner { - name: string; - person: string | null; - role: string | null; - image: string; - imageHeight: number; - url: string; + name: string; + person: string | null; + role: string | null; + image: string; + imageHeight: number; + url: string; } const PartnersModal: FC = () => { - const { t } = useTranslation(); - const partners: Partner[] = [ - { - name: "CloudFerro", - person: null, - role: null, - image: "cloudferro_logo-dark.png", - imageHeight: 55, - url: "https://cloudferro.com/", - }, - { - name: t("partners.wroclaw_medical_university"), - person: null, - role: t("partners.main_scientific_partner"), - image: "logo-umw.jpg", // TODO: vector logo - imageHeight: 40, - url: "https://www.umw.edu.pl", - }, - { - name: t("partners.e_health_centre"), - person: null, - role: null, - image: "logo-cez.png", - imageHeight: 67, - url: "https://cez.gov.pl", - }, - { - name: t("partners.warsaw_university_of_technology"), - person: null, - role: null, - image: "logo-pw.png", - imageHeight: 80, - url: "https://pw.edu.pl/", - }, - { - name: t("partners.gugik"), - person: "Alicja Kulka", - role: `${t("partners.acting")} ${t("partners.chief_geodesist")}`, - image: "logo-gugik-short.png", - imageHeight: 55, - url: "https://www.gov.pl/web/gugik", - }, - ]; - return ( - <> -
-
- {partners.map((partner) => ( -
-
-
- {partner.name} -
-
-
- {partner.name} - {partner.person !== null && ( -

- {partner.person} -

- )} - {partner.role !== null && ( -
- {partner.role} -
- )} -
-
- ))} -
-
- - - ); + const { t } = useTranslation(); + const partners: Partner[] = [ + { + name: "CloudFerro", + person: null, + role: null, + image: "cloudferro_logo-dark.png", + imageHeight: 55, + url: "https://cloudferro.com/", + }, + { + name: t("partners.wroclaw_medical_university"), + person: null, + role: t("partners.main_scientific_partner"), + image: "logo-umw.jpg", // TODO: vector logo + imageHeight: 40, + url: "https://www.umw.edu.pl", + }, + { + name: t("partners.e_health_centre"), + person: null, + role: null, + image: "logo-cez.png", + imageHeight: 67, + url: "https://cez.gov.pl", + }, + { + name: t("partners.warsaw_university_of_technology"), + person: null, + role: null, + image: "logo-pw.png", + imageHeight: 80, + url: "https://pw.edu.pl/", + }, + { + name: t("partners.gugik"), + person: "Alicja Kulka", + role: `${t("partners.acting")} ${t("partners.chief_geodesist")}`, + image: "logo-gugik-short.png", + imageHeight: 55, + url: "https://www.gov.pl/web/gugik", + }, + ]; + return ( + <> +
+
+ {partners.map((partner) => ( +
+
+
+ {partner.name} +
+
+
+ + {partner.name} + + {partner.person !== null && ( +

+ + {partner.person} + +

+ )} + {partner.role !== null && ( +
+ {partner.role} +
+ )} +
+
+ ))} +
+
+ + + ); }; -export default PartnersModal; \ No newline at end of file +export default PartnersModal; diff --git a/src/components/sidebar-left.tsx b/src/components/sidebar-left.tsx index ea747f8..20d2e9f 100644 --- a/src/components/sidebar-left.tsx +++ b/src/components/sidebar-left.tsx @@ -9,64 +9,66 @@ import PhotoReport from "./sidebar/photoReporter"; import PhotoUpload from "./sidebar/photoUploader"; const SidebarLeft: FC = (props) => { - const { - action, data, closeSidebar, visible, marker, openChangesetId, setOpenChangesetId, - } = props; + const { + action, + data, + closeSidebar, + visible, + marker, + openChangesetId, + setOpenChangesetId, + } = props; - console.log("Opening left sidebar with action: ", action, " and data:", data); + console.log("Opening left sidebar with action: ", action, " and data:", data); - if (!visible) return null; + if (!visible) return null; - switch (action) { - case SidebarAction.init: - return null; - case SidebarAction.showDetails: - return ; - case SidebarAction.addNode: - if (marker === null) { - console.error("Marker shouldn't be null"); - return null; - } - return ( - - ); - case SidebarAction.editNode: - return ( - - ); - case SidebarAction.reportPhoto: - return ( - - ); - case SidebarAction.uploadPhoto: - return ( - - ); - default: - return null; - } + switch (action) { + case SidebarAction.init: + return null; + case SidebarAction.showDetails: + return ; + case SidebarAction.addNode: + if (marker === null) { + console.error("Marker shouldn't be null"); + return null; + } + return ( + + ); + case SidebarAction.editNode: + return ( + + ); + case SidebarAction.reportPhoto: + return ; + case SidebarAction.uploadPhoto: + return ; + default: + return null; + } }; interface SidebarLeftProps { - action: SidebarAction, - data: DefibrillatorData | null, - closeSidebar: () => void, - visible: boolean, - marker: Marker | null, - openChangesetId: string, - setOpenChangesetId: (changesetId: string) => void, + action: SidebarAction; + data: DefibrillatorData | null; + closeSidebar: () => void; + visible: boolean; + marker: Marker | null; + openChangesetId: string; + setOpenChangesetId: (changesetId: string) => void; } -export default SidebarLeft; \ No newline at end of file +export default SidebarLeft; diff --git a/src/components/sidebar-right.tsx b/src/components/sidebar-right.tsx index b2b9c63..bdc1d9c 100644 --- a/src/components/sidebar-right.tsx +++ b/src/components/sidebar-right.tsx @@ -5,16 +5,18 @@ import MapLegend from "./legend"; import "./sidebar.css"; import { CloseSidebarButton } from "./sidebar/buttons"; -export default function SidebarRight({ closeSidebar }: { closeSidebar: () => void }) { - return ( -
- - - - - - - -
- ); +export default function SidebarRight({ + closeSidebar, +}: { closeSidebar: () => void }) { + return ( +
+ + + + + + + +
+ ); } diff --git a/src/components/sidebar/access.tsx b/src/components/sidebar/access.tsx index 3742bf3..c989d39 100644 --- a/src/components/sidebar/access.tsx +++ b/src/components/sidebar/access.tsx @@ -2,58 +2,58 @@ import React from "react"; import { useTranslation } from "react-i18next"; const accessToColourMapping = { - yes: "has-background-green has-text-white-ter", - no: "has-background-red has-text-white-ter", - private: "has-background-blue has-text-white-ter", - permissive: "has-background-blue has-text-white-ter", - customers: "has-background-yellow has-text-black-ter", - default: "has-background-gray has-text-white-ter", + yes: "has-background-green has-text-white-ter", + no: "has-background-red has-text-white-ter", + private: "has-background-blue has-text-white-ter", + permissive: "has-background-blue has-text-white-ter", + customers: "has-background-yellow has-text-black-ter", + default: "has-background-gray has-text-white-ter", }; export function accessColourClass(access: string): string { - if (access in accessToColourMapping) { - return accessToColourMapping[access as keyof typeof accessToColourMapping]; - } - return accessToColourMapping.default; + if (access in accessToColourMapping) { + return accessToColourMapping[access as keyof typeof accessToColourMapping]; + } + return accessToColourMapping.default; } -export default function AccessFormField({ access, setAccess }: AccessFormFieldProps) { - const { t } = useTranslation(); - const groupName = "aedAccess"; - const accessOptions: Array<{ value: string, label: string }> = [ - { value: "yes", label: t("access.yes") }, - { value: "private", label: t("access.private") }, - { value: "customers", label: t("access.customers") }, - ]; - const accessLabelText = `${t("form.accessibility")}:`; - return ( -
- {accessLabelText} - {accessOptions.map(({ value, label }) => ( -
- setAccess(value)} - /> - -
- ))} -
- ); +export default function AccessFormField({ + access, + setAccess, +}: AccessFormFieldProps) { + const { t } = useTranslation(); + const groupName = "aedAccess"; + const accessOptions: Array<{ value: string; label: string }> = [ + { value: "yes", label: t("access.yes") }, + { value: "private", label: t("access.private") }, + { value: "customers", label: t("access.customers") }, + ]; + const accessLabelText = `${t("form.accessibility")}:`; + return ( +
+ {accessLabelText} + {accessOptions.map(({ value, label }) => ( +
+ setAccess(value)} + /> + +
+ ))} +
+ ); } interface AccessFormFieldProps { - access: string, - setAccess: (access: string) => void, -} \ No newline at end of file + access: string; + setAccess: (access: string) => void; +} diff --git a/src/components/sidebar/buttons.tsx b/src/components/sidebar/buttons.tsx index c8e6692..0d0675f 100644 --- a/src/components/sidebar/buttons.tsx +++ b/src/components/sidebar/buttons.tsx @@ -1,6 +1,10 @@ -import {mdiContentCopy, - mdiGoogleMaps, - mdiMagnify, mdiMap, mdiMapMarkerPlus,mdiPencil, +import { + mdiContentCopy, + mdiGoogleMaps, + mdiMagnify, + mdiMap, + mdiMapMarkerPlus, + mdiPencil, } from "@mdi/js"; import Icon from "@mdi/react"; import React, { FC } from "react"; @@ -15,126 +19,144 @@ import { fetchNodeDataFromOsm } from "~/osm"; type OsmId = string; export function EditButton({ osmId }: { osmId: OsmId }) { - const { t } = useTranslation(); - const { - setSidebarData, setSidebarAction, setModalState, authState: { auth }, - } = useAppContext(); - const startEdit = () => { - fetchNodeDataFromOsm(osmId).then((data) => { - setSidebarData(data); - if (auth === null || !auth.authenticated()) { - setModalState({ ...initialModalState, visible: true, type: ModalType.NeedToLogin }); - } else { - setSidebarAction(SidebarAction.editNode); - } - }); - }; - return ( - - ); + const { t } = useTranslation(); + const { + setSidebarData, + setSidebarAction, + setModalState, + authState: { auth }, + } = useAppContext(); + const startEdit = () => { + fetchNodeDataFromOsm(osmId).then((data) => { + setSidebarData(data); + if (auth === null || !auth.authenticated()) { + setModalState({ + ...initialModalState, + visible: true, + type: ModalType.NeedToLogin, + }); + } else { + setSidebarAction(SidebarAction.editNode); + } + }); + }; + return ( + + ); } export function ViewButton({ osmId }: { osmId: OsmId }) { - const { t } = useTranslation(); - return ( - - - {t("sidebar.view")} - - ); + const { t } = useTranslation(); + return ( + + + {t("sidebar.view")} + + ); } export function CopyUrlButton() { - const { t } = useTranslation(); - return ( - - ); + const { t } = useTranslation(); + return ( + + ); } -export function CloseSidebarButton({ closeSidebarFunction }: { closeSidebarFunction: () => void }) { - const { t } = useTranslation(); - // Button seems to have issues with delete class, using button instead - return ( - - ); +export function AddAedButton({ + nextStep, +}: { nextStep: (event: React.MouseEvent) => void }) { + const { t } = useTranslation(); + return ( + + ); } -export function SaveAedButton({ nextStep }: { nextStep: (event: React.MouseEvent) => void }) { - const { t } = useTranslation(); - return ( - - ); +export function SaveAedButton({ + nextStep, +}: { nextStep: (event: React.MouseEvent) => void }) { + const { t } = useTranslation(); + return ( + + ); } interface NavigationButtonProps { - lat: number, - lon: number, + lat: number; + lon: number; } -export const OpenStreetMapNavigationButton: FC = ({ lat, lon }) => { - const { t } = useTranslation(); - const OSM_NAVIGATION_ZOOM = 14; - return ( - - - {t("sidebar.openstreetmap_navigation")} - - ); +export const OpenStreetMapNavigationButton: FC = ({ + lat, + lon, +}) => { + const { t } = useTranslation(); + const OSM_NAVIGATION_ZOOM = 14; + return ( + + + {t("sidebar.openstreetmap_navigation")} + + ); }; -export const GoogleMapsNavigationButton: FC = ({ lat, lon }) => { - const { t } = useTranslation(); - return ( - - - {t("sidebar.google_maps_navigation")} - - ); -}; \ No newline at end of file +export const GoogleMapsNavigationButton: FC = ({ + lat, + lon, +}) => { + const { t } = useTranslation(); + return ( + + + {t("sidebar.google_maps_navigation")} + + ); +}; diff --git a/src/components/sidebar/contactNumber.tsx b/src/components/sidebar/contactNumber.tsx index 00c5193..5b1f617 100644 --- a/src/components/sidebar/contactNumber.tsx +++ b/src/components/sidebar/contactNumber.tsx @@ -3,35 +3,41 @@ import Icon from "@mdi/react"; import React from "react"; import { useTranslation } from "react-i18next"; -export default function ContactPhoneFormField({ phoneNumber, setPhoneNumber }: ContactPhoneFormFieldProps) { - const { t } = useTranslation(); +export default function ContactPhoneFormField({ + phoneNumber, + setPhoneNumber, +}: ContactPhoneFormFieldProps) { + const { t } = useTranslation(); - const phoneRegex = "^[+][0-9]{2}[ ]?((?:[0-9]{9})|(?:[0-9]{3} [0-9]{3} " - + "[0-9]{3})|(?:[0-9]{2} [0-9]{3} [0-9]{2} [0-9]{2}))$"; + const phoneRegex = + "^[+][0-9]{2}[ ]?((?:[0-9]{9})|(?:[0-9]{3} [0-9]{3} " + + "[0-9]{3})|(?:[0-9]{2} [0-9]{3} [0-9]{2} [0-9]{2}))$"; - return ( -
- -
- setPhoneNumber(event.target.value)} - placeholder="+48 123 456 789" - name="aedPhone" - pattern={phoneRegex} - /> - - - -
-

{t("form.optional_field")}

-
- ); + return ( +
+ +
+ setPhoneNumber(event.target.value)} + placeholder="+48 123 456 789" + name="aedPhone" + pattern={phoneRegex} + /> + + + +
+

{t("form.optional_field")}

+
+ ); } interface ContactPhoneFormFieldProps { - phoneNumber: string, - setPhoneNumber: (phoneNumber: string) => void, -} \ No newline at end of file + phoneNumber: string; + setPhoneNumber: (phoneNumber: string) => void; +} diff --git a/src/components/sidebar/defibrillatorDetails.tsx b/src/components/sidebar/defibrillatorDetails.tsx index 48e1da5..1293cc9 100644 --- a/src/components/sidebar/defibrillatorDetails.tsx +++ b/src/components/sidebar/defibrillatorDetails.tsx @@ -1,207 +1,274 @@ import { - mdiAccountSupervisorOutline, mdiClockOutline, mdiHomeRoof,mdiImagePlus, - mdiInformationOutline, mdiMapMarkerOutline, mdiPhoneOutline, + mdiAccountSupervisorOutline, + mdiClockOutline, + mdiHomeRoof, + mdiImagePlus, + mdiInformationOutline, + mdiMapMarkerOutline, + mdiPhoneOutline, } from "@mdi/js"; import Icon from "@mdi/react"; import React, { FC } from "react"; -import { - Button, - Card, Columns, Image, -} from "react-bulma-components"; +import { Button, Card, Columns, Image } from "react-bulma-components"; import { useTranslation } from "react-i18next"; import ImageGallery, { ReactImageGalleryItem } from "react-image-gallery"; import "react-image-gallery/styles/css/image-gallery.css"; import { useAppContext } from "~/appContext"; import { backendBaseUrl } from "~/backend"; -import {useLanguage} from "~/i18n"; +import { useLanguage } from "~/i18n"; import { DefibrillatorData } from "~/model/defibrillatorData"; import { ModalType, initialModalState } from "~/model/modal"; import SidebarAction from "~/model/sidebarAction"; import { accessColourClass } from "./access"; import { - CloseSidebarButton, - CopyUrlButton, EditButton, - GoogleMapsNavigationButton, - OpenStreetMapNavigationButton, - ViewButton, + CloseSidebarButton, + CopyUrlButton, + EditButton, + GoogleMapsNavigationButton, + OpenStreetMapNavigationButton, + ViewButton, } from "./buttons"; import DetailTextRow from "./detailTextRow"; import { OpeningHoursField } from "./openingHours"; import { CheckDateField } from "./verificationDate"; function photoGallery(data: DefibrillatorData, closeSidebar: () => void) { - const { t } = useTranslation(); - const { authState: { auth }, setSidebarAction, setModalState } = useAppContext(); - let images: ReactImageGalleryItem[] = []; - // Currently only one photo allowed - if (data.photoRelativeUrl !== undefined && data.photoRelativeUrl !== null) { - images = [ - { - original: backendBaseUrl + data.photoRelativeUrl, - thumbnail: backendBaseUrl + data.photoRelativeUrl, - }, - ]; - } - if (images.length > 0) { - // ref would be used if there were more photos to see which one is selected - // const refImg = useRef(null); - const renderCustomControls = () => ( - - ); + const { t } = useTranslation(); + const { + authState: { auth }, + setSidebarAction, + setModalState, + } = useAppContext(); + let images: ReactImageGalleryItem[] = []; + // Currently only one photo allowed + if (data.photoRelativeUrl !== undefined && data.photoRelativeUrl !== null) { + images = [ + { + original: backendBaseUrl + data.photoRelativeUrl, + thumbnail: backendBaseUrl + data.photoRelativeUrl, + }, + ]; + } + if (images.length > 0) { + // ref would be used if there were more photos to see which one is selected + // const refImg = useRef(null); + const renderCustomControls = () => ( + + ); - return ( -
- 1} - renderCustomControls={data.photoId === null ? undefined : renderCustomControls} - /> -
-
- ); - } - return ( -
- -
-
- ); + return ( +
+ 1} + renderCustomControls={ + data.photoId === null ? undefined : renderCustomControls + } + /> +
+
+ ); + } + return ( +
+ +
+
+ ); } const DefibrillatorDetails: FC = (props) => { - const { t} = useTranslation(); - const language = useLanguage(); - const { - data, closeSidebar, - } = props; - if (data === null) return null; - const accessText = data.tags.access ? ` - ${t(`access.${data.tags.access}`)}` : ""; - const defibrillatorLocation = data.tags[`defibrillator:location:${language}`] - ?? data.tags["defibrillator:location"]; - const levelText = data.tags.level ? ` (${t("sidebar.level")}: ${data.tags.level})` : ""; - const indoorText = data.tags.indoor ? t(`indoor.${data.tags.indoor}`) + levelText : ""; + const { t } = useTranslation(); + const language = useLanguage(); + const { data, closeSidebar } = props; + if (data === null) return null; + const accessText = data.tags.access + ? ` - ${t(`access.${data.tags.access}`)}` + : ""; + const defibrillatorLocation = + data.tags[`defibrillator:location:${language}`] ?? + data.tags["defibrillator:location"]; + const levelText = data.tags.level + ? ` (${t("sidebar.level")}: ${data.tags.level})` + : ""; + const indoorText = data.tags.indoor + ? t(`indoor.${data.tags.indoor}`) + levelText + : ""; - return ( - - ); + return ( + + ); }; interface DefibrillatorDetailsProps { - data: DefibrillatorData | null, - closeSidebar: () => void, + data: DefibrillatorData | null; + closeSidebar: () => void; } export default DefibrillatorDetails; diff --git a/src/components/sidebar/defibrillatorEditor.tsx b/src/components/sidebar/defibrillatorEditor.tsx index 5c7182f..89439fe 100644 --- a/src/components/sidebar/defibrillatorEditor.tsx +++ b/src/components/sidebar/defibrillatorEditor.tsx @@ -3,10 +3,14 @@ import React, { FC, useState } from "react"; import { Card, Image } from "react-bulma-components"; import { useTranslation } from "react-i18next"; import { useAppContext } from "~/appContext"; -import {useLanguage} from "~/i18n"; +import { useLanguage } from "~/i18n"; import { DefibrillatorData } from "~/model/defibrillatorData"; import { ModalType, initialModalState } from "~/model/modal"; -import { addDefibrillatorToOSM, editDefibrillatorInOSM, getOpenChangesetId } from "~/osm"; +import { + addDefibrillatorToOSM, + editDefibrillatorInOSM, + getOpenChangesetId, +} from "~/osm"; import AccessFormField from "./access"; import { AddAedButton, CloseSidebarButton, SaveAedButton } from "./buttons"; import ContactPhoneFormField from "./contactNumber"; @@ -15,134 +19,197 @@ import LocationFormField from "./location"; import { CheckDateFormField } from "./verificationDate"; const DefibrillatorEditor: FC = ({ - closeSidebar, marker, openChangesetId, setOpenChangesetId, data, + closeSidebar, + marker, + openChangesetId, + setOpenChangesetId, + data, }) => { - const { t } = useTranslation(); - const language = useLanguage(); - const { authState: { auth }, setModalState } = useAppContext(); - const newAED = data === null; - const initialTags = data !== null ? data.tags : { emergency: "defibrillator" }; - const [access, setAccess] = useState(initialTags.access ?? ""); - const [indoor, setIndoor] = useState(initialTags.indoor ?? ""); - const [level, setLevel] = useState(initialTags.level ?? ""); - const [location, setLocation] = useState( - initialTags[`defibrillator:location:${language}`] ?? "", - ); - const [phoneNumber, setPhoneNumber] = useState(initialTags.phone ?? initialTags["contact:phone"] ?? ""); - const todayDate = new Date().toISOString().substring(0, 10); - const [checkDate, setCheckDate] = useState(todayDate); + const { t } = useTranslation(); + const language = useLanguage(); + const { + authState: { auth }, + setModalState, + } = useAppContext(); + const newAED = data === null; + const initialTags = + data !== null ? data.tags : { emergency: "defibrillator" }; + const [access, setAccess] = useState(initialTags.access ?? ""); + const [indoor, setIndoor] = useState(initialTags.indoor ?? ""); + const [level, setLevel] = useState(initialTags.level ?? ""); + const [location, setLocation] = useState( + initialTags[`defibrillator:location:${language}`] ?? "", + ); + const [phoneNumber, setPhoneNumber] = useState( + initialTags.phone ?? initialTags["contact:phone"] ?? "", + ); + const todayDate = new Date().toISOString().substring(0, 10); + const [checkDate, setCheckDate] = useState(todayDate); - const parseTags: () => Record = () => { - const tags = { ...initialTags }; - if (access.length > 0) tags.access = access; - if (indoor.length > 0) tags.indoor = indoor; - if (level.length > 0) tags.level = level.trim(); - if (location.trim().length > 0) tags[`defibrillator:location:${language}`] = location.trim(); - if (phoneNumber.trim().length > 0) tags.phone = phoneNumber.trim(); - if (checkDate.trim().length > 0) tags.check_date = checkDate.trim(); - return tags; - }; + const parseTags: () => Record = () => { + const tags = { ...initialTags }; + if (access.length > 0) tags.access = access; + if (indoor.length > 0) tags.indoor = indoor; + if (level.length > 0) tags.level = level.trim(); + if (location.trim().length > 0) + tags[`defibrillator:location:${language}`] = location.trim(); + if (phoneNumber.trim().length > 0) tags.phone = phoneNumber.trim(); + if (checkDate.trim().length > 0) tags.check_date = checkDate.trim(); + return tags; + }; - const sendFormData = (event: React.MouseEvent) => { - event.preventDefault(); - if (event.target === null) { - console.error("Form target null"); - return; - } - const button = event.target as HTMLFormElement; - button.classList.add("is-loading"); - const tags = parseTags(); - if (auth === null) return; - const handleError = (err: XMLHttpRequest) => { - button.classList.remove("is-loading"); - closeSidebar(); - console.error(err); - const errorMessage = `${err}
status: ${err.status} ${err.statusText}
${err.responseText}`; - setModalState({ - ...initialModalState, visible: true, type: ModalType.Error, errorMessage, - }); - }; - if (newAED) { - if (marker === null) { - console.error("Marker shouldn't be null"); - return null; - } - const lngLat = marker.getLngLat(); - const newDefibrillatorData = { - lon: lngLat.lng, - lat: lngLat.lat, - tags, - }; - getOpenChangesetId(auth, openChangesetId, setOpenChangesetId, language, newAED) - .then((changesetId) => addDefibrillatorToOSM(auth, changesetId, newDefibrillatorData)) - .then((newNodeId) => { - button.classList.remove("is-loading"); - closeSidebar(); - console.log("created new node with id: ", newNodeId); - setModalState({ - ...initialModalState, visible: true, type: ModalType.NodeAddedSuccessfully, nodeId: newNodeId, - }); - }) - .catch(handleError); - } else { - const defibrillatorData: DefibrillatorData = { - ...data, - tags, - }; - getOpenChangesetId(auth, openChangesetId, setOpenChangesetId, language, newAED) - .then((changesetId) => editDefibrillatorInOSM(auth, changesetId, defibrillatorData)) - .then((newVersion) => { - button.classList.remove("is-loading"); - closeSidebar(); - console.log("updated node with id: ", newVersion); - setModalState({ - ...initialModalState, - visible: true, - type: ModalType.NodeUpdatedSuccessfully, - nodeId: data.osmId, - }); - }) - .catch(handleError); - } - }; - return ( - + ); }; interface DefibrillatorEditorProps { - closeSidebar: () => void, - marker: Marker | null, - openChangesetId: string, - setOpenChangesetId: (changesetId: string) => void, - data: DefibrillatorData | null, + closeSidebar: () => void; + marker: Marker | null; + openChangesetId: string; + setOpenChangesetId: (changesetId: string) => void; + data: DefibrillatorData | null; } -export default DefibrillatorEditor; \ No newline at end of file +export default DefibrillatorEditor; diff --git a/src/components/sidebar/detailTextRow.tsx b/src/components/sidebar/detailTextRow.tsx index bfbe6e7..6fff48b 100644 --- a/src/components/sidebar/detailTextRow.tsx +++ b/src/components/sidebar/detailTextRow.tsx @@ -3,19 +3,21 @@ import { useTranslation } from "react-i18next"; import SpanNoData from "./spanNoData"; const DetailTextRow: FC = ({ translationId, text }) => { - const { t } = useTranslation(); - const labelText = `${t(translationId)}: `; - return ( -
-

{labelText}

- { text - ? {text} - : } -
- ); + const { t } = useTranslation(); + const labelText = `${t(translationId)}: `; + return ( +
+

{labelText}

+ {text ? ( + {text} + ) : ( + + )} +
+ ); }; interface DetailTextRowProps { - translationId: string, - text: string, + translationId: string; + text: string; } -export default DetailTextRow; \ No newline at end of file +export default DetailTextRow; diff --git a/src/components/sidebar/indoor.tsx b/src/components/sidebar/indoor.tsx index 720b19b..fe6b354 100644 --- a/src/components/sidebar/indoor.tsx +++ b/src/components/sidebar/indoor.tsx @@ -2,61 +2,68 @@ import React from "react"; import { useTranslation } from "react-i18next"; export default function IndoorFormField({ - indoor, setIndoor, level, setLevel, + indoor, + setIndoor, + level, + setLevel, }: IndoorFormFieldProps) { - const { t } = useTranslation(); - const groupName = "aedIndoor"; - const indoorOptions: Array<{ value: string, label: string }> = [ - { value: "no", label: t("form.outside") }, - { value: "yes", label: t("form.inside") }, - ]; - return ( -
- {t("form.is_indoor")} -
- {indoorOptions.map(({ value, label }) => ( - - setIndoor(value)} - /> - - - ))} -
- {indoor === "yes" && ( -
- -
- setLevel(event.target.value)} - /> -
-
- )} -
- ); + const { t } = useTranslation(); + const groupName = "aedIndoor"; + const indoorOptions: Array<{ value: string; label: string }> = [ + { value: "no", label: t("form.outside") }, + { value: "yes", label: t("form.inside") }, + ]; + return ( +
+ + {t("form.is_indoor")} + +
+ {indoorOptions.map(({ value, label }) => ( + + setIndoor(value)} + /> + + + ))} +
+ {indoor === "yes" && ( +
+ +
+ setLevel(event.target.value)} + /> +
+
+ )} +
+ ); } interface IndoorFormFieldProps { - indoor: string, - setIndoor: (indoor: string) => void, - level: string, - setLevel: (level: string) => void, -} \ No newline at end of file + indoor: string; + setIndoor: (indoor: string) => void; + level: string; + setLevel: (level: string) => void; +} diff --git a/src/components/sidebar/location.tsx b/src/components/sidebar/location.tsx index 2146428..9cd5136 100644 --- a/src/components/sidebar/location.tsx +++ b/src/components/sidebar/location.tsx @@ -2,28 +2,33 @@ import React from "react"; import { useTranslation } from "react-i18next"; import { useLanguage } from "~/i18n"; -export default function LocationFormField({ location, setLocation }: LocationFormFieldProps) { - const { t} = useTranslation(); - const language = useLanguage(); - const locationLabelText = `${t("form.location")} (${language}):`; - return ( -
- -
-