diff --git a/.gitignore b/.gitignore index 7bf522f..cf228b8 100644 --- a/.gitignore +++ b/.gitignore @@ -40,3 +40,6 @@ Thumbs.db # Production build www/ + +# Pagekite for localhost tunneling (https://pagekite.net/) +pagekite.py \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 5e81ea1..a40ebed 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,9 +14,11 @@ "framework7": "^7.0.9", "framework7-icons": "^5.0.5", "framework7-react": "^7.0.9", + "leaflet": "^1.9.3", "material-icons": "^1.13.1", "react": "^18.2.0", "react-dom": "^18.2.0", + "react-leaflet": "^4.2.0", "skeleton-elements": "^4.0.1", "swiper": "^8.4.5" }, @@ -2214,6 +2216,16 @@ "node": ">= 8" } }, + "node_modules/@react-leaflet/core": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@react-leaflet/core/-/core-2.1.0.tgz", + "integrity": "sha512-Qk7Pfu8BSarKGqILj4x7bCSZ1pjuAPZ+qmRwH5S7mDS91VSbVVsJSrW4qA+GPrro8t69gFYVMWb1Zc4yFmPiVg==", + "peerDependencies": { + "leaflet": "^1.9.0", + "react": "^18.0.0", + "react-dom": "^18.0.0" + } + }, "node_modules/@rollup/plugin-babel": { "version": "5.3.1", "resolved": "https://registry.npmjs.org/@rollup/plugin-babel/-/plugin-babel-5.3.1.tgz", @@ -6150,6 +6162,11 @@ "node": ">=8" } }, + "node_modules/leaflet": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/leaflet/-/leaflet-1.9.3.tgz", + "integrity": "sha512-iB2cR9vAkDOu5l3HAay2obcUHZ7xwUBBjph8+PGtmW/2lYhbLizWtG7nTeYht36WfOslixQF9D/uSIzhZgGMfQ==" + }, "node_modules/leven": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", @@ -8137,6 +8154,19 @@ "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", "dev": true }, + "node_modules/react-leaflet": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/react-leaflet/-/react-leaflet-4.2.0.tgz", + "integrity": "sha512-9d8T7hzYrQA5GLe3vn0qtRLJzQKgjr080NKa45yArGwuSl1nH/6aK9gp7DeYdktpdO1vKGSUTGW5AsUS064X0A==", + "dependencies": { + "@react-leaflet/core": "^2.1.0" + }, + "peerDependencies": { + "leaflet": "^1.9.0", + "react": "^18.0.0", + "react-dom": "^18.0.0" + } + }, "node_modules/react-refresh": { "version": "0.10.0", "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.10.0.tgz", diff --git a/package.json b/package.json index 9b07485..7085956 100644 --- a/package.json +++ b/package.json @@ -29,9 +29,11 @@ "framework7": "^7.0.9", "framework7-icons": "^5.0.5", "framework7-react": "^7.0.9", + "leaflet": "^1.9.3", "material-icons": "^1.13.1", "react": "^18.2.0", "react-dom": "^18.2.0", + "react-leaflet": "^4.2.0", "skeleton-elements": "^4.0.1", "swiper": "^8.4.5" }, diff --git a/public/img/LocationMarker.png b/public/img/LocationMarker.png new file mode 100644 index 0000000..413a763 Binary files /dev/null and b/public/img/LocationMarker.png differ diff --git a/public/img/OwnLocationMarker.png b/public/img/OwnLocationMarker.png new file mode 100644 index 0000000..7c24645 Binary files /dev/null and b/public/img/OwnLocationMarker.png differ diff --git a/src/components/AccuracyCircle.jsx b/src/components/AccuracyCircle.jsx new file mode 100644 index 0000000..af0f076 --- /dev/null +++ b/src/components/AccuracyCircle.jsx @@ -0,0 +1,35 @@ +import React from "react"; +import { Circle } from "react-leaflet"; + +class AccuracyCircle extends React.Component { + render() { + if ( + this.props.visible === false || + this.props.center === undefined || + this.props.center === null || + this.props.center.lat === undefined || + this.props.center.lng === undefined || + this.props.radius === undefined || + this.props.radius === null + ) { + return null; + } + + return ( + + ); + } +} + +// define the types of the properties that are passed to the component +AccuracyCircle.prototype.props = /** @type { { + visible: boolean, + center: { lat: number, lng: number } | undefined, + radius: number | undefined, +} } */ ({}); + +export default AccuracyCircle; diff --git a/src/components/LocationMarker.jsx b/src/components/LocationMarker.jsx new file mode 100644 index 0000000..47e21cb --- /dev/null +++ b/src/components/LocationMarker.jsx @@ -0,0 +1,28 @@ +import React from "react"; +import { Marker } from "react-leaflet"; +import L from "leaflet"; + +class LocationMarker extends React.Component { + markerIcon = L.icon({ + iconUrl: this.props.iconUrl, + iconSize: [this.props.size.width, this.props.size.height], + // Center of the icon: + iconAnchor: [this.props.anchor?.x || this.props.size.width / 2, this.props.anchor?.y || this.props.size.height / 2], + }); + + render() { + if ( + this.props.position === undefined || + this.props.position === null || + this.props.position.lat === undefined || + this.props.position.lng === undefined || + !this.props.visible + ) { + return null; + } + + return ; + } +} + +export default LocationMarker; diff --git a/src/components/OutlinePolygon.jsx b/src/components/OutlinePolygon.jsx new file mode 100644 index 0000000..3bdb6ec --- /dev/null +++ b/src/components/OutlinePolygon.jsx @@ -0,0 +1,86 @@ +import React from "react"; +import { Polygon } from "react-leaflet"; + +class OutlinePolygon extends React.Component { + constructor(props) { + super(props); + this.state = { + polyLatLngs: [], + }; + } + + async componentDidUpdate(prevProps) { + if (this.props.placeName === prevProps.placeName) return; + this.setState({ polyLatLngs: [] }); + + // fetch the polygon outline from the OpenStreetMap API (Nominatim) + // this is not done in the main component because loading the polygon outline + // takes more time than the normal API call + const response = await fetch( + // eslint-disable-next-line max-len + `https://nominatim.openstreetmap.org/search?q=${this.props.placeName}&format=json&limit=1&polygon_geojson=1&polygon_threshold=0.0005`, + ); + // check if the response is suited for a polygon outline + const data = await response.json(); + if ( + data[0]?.geojson?.coordinates === undefined || + data[0]?.geojson?.type === undefined || + data[0]?.boundingbox === undefined || + (data[0]?.geojson?.type !== "Polygon" && data[0]?.geojson?.type !== "MultiPolygon") || + (data[0]?.class !== "boundary" && data[0]?.class !== "place" && data[0]?.class !== "landuse") + ) { + this.setState({ polyLatLngs: [] }); + return; + } + + const latLngs = this.convertGeoJsonCoordsToLeafletLatLng(data[0].geojson.coordinates); + this.setState({ polyLatLngs: latLngs }); + } + + /** + * Converts the coordinates of a GeoJSON polygon to the format used by Leaflet + * @param {number[]} coords The coordinates of the GeoJSON polygon + * @returns {number[]} The coordinates for the Leaflet polygon + */ + convertGeoJsonCoordsToLeafletLatLng = coords => { + if (coords === undefined) return []; + + const transformCoords = coords => { + if ( + Array.isArray(coords) && + coords.length === 2 && + typeof coords[0] === "number" && + typeof coords[1] === "number" + ) { + return { lat: coords[1], lng: coords[0] }; + } + return coords.map(transformCoords); + }; + + const latLngs = transformCoords(coords); + + return latLngs; + }; + + render() { + if (this.state.polyLatLngs.length === 0) return null; + + return ( + + ); + } +} + +// define the types of the properties that are passed to the component +OutlinePolygon.prototype.props = /** @type { { + placeName: string, +} } */ ({}); + +export default OutlinePolygon; diff --git a/src/components/SnappingSheet.jsx b/src/components/SnappingSheet.jsx new file mode 100644 index 0000000..27e6c8d --- /dev/null +++ b/src/components/SnappingSheet.jsx @@ -0,0 +1,151 @@ +import React from "react"; +import { Sheet } from "framework7-react"; + +class SnappingSheet extends React.Component { + dragStartPositionY = 0; + sheetScrollAreaRef = React.createRef(); + isMoving = false; + + constructor(props) { + super(props); + this.state = { + sheetOpened: true, + sheetHeight: this.props.snapHeightStates[this.props.currentState || 0], + sheetHeightTransitionStyle: "0.3s ease-in-out", + }; + } + + /** + * Adds touch and mouse listeners to the sheet + */ + componentDidMount() { + // add touch listeners to the sheet + this.sheetScrollAreaRef.current.addEventListener("touchstart", this.dragStart, { passive: false }); + this.sheetScrollAreaRef.current.addEventListener("touchmove", this.dragMove, { passive: false }); + this.sheetScrollAreaRef.current.addEventListener("touchend", this.dragEnd, { passive: false }); + + // add mouse listeners to the sheet + this.sheetScrollAreaRef.current.addEventListener("mousedown", this.dragStart, { passive: false }); + window.addEventListener("mousemove", this.dragMove, { passive: false }); + window.addEventListener("mouseup", this.dragEnd, { passive: false }); + + // let the parent know that the sheet is snapped to the current state (to clear the state) + if (this.props.currentState !== undefined) this.props.snappedToHeight(this.props.currentState); + } + + /** + * Removes touch and mouse listeners from the sheet + */ + componentWillUnmount() { + // remove touch listeners from the sheet + this.sheetScrollAreaRef.current.removeEventListener("touchstart", this.dragStart); + this.sheetScrollAreaRef.current.removeEventListener("touchmove", this.dragMove); + this.sheetScrollAreaRef.current.removeEventListener("touchend", this.dragEnd); + + // remove mouse listeners from the sheet + this.sheetScrollAreaRef.current.removeEventListener("mousedown", this.dragStart); + window.removeEventListener("mousemove", this.dragMove); + window.removeEventListener("mouseup", this.dragEnd); + } + + /** + * Snaps the sheet to a given state if the state is defined + */ + componentDidUpdate() { + if (this.props.currentState !== undefined) { + this.setState({ + sheetHeightTransitionStyle: "0.3s ease-in-out", + sheetHeight: this.props.snapHeightStates[this.props.currentState], + }); + this.props.snappedToHeight(this.props.snapHeightStates[this.props.currentState]); + } + } + + /** + * Starts the dragging process + * @param {Event} event + */ + dragStart = event => { + this.dragStartPositionY = event.touches ? event.touches[0].clientY : event.clientY; + this.isMoving = true; + this.setState({ sheetHeightTransitionStyle: "0s" }); + }; + + /** + * Moves the sheet (param is a touch event or a mouse event) + * @param { Event } event + */ + dragMove = event => { + event.preventDefault(); + + if (!this.isMoving) return; + + // check if it is a touch event or a mouse event and get the y position + const dragStartPositionY = event.touches ? event.touches[0].clientY : event.clientY; + + // calculate the new sheet height + const difference = this.dragStartPositionY - dragStartPositionY; + const newSheetHeight = this.state.sheetHeight + difference; + + // check if the new sheet height is in the allowed range + if (newSheetHeight < this.props.snapHeightStates[0]) { + this.setState({ sheetHeight: this.props.snapHeightStates[0] }); + return; + } + if (newSheetHeight > this.props.snapHeightStates[this.props.snapHeightStates.length - 1]) { + this.setState({ sheetHeight: this.props.snapHeightStates[this.props.snapHeightStates.length - 1] }); + return; + } + + // set the new sheet height and update the touch start position + this.setState({ sheetHeight: newSheetHeight }); + this.dragStartPositionY = dragStartPositionY; + }; + + /** + * Ends the dragging process + */ + dragEnd = () => { + this.isMoving = false; + + // snap the sheet to the nearest height state + const closestSheetHeightState = this.props.snapHeightStates.reduce((prev, curr) => { + return Math.abs(curr - this.state.sheetHeight) < Math.abs(prev - this.state.sheetHeight) ? curr : prev; + }); + + // transition to the new sheet height + this.setState({ sheetHeightTransitionStyle: "0.3s ease-out", sheetHeight: closestSheetHeightState }); + this.props.snappedToHeight(closestSheetHeightState); + }; + + render() { + if (this.props.children === undefined) { + return null; + } + + return ( +
+ + {this.props.children} + +
+ ); + } +} + +// define the types of the properties that are passed to the component +SnappingSheet.prototype.props = /** @type { { + children: React.ReactNode + snapHeightStates: number[], + currentState: number | undefined, + snappedToHeight: (height: number) => void +} } */ ({}); + +export default SnappingSheet; diff --git a/src/components/app.jsx b/src/components/app.jsx index 42fd95f..c94b501 100644 --- a/src/components/app.jsx +++ b/src/components/app.jsx @@ -2,7 +2,7 @@ import React from "react"; import process from "node:process"; -import { App, Block, Navbar, Page, Panel, View, Views } from "framework7-react"; +import { App, View, Views } from "framework7-react"; import routes from "../js/routes"; import store from "../js/store"; @@ -27,16 +27,6 @@ class MyApp extends React.Component { render() { return ( - {/* Left panel with cover effect*/} - - - - - Left panel content goes here - - - - {/* Views/Tabs container */} diff --git a/src/js/routes.js b/src/js/routes.js index 70ce699..07d141f 100644 --- a/src/js/routes.js +++ b/src/js/routes.js @@ -1,10 +1,15 @@ import Home from "../pages/home.jsx"; +import Impressum from "../pages/impressum.jsx"; var routes = [ { path: "/", component: Home, }, + { + path: "/impressum", + component: Impressum, + }, ]; export default routes; diff --git a/src/pages/home.jsx b/src/pages/home.jsx index 0ae74a5..36dd1df 100644 --- a/src/pages/home.jsx +++ b/src/pages/home.jsx @@ -1,19 +1,406 @@ import React from "react"; -import { Page, Navbar, NavLeft, Link } from "framework7-react"; +import { Page, Searchbar, List, BlockTitle, Button, ListItem } from "framework7-react"; +import { MapContainer, TileLayer, useMap, useMapEvents } from "react-leaflet"; +import "leaflet/dist/leaflet.css"; +import SnappingSheet from "../components/SnappingSheet"; +import LocationMarker from "../components/LocationMarker"; +import AccuracyCircle from "../components/AccuracyCircle"; +import OutlinePolygon from "../components/OutlinePolygon"; + +const SEARCH_BAR_HEIGHT = 70; class Home extends React.Component { + constructor(props) { + super(props); + this.state = { + currentLocation: { + lat: 47.665575312188025, + lng: 9.447241869601651, + accuracy: 0, + }, + snapSheetToState: 1, + searchText: "", + place: {}, + mapHeight: window.innerHeight - SEARCH_BAR_HEIGHT, + selectedCoords: undefined, + searchSuggestions: [], + showSearchSuggestions: false, + }; + + this.sheetHeightStates = [ + SEARCH_BAR_HEIGHT, + window.innerHeight * 0.25 + SEARCH_BAR_HEIGHT, + window.innerHeight * 0.8 + SEARCH_BAR_HEIGHT, + ]; + this.suggestionTimeout = undefined; + this.mapNeedsUpdate = false; + this.mapSlowAnimation = true; + this.mapZoom = 4; + this.mapCenter = { + lat: 47.665575312188025, + lng: 9.447241869601651, + }; + } + + componentDidMount() { + // fire a resize event to fix leaflets map sizing bug + window.dispatchEvent(new Event("resize")); + + // get the current location + navigator.geolocation.getCurrentPosition(position => { + console.log(position); + this.setState({ + currentLocation: { + lat: position.coords.latitude, + lng: position.coords.longitude, + accuracy: position.coords.accuracy, + }, + }); + this.updatePlaceByCoords({ lat: position.coords.latitude, lng: position.coords.longitude }, 18, true); + }); + + // update the current location every 5 seconds + this.currentLocationInterval = setInterval(() => { + navigator.geolocation.getCurrentPosition(position => { + this.setState({ + currentLocation: { + lat: position.coords.latitude, + lng: position.coords.longitude, + accuracy: position.coords.accuracy, + }, + }); + }); + }, 5000); + } + + /** + * Search for a place by text or coordinates + * @param {string} searchText + */ + updatePlaceBySearch = async searchText => { + const coords = this.getCoordsFromSearchText(searchText); + + let place = {}; + if (coords !== undefined) place = await this.getPlaceByCoords(coords); + else place = await this.getPlaceByText(searchText); + + console.log("place in updatePlaceBySearch", place); + + if (place === undefined) return; + this.mapNeedsUpdate = true; + this.mapZoom = place.zoomLevel; + this.mapCenter = { + lat: place.realCoords.lat, + lng: place.realCoords.lng, + }; + this.setState({ + place: place, + snapSheetToState: 1, + selectedCoords: place.realCoords, + showSearchSuggestions: false, + }); + }; + + /** + * Update the place by given coordinates + * @param {{lat: number, lng: number}} coords + * @param {number} zoom + */ + updatePlaceByCoords = async (coords, zoom, updateMap = false) => { + if (updateMap) { + this.mapNeedsUpdate = true; + this.mapZoom = zoom; + this.mapCenter = { + lat: coords.lat, + lng: coords.lng, + }; + } + const place = await this.getPlaceByCoords(coords, zoom); + this.setState({ + place: place, + snapSheetToState: 1, + selectedCoords: coords, + }); + console.log("state in updatePlaceByCoords", this.state); + }; + + /** + * Get coordinates from a string + * @param {string} text + * @returns {{lat: number, lng: number} | undefined} undefined if no coordinates were found + */ + getCoordsFromSearchText = text => { + const coordinateRegex = /^(-?\d+(\.\d+)?),\s*(-?\d+(\.\d+)?)$/; + coordinateRegex.test(text); + const match = text.match(coordinateRegex); + if (!match) return undefined; + const lat = parseFloat(match[1]); + const lng = parseFloat(match[3]); + console.log(`lat: ${lat}, lng: ${lng}`); + return { lat: lat, lng: lng }; + }; + + /** + * Get a place from nominatim by text + * @param {string} searchText + * @returns Promise + */ + getPlaceByText = async searchText => { + const response = await fetch( + `https://nominatim.openstreetmap.org/search?q=${searchText}&format=json&addressdetails=1&limit=1&extratags=1`, + ); + const data = await response.json(); + if (data[0] === undefined) return; + const place = this.getPlaceByNominatimData(data[0]); + return place; + }; + + /** + * Get a place from nominatim by coordinates + * @param {{lat: number, lng: number}} coords + * @returns {Promise} + */ + getPlaceByCoords = async (coords, zoom = 20) => { + const response = await fetch( + // eslint-disable-next-line max-len + `https://nominatim.openstreetmap.org/reverse?format=json&lat=${coords.lat}&lon=${coords.lng}&extratags=1&zoom=${ + zoom + 1 + }&addressdetails=1`, + ); + const data = await response.json(); + console.log(data); + if (data === undefined) return; + const place = this.getPlaceByNominatimData(data); + if (place?.realCoords?.lat === undefined || place?.realCoords?.lng === undefined) { + place.realCoords = coords; + } + return place; + }; + + /** + * Get a place from nominatim response data + * @param {any} placeData + * @returns {any} + */ + getPlaceByNominatimData = placeData => { + if (placeData === undefined) return; + // console.log(placeData); + let place = { + name: placeData?.display_name || "Unknown location", + address: { + amenity: placeData?.address?.amenity || "", + city: placeData?.address?.city || "", + cityDistrict: placeData?.address?.city_district || "", + municipality: placeData?.address?.municipality || "", + country: placeData?.address?.country || "", + countryCode: placeData?.address?.country_code || "", + neighbourhood: placeData?.address?.neighbourhood || "", + postcode: placeData?.address?.postcode || "", + road: placeData?.address?.road || "", + houseNumber: placeData?.address?.house_number || "", + state: placeData?.address?.state || "", + suburb: placeData?.address?.suburb || "", + }, + type: placeData?.type || "", + importance: placeData?.importance ? parseFloat(placeData?.lat) : 0, + osmId: placeData?.osm_id || 0, + realCoords: { + lat: placeData?.lat ? parseFloat(placeData?.lat) : undefined, + lng: placeData?.lon ? parseFloat(placeData?.lon) : undefined, + }, + userInputCoords: { + lat: 0, + lng: 0, + }, + zoomLevel: this.getZoomByBoundingBox(placeData?.boundingbox) || 10, + searchedByCoords: false, + searchedByCurrentLocation: false, + searchedByPlace: false, + searchedByAddress: false, + }; + // console.log(place); + return place; + }; + + /** + * Get the zoom level by a given bounding box + * @param {number[] | string[] | undefined} boundingbox + * @returns {number | undefined} + * @see https://wiki.openstreetmap.org/wiki/Zoom_levels + * @see https://wiki.openstreetmap.org/wiki/Slippy_map_tilenames#Zoom_levels + */ + getZoomByBoundingBox(boundingbox) { + if (boundingbox === undefined) return undefined; + + const lat1 = parseFloat(`${boundingbox[0]}`); + const lat2 = parseFloat(`${boundingbox[1]}`); + const lng1 = parseFloat(`${boundingbox[2]}`); + const lng2 = parseFloat(`${boundingbox[3]}`); + const latDiff = Math.abs(lat1 - lat2); + const lngDiff = Math.abs(lng1 - lng2); + const maxDiff = Math.max(latDiff, lngDiff); + const zoom = Math.min(Math.round(Math.log(360 / maxDiff) / Math.log(2)), 18); + return zoom; + } + + /** + * Get the search suggestions from nominatim + * @param {string} searchText + * @returns {Promise} + */ + updateSearchSuggestions = async searchText => { + if (searchText.trim().length < 3) { + this.setState({ searchSuggestions: [] }); + return; + } + clearTimeout(this.suggestionTimeout); + this.suggestionTimeout = setTimeout(async () => { + const response = await fetch(`https://nominatim.openstreetmap.org/search?q=${searchText}&format=json&limit=5`); + const data = await response.json(); + const searchSuggestions = data.map(place => { + const placeData = { + displayName: place?.display_name || "Unknown location", + }; + return placeData; + }); + this.setState({ searchSuggestions }); + }, 200); + }; + + /** + * Small Component to interact with the leaflet map + * @returns {null} + */ + MapHook = () => { + const map = useMap(); + + // set the center of the map to this.state.mapCenter (but let the user move it) + if (this.mapNeedsUpdate) { + map.flyTo(this.mapCenter, this.mapZoom, { animate: true, duration: this.mapSlowAnimation ? 4 : 1 }); + this.mapNeedsUpdate = false; + this.mapSlowAnimation = false; + } + + useMapEvents({ + click: event => { + this.updatePlaceByCoords(event.latlng, this.mapZoom); + }, + zoom: () => { + this.mapZoom = map.getZoom(); + }, + contextmenu: event => { + this.updatePlaceByCoords(event.latlng, 18, true); + }, + }); + + return null; + }; + render() { + // address object -> string + let address = ""; + if (this.state.place.address !== undefined) { + Object.values(this.state.place.address).forEach(value => { + if (value !== undefined && value !== "") address += value + ", "; + }); + } + return ( - {/* Top Navbar */} + + + + + + + + - - - - - + this.setState({ snapSheetToState: undefined })} + > + { + this.setState({ snapSheetToState: 2, showSearchSuggestions: true }); + }} + placeholder="Place, address, or coordinates (lat, lng)" + onChange={event => { + this.setState({ searchText: event.target.value }); + this.updateSearchSuggestions(event.target.value); + }} + onSubmit={event => { + event.target.blur(); // hide keyboard TODO: this is not working yet + this.updatePlaceBySearch(this.state.searchText); + }} + onClickClear={() => { + this.setState({ searchText: "", showSearchSuggestions: false, searchSuggestions: [] }); + }} + onSearchbarDisable={() => { + this.setState({ snapSheetToState: 0, showSearchSuggestions: false }); + }} + onSearchbarClear={() => { + this.setState({ searchText: "", showSearchSuggestions: false }); + }} + onClickDisable={() => { + this.setState({ snapSheetToState: 0, showSearchSuggestions: false }); + }} + /> + + {this.state.showSearchSuggestions && + this.state.searchSuggestions.map((suggestion, index) => { + return ( + { + this.setState({ searchText: suggestion["displayName"], showSearchSuggestions: false }); + this.updatePlaceBySearch(suggestion["displayName"]); + }} + style={{ cursor: "pointer" }} + /> + ); + })} + - {/* Page content */} + {this.state.place.name} + {address} + + ); } diff --git a/src/pages/impressum.jsx b/src/pages/impressum.jsx new file mode 100644 index 0000000..b48ebe8 --- /dev/null +++ b/src/pages/impressum.jsx @@ -0,0 +1,129 @@ +import React from "react"; +import { Page, Block, BlockTitle, Link } from "framework7-react"; + +class Impressum extends React.Component { + constructor(props) { + super(props); + } + + render() { + return ( + + + +

