diff --git a/components/event/EventCard.js b/components/event/EventCard.js
index 5b9eabb1b6c..33a64469d1f 100644
--- a/components/event/EventCard.js
+++ b/components/event/EventCard.js
@@ -52,7 +52,9 @@ export default function EventCard({ manage, event, usernames }) {
{event.isVirtual && (
)}
- {event.isInPerson && }
+ {event.location?.country && (
+
+ )}
{event.date.cfpOpen && }
{event.price?.startingFrom > 0 && }
{event.price?.startingFrom === 0 && }
@@ -97,17 +99,11 @@ export default function EventCard({ manage, event, usernames }) {
{event.description}
- {(event.isVirtual || (event.isInPerson && event.location)) && (
-
- )}
+ {(event.isVirtual || event.location?.country) && }
{event.isVirtual && "Remote"}
- {event.isVirtual &&
- event.isInPerson &&
- event.location &&
- " AND in "}
- {event.isInPerson &&
- event.location &&
+ {event.isVirtual && event.location?.country && " AND in "}
+ {event.location?.country &&
Object.values(event.location).join(", ")}
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/components/user/UserEvents.js b/components/user/UserEvents.js
index 81674f8de41..3113e56617b 100644
--- a/components/user/UserEvents.js
+++ b/components/user/UserEvents.js
@@ -3,8 +3,12 @@ import EventCard from "@components/event/EventCard";
import Alert from "@components/Alert";
import DropdownMenu from "@components/form/DropDown";
-export default function UserEvents({ manage = false, events }) {
- const [eventType, setEventType] = useState("future");
+export default function UserEvents({
+ manage = false,
+ events,
+ filter = "future",
+}) {
+ const [eventType, setEventType] = useState(filter);
const eventOptions = [
{ value: "all", name: "All Events" },
@@ -31,7 +35,7 @@ export default function UserEvents({ manage = false, events }) {
case "virtual":
return event.date.future && event.isVirtual;
case "inPerson":
- return event.date.future && event.isInPerson;
+ return event.date.future && event.location?.country;
case "cfpOpen":
return event.date.cfpOpen;
case "free":
diff --git a/models/Profile/Event.js b/models/Profile/Event.js
index df9449779b0..08a06cc7dc4 100644
--- a/models/Profile/Event.js
+++ b/models/Profile/Event.js
@@ -34,6 +34,30 @@ const EventSchema = new Schema({
price: {
startingFrom: Number,
},
+ location: {
+ road: {
+ type: String,
+ min: 2,
+ max: 128,
+ },
+ city: {
+ type: String,
+ min: 2,
+ max: 128,
+ },
+ state: {
+ type: String,
+ min: 2,
+ max: 128,
+ },
+ country: {
+ type: String,
+ min: 2,
+ max: 128,
+ },
+ lat: Number,
+ lon: Number,
+ },
color: {
type: String,
min: 2,
diff --git a/pages/account/manage/event/[[...data]].js b/pages/account/manage/event/[[...data]].js
index 59bb6e66382..9304c7d0c8e 100644
--- a/pages/account/manage/event/[[...data]].js
+++ b/pages/account/manage/event/[[...data]].js
@@ -70,7 +70,11 @@ export default function ManageEvent({ BASE_URL, event }) {
event.date?.end && formatDate(event.date?.end)
);
const [price, setPrice] = useState(event.price?.startingFrom || 0);
- const [color, setColor] = useState(event.color || "" );
+ const [color, setColor] = useState(event.color || "");
+ const [road, setRoad] = useState(event.location.road || "");
+ const [city, setCity] = useState(event.location.city || "");
+ const [state, setState] = useState(event.location.state || "");
+ const [country, setCountry] = useState(event.location.country || "");
const handleSubmit = async (e) => {
e.preventDefault();
@@ -83,6 +87,7 @@ export default function ManageEvent({ BASE_URL, event }) {
isVirtual,
price: { startingFrom: price },
color,
+ location: { road, city, state, country },
};
let apiUrl = `${BASE_URL}/api/account/manage/event/`;
if (event._id) {
@@ -156,11 +161,11 @@ export default function ManageEvent({ BASE_URL, event }) {
additionalMessage={showNotification.additionalMessage}
/>
-
-
-
+
>
);
diff --git a/pages/api/events.js b/pages/api/events.js
index d85a204f9a6..21db480f589 100644
--- a/pages/api/events.js
+++ b/pages/api/events.js
@@ -8,38 +8,55 @@ export default async function handler(req, res) {
.json({ error: "Invalid request: GET request required" });
}
- const events = await getEvents();
+ const { withLocation } = req.query;
+
+ const events = await getEvents(withLocation);
return res.status(200).json(events);
}
-export async function getEvents() {
+export async function getEvents(withLocation = false) {
let events = [];
- try {
- events = await Profile.aggregate([
- { $project: { username: 1, events: 1, isEnabled: 1 } },
- { $match: { "events.date.start": { $gt: new Date() }, isEnabled: true } },
- { $unwind: "$events" },
- { $match: { "events.date.end": { $gt: new Date() } } },
- {
- $group: {
- _id: "$events.url",
- usernames: { $addToSet: "$username" },
- isVirtual: { $first: "$events.isVirtual" },
- color: { $first: "$events.color" },
- date: { $first: "$events.date" },
- url: { $first: "$events.url" },
- name: { $first: "$events.name" },
- description: { $first: "$events.description" },
- isEnabled: { $first: "$isEnabled" },
- },
- },
-
- {
- $sort: { "date.start": 1 },
+ let aggregate = [
+ { $project: { username: 1, events: 1, isEnabled: 1 } },
+ { $match: { "events.date.start": { $gt: new Date() }, isEnabled: true } },
+ { $unwind: "$events" },
+ { $match: { "events.date.end": { $gt: new Date() } } },
+ ];
+
+ if (withLocation) {
+ aggregate.push({
+ $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 } },
+ ],
},
+ });
+ }
- ]).exec();
+ aggregate.push(
+ {
+ $group: {
+ _id: "$events.url",
+ usernames: { $addToSet: "$username" },
+ isVirtual: { $first: "$events.isVirtual" },
+ color: { $first: "$events.color" },
+ date: { $first: "$events.date" },
+ url: { $first: "$events.url" },
+ name: { $first: "$events.name" },
+ description: { $first: "$events.description" },
+ location: { $mergeObjects: "$events.location" },
+ isEnabled: { $first: "$isEnabled" },
+ },
+ },
+ { $sort: { "date.start": 1 } }
+ );
+ try {
+ events = await Profile.aggregate(aggregate).exec();
let dateEvents = [];
const today = new Date();
for (const event of events) {
diff --git a/pages/api/system/reload.js b/pages/api/system/reload.js
index 9d1ecad91d6..6a2c48eca92 100644
--- a/pages/api/system/reload.js
+++ b/pages/api/system/reload.js
@@ -246,28 +246,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/docs/how-to-guides/events-json.mdx b/pages/docs/how-to-guides/events-json.mdx
index ff486500399..237c06937e5 100644
--- a/pages/docs/how-to-guides/events-json.mdx
+++ b/pages/docs/how-to-guides/events-json.mdx
@@ -24,14 +24,13 @@ All future events also appear on the Events page for the app.
_If you need help on how to edit this file, please see the Editing Guide_
-3. This `json` file will contain one object for the event and must have these six fields: `isVirtual` and/or `isInPerson`, `name`, `description`, `date`, `url`, and `location` (this field will be required only when `isInPerson` is `true`).
+3. This `json` file will contain one object for the event and must have these six fields: `isVirtual`, `name`, `description`, `date`, `url`, and `location`.
It can also optionally include other fields. For instance: `userStatus` & `speakingTopic`. It will look like this:
```js
{
"isVirtual": true,
- "isInPerson": true,
"name": "Open Source GitHub reviews",
"description": "In this livestream I will be going reviewing your **Open Source projects** and profiles! I will be joined by **Amanda**, a Developer Advocate.",
"date": {
@@ -55,18 +54,18 @@ _If you need help on how to edit this file, please see the {
- switch (index % 4 ) {
+ switch (index % 4) {
case 0:
return [coords[0] + offset, coords[1] + offset2];
case 1:
@@ -49,7 +52,7 @@ export async function getStaticProps() {
default:
return [coords[0] + offset, coords[1] - offset2];
}
- }
+ };
data.users = data.users.map((user, index) => {
const offset = Math.random() * 0.02; // ~2.2km
@@ -62,21 +65,18 @@ export async function getStaticProps() {
username: user.username,
name: user.name,
location: user.location.provided,
- bio: user.bio || ''
+ bio: user.bio || "",
},
geometry: {
type: "Point",
- coordinates:adjustCoords(
- [
- parseFloat(user.location.lon),
- parseFloat(user.location.lat)
- ],
+ coordinates: adjustCoords(
+ [parseFloat(user.location.lon), parseFloat(user.location.lat)],
offset,
offset2,
index
- )
- }
- }
+ ),
+ },
+ };
});
try {
@@ -85,6 +85,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 +124,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 +147,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 +161,11 @@ export default function Map({ data }) {
return false;
});
- setFilteredUsers(results);
+ setFilteredPoints(results);
};
const resetFilter = () => {
- setFilteredUsers([]);
+ setFilteredPoints([]);
setSelectedTags(new Set());
};
@@ -172,14 +204,16 @@ export default function Map({ data }) {
0 ? filteredUsers.length : users.length
+ filteredPoints.length > 0 ? filteredPoints.length : points.length
}
>
+ >
+ Clear/Reset Filters
+
{tags &&
tags
@@ -196,7 +230,7 @@ export default function Map({ data }) {
0 ? filteredUsers : users}
+ points={filteredPoints.length > 0 ? filteredPoints : points}
/>