From eddfe0389047ac1df5a8194d36c3bde1fcc05866 Mon Sep 17 00:00:00 2001 From: Raphael Kabo Date: Mon, 30 Sep 2019 13:29:32 +0100 Subject: Event group functionality --- models/Event.js | 81 +++--- models/EventGroup.js | 49 ++++ public/css/style.css | 22 +- routes.js | 338 +++++++++++++++++++++++++- start.js | 1 + views/event.handlebars | 46 ++-- views/eventgroup.handlebars | 164 +++++++++++++ views/newevent.handlebars | 33 ++- views/partials/editeventgroupmodal.handlebars | 45 ++++ views/partials/editeventmodal.handlebars | 205 +++++++++------- views/partials/importeventform.handlebars | 1 + views/partials/neweventform.handlebars | 52 +++- views/partials/neweventgroupform.handlebars | 66 +++++ 13 files changed, 941 insertions(+), 162 deletions(-) create mode 100755 models/EventGroup.js create mode 100755 views/eventgroup.handlebars create mode 100644 views/partials/editeventgroupmodal.handlebars create mode 100755 views/partials/neweventgroupform.handlebars diff --git a/models/Event.js b/models/Event.js index 43af171..3c0bb8c 100755 --- a/models/Event.js +++ b/models/Event.js @@ -19,7 +19,7 @@ const ReplySchema = new mongoose.Schema({ id: { type: String, required: true, - unique: true, + unique: true, sparse: true }, author: { @@ -43,7 +43,7 @@ const CommentSchema = new mongoose.Schema({ id: { type: String, required: true, - unique: true, + unique: true, sparse: true }, author: { @@ -68,37 +68,37 @@ const EventSchema = new mongoose.Schema({ id: { type: String, required: true, - unique: true + unique: true }, type: { - type: String, - trim: true, + type: String, + trim: true, required: true - }, - name: { - type: String, - trim: true, + }, + name: { + type: String, + trim: true, required: true - }, - location: { - type: String, - trim: true, + }, + location: { + type: String, + trim: true, required: true - }, + }, start: { // Stored as a UTC timestamp - type: Date, - trim: true, + type: Date, + trim: true, required: true - }, + }, end: { // Stored as a UTC timestamp - type: Date, - trim: true, + type: Date, + trim: true, required: true - }, - timezone: { + }, + timezone: { type: String, default: 'Etc/UTC' - }, + }, description: { type: String, trim: true, @@ -121,34 +121,35 @@ const EventSchema = new mongoose.Schema({ trim: true }, viewPassword: { - type: String, - trim: true - }, + type: String, + trim: true + }, editPassword: { - type: String, - trim: true - }, + type: String, + trim: true + }, editToken: { - type: String, - trim: true, + type: String, + trim: true, minlength: 32, maxlength: 32 - }, + }, + eventGroup: { type: mongoose.Schema.Types.ObjectId, ref: 'EventGroup' }, usersCanAttend: { - type: Boolean, - trim: true, + type: Boolean, + trim: true, default: false - }, + }, showUsersList: { - type: Boolean, - trim: true, + type: Boolean, + trim: true, default: false - }, + }, usersCanComment: { - type: Boolean, - trim: true, + type: Boolean, + trim: true, default: false - }, + }, firstLoad: { type: Boolean, trim: true, diff --git a/models/EventGroup.js b/models/EventGroup.js new file mode 100755 index 0000000..336074c --- /dev/null +++ b/models/EventGroup.js @@ -0,0 +1,49 @@ +const mongoose = require('mongoose'); + +const EventGroupSchema = new mongoose.Schema({ + id: { + type: String, + required: true, + unique: true + }, + name: { + type: String, + trim: true, + required: true + }, + description: { + type: String, + trim: true, + required: true + }, + image: { + type: String, + trim: true + }, + url: { + type: String, + trim: true + }, + creatorEmail: { + type: String, + trim: true + }, + hostName: { + type: String, + trim: true + }, + editToken: { + type: String, + trim: true, + minlength: 32, + maxlength: 32 + }, + firstLoad: { + type: Boolean, + trim: true, + default: true + }, + events: [{ type: mongoose.Schema.Types.ObjectId, ref: 'Event' }] +}); + +module.exports = mongoose.model('EventGroup', EventGroupSchema); diff --git a/public/css/style.css b/public/css/style.css index 18f9b73..2202f8d 100755 --- a/public/css/style.css +++ b/public/css/style.css @@ -194,7 +194,7 @@ body, html { /* IMAGE UPLOAD FORM */ -#image-preview { +.image-preview { width: 100%; height: 200px; position: relative; @@ -204,14 +204,14 @@ body, html { border-radius: 5px; border: 1px dashed #ced4da; } -#image-preview input { +.image-preview input { line-height: 200px; font-size: 200px; position: absolute; opacity: 0; z-index: 10; } -#image-preview label { +.image-preview label { position: absolute; z-index: 5; opacity: 0.8; @@ -237,7 +237,8 @@ body, html { } #newEventFormContainer, -#importEventFormContainer { +#importEventFormContainer, +#newEventGroupFormContainer { display: none; } @@ -293,6 +294,10 @@ body, html { display: none; } +#eventGroupData { + display: none; +} + .edit-buttons { text-align: right; } @@ -324,3 +329,12 @@ body, html { margin-top: 0; } } + +.list-group-item-action:hover { + background-color: #d4edda; +} + +.code { + font-family: 'Courier New', Courier, monospace; + overflow-wrap: anywhere; +} \ No newline at end of file diff --git a/routes.js b/routes.js index 4e665ba..80014e5 100755 --- a/routes.js +++ b/routes.js @@ -13,6 +13,7 @@ const { body, validationResult } = require('express-validator/check'); const router = express.Router(); const Event = mongoose.model('Event'); +const EventGroup = mongoose.model('EventGroup'); const Log = mongoose.model('Log'); var moment = require('moment-timezone'); @@ -177,6 +178,7 @@ router.get('/:eventID', (req, res) => { Event.findOne({ id: req.params.eventID }) + .populate('eventGroup') .then((event) => { if (event) { parsedLocation = event.location.replace(/\s+/g, '+'); @@ -250,7 +252,7 @@ router.get('/:eventID', (req, res) => { if (spotsRemaining <= 0) { noMoreSpots = true; } - } + } let metadata = { title: event.name, description: marked(event.description, { renderer: render_plain()}).split(" ").splice(0,40).join(" ").trim(), @@ -298,6 +300,114 @@ router.get('/:eventID', (req, res) => { }); }) +router.get('/group/:eventGroupID', (req, res) => { + EventGroup.findOne({ + id: req.params.eventGroupID + }) + .then(async (eventGroup) => { + if (eventGroup) { + parsedDescription = marked(eventGroup.description); + eventGroupEditToken = eventGroup.editToken; + + escapedName = eventGroup.name.replace(/\s+/g, '+'); + + let eventGroupHasCoverImage = false; + if( eventGroup.image ) { + eventGroupHasCoverImage = true; + } + else { + eventGroupHasCoverImage = false; + } + let eventGroupHasHost = false; + if( eventGroup.hostName ) { + eventGroupHasHost = true; + } + else { + eventGroupHasHost = false; + } + + let events = await Event.find({eventGroup: eventGroup._id}).sort('start') + + events.forEach(event => { + if (moment.tz(event.end, event.timezone).isSame(event.start, 'day')){ + // Happening during one day + event.displayDate = moment.tz(event.start, event.timezone).format('D MMM YYYY'); + } + else { + event.displayDate = moment.tz(event.start, event.timezone).format('D MMM YYYY') + moment.tz(event.end, event.timezone).format(' - D MMM YYYY'); + } + if (moment.tz(event.end, event.timezone).isBefore(moment.tz(event.timezone))){ + event.eventHasConcluded = true; + } else { + event.eventHasConcluded = false; + } + }) + + let upcomingEventsExist = false; + if (events.some(e => e.eventHasConcluded == false)) { + upcomingEventsExist = true; + } + + let firstLoad = false; + if (eventGroup.firstLoad === true) { + firstLoad = true; + EventGroup.findOneAndUpdate({id: req.params.eventGroupID}, {firstLoad: false}, function(err, raw) { + if (err) { + res.send(err); + } + }); + } + let editingEnabled = false; + if (Object.keys(req.query).length !== 0) { + if (!req.query.e) { + editingEnabled = false; + console.log("No edit token set"); + } + else { + if (req.query.e == eventGroupEditToken){ + editingEnabled = true; + } + else { + editingEnabled = false; + } + } + } + let metadata = { + title: eventGroup.name, + description: marked(eventGroup.description, { renderer: render_plain()}).split(" ").splice(0,40).join(" ").trim(), + image: (eventGroupHasCoverImage ? 'https://gath.io/events/' + eventGroup.image : null), + url: 'https://gath.io/' + req.params.eventID + }; + res.set("X-Robots-Tag", "noindex"); + res.render('eventgroup', { + title: eventGroup.name, + eventGroupData: eventGroup, + escapedName: escapedName, + events: events, + upcomingEventsExist: upcomingEventsExist, + parsedDescription: parsedDescription, + editingEnabled: editingEnabled, + eventGroupHasCoverImage: eventGroupHasCoverImage, + eventGroupHasHost: eventGroupHasHost, + firstLoad: firstLoad, + metadata: metadata + }) + } + else { + res.status(404); + res.render('404', { url: req.url }); + } + + }) + .catch((err) => { + addToLog("displayEventGroup", "error", "Attempt to display event group " + req.params.eventGroupID + " failed with error: " + err); + console.log(err) + res.status(404); + res.render('404', { url: req.url }); + return; + }); +}) + // BACKEND ROUTES //router.post('/login', @@ -307,10 +417,11 @@ router.get('/:eventID', (req, res) => { //); -router.post('/newevent', (req, res) => { +router.post('/newevent', async (req, res) => { let eventID = shortid.generate(); let editToken = randomstring.generate(); let eventImageFilename = ""; + let isPartOfEventGroup = false; if (req.files && Object.keys(req.files).length != 0) { let eventImageBuffer = req.files.imageUpload.data; Jimp.read(eventImageBuffer, (err, img) => { @@ -324,6 +435,16 @@ router.post('/newevent', (req, res) => { } startUTC = moment.tz(req.body.eventStart, 'D MMMM YYYY, hh:mm a', req.body.timezone); endUTC = moment.tz(req.body.eventEnd, 'D MMMM YYYY, hh:mm a', req.body.timezone); + let eventGroup; + if (req.body.eventGroupCheckbox) { + eventGroup = await EventGroup.findOne({ + id: req.body.eventGroupID, + editToken: req.body.eventGroupEditToken + }) + if (eventGroup) { + isPartOfEventGroup = true; + } + } const event = new Event({ id: eventID, type: req.body.eventType, @@ -340,6 +461,7 @@ router.post('/newevent', (req, res) => { viewPassword: req.body.viewPassword, editPassword: req.body.editPassword, editToken: editToken, + eventGroup: isPartOfEventGroup ? eventGroup._id : null, usersCanAttend: req.body.joinCheckbox ? true : false, showUsersList: req.body.guestlistCheckbox ? true : false, usersCanComment: req.body.interactionCheckbox ? true : false, @@ -347,7 +469,7 @@ router.post('/newevent', (req, res) => { firstLoad: true }); event.save() - .then(() => { + .then((event) => { addToLog("createEvent", "success", "Event " + eventID + "created"); // Send email with edit link if (sendEmails) { @@ -374,7 +496,7 @@ router.post('/newevent', (req, res) => { }); res.end(); }) - .catch((err) => { res.send('Database error, please try again :('); addToLog("createEvent", "error", "Attempt to create event failed with error: " + err);}); + .catch((err) => { res.send('Database error, please try again :( - ' + err); addToLog("createEvent", "error", "Attempt to create event failed with error: " + err);}); }); router.post('/importevent', (req, res) => { @@ -443,12 +565,70 @@ router.post('/importevent', (req, res) => { } }); +router.post('/neweventgroup', (req, res) => { + let eventGroupID = shortid.generate(); + let editToken = randomstring.generate(); + let eventGroupImageFilename = ""; + if (req.files && Object.keys(req.files).length != 0) { + let eventImageBuffer = req.files.imageUpload.data; + Jimp.read(eventImageBuffer, (err, img) => { + if (err) addToLog("Jimp", "error", "Attempt to edit image failed with error: " + err); + img + .resize(920, Jimp.AUTO) // resize + .quality(80) // set JPEG quality + .write('./public/events/' + eventGroupID + '.jpg'); // save + }); + eventGroupImageFilename = eventGroupID + '.jpg'; + } + const eventGroup = new EventGroup({ + id: eventGroupID, + name: req.body.eventGroupName, + description: req.body.eventGroupDescription, + image: eventGroupImageFilename, + creatorEmail: req.body.creatorEmail, + url: req.body.eventGroupURL, + hostName: req.body.hostName, + editToken: editToken, + firstLoad: true + }); + eventGroup.save() + .then(() => { + addToLog("createEventGroup", "success", "Event group " + eventGroupID + " created"); + // Send email with edit link + if (sendEmails) { + const msg = { + to: req.body.creatorEmail, + from: { + name: 'Gathio', + email: 'notifications@gath.io', + }, + templateId: 'd-4c5ddcb34ac44ec5b2313c6da4e405f3', + dynamic_template_data: { + subject: 'gathio: ' + req.body.eventGroupName, + eventGroupID: eventGroupID, + editToken: editToken + }, + }; + sgMail.send(msg).catch(e => { + console.error(e.toString()); + res.status(500).end(); + }); + } + res.writeHead(302, { + 'Location': '/group/' + eventGroupID + '?e=' + editToken + }); + res.end(); + }) + .catch((err) => { res.send('Database error, please try again :( - ' + err); addToLog("createEvent", "error", "Attempt to create event failed with error: " + err);}); +}); + router.post('/editevent/:eventID/:editToken', (req, res) => { + console.log(req.body); let submittedEditToken = req.params.editToken; Event.findOne(({ id: req.params.eventID, })) - .then((event) => { + .then(async (event) => { if (event.editToken === submittedEditToken) { // Token matches @@ -468,6 +648,17 @@ router.post('/editevent/:eventID/:editToken', (req, res) => { } startUTC = moment.tz(req.body.eventStart, 'D MMMM YYYY, hh:mm a', req.body.timezone); endUTC = moment.tz(req.body.eventEnd, 'D MMMM YYYY, hh:mm a', req.body.timezone); + + var isPartOfEventGroup = false; + if (req.body.eventGroupCheckbox) { + var eventGroup = await EventGroup.findOne({ + id: req.body.eventGroupID, + editToken: req.body.eventGroupEditToken + }) + if (eventGroup) { + isPartOfEventGroup = true; + } + } const updatedEvent = { name: req.body.eventName, location: req.body.eventLocation, @@ -482,7 +673,7 @@ router.post('/editevent/:eventID/:editToken', (req, res) => { showUsersList: req.body.guestlistCheckbox ? true : false, usersCanComment: req.body.interactionCheckbox ? true : false, maxAttendees: req.body.maxAttendeesCheckbox ? req.body.maxAttendees : null, - + eventGroup: isPartOfEventGroup ? eventGroup._id : null } Event.findOneAndUpdate({id: req.params.eventID}, updatedEvent, function(err, raw) { if (err) { @@ -534,6 +725,86 @@ router.post('/editevent/:eventID/:editToken', (req, res) => { .catch((err) => { console.error(err); res.send('Sorry! Something went wrong!'); addToLog("editEvent", "error", "Attempt to edit event " + req.params.eventID + " failed with error: " + err);}); }); +router.post('/editeventgroup/:eventGroupID/:editToken', (req, res) => { + let submittedEditToken = req.params.editToken; + EventGroup.findOne(({ + id: req.params.eventGroupID, + })) + .then((eventGroup) => { + if (eventGroup.editToken === submittedEditToken) { + // Token matches + + // If there is a new image, upload that first + let eventGroupID = req.params.eventGroupID; + let eventGroupImageFilename = eventGroup.image; + if (req.files && Object.keys(req.files).length != 0) { + let eventImageBuffer = req.files.eventGroupImageUpload.data; + Jimp.read(eventImageBuffer, (err, img) => { + if (err) throw err; + img + .resize(920, Jimp.AUTO) // resize + .quality(80) // set JPEG + .write('./public/events/' + eventGroupID + '.jpg'); // save + }); + eventGroupImageFilename = eventGroupID + '.jpg'; + } + const updatedEventGroup = { + name: req.body.eventGroupName, + description: req.body.eventGroupDescription, + url: req.body.eventGroupURL, + hostName: req.body.hostName, + image: eventGroupImageFilename + } + EventGroup.findOneAndUpdate({id: req.params.eventGroupID}, updatedEventGroup, function(err, raw) { + if (err) { + addToLog("editEventGroup", "error", "Attempt to edit event group " + req.params.eventGroupID + " failed with error: " + err); + res.send(err); + } + }) + .then(() => { + addToLog("editEventGroup", "success", "Event group " + req.params.eventGroupID + " edited"); + // if (sendEmails) { + // Event.findOne({id: req.params.eventID}).distinct('attendees.email', function(error, ids) { + // attendeeEmails = ids; + // if (!error && attendeeEmails != ""){ + // console.log("Sending emails to: " + attendeeEmails); + // const msg = { + // to: attendeeEmails, + // from: { + // name: 'Gathio', + // email: 'notifications@gath.io', + // }, + // templateId: 'd-e21f3ca49d82476b94ddd8892c72a162', + // dynamic_template_data: { + // subject: 'gathio: Event edited', + // actionType: 'edited', + // eventExists: true, + // eventID: req.params.eventID + // } + // } + // sgMail.sendMultiple(msg); + // } + // else { + // console.log("Nothing to send!"); + // } + // }) + // } + res.writeHead(302, { + 'Location': '/group/' + req.params.eventGroupID + '?e=' + req.params.editToken + }); + res.end(); + }) + .catch((err) => { console.error(err); res.send('Sorry! Something went wrong!'); addToLog("editEventGroup", "error", "Attempt to edit event group " + req.params.eventGroupID + " failed with error: " + err);}); + } + else { + // Token doesn't match + res.send('Sorry! Something went wrong'); + addToLog("editEventGroup", "error", "Attempt to edit event group " + req.params.eventGroupID + " failed with error: token does not match"); + } + }) + .catch((err) => { console.error(err); res.send('Sorry! Something went wrong!'); addToLog("editEventGroup", "error", "Attempt to edit event group " + req.params.eventGroupID + " failed with error: " + err);}); +}); + router.post('/deleteevent/:eventID/:editToken', (req, res) => { let submittedEditToken = req.params.editToken; Event.findOne(({ @@ -609,6 +880,61 @@ router.post('/deleteevent/:eventID/:editToken', (req, res) => { .catch((err) => { res.send('Sorry! Something went wrong: ' + err); addToLog("deleteEvent", "error", "Attempt to delete event " + req.params.eventID + " failed with error: " + err);}); }); +router.post('/deleteeventgroup/:eventGroupID/:editToken', (req, res) => { + let submittedEditToken = req.params.editToken; + EventGroup.findOne(({ + id: req.params.eventGroupID, + })) + .then(async (eventGroup) => { + if (eventGroup.editToken === submittedEditToken) { + // Token matches + + let linkedEvents = await Event.find({eventGroup: eventGroup._id}); + + let linkedEventIDs = linkedEvents.map(event => event._id); + let eventGroupImage = false; + if (eventGroup.image){ + eventGroupImage = eventGroup.image; + } + + EventGroup.deleteOne({id: req.params.eventGroupID}, function(err, raw) { + if (err) { + res.send(err); + addToLog("deleteEventGroup", "error", "Attempt to delete event group " + req.params.eventGroupID + " failed with error: " + err); + } + }) + .then(() => { + // Delete image + if (eventGroupImage){ + fs.unlink(global.appRoot + '/public/events/' + eventGroupImage, (err) => { + if (err) { + res.send(err); + addToLog("deleteEventGroup", "error", "Attempt to delete event image for event group " + req.params.eventGroupID + " failed with error: " + err); + } + }) + } + Event.update({_id: {$in: linkedEventIDs}}, { $set: { eventGroup: null } }, { multi: true }) + .then(response => { + console.log(response); + addToLog("deleteEventGroup", "success", "Event group " + req.params.eventGroupID + " deleted"); + res.writeHead(302, { + 'Location': '/' + }); + res.end(); + }) + .catch((err) => { res.send('Sorry! Something went wrong (error deleting): ' + err); addToLog("deleteEventGroup", "error", "Attempt to delete event group " + req.params.eventGroupID + " failed with error: " + err);}); + }) + .catch((err) => { res.send('Sorry! Something went wrong (error deleting): ' + err); addToLog("deleteEventGroup", "error", "Attempt to delete event group " + req.params.eventGroupID + " failed with error: " + err);}); + } + else { + // Token doesn't match + res.send('Sorry! Something went wrong'); + addToLog("deleteEventGroup", "error", "Attempt to delete event group " + req.params.eventGroupID + " failed with error: token does not match"); + } + }) + .catch((err) => { res.send('Sorry! Something went wrong: ' + err); addToLog("deleteEventGroup", "error", "Attempt to delete event group " + req.params.eventGroupID + " failed with error: " + err);}); +}); + router.post('/attendevent/:eventID', (req, res) => { const newAttendee = { name: req.body.attendeeName, diff --git a/start.js b/start.js index 1bf0456..b47d73c 100755 --- a/start.js +++ b/start.js @@ -20,6 +20,7 @@ mongoose.connection require('./models/Event'); require('./models/Log'); +require('./models/EventGroup'); const app = require('./app'); diff --git a/views/event.handlebars b/views/event.handlebars index d4637f4..4d0cf28 100755 --- a/views/event.handlebars +++ b/views/event.handlebars @@ -32,17 +32,9 @@ Show on OpenStreetMap - {{#if eventHasHost}} -
  • - - - - Hosted by {{eventData.hostName}} -
  • - {{/if}}
  • - + {{{displayDate}}}
    @@ -54,10 +46,26 @@ Add to Google Calendar
  • + {{#if eventHasHost}} +
  • + + + + Hosted by {{eventData.hostName}} +
  • + {{/if}} + {{#if eventData.eventGroup}} +
  • + + + + Part of {{eventData.eventGroup.name}} +
  • + {{/if}} {{#if eventData.url}}
  • - + {{eventData.url}} @@ -66,11 +74,9 @@ {{/if}}
  • - + - - gath.io/{{eventData.id}} - + gath.io/{{eventData.id}} @@ -416,6 +422,18 @@ $("#maxAttendeesContainer").slideUp('fast'); $("#maxAttendees").attr("data-validation-optional","true").val("").removeClass('is-valid is-invalid'); } + }); + $("#eventGroupCheckbox").on("click", function() { + if ($(this).is(':checked')) { + $("#eventGroupData").slideDown('fast'); + $("#eventGroupID").removeAttr("data-validation-optional").attr("data-validation","required"); + $("#eventGroupEditToken").removeAttr("data-validation-optional").attr("data-validation","required"); + } + else { + $("#eventGroupData").slideUp('fast'); + $("#eventGroupID").removeAttr("data-validation").attr("data-validation-optional","true").val(""); + $("#eventGroupEditToken").removeAttr("data-validation").attr("data-validation-optional","true").val(""); + } }); }); diff --git a/views/eventgroup.handlebars b/views/eventgroup.handlebars new file mode 100755 index 0000000..00bae2c --- /dev/null +++ b/views/eventgroup.handlebars @@ -0,0 +1,164 @@ +{{#if eventGroupHasCoverImage}} +
    +{{else}} +
    +{{/if}} +
    +
    +

    {{eventGroupData.name}}

    +
    + {{#if editingEnabled}} +
    +
    + + +
    +
    + {{/if}} +
    +{{#if firstLoad}} + +{{/if}} +
    +
    + +
    +
    + +{{#if editingEnabled}} +
    +

    To add an event to this group, copy and paste the two codes below into the 'Event Group' box when creating a new event or editing an existing event.

    +
    + + + + + + + + + +
    Event group ID{{eventGroupData.id}}
    Event group secret editing code{{eventGroupData.editToken}}
    +
    + +
    +{{/if}} + +
    +
    About
    +
    + {{{parsedDescription}}} +
    +
    +
    +
    Upcoming events
    +
    + {{#if upcomingEventsExist}} + {{#each events}} + {{#unless this.eventHasConcluded}} + + + {{this.name}} + {{this.displayDate}} + + {{/unless}} + {{/each}} + {{else}} +
    No events!
    + {{/if}} +
    +
    + +{{#if editingEnabled}} +{{> editeventgroupmodal }} + + + +{{/if}} + + diff --git a/views/newevent.handlebars b/views/newevent.handlebars index b63b43c..81d39c5 100755 --- a/views/newevent.handlebars +++ b/views/newevent.handlebars @@ -34,12 +34,15 @@
    -
    +
    -
    +
    +
    + +
    @@ -58,6 +61,9 @@ {{>importeventform}}
    +
    + {{>neweventgroupform}} +
    diff --git a/views/partials/neweventgroupform.handlebars b/views/partials/neweventgroupform.handlebars new file mode 100755 index 0000000..b7524d4 --- /dev/null +++ b/views/partials/neweventgroupform.handlebars @@ -0,0 +1,66 @@ +

    Create an event group

    +

    An event group is a holding area for a set of linked events, like a series of film nights, a festival, or a band tour. You can share a public link to your event group just like an individual event link, and people who know the secret editing code (sent in an email when you create the event group) will be able to add future events to the group.

    +

    Event groups do not get automatically removed like events do, but events which have been removed from Gathio will of course not show up in an event group.

    +
    +
    + +
    + +
    +
    +
    + +
    + + Markdown formatting supported. +
    +
    +
    + +
    + +
    +
    +
    + +
    +
    + + +
    + Recommended dimensions (w x h): 920px by 300px. +
    +
    +
    + +
    + +
    +
    +
    + +
    + + We will send your secret editing link to this email address. +
    +
    +
    +
    + +
    +
    +
    + + -- cgit v1.2.3