About Know-Where-You-Go

+
+ +

+ {" "} + Know-Where-You-Go ist eine Webanwendung, die Ihnen hilft, herauszufinden, wo Sie sind und wohin Sie gehen. +

+

+ Es basiert auf dem OpenStreetMap-Projekt und verwendet + die OpenStreetMap-API, um die Daten zu erhalten. +

+

+ Die Anwendung ist open source und kann auf{" "} + GitHub gefunden werden. +

+

Die App wird von Studenten der DHBW Friedrichshafen entwickelt:

+
    +
  • + Baldur Siegel +
  • +
  • + Henry Schuler +
  • +
  • + Florian Herkommer +
  • +
  • + Florian Glaser +
  • +
  • + Johannes Brandenburger +
  • +
  • + David Felder +
  • +
  • + Lukas Braun +
  • +
  • + Phillipp Patzelt +
  • +
+
+ +

Impressum

+
+ +

Kontakt

+
+ +

Angaben gemäß § 5 TMG

+

+ Florian Glaser

+

+

+ Fallenbrunnen 2

+ 88045 Friedrichshafen

+ Telefon: 49-123456789 +

+ E-Mail:{" "} + glaser.florian-it20@it.dhbw-ravensburg.de +
+ +

Haftungsausschluss:

+
+ +

