From dc69bfa6038dca71278b8028e38bc8c61845b0fc Mon Sep 17 00:00:00 2001 From: Benjamin Rupp Date: Wed, 7 Aug 2024 08:13:14 +0200 Subject: [PATCH 1/2] Parse unique locations from events We use all upcoming and all events within the last 52 weeks to figure out 'active' Hackergarten locations. The venue of the event is used as sponsor. Later on we can add all different venues of a location to the sponsors list. We parse the link of the meetup group from the event link with no fallback. TODOs: - The citiy in the title should be replaced by the city of the address. - Long and Lat should be determined using the address and the openstreetmap api (used in hero-geo-location npm package). - Use modern JS to simplify the code. --- .gitignore | 1 + gulpfile.js | 52 +++++++++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 52 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index d9bc85e..1db6620 100644 --- a/.gitignore +++ b/.gitignore @@ -10,3 +10,4 @@ node_modules # generated by gulp feed.xml projects.html +locations.json diff --git a/gulpfile.js b/gulpfile.js index 819d22e..18b4a31 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -101,6 +101,56 @@ gulp.task('generate-xml', gulp.series('validate-events', function () { .pipe(gulp.dest('.')); })); +/** + * Generate the location json file based on the events. + */ +gulp.task('generate-locations', gulp.series('validate-events', function () { + + return gulp.src("./events.json") + .pipe(jsonlint()) + .pipe(jsonlint.failOnError()) + .pipe(jsonlint.reporter()) + .pipe(transform(function (contents) { + var oneYearAgo = new Date(); + oneYearAgo.setDate(oneYearAgo.getDate()-52*7); + var events = JSON.parse(contents); + var recentEvents = events.filter(function(event) { + return new Date(event.date) > oneYearAgo; + }); + var uniqueLocations = {}; + for (var i = 0; i < recentEvents.length; i++) { + if (!uniqueLocations[recentEvents[i].address]) { + uniqueLocations[recentEvents[i].address] = []; + } + uniqueLocations[recentEvents[i].address].push(recentEvents[i]); + } + var locations = []; + for (var address in uniqueLocations) { + // TODO: use https://www.npmjs.com/package/hero-geo-location search to get long, lat and city + locations.push({ + title: 'Hackergarten City', + sponsors: [uniqueLocations[address][0].venue], + link: getLink(uniqueLocations[address][0]), + lat: 0, + long: 0, + }); + } + console.log('Found', events.length, 'events at', locations.length, 'unique locations.'); + return JSON.stringify(locations); + })) + .pipe(rename("locations.json")) + .pipe(gulp.dest('.')); +})); + +function getLink(event) { + if(event.links && event.links[0]) { + var url = event.links[0].url; + if(url && url.includes('meetup.com')) { + return url.split('events')[0]; + } + } +} + /** * Generate the HTML file containing the top contributed projects. */ @@ -167,4 +217,4 @@ gulp.task('generate-projects', gulp.series('validate-events', function () { .pipe(gulp.dest('.')); })); -gulp.task('default', gulp.series('generate-xml', 'generate-projects')); +gulp.task('default', gulp.series('generate-xml', 'generate-projects', 'generate-locations')); From ed4b69f3ef3d09f3b63d107fd971297fe6eb9497 Mon Sep 17 00:00:00 2001 From: Kai Reeh Date: Tue, 5 Nov 2024 20:31:25 +0100 Subject: [PATCH 2/2] set locations dynamically - oops --- gulpfile.js | 434 ++-- index.html | 401 ++- package-lock.json | 5840 +++++++++++++++++++++++++++++++++++++++++- scripts/locations.js | 88 +- server.sh | 2 +- 5 files changed, 6332 insertions(+), 433 deletions(-) diff --git a/gulpfile.js b/gulpfile.js index 18b4a31..369881a 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -1,27 +1,29 @@ var gulp = require('gulp'); -var jsonlint = require("gulp-jsonlint"); +var jsonlint = require('gulp-jsonlint'); var transform = require('gulp-transform'); var rename = require('gulp-rename'); -var jsonSchema = require("gulp-json-schema"); +var jsonSchema = require('gulp-json-schema'); -var hashCode = function (str) { +const LOCATION_RESOLVER_BASE_URL = 'https://nominatim.openstreetmap.org'; + +https: var hashCode = function (str) { var hash = 0; if (str.length === 0) return hash; for (var i = 0; i < str.length; i++) { var char = str.charCodeAt(i); - hash = ((hash << 5) - hash) + char; + hash = (hash << 5) - hash + char; hash = hash & hash; // Convert to 32bit integer } return Math.abs(hash); }; var entityMap = { - "&": "&", - "<": "<", - ">": ">", + '&': '&', + '<': '<', + '>': '>', '"': '"', "'": ''', - "/": '/' + '/': '/', }; /** @@ -42,179 +44,301 @@ function escapeHTML(string) { * if a structure ist not defined. */ gulp.task('validate-events', function () { - - return gulp.src("./events.json") + return gulp + .src('./events.json') .pipe(jsonlint()) .pipe(jsonlint.failOnError()) - .pipe(jsonSchema({ - schema: "./events.schema.json", - missing: "warn", - emitError: true, - })); + .pipe( + jsonSchema({ + schema: './events.schema.json', + missing: 'warn', + emitError: true, + }) + ); }); /** * Generate the xml file based on the events. */ -gulp.task('generate-xml', gulp.series('validate-events', function () { - - return gulp.src("./events.json") - .pipe(jsonlint()) - .pipe(jsonlint.failOnError()) - .pipe(jsonlint.reporter()) - .pipe(transform(function (contents) { - var today = new Date(); - today = new Date(today.setHours(0, 0, 0)); - var events = JSON.parse(contents); - console.log('found ' + events.length + ' events'); - var xml = []; - xml.push(""); - xml.push(""); - xml.push(""); - xml.push("Hackergarten Events"); - xml.push("https://hackergarten.net"); - xml.push("en-en"); - for (var i = 0; i < events.length; i++) { - var event = events[i]; - var hash = hashCode(event.date + event.location); - if (new Date(event.date) >= today) { - // This code is duplicated in eventlist.js - if (event.title && event.location) { // old JSON format without venue and address - event.title = "on " + event.date + " at " + event.title + " in " + event.location; - } else if (event.venue && event.address) { // new JSON format with venue and address - event.title = "on " + event.date + " at " + event.venue + ", " + event.address; - } else { // fallback with title and date - event.title += " on " + event.date; +gulp.task( + 'generate-xml', + gulp.series('validate-events', function () { + return gulp + .src('./events.json') + .pipe(jsonlint()) + .pipe(jsonlint.failOnError()) + .pipe(jsonlint.reporter()) + .pipe( + transform(function (contents) { + var today = new Date(); + today = new Date(today.setHours(0, 0, 0)); + var events = JSON.parse(contents); + console.log('found ' + events.length + ' events'); + var xml = []; + xml.push(''); + xml.push(''); + xml.push(''); + xml.push('Hackergarten Events'); + xml.push('https://hackergarten.net'); + xml.push('en-en'); + for (var i = 0; i < events.length; i++) { + var event = events[i]; + var hash = hashCode(event.date + event.location); + if (new Date(event.date) >= today) { + // This code is duplicated in eventlist.js + if (event.title && event.location) { + // old JSON format without venue and address + event.title = + 'on ' + + event.date + + ' at ' + + event.title + + ' in ' + + event.location; + } else if (event.venue && event.address) { + // new JSON format with venue and address + event.title = + 'on ' + + event.date + + ' at ' + + event.venue + + ', ' + + event.address; + } else { + // fallback with title and date + event.title += ' on ' + event.date; + } + xml.push(''); + xml.push( + '' + + escapeHTML('Hackergarten ' + event.title) + + '' + ); + xml.push( + 'https://hackergarten.net/#event-' + + hash + + '' + ); + xml.push('' + hash + ''); + xml.push('\n'); + } } - xml.push(""); - xml.push("" + escapeHTML("Hackergarten " + event.title) + ""); - xml.push("https://hackergarten.net/#event-" + hash + ""); - xml.push("" + hash + ""); - xml.push("\n"); - } - } - xml.push(""); - xml.push(""); - return xml.join(''); - })) - .pipe(rename("feed.xml")) - .pipe(gulp.dest('.')); -})); + xml.push(''); + xml.push(''); + return xml.join(''); + }) + ) + .pipe(rename('feed.xml')) + .pipe(gulp.dest('.')); + }) +); /** * Generate the location json file based on the events. */ -gulp.task('generate-locations', gulp.series('validate-events', function () { +gulp.task( + 'generate-locations', + gulp.series('validate-events', function () { + return gulp + .src('./events.json') + .pipe(jsonlint()) + .pipe(jsonlint.failOnError()) + .pipe(jsonlint.reporter()) + .pipe( + transform(async function (contents) { + var oneYearAgo = new Date(); + oneYearAgo.setDate(oneYearAgo.getDate() - 52 * 7); + var events = JSON.parse(contents); + var recentEvents = events.filter(function (event) { + return new Date(event.date) > oneYearAgo; + }); + var uniqueLocations = {}; + for (var i = 0; i < recentEvents.length; i++) { + if (!uniqueLocations[recentEvents[i].address]) { + uniqueLocations[recentEvents[i].address] = []; + } + uniqueLocations[recentEvents[i].address].push( + recentEvents[i] + ); + } + var locations = []; + for (var address in uniqueLocations) { + const event = uniqueLocations[address][0]; - return gulp.src("./events.json") - .pipe(jsonlint()) - .pipe(jsonlint.failOnError()) - .pipe(jsonlint.reporter()) - .pipe(transform(function (contents) { - var oneYearAgo = new Date(); - oneYearAgo.setDate(oneYearAgo.getDate()-52*7); - var events = JSON.parse(contents); - var recentEvents = events.filter(function(event) { - return new Date(event.date) > oneYearAgo; - }); - var uniqueLocations = {}; - for (var i = 0; i < recentEvents.length; i++) { - if (!uniqueLocations[recentEvents[i].address]) { - uniqueLocations[recentEvents[i].address] = []; - } - uniqueLocations[recentEvents[i].address].push(recentEvents[i]); - } - var locations = []; - for (var address in uniqueLocations) { - // TODO: use https://www.npmjs.com/package/hero-geo-location search to get long, lat and city - locations.push({ - title: 'Hackergarten City', - sponsors: [uniqueLocations[address][0].venue], - link: getLink(uniqueLocations[address][0]), - lat: 0, - long: 0, - }); - } - console.log('Found', events.length, 'events at', locations.length, 'unique locations.'); - return JSON.stringify(locations); - })) - .pipe(rename("locations.json")) - .pipe(gulp.dest('.')); -})); + const { lat, long } = await searchLocation( + event.address + ); + + locations.push({ + title: 'Hackergarten City', + sponsors: [event.venue], + link: getLink(event), + lat, + long, + }); + } + console.log( + 'Found', + events.length, + 'events at', + locations.length, + 'unique locations.' + ); + return JSON.stringify(locations); + }) + ) + .pipe(rename('locations.json')) + .pipe(gulp.dest('.')); + }) +); function getLink(event) { - if(event.links && event.links[0]) { + if (event.links && event.links[0]) { var url = event.links[0].url; - if(url && url.includes('meetup.com')) { + if (url && url.includes('meetup.com')) { return url.split('events')[0]; } } } +async function searchLocation(query) { + const url = `${LOCATION_RESOLVER_BASE_URL}/search?q=${encodeURIComponent( + query + )}&format=json&addressdetails=1&limit=1`; + + try { + const response = await fetch(url, { + method: 'GET', + headers: { + userAgent: 'i bims', + }, + }); + + if (!response.ok) { + console.log( + "An error occured, but I don't know why", + await response.text() + ); + } + + const [first, ...data] = await response.json(); + + return { + lat: first.lat, + long: first.lon, + }; + } catch (error) { + return { error: 'Error fetching data', details: error.message }; + } +} + /** * Generate the HTML file containing the top contributed projects. */ -gulp.task('generate-projects', gulp.series('validate-events', function () { +gulp.task( + 'generate-projects', + gulp.series('validate-events', function () { + let projects = new Map(); - let projects = new Map(); - - function addProject(project, url) { - let value = projects.get(project) || {url: url, count: 0}; - value.count++; - projects.set(project, value); - } + function addProject(project, url) { + let value = projects.get(project) || { url: url, count: 0 }; + value.count++; + projects.set(project, value); + } - return gulp.src("./events.json") - .pipe(jsonlint()) - .pipe(jsonlint.failOnError()) - .pipe(jsonlint.reporter()) - .pipe(transform(function(contents) { - let events = JSON.parse(contents); - let html = []; - html.push(''); - html.push(''); - html.push(''); - html.push(''); - html.push('Top Contributed Projects'); - html.push(''); - html.push(''); - html.push(''); - html.push(''); - html.push(''); - html.push(''); - for (let i = 0; i < events.length; i++) { - let event = events[i]; - if (event.achievements) { - for (let j = 0; j < event.achievements.length; j++) { - let achievement = event.achievements[j]; - if (achievement.url) { - if (achievement.url.startsWith("https://github.com/")) { - let urlParts = achievement.url.split("/"); - let project = urlParts[4]; - let url = 'https://github.com/' + urlParts[3] + '/' + urlParts[4]; - if (project !== undefined) addProject(project, url); + return gulp + .src('./events.json') + .pipe(jsonlint()) + .pipe(jsonlint.failOnError()) + .pipe(jsonlint.reporter()) + .pipe( + transform(function (contents) { + let events = JSON.parse(contents); + let html = []; + html.push(''); + html.push(''); + html.push(''); + html.push(''); + html.push('Top Contributed Projects'); + html.push( + '' + ); + html.push( + '' + ); + html.push( + '' + ); + html.push(''); + html.push(''); + html.push( + '
#ProjectCount
' + ); + for (let i = 0; i < events.length; i++) { + let event = events[i]; + if (event.achievements) { + for ( + let j = 0; + j < event.achievements.length; + j++ + ) { + let achievement = event.achievements[j]; + if (achievement.url) { + if ( + achievement.url.startsWith( + 'https://github.com/' + ) + ) { + let urlParts = + achievement.url.split('/'); + let project = urlParts[4]; + let url = + 'https://github.com/' + + urlParts[3] + + '/' + + urlParts[4]; + if (project !== undefined) + addProject(project, url); + } + } } } } - } - } - let sortedProjects = new Map([...projects].sort((a, b) => - a[1].count === b[1].count ? 0 : a[1].count < b[1].count ? 1 : -1)); - let i = 1; - sortedProjects.forEach((value, project) => { - html.push(''); - html.push(''); - html.push(''); - html.push(''); - html.push(''); - }); - html.push('
#ProjectCount
' + i++ + '' + project + '' + value.count + '
'); - html.push(''); - html.push(''); - return html.join(''); - })) - .pipe(rename("projects.html")) - .pipe(gulp.dest('.')); -})); - -gulp.task('default', gulp.series('generate-xml', 'generate-projects', 'generate-locations')); + let sortedProjects = new Map( + [...projects].sort((a, b) => + a[1].count === b[1].count + ? 0 + : a[1].count < b[1].count + ? 1 + : -1 + ) + ); + let i = 1; + sortedProjects.forEach((value, project) => { + html.push(''); + html.push('' + i++ + ''); + html.push( + '' + + project + + '' + ); + html.push('' + value.count + ''); + html.push(''); + }); + html.push(''); + html.push(''); + html.push(''); + return html.join(''); + }) + ) + .pipe(rename('projects.html')) + .pipe(gulp.dest('.')); + }) +); + +gulp.task( + 'default', + gulp.series('generate-xml', 'generate-projects', 'generate-locations') +); diff --git a/index.html b/index.html index 1cab5f0..89f6c26 100644 --- a/index.html +++ b/index.html @@ -1,5 +1,6 @@ + @@ -20,9 +21,7 @@ - + @@ -38,213 +37,212 @@ - -
-
+ +
+
- - + + +
-
- -
- -
-
- -
-
Next Events
-
-
    - -
  • - Hack #{{allEventlistLength + $index}} {{event.title}} -
    → {{event.status.title}}
    - -
  • - -
