diff options
-rw-r--r-- | package-lock.json | 24 | ||||
-rw-r--r-- | package.json | 1 | ||||
-rwxr-xr-x | public/css/style.css | 16 | ||||
-rwxr-xr-x | routes.js | 43 | ||||
-rwxr-xr-x | views/event.handlebars | 169 |
5 files changed, 180 insertions, 73 deletions
diff --git a/package-lock.json b/package-lock.json index c58be84..4256f57 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2698,9 +2698,9 @@ } }, "handlebars": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.1.2.tgz", - "integrity": "sha512-nvfrjqvt9xQ8Z/w0ijewdD/vvWDTOweBUm96NTr66Wfvo1mJenBLwcYmPs3TIBP5ruzYGD7Hx/DaM9RmhroGPw==", + "version": "4.5.3", + "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.5.3.tgz", + "integrity": "sha512-3yPecJoJHK/4c6aZhSvxOyG4vJKDshV36VHp0iVCDVh7o9w2vwi3NSnL2MMPj3YdduqaBcu7cGbggJQM0br9xA==", "requires": { "neo-async": "^2.6.0", "optimist": "^0.6.1", @@ -2810,6 +2810,24 @@ "rrule": "2.4.1" } }, + "ical-generator": { + "version": "1.9.2", + "resolved": "https://registry.npmjs.org/ical-generator/-/ical-generator-1.9.2.tgz", + "integrity": "sha512-z3OLKk/b9TbyOLKOIMpjLJ3u7gq/tXPNsH5uOz+Ai3sqn2kcpjFlZUafKrlduwZn3Xu3fV5WqZ2dddZFrhQTfg==", + "requires": { + "moment-timezone": "^0.5.27" + }, + "dependencies": { + "moment-timezone": { + "version": "0.5.27", + "resolved": "https://registry.npmjs.org/moment-timezone/-/moment-timezone-0.5.27.tgz", + "integrity": "sha512-EIKQs7h5sAsjhPCqN6ggx6cEbs94GK050254TIJySD1bzoM5JTYDwAU1IoVOeTOL6Gm27kYJ51/uuvq1kIlrbw==", + "requires": { + "moment": ">= 2.9.0" + } + } + } + }, "iconv-lite": { "version": "0.4.24", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", diff --git a/package.json b/package.json index 2d855c7..bce9181 100644 --- a/package.json +++ b/package.json @@ -23,6 +23,7 @@ "greenlock": "^2.6.7", "greenlock-express": "^2.6.7", "ical": "^0.6.0", + "ical-generator": "^1.9.2", "jimp": "^0.6.0", "jsonwebtoken": "^8.4.0", "marked": "^0.7.0", diff --git a/public/css/style.css b/public/css/style.css index 4085875..e55f07a 100755 --- a/public/css/style.css +++ b/public/css/style.css @@ -115,6 +115,11 @@ body, html { text-transform:capitalize; } +#eventActions { + padding-left: 0; + margin-top: 1rem; +} + /* .location, .eventLink { display: flex; @@ -162,10 +167,6 @@ body, html { margin-left: 5px; } -.eventInformationAction:not(#copyEventLink) { - margin-top: 0.25rem; -} - .commentContainer { background: #fafafa; border-radius: 5px; @@ -308,6 +309,13 @@ body, html { } } +@media (min-width: 1120px) { + #eventActions { + margin-top: 0; + padding-left: 1rem; + } +} + @media (min-width: 577px) { #sidebar { border-right: 2px solid #e0e0e0; @@ -62,6 +62,11 @@ render_plain = function () { } const ical = require('ical'); +const icalGenerator = require('ical-generator'); +const cal = icalGenerator({ + domain: 'gath.io', + name: 'Gathio' +}); const sgMail = require('@sendgrid/mail'); @@ -408,6 +413,42 @@ router.get('/group/:eventGroupID', (req, res) => { }); }) +router.get('/exportevent/:eventID', (req, res) => { + Event.findOne({ + id: req.params.eventID + }) + .populate('eventGroup') + .then((event) => { + if (event) { + const icalEvent = cal.createEvent({ + start: moment.tz(event.start, event.timezone), + end: moment.tz(event.start, event.timezone), + timezone: event.timezone, + timestamp: moment(), + summary: event.name, + description: event.description, + organizer: { + name: event.hostName ? event.hostName : "Anonymous", + email: event.creatorEmail + }, + location: event.location, + url: 'https://gath.io/' + event.id + }); + + let string = cal.toString(); + console.log(string) + res.send(string); + } + }) + .catch((err) => { + addToLog("exportEvent", "error", "Attempt to export event " + req.params.eventID + " failed with error: " + err); + console.log(err) + res.status(404); + res.render('404', { url: req.url }); + return; + }); +}) + // BACKEND ROUTES //router.post('/login', @@ -529,7 +570,7 @@ router.post('/importevent', (req, res) => { image: '', creatorEmail: creatorEmail, url: '', - hostName: importedEventData.organizer ? importedEventData.organizer.params.CN : "", + hostName: importedEventData.organizer ? importedEventData.organizer.params.CN.replace(/["]+/g, '') : "", viewPassword: '', editPassword: '', editToken: editToken, diff --git a/views/event.handlebars b/views/event.handlebars index 4d0cf28..7a57783 100755 --- a/views/event.handlebars +++ b/views/event.handlebars @@ -8,82 +8,92 @@ <h3 id="eventName">{{eventData.name}}</h3> </div> {{#if editingEnabled}} - <div class="col-lg-2 ml-2 edit-buttons"> + <div class="col-lg-3 ml-2 edit-buttons"> <div class="btn-group" role="group" aria-label="Event controls"> - <button type="button" id="editEvent" class="btn btn-success" data-toggle="modal" data-target="#editModal" {{#if eventHasConcluded}}disabled{{/if}}><i class="fas fa-edit"></i></button> - <button type="button" id="deleteEvent" class="btn btn-danger" data-toggle="modal" data-target="#deleteModal"><i class="fas fa-trash"></i></button> + <button type="button" id="editEvent" class="btn btn-success" data-toggle="modal" data-target="#editModal" {{#if eventHasConcluded}}disabled{{/if}}><i class="fas fa-edit"></i> Edit</button> + <button type="button" id="deleteEvent" class="btn btn-danger" data-toggle="modal" data-target="#deleteModal"><i class="fas fa-trash"></i> Delete</button> </div> </div> {{/if}} </div> -<div class="card mt-4 mb-4"> - <div class="card-body"> - <ul class="fa-ul eventInformation"> - <li> - <span class="fa-li"> - <i class="fas fa-map-marker-alt"></i> - </span> - {{eventData.location}}<br /> - <a target="_blank" href="http://maps.google.com/?q={{parsedLocation}}" class="eventInformationAction btn btn-outline-secondary btn-sm"> +<div class="container my-4 pr-0"> + <div class="row"> + <div class="col-lg-9 card"> + <div class="card-body"> + <ul class="fa-ul eventInformation"> + <li> + <span class="fa-li"> + <i class="fas fa-map-marker-alt"></i> + </span> + {{eventData.location}} + </li> + <li> + <span class="fa-li"> + <i class="fas fa-fw fa-calendar-day"></i> + </span> + {{{displayDate}}} + <br> + <span class="text-muted"> + {{#if eventHasBegun}}{{#unless eventHasConcluded}}Started {{else}}Ended {{/unless}}{{/if}}{{fromNow}} + </span> + </li> + {{#if eventHasHost}} + <li> + <span class="fa-li"> + <i class="fas fa-fw fa-user-circle"></i> + </span> + <span class="text-muted">Hosted by</span> {{eventData.hostName}} + </li> + {{/if}} + {{#if eventData.eventGroup}} + <li> + <span class="fa-li"> + <i class="fas fa-fw fa-calendar-alt"></i> + </span> + <span class="text-muted">Part of</span> <a href="/group/{{eventData.eventGroup.id}}">{{eventData.eventGroup.name}}</a> + </li> + {{/if}} + {{#if eventData.url}} + <li> + <span class="fa-li"> + <i class="fas fa-fw fa-link"></i> + </span> + <a href="{{eventData.url}}"> + {{eventData.url}} + </a> + </li> + {{/if}} + <li> + <span class="fa-li"> + <i class="fas fa-fw fa-share-square"></i> + </span> + <a href="https://gath.io/{{eventData.id}}">gath.io/{{eventData.id}}</a> + <button type="button" id="copyEventLink" class="eventInformationAction btn btn-outline-secondary btn-sm" data-clipboard-text="https://gath.io/{{eventData.id}}"> + <i class="fas fa-copy"></i> Copy + </button> + </li> + </ul> + </div> + </div> + <div class="col-lg-3" id="eventActions"> + <aside class="btn-group-vertical d-flex" role="group" aria-label="Event actions"> + <a href="http://www.google.com/calendar/event?action=TEMPLATE&dates={{parsedStart}}%2F{{parsedEnd}}&text={{escapedName}}&location={{parsedLocation}}&ctz={{timezone}}" class="btn btn-outline-secondary btn-sm"> + <i class="far fa-calendar-plus"></i> Add to Google Calendar + </a> + <button type="button" id="exportICS" class="btn btn-outline-secondary btn-sm" data-event-id="{{eventData.id}}"> + <i class="fas fa-download"></i> Export as ICS + </button> + <a target="_blank" href="http://maps.google.com/?q={{parsedLocation}}" class="btn btn-outline-secondary btn-sm"> <i class="fas fa-map-marked"></i> Show on Google Maps </a> - - <a target="_blank" href="https://www.openstreetmap.org/search?query={{parsedLocation}}" class="eventInformationAction btn btn-outline-secondary btn-sm"> + <a target="_blank" href="https://www.openstreetmap.org/search?query={{parsedLocation}}" class="btn btn-outline-secondary btn-sm"> <i class="fas fa-map-marked"></i> Show on OpenStreetMap </a> - </li> - <li> - <span class="fa-li"> - <i class="fas fa-fw fa-calendar-day"></i> - </span> - {{{displayDate}}} - <br> - <span class="text-muted"> - {{#if eventHasBegun}}{{#unless eventHasConcluded}}Started {{else}}Ended {{/unless}}{{/if}}{{fromNow}} - </span> - <br /> - <a href="http://www.google.com/calendar/event?action=TEMPLATE&dates={{parsedStart}}%2F{{parsedEnd}}&text={{escapedName}}&location={{parsedLocation}}&ctz={{timezone}}" class="eventInformationAction btn btn-outline-secondary btn-sm"> - <i class="far fa-calendar-plus"></i> Add to Google Calendar - </a> - </li> - {{#if eventHasHost}} - <li> - <span class="fa-li"> - <i class="fas fa-fw fa-user-circle"></i> - </span> - <span class="text-muted">Hosted by</span> {{eventData.hostName}} - </li> - {{/if}} - {{#if eventData.eventGroup}} - <li> - <span class="fa-li"> - <i class="fas fa-fw fa-calendar-alt"></i> - </span> - <span class="text-muted">Part of</span> <a href="/group/{{eventData.eventGroup.id}}">{{eventData.eventGroup.name}}</a> - </li> - {{/if}} - {{#if eventData.url}} - <li> - <span class="fa-li"> - <i class="fas fa-fw fa-link"></i> - </span> - <a href="{{eventData.url}}"> - {{eventData.url}} - </a> - </li> - {{/if}} - <li> - <span class="fa-li"> - <i class="fas fa-fw fa-share-square"></i> - </span> - <a href="https://gath.io/{{eventData.id}}">gath.io/{{eventData.id}}</a> - <button type="button" id="copyEventLink" class="eventInformationAction btn btn-outline-secondary btn-sm" data-clipboard-text="https://gath.io/{{eventData.id}}"> - <i class="fas fa-copy"></i> Copy - </button> - </li> - </ul> + </aside> + </div> </div> </div> + {{#if eventHasConcluded}} <div class="alert alert-warning mb-4" role="alert"> This event has concluded. It can no longer be edited, and will be automatically deleted <span class="daysToDeletion"></span>. @@ -375,6 +385,29 @@ }) $(document).ready(function() { + // From https://davidwalsh.name/javascript-download + function downloadFile(data, fileName, type="text/plain") { + // Create an invisible A element + const a = document.createElement("a"); + a.style.display = "none"; + document.body.appendChild(a); + + // Set the HREF to a Blob representation of the data to be downloaded + a.href = window.URL.createObjectURL( + new Blob([data], { type }) + ); + + // Use download attribute to set set desired file name + a.setAttribute("download", fileName); + + // Trigger the download by simulating click + a.click(); + + // Cleanup + window.URL.revokeObjectURL(a.href); + document.body.removeChild(a); + } + $.uploadPreview({ input_field: "#image-upload", preview_box: "#image-preview", @@ -394,6 +427,12 @@ {{/if}} new ClipboardJS('#copyEventLink'); autosize($('textarea')); + $("#exportICS").click(function(){ + let eventID = $(this).attr('data-event-id'); + $.get('/exportevent/' + eventID, function(response) { + downloadFile(response, eventID + '.ics'); + }) + }) $("#copyEventLink").click(function(){ $(this).html('<i class="fas fa-copy"></i> Copied!'); setTimeout(function(){ $("#copyEventLink").html('<i class="fas fa-copy"></i> Copy');}, 5000); |