Haftung für Inhalte

+

+ Die Inhalte unserer Seiten wurden mit größter Sorgfalt erstellt. Für die Richtigkeit, Vollständigkeit und + Aktualität der Inhalte können wir jedoch keine Gewähr übernehmen. Als Diensteanbieter sind wir gemäß § 7 + Abs.1 TMG für eigene Inhalte auf diesen Seiten nach den allgemeinen Gesetzen verantwortlich. Nach §§ 8 bis + 10 TMG sind wir als Diensteanbieter jedoch nicht verpflichtet, übermittelte oder gespeicherte fremde + Informationen zu überwachen oder nach Umständen zu forschen, die auf eine rechtswidrige Tätigkeit hinweisen. + Verpflichtungen zur Entfernung oder Sperrung der Nutzung von Informationen nach den allgemeinen Gesetzen + bleiben hiervon unberührt. Eine diesbezügliche Haftung ist jedoch erst ab dem Zeitpunkt der Kenntnis einer + konkreten Rechtsverletzung möglich. Bei Bekanntwerden von entsprechenden Rechtsverletzungen werden wir diese + Inhalte umgehend entfernen. +

+

Haftung für Links

+

+ Unser Angebot enthält Links zu externen Webseiten Dritter, auf deren Inhalte wir keinen Einfluss haben. + Deshalb können wir für diese fremden Inhalte auch keine Gewähr übernehmen. Für die Inhalte der verlinkten + Seiten ist stets der jeweilige Anbieter oder Betreiber der Seiten verantwortlich. Die verlinkten Seiten + wurden zum Zeitpunkt der Verlinkung auf mögliche Rechtsverstöße überprüft. Rechtswidrige Inhalte waren zum + Zeitpunkt der Verlinkung nicht erkennbar. Eine permanente inhaltliche Kontrolle der verlinkten Seiten ist + jedoch ohne konkrete Anhaltspunkte einer Rechtsverletzung nicht zumutbar. Bei Bekanntwerden von + Rechtsverletzungen werden wir derartige Links umgehend entfernen. +

+

Urheberrecht

+

+ Die durch die Seitenbetreiber erstellten Inhalte und Werke auf diesen Seiten unterliegen dem deutschen + Urheberrecht. Die Vervielfältigung, Bearbeitung, Verbreitung und jede Art der Verwertung außerhalb der + Grenzen des Urheberrechtes bedürfen der schriftlichen Zustimmung des jeweiligen Autors bzw. Erstellers. + Downloads und Kopien dieser Seite sind nur für den privaten, nicht kommerziellen Gebrauch gestattet. Soweit + die Inhalte auf dieser Seite nicht vom Betreiber erstellt wurden, werden die Urheberrechte Dritter beachtet. + Insbesondere werden Inhalte Dritter als solche gekennzeichnet. Sollten Sie trotzdem auf eine + Urheberrechtsverletzung aufmerksam werden, bitten wir um einen entsprechenden Hinweis. Bei Bekanntwerden von + Rechtsverletzungen werden wir derartige Inhalte umgehend entfernen. +

+
+ + {/* // prettier-ignore */} + Website Impressum erstellt durch + impressum-generator.de + {" "} + von der{" "} + + Kanzlei Hasselbach + + +
+ ); + } +} + +export default Impressum;