diff --git a/components/map/Clusters.js b/components/map/Clusters.js index 1d64cfc2fa8..4f02bf347ca 100644 --- a/components/map/Clusters.js +++ b/components/map/Clusters.js @@ -4,8 +4,9 @@ import { Marker, useMap } from "react-leaflet"; import useSupercluster from "use-supercluster"; import UserMarker from "./UserMarker"; import styles from "./Clusters.module.css"; +import EventMarker from "./EventMarker"; -export default function Clusters({users}) { +export default function Clusters({points}) { const map = useMap(); const mapB = map.getBounds(); const [bounds, setBounds] = useState([ @@ -32,7 +33,7 @@ export default function Clusters({users}) { }) const { clusters, supercluster } = useSupercluster({ - points: users, + points: points, bounds, zoom, options: { @@ -40,7 +41,6 @@ export default function Clusters({users}) { maxZoom: 18 } }); - const icons = {}; const fetchIcon = (count) => { const size = @@ -65,7 +65,8 @@ export default function Clusters({users}) { const { cluster: isCluster, point_count: pointCount, - username + username, + name } = cluster.properties; // we have a cluster to render @@ -91,7 +92,9 @@ export default function Clusters({users}) { } // we have a single point to render - return ( + return cluster.properties.isEvent ? ( + + ) : ( ); })} diff --git a/components/map/EventMarker.js b/components/map/EventMarker.js new file mode 100644 index 00000000000..84cdc935383 --- /dev/null +++ b/components/map/EventMarker.js @@ -0,0 +1,58 @@ +import L from "leaflet"; +import { Marker, Popup } from "react-leaflet"; +import { ReactMarkdown } from "react-markdown/lib/react-markdown"; +import Link from "@components/Link"; + +export default function EventMarker({event}) { + // Custom component for rendering links within ReactMarkdown + const LinkRenderer = ({ href, children }) => ( + + {children} + + ); + + return ( + + + + + `, + popupAnchor: [0, -10], + iconSize: [40, 40], + iconAnchor: [20, 20], + })} + position={[event.geometry.coordinates[1], event.geometry.coordinates[0]]} + > + +
+

+ + {event.properties.name} + +

+ + {[ + event.properties.location.city, + event.properties.location.state, + event.properties.location.country, + ] + .filter((x) => x) + .join(", ")} + + + + {event.properties.description} + + + + {`${new Date(event.properties.date.start).toLocaleDateString()} - + ${new Date(event.properties.date.end).toLocaleDateString()}`} + +
+
+
+ ) +} diff --git a/components/map/Map.js b/components/map/Map.js index 34b05ae0121..5e7c5885211 100644 --- a/components/map/Map.js +++ b/components/map/Map.js @@ -2,7 +2,7 @@ import { MapContainer, TileLayer } from "react-leaflet"; import Clusters from "./Clusters"; import "leaflet/dist/leaflet.css"; -export default function Map({ users }) { +export default function Map({ points }) { const boundsMap = [ [-90, -180], // Southwest coordinates [90, 180], // Northeast coordinates @@ -23,7 +23,7 @@ export default function Map({ users }) { attribution='© OpenStreetMap contributors' url="https://b.tile.openstreetmap.org/{z}/{x}/{y}.png" /> - + ); } diff --git a/models/Profile/Event.js b/models/Profile/Event.js index ced1e30f612..4b62ea0ccf1 100644 --- a/models/Profile/Event.js +++ b/models/Profile/Event.js @@ -34,6 +34,14 @@ const EventSchema = new Schema({ price: { startingFrom: Number, }, + location: { + road: String, + city: String, + state: String, + country: String, + lat: Number, + lon: Number, + }, }); EventSchema.pre("save", () => { diff --git a/pages/api/events.js b/pages/api/events.js index 6927b4aac51..dd1b4316b79 100644 --- a/pages/api/events.js +++ b/pages/api/events.js @@ -12,7 +12,7 @@ export default async function handler(req, res) { return res.status(200).json(events); } -export async function getEvents() { +export async function getEvents(withLocation = false) { let events = []; try { events = await Profile.aggregate([ @@ -20,6 +20,21 @@ export async function getEvents() { { $match: { "events.date.start": { $gt: new Date() }, isEnabled: true } }, { $unwind: "$events" }, { $match: { "events.date.end": { $gt: new Date() } } }, + ...(withLocation + ? [ + { + $match: { + $and: [ + { "events.location": { $exists: true } }, + { "events.location.lat": { $exists: true } }, + { "events.location.lon": { $exists: true } }, + { "events.location.lat": { $ne: null } }, + { "events.location.lon": { $ne: null } }, + ], + }, + }, + ] + : []), { $sort: { "events.date.start": 1 } }, { $group: { @@ -31,6 +46,7 @@ export async function getEvents() { url: { $first: "$events.url" }, name: { $first: "$events.name" }, description: { $first: "$events.description" }, + location: { $first: "$events.location" }, isEnabled: { $first: "$isEnabled" }, }, }, diff --git a/pages/api/system/reload.js b/pages/api/system/reload.js index cd36cf9696d..e19d8446e8c 100644 --- a/pages/api/system/reload.js +++ b/pages/api/system/reload.js @@ -234,28 +234,68 @@ export default async function handler(req, res) { } // - events - try { - if (profile.events) { + async function getCoordinates(city, state, country) { + let locationDb = {}; + const provided = [city, state, country].filter((x) => x).join(","); + if (locationDb[provided]) { + return locationDb[provided]; + } + try { + const location = await fetch( + `https://nominatim.openstreetmap.org/?addressdetails=1&q= + ${encodeURIComponent(provided)}&format=json&limit=1` + ); + const coordinates = await location.json(); + if (coordinates) { + const point = { + lat: coordinates[0].lat, + lon: coordinates[0].lon, + }; + locationDb[provided] = point; + return point; + } + } catch (e) { + return null; + } + return null; + } + + if (profile.events) { + try { + const events = await Promise.all( + profile.events.map(async (event, position) => { + let location = {}; + if (event.location) { + location = { + location: { ...event.location }, + }; + if (new Date(event.date.start) > Date.now() || new Date(event.date.end) > Date.now()) { + const coordinates = await getCoordinates( + event.location.city, + event.location.state, + event.location.country + ); + if (coordinates) { + location.location.lat = coordinates.lat; + location.location.lon = coordinates.lon; + } + } + } + return { + order: position, + ...event, + ...location, + }; + }) + ); + await Profile.findOneAndUpdate( { username: profile.username }, - { - events: profile.events.map((event) => ({ - isVirtual: event.isVirtual, - color: event.color, - name: event.name, - description: event.description, - date: { - start: event.date.start, - end: event.date.end, - }, - url: event.url, - price: event.price, - })), - } + { events } ); + } catch (e) { + logger.error(e,`failed to update events for ${profile.username}`); } - } catch (e) { - logger.error(e, `failed to update events for ${profile.username}`); } }) ); diff --git a/pages/map.js b/pages/map.js index 70618bcd019..7117218271a 100644 --- a/pages/map.js +++ b/pages/map.js @@ -9,6 +9,7 @@ import Page from "@components/Page"; import Badge from "@components/Badge"; import { getTags } from "./api/discover/tags"; import { getUsers } from "./api/profiles"; +import { getEvents } from "./api/events"; import config from "@config/app.json"; //this is required as leaflet is not compatible with SSR @@ -21,6 +22,8 @@ export async function getStaticProps() { let data = { users: [], tags: [], + events:[], + points: [] }; try { data.users = await getUsers(); @@ -85,6 +88,38 @@ export async function getStaticProps() { logger.error(e, "ERROR loading tags"); } + try { + data.events = await getEvents(true); + } catch (e) { + logger.error(e, "ERROR loading Events"); + } + + data.events = data.events.map((event, index) => { + const offset = Math.random() * 0.02; // ~2.2km + const offset2 = Math.random() * 0.02; // ~2.2km + return { + type: "Feature", + properties: { + cluster: false, + isEvent: true, + description: event.description, + name: event.name, + location: event.location, + date: event.date, + url: event.url || '' + }, + geometry: { + type: "Point", + coordinates: adjustCoords( + [parseFloat(event.location.lon), parseFloat(event.location.lat)], + offset, + offset2, + index + ), + }, + }; + }); + data.points=[...data.users,...data.events] return { props: { data }, revalidate: pageConfig.revalidateSeconds, @@ -92,8 +127,8 @@ export async function getStaticProps() { } export default function Map({ data }) { - let { users, tags } = data; - const [filteredUsers, setFilteredUsers] = useState([]); + let { tags, points } = data; + const [filteredPoints, setFilteredPoints] = useState([]); const [selectedTags, setSelectedTags] = useState(new Set()); let results = []; @@ -115,12 +150,12 @@ export default function Map({ data }) { const valueLower = value.toLowerCase(); const terms = [...updateSelectedTagsFilter(value)]; - results = users.filter((user) => { - if (user.properties.name.toLowerCase().includes(valueLower)) { + results = points.filter((point) => { + if (point.properties.name.toLowerCase().includes(valueLower)) { return true; } - let userTags = user.properties.tags?.map((tag) => tag.toLowerCase()); + let userTags = point.properties.tags?.map((tag) => tag.toLowerCase()); if (terms.every((keyword) => userTags?.includes(keyword.toLowerCase()))) { return true; @@ -129,11 +164,11 @@ export default function Map({ data }) { return false; }); - setFilteredUsers(results); + setFilteredPoints(results); }; const resetFilter = () => { - setFilteredUsers([]); + setFilteredPoints([]); setSelectedTags(new Set()); }; @@ -172,9 +207,9 @@ export default function Map({ data }) { 0 ? filteredUsers.length : users.length + filteredPoints.length > 0 ? filteredPoints.length : points.length } - > + >