- more coming up ...look at the Join section below... + +
+ +
+
+ +
+
Next Events
+
+
    + +
  • + Hack #{{allEventlistLength + $index}} {{event.title}} +
    → {{event.status.title}}
    + +
  • + +
+ more coming up ...look at the Join section below... +
-
- -
-
About
-
- Hackergarten is a craftmen's workshop, classroom, a laboratory, a social circle, a writing group, a playground, and an artist's studio. Our goal is to create something that others can use; whether it be working software, improved documentation, or better educational materials. Our intent is to end each meeting with a patch or similar contribution submitted to an open and public project. Membership is open to anyone willing to contribute their time. + +
+
About
+
+ Hackergarten is a craftmen's workshop, classroom, a laboratory, a social circle, a writing group, a playground, and an artist's studio. Our goal is to create something that others can use; whether it be working software, improved documentation, or better educational materials. Our intent is to end each meeting with a patch or similar contribution submitted to an open and public project. Membership is open to anyone willing to contribute their time. +
-
- -
-
Locations
-
-
+ +
+
Locations
+
+
+
-
- -
-
Past Events
-
-
    -
  • - Hack #{{pastEventlistLength - $index}} {{event.title}} -
    → {{event.status.title}}
    - - -
  • -
-
- + +
+
Past Events
+
+
    +
  • + Hack #{{pastEventlistLength - $index}} {{event.title}} +
    → {{event.status.title}}
    + + +
  • +
+
+ +
-
-
-
Open Hackergarten Issues from GitHub (max. 30)
-
+
+
Open Hackergarten Issues from GitHub (max. 30)
+
+
+
- -
-
-
Downloads
-
-

- Please note: To use the Hackergarten logo, you must agree to the - license terms, which you can also find on - this page. -

-
- - -
- +
+
Downloads
+
+

+ Please note: To use the Hackergarten logo, you must agree to the + license terms, which you can also find on + this page. +

+
+ + +
+ +
-
-
+
- -
-