diff options
24 files changed, 1147 insertions, 851 deletions
diff --git a/.prettierignore b/.prettierignore index 96fa736..f02c836 100644 --- a/.prettierignore +++ b/.prettierignore @@ -1,4 +1,3 @@ dist -public views pnpm-lock.yaml
\ No newline at end of file diff --git a/cypress/e2e/group.cy.ts b/cypress/e2e/group.cy.ts new file mode 100644 index 0000000..8250179 --- /dev/null +++ b/cypress/e2e/group.cy.ts @@ -0,0 +1,69 @@ +const groupData = { + eventGroupName: "Test Group", + eventGroupDescription: "Test Group Description", + eventGroupURL: "https://example.com", + hostName: "Test Host", + creatorEmail: "test@example.com", +}; + +describe("Groups", () => { + beforeEach(() => { + cy.visit("/new"); + cy.get("#showNewEventGroupFormButton").click(); + + // Fill in the form + cy.get("#eventGroupName").type(groupData.eventGroupName); + cy.get("#eventGroupDescription").type(groupData.eventGroupDescription); + cy.get("#eventGroupURL").type(groupData.eventGroupURL); + cy.get("#eventGroupHostName").type(groupData.hostName); + cy.get("#eventGroupCreatorEmail").type(groupData.creatorEmail); + + // Submit the form + cy.get("#newEventGroupForm").submit(); + + // Wait for the new page to load + cy.url().should("not.include", "/new"); + + // Get the new group ID from the URL + cy.url().then((url) => { + const [groupID, editToken] = url.split("/").pop().split("?"); + cy.wrap(groupID).as("groupID"); + cy.wrap(editToken.slice(2)).as("editToken"); + }); + }); + it("creates a new group", function () { + cy.get("#eventGroupName").should("have.text", groupData.eventGroupName); + cy.get("#eventDescription").should( + "contain.text", + groupData.eventGroupDescription, + ); + cy.get("#eventGroupURL").should( + "contain.text", + groupData.eventGroupURL, + ); + cy.get("#hostName").should("contain.text", groupData.hostName); + cy.get("#eventGroupID").should("contain.text", this.groupID); + cy.get("#eventGroupEditToken").should("contain.text", this.editToken); + }); + + it("edits a group", function () { + // // Wait for the modal to not be visible + // cy.get("#editModal").should("not.be.visible"); + // // Check that all the data is correct + // cy.get(".p-name").should("have.text", "Edited Event Name"); + // cy.get(".p-location").should("have.text", "Edited Event Location"); + // cy.get(".p-summary").should("contain.text", "Edited Event Description"); + // cy.get("#hosted-by").should("contain.text", "Hosted by Edited Name"); + // cy.get(".dt-duration").should( + // "contain.text", + // "Sunday 1 December 2030 from 12:00 am to 1:00 am", + // ); + // cy.get(".dt-duration") + // .invoke("text") + // .should("match", /AE(D|S)T/); + // // Check that the comment form is not visible + // cy.get("#postComment").should("not.exist"); + // // Check that the attendee form is not visible + // cy.get("#attendEvent").should("not.exist"); + }); +}); diff --git a/public/css/style.css b/public/css/style.css index a312587..0f149e8 100755 --- a/public/css/style.css +++ b/public/css/style.css @@ -7,133 +7,152 @@ body, html { /* FONTS */ @font-face { - font-family: 'Fredoka One'; - font-style: normal; - font-weight: 400; - src: url('../fonts/fredoka-one-v7-latin-regular.eot'); /* IE9 Compat Modes */ - src: local('Fredoka One'), local('FredokaOne-Regular'), - url('../fonts/fredoka-one-v7-latin-regular.eot?#iefix') format('embedded-opentype'), /* IE6-IE8 */ - url('../fonts/fredoka-one-v7-latin-regular.woff2') format('woff2'), /* Super Modern Browsers */ - url('../fonts/fredoka-one-v7-latin-regular.woff') format('woff'), /* Modern Browsers */ - url('../fonts/fredoka-one-v7-latin-regular.ttf') format('truetype'), /* Safari, Android, iOS */ - url('../fonts/fredoka-one-v7-latin-regular.svg#FredokaOne') format('svg'); /* Legacy iOS */ - } - + font-family: "Fredoka One"; + font-style: normal; + font-weight: 400; + src: url("../fonts/fredoka-one-v7-latin-regular.eot"); /* IE9 Compat Modes */ + src: + local("Fredoka One"), + local("FredokaOne-Regular"), + url("../fonts/fredoka-one-v7-latin-regular.eot?#iefix") + format("embedded-opentype"), + /* IE6-IE8 */ url("../fonts/fredoka-one-v7-latin-regular.woff2") + format("woff2"), + /* Super Modern Browsers */ + url("../fonts/fredoka-one-v7-latin-regular.woff") format("woff"), + /* Modern Browsers */ url("../fonts/fredoka-one-v7-latin-regular.ttf") + format("truetype"), + /* Safari, Android, iOS */ + url("../fonts/fredoka-one-v7-latin-regular.svg#FredokaOne") + format("svg"); /* Legacy iOS */ +} @media (max-width: 576px) { - #container { - height: auto !important; - overflow-x: hidden; - } + #container { + height: auto !important; + overflow-x: hidden; + } } #content { - display: flex; - min-height: 100vh; - flex-direction: column; + display: flex; + min-height: 100vh; + flex-direction: column; } #bodyContainer { - flex: 1; + flex: 1; } #fixedContainer { - position: sticky; - top: 0; + position: sticky; + top: 0; } #footerContainer { - border-top: 1px solid #e0e0e0; - text-align: center; - padding: 5px 0; + border-top: 1px solid #e0e0e0; + text-align: center; + padding: 5px 0; } #sidebar { - background: #f5f5f5; - border-bottom: 2px solid #e0e0e0; + background: #f5f5f5; + border-bottom: 2px solid #e0e0e0; } #sidebar h1 { - font-family: "Fredoka One", sans-serif; - font-weight: 700; - text-align: center; - letter-spacing: -0.5px; - font-size: 3rem; - color: transparent !important; - margin-bottom: 0; + font-family: "Fredoka One", sans-serif; + font-weight: 700; + text-align: center; + letter-spacing: -0.5px; + font-size: 3rem; + color: transparent !important; + margin-bottom: 0; } #sidebar h1 a { - background: rgb(33, 37, 41); - background-clip: text; - -webkit-background-clip: text; - color: transparent !important; + background: rgb(33, 37, 41); + background-clip: text; + -webkit-background-clip: text; + color: transparent !important; } #sidebar h1 a:hover { - text-decoration: none; - background: linear-gradient(to right, #27aa45, #7fe0c8, #5d26c1); - background-size: 100% 100%; - background-clip: text; - -webkit-background-clip: text; - color: transparent !important; + text-decoration: none; + background: linear-gradient(to right, #27aa45, #7fe0c8, #5d26c1); + background-size: 100% 100%; + background-clip: text; + -webkit-background-clip: text; + color: transparent !important; } #content { - background: #ffffff; - box-shadow: -8px 0 6px -6px rgba(0,0,0,0.3); + background: #ffffff; + box-shadow: -8px 0 6px -6px rgba(0, 0, 0, 0.3); } #genericEventImageContainer { - height:150px; - border-radius: 5px; + height: 150px; + border-radius: 5px; } #genericEventImageContainer:before { - content: ''; - background: linear-gradient(to bottom, rgba(30,87,153,0) 0%,rgba(242,245,249,0) 75%,rgba(255,255,255,1) 95%,rgba(255,255,255,1) 100%); - position: absolute; - width: 97%; - height: 150px; + content: ""; + background: linear-gradient( + to bottom, + rgba(30, 87, 153, 0) 0%, + rgba(242, 245, 249, 0) 75%, + rgba(255, 255, 255, 1) 95%, + rgba(255, 255, 255, 1) 100% + ); + position: absolute; + width: 97%; + height: 150px; } #eventImageContainer { - height:300px; - background-size: cover; - background-repeat: no-repeat; - background-position: center; - border-radius: 5px; + height: 300px; + background-size: cover; + background-repeat: no-repeat; + background-position: center; + border-radius: 5px; } #eventImageContainer:before { - content: ''; - background: linear-gradient(to bottom, rgba(30,87,153,0) 0%,rgba(242,245,249,0) 85%,rgba(255,255,255,1) 95%,rgba(255,255,255,1) 100%); - position: absolute; - width: 100%; - height: 300px; + content: ""; + background: linear-gradient( + to bottom, + rgba(30, 87, 153, 0) 0%, + rgba(242, 245, 249, 0) 85%, + rgba(255, 255, 255, 1) 95%, + rgba(255, 255, 255, 1) 100% + ); + position: absolute; + width: 100%; + height: 300px; } #eventName { - padding: 0 0 0 10px; - width: 100%; - display: flex; - justify-content: space-between; + padding: 0 0 0 10px; + width: 100%; + display: flex; + justify-content: space-between; } #eventPrivacy { - text-transform:capitalize; + text-transform: capitalize; } #eventFromNow { - padding-left: 25px; + padding-left: 25px; } #eventFromNow::first-letter { - text-transform:capitalize; + text-transform: capitalize; } #eventActions { - padding-left: 0; - margin-top: 1rem; + padding-left: 0; + margin-top: 1rem; } /* @@ -145,129 +164,127 @@ body, html { */ .attendeesList { - margin: 0; - padding: 0; - list-style-type: none; - display: flex; - flex-wrap: wrap; + margin: 0; + padding: 0; + list-style-type: none; + display: flex; + flex-wrap: wrap; } .attendeesList > li { - border: 4px solid #0ea130; - border-radius: 2em; - padding: .5em 1em; - margin-right: 5px; - margin-bottom: 10px; - background: #57b76d; - color: white; - font-size: 0.95em; - font-weight: bold; + border: 4px solid #0ea130; + border-radius: 2em; + padding: 0.5em 1em; + margin-right: 5px; + margin-bottom: 10px; + background: #57b76d; + color: white; + font-size: 0.95em; + font-weight: bold; } .expand { - -webkit-transition: height 0.2s; - -moz-transition: height 0.2s; - transition: height 0.2s; + -webkit-transition: height 0.2s; + -moz-transition: height 0.2s; + transition: height 0.2s; } .eventInformation { - margin-left: 1.6em; + margin-left: 1.6em; } .eventInformation > li { -/* line-height: 2.1em;*/ - margin-bottom: 0.8em; + /* line-height: 2.1em;*/ + margin-bottom: 0.8em; } #copyEventLink { - margin-left: 5px; + margin-left: 5px; } .commentContainer { - background: #fafafa; - border-radius: 5px; - padding: 10px; - margin-bottom: 10px; - border: 1px solid #dfdfdf; + background: #fafafa; + border-radius: 5px; + padding: 10px; + margin-bottom: 10px; + border: 1px solid #dfdfdf; } .replyContainer { - display: none; - background: #efefef; - padding: 10px; - border-radius: 0 0 5px 5px; - border-bottom: 1px solid #d2d2d2; - border-left: 1px solid #d2d2d2; - border-right: 1px solid #d2d2d2; - width: 95%; - margin: -10px auto 10px auto; + display: none; + background: #efefef; + padding: 10px; + border-radius: 0 0 5px 5px; + border-bottom: 1px solid #d2d2d2; + border-left: 1px solid #d2d2d2; + border-right: 1px solid #d2d2d2; + width: 95%; + margin: -10px auto 10px auto; } .repliesContainer { - font-size: smaller; - padding-left:20px; + font-size: smaller; + padding-left: 20px; } /* IMAGE UPLOAD FORM */ - - .image-preview { - width: 100%; - height: 200px; - position: relative; - overflow: hidden; - background-color: #ffffff; - color: #ecf0f1; - border-radius: 5px; - border: 1px dashed #ced4da; + width: 920px; + height: 200px; + position: relative; + overflow: hidden; + background-color: #ffffff; + color: #ecf0f1; + border-radius: 5px; + border: 1px dashed #ced4da; } .image-preview input { - line-height: 200px; - font-size: 200px; - position: absolute; - opacity: 0; - z-index: 10; + line-height: 200px; + font-size: 200px; + position: absolute; + opacity: 0; + z-index: 10; } .image-preview label { - position: absolute; - z-index: 5; - opacity: 0.8; - cursor: pointer; - background-color: #ced4da; - color: #555; - width: 200px; - height: 50px; - font-size: 20px; - line-height: 50px; - text-transform: uppercase; - top: 0; - left: 0; - right: 0; - bottom: 0; - margin: auto; - text-align: center; - border-radius: 5px; + position: absolute; + z-index: 5; + opacity: 0.8; + cursor: pointer; + background-color: #ced4da; + color: #555; + width: 200px; + height: 50px; + font-size: 20px; + line-height: 50px; + text-transform: uppercase; + top: 0; + left: 0; + right: 0; + bottom: 0; + margin: auto; + text-align: center; + border-radius: 5px; } .datepickers-container { - z-index: 1600 !important; /* has to be larger than 1050 */ + z-index: 1600 !important; /* has to be larger than 1050 */ } #newEventFormContainer, #importEventFormContainer, #newEventGroupFormContainer { - display: none; + display: none; } #icsImportLabel { - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; - color: #6c757d; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + color: #6c757d; } .select2-container { - width: 100% !important; + width: 100% !important; } .select2-selection__rendered { line-height: 2.25rem !important; @@ -280,75 +297,102 @@ body, html { } .attendee-name { - white-space: nowrap; - overflow: hidden; - text-overflow: ""; - overflow: hidden; - max-width: 62px; - color: #fff; + white-space: nowrap; + overflow: hidden; + text-overflow: ""; + overflow: hidden; + max-width: 62px; + color: #fff; } .remove-attendee { - color: #fff; + color: #fff; } .remove-attendee:hover { - color: #016418; + color: #016418; } #eventAttendees h5 { - display: flex; - flex-direction: column; - align-items: flex-start; + display: flex; + flex-direction: column; + align-items: flex-start; } #eventAttendees h5 .btn-group { - margin-top: 0.5rem; + margin-top: 0.5rem; } .edit-buttons { - text-align: right; + text-align: right; } @media (max-width: 1199.98px) { - .edit-buttons { - text-align: left; - } + .edit-buttons { + text-align: left; + } } @media (min-width: 1120px) { - #eventActions { - margin-top: 0; - padding-left: 1rem; - } + #eventActions { + margin-top: 0; + padding-left: 1rem; + } } @media (min-width: 577px) { - #sidebar { - border-right: 2px solid #e0e0e0; - min-height: 100vh; - } - body { - background: #f5f5f5; /* Old browsers */ - background: -moz-linear-gradient(left, #f5f5f5 0%, #f5f5f5 50%, #ffffff 51%, #ffffff 100%); /* FF3.6-15 */ - background: -webkit-linear-gradient(left, #f5f5f5 0%,#f5f5f5 50%,#ffffff 51%,#ffffff 100%); /* Chrome10-25,Safari5.1-6 */ - background: linear-gradient(to right, #f5f5f5 0%,#f5f5f5 50%,#ffffff 51%,#ffffff 100%); /* W3C, IE10+, FF16+, Chrome26+, Opera12+, Safari7+ */ - filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='#f5f5f5', endColorstr='#ffffff',GradientType=1 ); /* IE6-9 */ - } - #eventAttendees h5 { - flex-direction: row; - justify-content: space-between; - align-items: center; - } - #eventAttendees h5 .btn-group { - margin-top: 0; - } + #sidebar { + border-right: 2px solid #e0e0e0; + min-height: 100vh; + } + body { + background: #f5f5f5; /* Old browsers */ + background: -moz-linear-gradient( + left, + #f5f5f5 0%, + #f5f5f5 50%, + #ffffff 51%, + #ffffff 100% + ); /* FF3.6-15 */ + background: -webkit-linear-gradient( + left, + #f5f5f5 0%, + #f5f5f5 50%, + #ffffff 51%, + #ffffff 100% + ); /* Chrome10-25,Safari5.1-6 */ + background: linear-gradient( + to right, + #f5f5f5 0%, + #f5f5f5 50%, + #ffffff 51%, + #ffffff 100% + ); /* W3C, IE10+, FF16+, Chrome26+, Opera12+, Safari7+ */ + filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='#f5f5f5', endColorstr='#ffffff',GradientType=1 ); /* IE6-9 */ + } + #eventAttendees h5 { + flex-direction: row; + justify-content: space-between; + align-items: center; + } + #eventAttendees h5 .btn-group { + margin-top: 0; + } } .list-group-item-action:hover { - background-color: #d4edda; + background-color: #d4edda; } .code { - font-family: 'Courier New', Courier, monospace; - overflow-wrap: anywhere; + font-family: "Courier New", Courier, monospace; + overflow-wrap: anywhere; +} + +/* FORMS */ +label:not(.form-check-label) { + font-weight: 500; +} + +input[type="datetime-local"] { + max-width: 20rem; } diff --git a/public/js/modules/event-edit.js b/public/js/modules/event-edit.js new file mode 100644 index 0000000..65d9889 --- /dev/null +++ b/public/js/modules/event-edit.js @@ -0,0 +1,91 @@ +$(document).ready(function () { + $.uploadPreview({ + input_field: "#event-image-upload", + preview_box: "#event-image-preview", + label_field: "#event-image-label", + label_default: "Choose file", + label_selected: "Change file", + no_label: false, + }); + autosize($("textarea")); + if (window.eventData.image) { + $("#event-image-preview").css( + "background-image", + `url('/events/${window.eventData.image}')`, + ); + $("#event-image-preview").css("background-size", "cover"); + $("#event-image-preview").css("background-position", "center center"); + } + $("#timezone").val(window.eventData.timezone).trigger("change"); +}); + +function editEventForm() { + return { + data: { + eventName: window.eventData.name, + eventLocation: window.eventData.location, + eventStart: window.eventData.startForDateInput, + eventEnd: window.eventData.endForDateInput, + timezone: window.eventData.timezone, + eventDescription: window.eventData.description, + eventURL: window.eventData.url, + hostName: window.eventData.hostName, + creatorEmail: window.eventData.creatorEmail, + eventGroupID: window.eventData.eventGroupID, + eventGroupEditToken: window.eventData.eventGroupEditToken, + interactionCheckbox: window.eventData.usersCanComment, + joinCheckbox: window.eventData.usersCanAttend, + maxAttendeesCheckbox: window.eventData.maxAttendees !== null, + maxAttendees: window.eventData.maxAttendees, + }, + errors: [], + submitting: false, + init() { + // Set up Select2 + this.select2 = $(this.$refs.timezone).select2(); + this.select2.on("select2:select", (event) => { + this.data.timezone = event.target.value; + }); + this.data.timezone = this.select2.val(); + }, + async submitForm() { + this.submitting = true; + this.errors = []; + const formData = new FormData(); + for (const [key, value] of Object.entries(this.data)) { + formData.append(key, value); + } + formData.append( + "imageUpload", + this.$refs.eventImageUpload.files[0], + ); + formData.append("editToken", window.eventData.editToken); + try { + const response = await fetch(`/event/${window.eventData.id}`, { + method: "PUT", + body: formData, + }); + this.submitting = false; + if (!response.ok) { + if (response.status !== 400) { + this.errors = unexpectedError; + return; + } + const json = await response.json(); + this.errors = json.errors; + // Set Bootstrap validation classes using 'field' property + $("input, textarea").removeClass("is-invalid"); + this.errors.forEach((error) => { + $(`#${error.field}`).addClass("is-invalid"); + }); + return; + } + window.location.reload(); + } catch (error) { + console.log(error); + this.errors = unexpectedError; + this.submitting = false; + } + }, + }; +} diff --git a/public/js/modules/group-edit.js b/public/js/modules/group-edit.js new file mode 100644 index 0000000..1a2c1db --- /dev/null +++ b/public/js/modules/group-edit.js @@ -0,0 +1,72 @@ +$(document).ready(function () { + $.uploadPreview({ + input_field: "#group-image-upload", + preview_box: "#group-image-preview", + label_field: "#group-image-label", + label_default: "Choose file", + label_selected: "Change file", + no_label: false, + }); + autosize($("textarea")); + if (window.groupData.image) { + $("#group-image-preview").css( + "background-image", + `url('/events/${window.groupData.image}')`, + ); + $("#group-image-preview").css("background-size", "cover"); + $("#group-image-preview").css("background-position", "center center"); + } + $("#timezone").val(window.groupData.timezone).trigger("change"); +}); + +function editEventGroupForm() { + return { + data: { + eventGroupName: window.groupData.name, + eventGroupDescription: window.groupData.description, + eventGroupURL: window.groupData.url, + hostName: window.groupData.hostName, + creatorEmail: window.groupData.creatorEmail, + }, + errors: [], + submitting: false, + async submitForm() { + this.submitting = true; + this.errors = []; + const formData = new FormData(); + for (const [key, value] of Object.entries(this.data)) { + formData.append(key, value); + } + formData.append( + "imageUpload", + this.$refs.eventGroupImageUpload.files[0], + ); + formData.append("editToken", window.groupData.editToken); + try { + const response = await fetch(`/group/${window.groupData.id}`, { + method: "PUT", + body: formData, + }); + this.submitting = false; + if (!response.ok) { + if (response.status !== 400) { + this.errors = unexpectedError; + return; + } + const json = await response.json(); + this.errors = json.errors; + $("input, textarea").removeClass("is-invalid"); + this.errors.forEach((error) => { + $(`#${error.field}`).addClass("is-invalid"); + }); + return; + } + window.location.reload(); + } catch (error) { + console.log(error); + this.errors = unexpectedError; + this.submitting = false; + } + }, + }; +} diff --git a/public/js/modules/new.js b/public/js/modules/new.js new file mode 100644 index 0000000..fbb99ed --- /dev/null +++ b/public/js/modules/new.js @@ -0,0 +1,201 @@ +$(document).ready(function () { + if ($("#icsImportControl")[0].files[0] != null) { + var file = $("#icsImportControl")[0].files[0].name; + $("#icsImportControl") + .next("label") + .html('<i class="far fa-file-alt"></i> ' + file); + } + $("#showNewEventFormButton").click(function () { + $("button").removeClass("active"); + $( + "#showImportEventFormButton #showNewEventGroupFormButton", + ).removeClass("active"); + if ($("#newEventFormContainer").is(":visible")) { + $("#newEventFormContainer").slideUp("fast"); + } else { + $("#newEventFormContainer").slideDown("fast"); + $("#importEventFormContainer").slideUp("fast"); + $("#newEventGroupFormContainer").slideUp("fast"); + $(this).addClass("active"); + } + }); + $("#showImportEventFormButton").click(function () { + $("button").removeClass("active"); + $("#showNewEventFormButton #showNewEventGroupFormButton").removeClass( + "active", + ); + if ($("#importEventFormContainer").is(":visible")) { + $("#importEventFormContainer").slideUp("fast"); + } else { + $("#importEventFormContainer").slideDown("fast"); + $("#newEventFormContainer").slideUp("fast"); + $("#newEventGroupFormContainer").slideUp("fast"); + $(this).addClass("active"); + } + }); + $("#showNewEventGroupFormButton").click(function () { + $("button").removeClass("active"); + $("#showNewEventFormButton #showImportEventFormButton").removeClass( + "active", + ); + if ($("#newEventGroupFormContainer").is(":visible")) { + $("#newEventGroupFormContainer").slideUp("fast"); + } else { + $("#newEventGroupFormContainer").slideDown("fast"); + $("#newEventFormContainer").slideUp("fast"); + $("#importEventFormContainer").slideUp("fast"); + $(this).addClass("active"); + } + }); + $("#icsImportControl").change(function () { + var file = $("#icsImportControl")[0].files[0].name; + $(this) + .next("label") + .html('<i class="far fa-file-alt"></i> ' + file); + }); + + $.uploadPreview({ + input_field: "#event-image-upload", + preview_box: "#event-image-preview", + label_field: "#event-image-label", + label_default: "Choose file", + label_selected: "Change file", + no_label: false, + }); + $.uploadPreview({ + input_field: "#group-image-upload", + preview_box: "#group-image-preview", + label_field: "#group-image-label", + label_default: "Choose file", + label_selected: "Change file", + no_label: false, + }); + autosize($("textarea")); +}); + +function newEventForm() { + return { + data: { + eventName: "", + eventLocation: "", + eventStart: "", + eventEnd: "", + timezone: "", + eventDescription: "", + eventURL: "", + hostName: "", + creatorEmail: "", + eventGroupID: "", + eventGroupEditToken: "", + interactionCheckbox: false, + joinCheckbox: false, + maxAttendeesCheckbox: false, + maxAttendees: "", + }, + errors: [], + submitting: false, + init() { + // Set up Select2 + this.select2 = $(this.$refs.timezone).select2(); + this.select2.on("select2:select", (event) => { + this.data.timezone = event.target.value; + }); + this.data.timezone = this.select2.val(); + // Reset checkboxes + this.data.eventGroupCheckbox = false; + this.data.interactionCheckbox = false; + this.data.joinCheckbox = false; + this.data.maxAttendeesCheckbox = false; + }, + async submitForm() { + this.submitting = true; + this.errors = []; + const formData = new FormData(); + for (const [key, value] of Object.entries(this.data)) { + formData.append(key, value); + } + formData.append( + "imageUpload", + this.$refs.eventImageUpload.files[0], + ); + try { + const response = await fetch("/event", { + method: "POST", + body: formData, + }); + this.submitting = false; + if (!response.ok) { + if (response.status !== 400) { + this.errors = unexpectedError; + return; + } + const json = await response.json(); + this.errors = json.errors; + $("input, textarea").removeClass("is-invalid"); + this.errors.forEach((error) => { + $(`#${error.field}`).addClass("is-invalid"); + }); + return; + } + const json = await response.json(); + window.location.assign(json.url); + } catch (error) { + console.log(error); + this.errors = unexpectedError; + this.submitting = false; + } + }, + }; +} +function newEventGroupForm() { + return { + data: { + eventGroupName: "", + eventGroupDescription: "", + eventGroupURL: "", + hostName: "", + creatorEmail: "", + }, + errors: [], + submitting: false, + async submitForm() { + this.submitting = true; + this.errors = []; + console.log(this.data); + const formData = new FormData(); + for (const [key, value] of Object.entries(this.data)) { + formData.append(key, value); + } + formData.append( + "imageUpload", + this.$refs.eventGroupImageUpload.files[0], + ); + try { + const response = await fetch("/group", { + method: "POST", + body: formData, + }); + this.submitting = false; + if (!response.ok) { + if (response.status !== 400) { + this.errors = unexpectedError; + return; + } + const json = await response.json(); + this.errors = json.errors; + $("input, textarea").removeClass("is-invalid"); + this.errors.forEach((error) => { + $(`#${error.field}`).addClass("is-invalid"); + }); + return; + } + const json = await response.json(); + window.location.assign(json.url); + } catch (error) { + console.log(error); + this.errors = unexpectedError; + this.submitting = false; + } + }, + }; +} diff --git a/public/js/util.js b/public/js/util.js index cbfd239..0499a4d 100644 --- a/public/js/util.js +++ b/public/js/util.js @@ -1,31 +1,38 @@ -const getStoredToken = function(eventID) { +const getStoredToken = function (eventID) { try { - let editTokens = JSON.parse(localStorage.getItem('editTokens')); + let editTokens = JSON.parse(localStorage.getItem("editTokens")); return editTokens[eventID]; - } catch(e) { - localStorage.setItem('editTokens', JSON.stringify({})); + } catch (e) { + localStorage.setItem("editTokens", JSON.stringify({})); return false; } -} +}; -const addStoredToken = function(eventID, token) { +const addStoredToken = function (eventID, token) { try { - let editTokens = JSON.parse(localStorage.getItem('editTokens')); + let editTokens = JSON.parse(localStorage.getItem("editTokens")); editTokens[eventID] = token; - localStorage.setItem('editTokens', JSON.stringify(editTokens)); - } catch(e) { - localStorage.setItem('editTokens', JSON.stringify({ [eventID]: token })); + localStorage.setItem("editTokens", JSON.stringify(editTokens)); + } catch (e) { + localStorage.setItem( + "editTokens", + JSON.stringify({ [eventID]: token }), + ); return false; } -} +}; -const removeStoredToken = function(eventID) { +const removeStoredToken = function (eventID) { try { - let editTokens = JSON.parse(localStorage.getItem('editTokens')); + let editTokens = JSON.parse(localStorage.getItem("editTokens")); delete editTokens[eventID]; - localStorage.setItem('editTokens', JSON.stringify(editTokens)); - } catch(e) { - localStorage.setItem('editTokens', JSON.stringify({})); + localStorage.setItem("editTokens", JSON.stringify(editTokens)); + } catch (e) { + localStorage.setItem("editTokens", JSON.stringify({})); return false; } -} +}; + +const unexpectedError = [ + { message: "An unexpected error has occurred. Please try again later." }, +]; @@ -5,6 +5,7 @@ import routes from "./routes.js"; import frontend from "./routes/frontend.js"; import activitypub from "./routes/activitypub.js"; import event from "./routes/event.js"; +import group from "./routes/group.js"; import { initEmailService } from "./lib/email.js"; @@ -33,6 +34,9 @@ const hbsInstance = hbs.create({ match[1] + (match[3] || "s") ); // Plural case: 'bagel(s)' or 'bagel' --> bagels }, + json: function (context: any) { + return JSON.stringify(context); + }, }, }); app.engine("handlebars", hbsInstance.engine); @@ -52,6 +56,7 @@ app.use(express.urlencoded({ extended: true })); app.use("/", frontend); app.use("/", activitypub); app.use("/", event); +app.use("/", group); app.use("/", routes); export default app; diff --git a/src/routes.js b/src/routes.js index e4ef3cb..96420c7 100755 --- a/src/routes.js +++ b/src/routes.js @@ -298,102 +298,6 @@ router.post("/importevent", (req, res) => { } }); -router.post("/neweventgroup", (req, res) => { - let eventGroupID = nanoid(); - 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 (req.body.creatorEmail && sendEmails) { - req.app.get("hbsInstance").renderView( - "./views/emails/createeventgroup.handlebars", - { - eventGroupID, - editToken, - siteName, - siteLogo, - domain, - cache: true, - layout: "email.handlebars", - }, - function (err, html) { - const msg = { - to: req.body.creatorEmail, - from: { - name: siteName, - email: contactEmail, - address: contactEmail, - }, - subject: `${siteName}: ${req.body.eventGroupName}`, - html, - }; - switch (mailService) { - case "sendgrid": - sgMail.send(msg).catch((e) => { - console.error(e.toString()); - res.status(500).end(); - }); - break; - case "nodemailer": - nodemailerTransporter - .sendMail(msg) - .catch((e) => { - console.error(e.toString()); - res.status(500).end(); - }); - break; - } - }, - ); - } - 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("/verifytoken/event/:eventID", (req, res) => { Event.findOne({ id: req.params.eventID, @@ -414,107 +318,6 @@ router.post("/verifytoken/group/:eventGroupID", (req, res) => { }); }); -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", - ); - 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("/deleteimage/:eventID/:editToken", (req, res) => { let submittedEditToken = req.params.editToken; let eventImage; diff --git a/src/routes/event.ts b/src/routes/event.ts index 375871b..be27fd4 100644 --- a/src/routes/event.ts +++ b/src/routes/event.ts @@ -74,8 +74,7 @@ router.post( img.resize(920, Jimp.AUTO) // resize .quality(80) // set JPEG quality .write("./public/events/" + eventID + ".jpg"); // save - const filename = eventID + ".jpg"; - return filename; + return eventID + ".jpg"; }) .catch((err) => { addToLog( @@ -280,8 +279,8 @@ router.put( }); } - let submittedEditToken = req.body.editToken; try { + const submittedEditToken = req.body.editToken; const event = await Event.findOne({ id: req.params.eventID, }); diff --git a/src/routes/frontend.ts b/src/routes/frontend.ts index 56ce4db..c9594ef 100644 --- a/src/routes/frontend.ts +++ b/src/routes/frontend.ts @@ -6,7 +6,7 @@ import { renderPlain } from "../util/markdown.js"; import getConfig from "../lib/config.js"; import { addToLog, exportICal } from "../helpers.js"; import Event from "../models/Event.js"; -import EventGroup from "../models/EventGroup.js"; +import EventGroup, { IEventGroup } from "../models/EventGroup.js"; const config = getConfig(); @@ -215,6 +215,31 @@ router.get("/:eventID", async (req: Request, res: Response) => { eventHasConcluded: eventHasConcluded, eventHasBegun: eventHasBegun, metadata: metadata, + jsonData: { + name: event.name, + id: event.id, + description: event.description, + location: event.location, + timezone: event.timezone, + url: event.url, + hostName: event.hostName, + creatorEmail: event.creatorEmail, + eventGroupID: event.eventGroup + ? (event.eventGroup as unknown as IEventGroup).id + : null, + eventGroupEditToken: event.eventGroup + ? (event.eventGroup as unknown as IEventGroup).editToken + : null, + usersCanAttend: event.usersCanAttend, + usersCanComment: event.usersCanComment, + maxAttendees: event.maxAttendees, + startISO: eventStartISO, + endISO: eventEndISO, + startForDateInput: parsedStartForDateInput, + endForDateInput: parsedEndForDateInput, + image: event.image, + editToken: editingEnabled ? eventEditToken : null, + }, }); } } catch (err) { @@ -321,6 +346,16 @@ router.get("/group/:eventGroupID", async (req: Request, res: Response) => { eventGroupHasHost: eventGroupHasHost, firstLoad: firstLoad, metadata: metadata, + jsonData: { + name: eventGroup.name, + id: eventGroup.id, + description: eventGroup.description, + url: eventGroup.url, + hostName: eventGroup.hostName, + creatorEmail: eventGroup.creatorEmail, + image: eventGroup.image, + editToken: editingEnabled ? eventGroupEditToken : null, + }, }); } catch (err) { addToLog( diff --git a/src/routes/group.ts b/src/routes/group.ts new file mode 100644 index 0000000..2801248 --- /dev/null +++ b/src/routes/group.ts @@ -0,0 +1,240 @@ +import { Router, Response, Request } from "express"; +import getConfig from "../lib/config.js"; +import multer from "multer"; +import { generateEditToken, generateEventID } from "../util/generator.js"; +import { validateGroupData } from "../util/validation.js"; +import Jimp from "jimp"; +import { addToLog } from "../helpers.js"; +import EventGroup from "../models/EventGroup.js"; +import { sendEmailFromTemplate } from "../lib/email.js"; + +const config = getConfig(); + +const storage = multer.memoryStorage(); +// Accept only JPEG, GIF or PNG images, up to 10MB +const upload = multer({ + storage: storage, + limits: { fileSize: 10 * 1024 * 1024 }, + fileFilter: function (_, file, cb) { + const filetypes = /jpeg|jpg|png|gif/; + const mimetype = filetypes.test(file.mimetype); + if (!mimetype) { + return cb(new Error("Only JPEG, PNG and GIF images are allowed.")); + } + cb(null, true); + }, +}); + +const router = Router(); + +router.post( + "/group", + upload.single("imageUpload"), + async (req: Request, res: Response) => { + const { data: groupData, errors } = validateGroupData(req.body); + if (errors && errors.length > 0) { + return res.status(400).json({ errors }); + } + if (!groupData) { + return res.status(400).json({ + errors: [ + { + message: "No group data was provided.", + }, + ], + }); + } + + try { + const groupID = generateEventID(); + const editToken = generateEditToken(); + let groupImageFilename; + + if (req.file?.buffer) { + groupImageFilename = await Jimp.read(req.file.buffer) + .then((img) => { + img.resize(920, Jimp.AUTO) // resize + .quality(80) // set JPEG quality + .write("./public/events/" + groupID + ".jpg"); // save + return groupID + ".jpg"; + }) + .catch((err) => { + addToLog( + "Jimp", + "error", + "Attempt to edit image failed with error: " + err, + ); + }); + } + + const eventGroup = new EventGroup({ + id: groupID, + name: groupData.eventGroupName, + description: groupData.eventGroupDescription, + image: groupImageFilename, + creatorEmail: groupData.creatorEmail, + url: groupData.eventGroupURL, + hostName: groupData.hostName, + editToken: editToken, + firstLoad: true, + }); + + await eventGroup.save(); + + addToLog( + "createEventGroup", + "success", + "Event group " + groupID + " created", + ); + + // Send email with edit link + if (groupData.creatorEmail && req.app.locals.sendEmails) { + sendEmailFromTemplate( + groupData.creatorEmail, + `${eventGroup.name}`, + "createEventGroup", + { + eventGroupID: eventGroup.id, + editToken: eventGroup.editToken, + siteName: config.general.site_name, + siteLogo: config.general.email_logo_url, + domain: config.general.domain, + }, + req, + ); + } + + res.status(200).json({ + id: groupID, + editToken: editToken, + url: `/group/${groupID}?e=${editToken}`, + }); + } catch (err) { + console.error(err); + addToLog( + "createEvent", + "error", + "Attempt to create event failed with error: " + err, + ); + return res.status(500).json({ + errors: [ + { + message: err, + }, + ], + }); + } + }, +); + +router.put( + "/group/:eventGroupID", + upload.single("imageUpload"), + async (req: Request, res: Response) => { + const { data: groupData, errors } = validateGroupData(req.body); + if (errors && errors.length > 0) { + return res.status(400).json({ errors }); + } + if (!groupData) { + return res.status(400).json({ + errors: [ + { + message: "No group data was provided.", + }, + ], + }); + } + + try { + const submittedEditToken = req.body.editToken; + const eventGroup = await EventGroup.findOne({ + id: req.params.eventGroupID, + }); + if (!eventGroup) { + return res.status(404).json({ + errors: [ + { + message: "Event group not found.", + }, + ], + }); + } + + if (eventGroup.editToken !== submittedEditToken) { + // Token doesn't match + addToLog( + "editEventGroup", + "error", + `Attempt to edit event group ${req.params.eventGroupID} failed with error: token does not match`, + ); + return res.status(403).json({ + errors: [ + { + message: "Edit token is invalid.", + }, + ], + }); + } + // Token matches + // If there is a new image, upload that first + let eventGroupID = req.params.eventGroupID; + let eventGroupImageFilename = eventGroup.image; + if (req.file?.buffer) { + Jimp.read(req.file.buffer) + .then((img) => { + img.resize(920, Jimp.AUTO) // resize + .quality(80) // set JPEG quality + .write(`./public/events/${eventGroupID}.jpg`); // save + }) + .catch((err) => { + addToLog( + "Jimp", + "error", + "Attempt to edit image failed with error: " + err, + ); + }); + eventGroupImageFilename = eventGroupID + ".jpg"; + } + + const updatedEventGroup = { + name: req.body.eventGroupName, + description: req.body.eventGroupDescription, + url: req.body.eventGroupURL, + hostName: req.body.hostName, + image: eventGroupImageFilename, + }; + + await EventGroup.findOneAndUpdate( + { id: req.params.eventGroupID }, + updatedEventGroup, + ); + + addToLog( + "editEventGroup", + "success", + "Event group " + req.params.eventGroupID + " edited", + ); + + res.sendStatus(200); + } catch (err) { + console.error(err); + addToLog( + "editEventGroup", + "error", + "Attempt to edit event group " + + req.params.eventGroupID + + " failed with error: " + + err, + ); + return res.status(500).json({ + errors: [ + { + message: err, + }, + ], + }); + } + }, +); + +export default router; diff --git a/src/util/validation.ts b/src/util/validation.ts index f51769e..732fbf3 100644 --- a/src/util/validation.ts +++ b/src/util/validation.ts @@ -44,6 +44,14 @@ export type ValidatedEventData = Omit< maxAttendeesBoolean: boolean; }; +interface EventGroupData { + eventGroupName: string; + eventGroupDescription: string; + eventGroupURL: string; + hostName: string; + creatorEmail: string; +} + const validateEmail = (email: string) => { if (!email || email.length === 0 || typeof email !== "string") { return false; @@ -83,23 +91,11 @@ export const validateEventTime = (start: Date, end: Date): Error | boolean => { export const validateEventData = (eventData: EventData): ValidationResponse => { const validatedData: ValidatedEventData = { - eventName: eventData.eventName, - eventLocation: eventData.eventLocation, - eventStart: eventData.eventStart, - eventEnd: eventData.eventEnd, - timezone: eventData.timezone, - eventDescription: eventData.eventDescription, - eventURL: eventData.eventURL, - imagePath: eventData.imagePath, - hostName: eventData.hostName, - creatorEmail: eventData.creatorEmail, + ...eventData, eventGroupBoolean: eventData.eventGroupCheckbox === "true", interactionBoolean: eventData.interactionCheckbox === "true", joinBoolean: eventData.joinCheckbox === "true", maxAttendeesBoolean: eventData.maxAttendeesCheckbox === "true", - eventGroupID: eventData.eventGroupID, - eventGroupEditToken: eventData.eventGroupEditToken, - maxAttendees: eventData.maxAttendees, }; const errors: Error[] = []; if (!validatedData.eventName) { @@ -189,3 +185,32 @@ export const validateEventData = (eventData: EventData): ValidationResponse => { errors: errors, }; }; + +export const validateGroupData = (groupData: EventGroupData) => { + const errors: Error[] = []; + if (!groupData.eventGroupName) { + errors.push({ + message: "Event group name is required.", + field: "eventGroupName", + }); + } + if (!groupData.eventGroupDescription) { + errors.push({ + message: "Event group description is required.", + field: "eventGroupDescription", + }); + } + if (groupData.creatorEmail) { + if (!validateEmail(groupData.creatorEmail)) { + errors.push({ + message: "Email address is invalid.", + field: "creatorEmail", + }); + } + } + + return { + data: groupData, + errors: errors, + }; +}; diff --git a/views/emails/createEventGroup/createEventGroupHtml.handlebars b/views/emails/createEventGroup/createEventGroupHtml.handlebars index 9951a28..0a12e91 100644 --- a/views/emails/createEventGroup/createEventGroupHtml.handlebars +++ b/views/emails/createEventGroup/createEventGroupHtml.handlebars @@ -1,6 +1,6 @@ <p style="font-family: sans-serif; font-size: 14px; font-weight: normal; margin: 0; Margin-bottom: 15px;">You just created a new event group on {{siteName}}! Thanks a bunch - we're delighted to have you.</p> <p style="font-family: sans-serif; font-size: 14px; font-weight: normal; margin: 0; Margin-bottom: 15px;">You can edit your event group by clicking the button below, or just following this link: <a href="https://{{domain}}/group/{{eventGroupID}}?e={{editToken}}">https://{{domain}}/{{eventGroupID}}?e={{editToken}}</a></p> -<p style="font-family: sans-serif; font-size: 14px; font-weight: normal; margin: 0; Margin-bottom: 15px;">To add events to this group (whether brand new events or ones you've already made), click the 'This event is part of an event group' checkbox. You will need to copy the following two codes into the dark grey box which opens:</p> +<p style="font-family: sans-serif; font-size: 14px; font-weight: normal; margin: 0; Margin-bottom: 15px;">To add events to this group (whether brand new events or ones you've already made), click the 'This event is part of an event group' checkbox. You will need to copy the following two codes into the box which opens:</p> <p style="font-family: sans-serif; font-size: 14px; font-weight: normal; margin: 0; Margin-bottom: 15px;"><strong>Event group ID</strong>: {{eventGroupID}}</p> <p style="font-family: sans-serif; font-size: 14px; font-weight: normal; margin: 0; Margin-bottom: 15px;"><strong>Event group secret editing code</strong>: {{editToken}}</p> <table border="0" cellpadding="0" cellspacing="0" class="btn btn-primary" style="border-collapse: separate; mso-table-lspace: 0pt; mso-table-rspace: 0pt; width: 100%; box-sizing: border-box;"> diff --git a/views/emails/createEventGroup/createEventGroupText.handlebars b/views/emails/createEventGroup/createEventGroupText.handlebars index b017510..34ad618 100644 --- a/views/emails/createEventGroup/createEventGroupText.handlebars +++ b/views/emails/createEventGroup/createEventGroupText.handlebars @@ -2,7 +2,7 @@ You just created a new event group on {{siteName}}! Thanks a bunch - we're delig You can edit your event group by clicking the button below, or just following this link: https://{{domain}}/{{eventGroupID}}?e={{editToken}} -To add events to this group (whether brand new events or ones you've already made), click the 'This event is part of an event group' checkbox. You will need to copy the following two codes into the dark grey box which opens: +To add events to this group (whether brand new events or ones you've already made), click the 'This event is part of an event group' checkbox. You will need to copy the following two codes into the box which opens: Event group ID: {{eventGroupID}} diff --git a/views/event.handlebars b/views/event.handlebars index b759b0a..ae6674a 100755 --- a/views/event.handlebars +++ b/views/event.handlebars @@ -397,6 +397,11 @@ {{/if}} <script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script> + +<script> +window.eventData = {{{ json jsonData }}}; +</script> + <script> {{#if editingEnabled}} diff --git a/views/eventgroup.handlebars b/views/eventgroup.handlebars index c922b99..0643ed6 100755 --- a/views/eventgroup.handlebars +++ b/views/eventgroup.handlebars @@ -5,7 +5,7 @@ {{/if}} <div class="row"> <div class="col-lg"> - <h3 id="eventName" data-event-id="{{eventGroupData.id}}">{{eventGroupData.name}}</h3> + <h3 id="eventGroupName" data-event-id="{{eventGroupData.id}}">{{eventGroupData.name}}</h3> </div> <div class="col-lg-2 ml-2 edit-buttons"> {{#if editingEnabled}} @@ -16,7 +16,7 @@ </div> </div> {{#if firstLoad}} -<div class="alert alert-success alert-dismissible fade show" role="alert"> +<div class="alert alert-success alert-dismissible fade show mt-4" role="alert"> <button type="button" class="close" data-dismiss="alert" aria-label="Close"> <span aria-hidden="true">×</span> </button> @@ -30,7 +30,7 @@ <div class="card-body"> <ul class="fa-ul eventInformation"> {{#if eventGroupHasHost}} - <li> + <li id="hostName"> <span class="fa-li"> <i class="fas fa-fw fa-user-circle"></i> </span> @@ -38,7 +38,7 @@ </li> {{/if}} {{#if eventGroupData.url}} - <li> + <li id="eventGroupURL"> <span class="fa-li"> <i class="fas fa-link"></i> </span> @@ -91,17 +91,17 @@ </div> {{#if editingEnabled}} - <div class="alert alert-success"> + <div class="alert alert-info"> <p>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.</p> <div class="table-responsive"> <table style="width:100%"> <tr style="border-bottom:1px solid rgba(0,0,0,0.2)"> <td><strong>Event group ID</strong></td> - <td><span class="code">{{eventGroupData.id}}</span></td> + <td><span class="code" id="eventGroupID">{{eventGroupData.id}}</span></td> </tr> <tr> <td><strong>Event group editing password</strong></td> - <td><span class="code">{{eventGroupData.editToken}}</span></td> + <td><span class="code" id="eventGroupEditToken">{{eventGroupData.editToken}}</span></td> </tr> </table> </div> @@ -222,15 +222,11 @@ </div> </div> - <script> - $.validate({ - lang: 'en', - errorElementClass: "is-invalid", - errorMessageClass: "text-danger", - successElementClass: "is-valid" - }); +window.groupData = {{{ json jsonData }}}; +</script> +<script> $(document).ready(function() { // Save the editing token from the URL, if it is valid const eventID = $('#eventName').attr('data-event-id'); @@ -273,17 +269,6 @@ $('#editModal').modal('show'); } - $.uploadPreview({ - input_field: "#eventGroupImageUpload", - preview_box: "#eventGroupImagePreview", - label_field: "#eventGroupImageLabel", - label_default: "Choose file", - label_selected: "Change file", - no_label: false - }); - $("#eventGroupImagePreview").css("background-image", "url('/events/{{eventGroupData.image}}')"); - $("#eventGroupImagePreview").css("background-size", "cover"); - $("#eventGroupImagePreview").css("background-position", "center center"); new ClipboardJS('#copyEventLink'); new ClipboardJS('#copyFeedLink'); autosize($('textarea')); diff --git a/views/newevent.handlebars b/views/newevent.handlebars index 76b6a17..349c355 100755 --- a/views/newevent.handlebars +++ b/views/newevent.handlebars @@ -1,28 +1,21 @@ -<h2>New event</h2> -<hr> -<div class="alert alert-info mb-4 text-center" role="alert"> - <i class="fas fa-exclamation-circle"></i> Events are visible to anyone who knows the link. -</div> - - -{{#each errors}} - <div class="alert alert-danger" role="alert">{{this.msg}}</div> -{{/each}} - <div class="container mb-4"> <div class="row"> <div class="col-sm-4 p-2"> - <button type="button" id="showNewEventFormButton" class="btn btn-secondary w-100"><i class="fas fa-file"></i> Create a new event</button> + <button type="button" id="showNewEventFormButton" class="btn btn-secondary w-100"><i class="fas fa-calendar-day"></i> Create a new event</button> </div> <div class="col-sm-4 p-2"> <button type="button" id="showImportEventFormButton" class="btn btn-secondary w-100"><i class="fas fa-file-import"></i> Import an existing event</button> </div> <div class="col-sm-4 p-2"> - <button type="button" id="showNewEventGroupFormButton" class="btn btn-secondary w-100"><i class="fas fa-folder-open"></i> Create a new event group </button> + <button type="button" id="showNewEventGroupFormButton" class="btn btn-secondary w-100"><i class="fas fa-calendar-alt"></i> Create a new event group </button> </div> </div> </div> +<div class="alert alert-info mb-4 text-center" role="alert"> + <i class="fas fa-exclamation-circle"></i> Events are visible to anyone who knows the link. +</div> + <div id="newEventFormContainer"> <h4 class="mb-2">Create an event</h4> <form id="newEventForm" enctype="multipart/form-data" x-data="newEventForm()" x-init="init()" @submit.prevent="submitForm"> @@ -45,156 +38,18 @@ </div> <div id="newEventGroupFormContainer"> - {{>neweventgroupform}} + <h4 class="mb-2">Create an event group</h4> + <p class="text-muted">An event group is a holding area for a set of linked events, like a recurring game night, 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 will be able to add future events to the group.</p> + <p class="text-muted">Event groups do not get automatically removed like events do, but events which have been removed from {{siteName}} will of course not show up in an event group.</p> + <form id="newEventGroupForm" enctype="multipart/form-data" x-data="newEventGroupForm()" @submit.prevent="submitForm"> + {{> eventGroupForm }} + <div class="form-group row"> + <div class="col-sm-12 pt-3 pb-3 text-center"> + <button type="submit" class="btn btn-primary w-50" x-bind:disabled="submitting">Create</button> + </div> + </div> + </form> </div> - <script> - $(document).ready(function(){ - if ($('#icsImportControl')[0].files[0] != null){ - var file = $('#icsImportControl')[0].files[0].name; - $('#icsImportControl').next('label').html('<i class="far fa-file-alt"></i> ' + file); - } - $("#showNewEventFormButton").click(function(){ - $("button").removeClass("active"); - $("#showImportEventFormButton #showNewEventGroupFormButton").removeClass("active"); - if ($("#newEventFormContainer").is(":visible")){ - $("#newEventFormContainer").slideUp("fast"); - } - else { - $("#newEventFormContainer").slideDown("fast"); - $("#importEventFormContainer").slideUp("fast"); - $("#newEventGroupFormContainer").slideUp("fast"); - $(this).addClass("active"); - } - }) - $("#showImportEventFormButton").click(function(){ - $("button").removeClass("active"); - $("#showNewEventFormButton #showNewEventGroupFormButton").removeClass("active"); - if ($("#importEventFormContainer").is(":visible")){ - $("#importEventFormContainer").slideUp("fast"); - } - else { - $("#importEventFormContainer").slideDown("fast"); - $("#newEventFormContainer").slideUp("fast"); - $("#newEventGroupFormContainer").slideUp("fast"); - $(this).addClass("active"); - } - }) - $("#showNewEventGroupFormButton").click(function(){ - $("button").removeClass("active"); - $("#showNewEventFormButton #showImportEventFormButton").removeClass("active"); - if ($("#newEventGroupFormContainer").is(":visible")){ - $("#newEventGroupFormContainer").slideUp("fast"); - } - else { - $("#newEventGroupFormContainer").slideDown("fast"); - $("#newEventFormContainer").slideUp("fast"); - $("#importEventFormContainer").slideUp("fast"); - $(this).addClass("active"); - } - }) - $('#icsImportControl').change(function(){ - var file = $('#icsImportControl')[0].files[0].name; - $(this).next('label').html('<i class="far fa-file-alt"></i> ' + file); - }); - }) - </script> - -<script type="text/javascript" src="/js/generate-timezones.js"></script> - -<script> - $(document).ready(function() { - $.uploadPreview({ - input_field: "#image-upload", - preview_box: "#image-preview", - label_field: "#image-label", - label_default: "Choose file", - label_selected: "Change file", - no_label: false - }); - autosize($('textarea')); - }); - - function newEventForm() { - return { - data: { - eventName: '', - eventLocation: '', - eventStart: '', - eventEnd: '', - timezone: '', - eventDescription: '', - eventURL: '', - hostName: '', - creatorEmail: '', - eventGroupID: '', - eventGroupEditToken: '', - interactionCheckbox: false, - joinCheckbox: false, - maxAttendeesCheckbox: false, - maxAttendees: '', - }, - errors: [], - submitting: false, - init() { - // Set up Select2 - this.select2 = $(this.$refs.timezone).select2(); - this.select2.on("select2:select", (event) => { - this.data.timezone = event.target.value; - }); - this.data.timezone = this.select2.val(); - // Reset checkboxes - this.data.eventGroupCheckbox = false; - this.data.interactionCheckbox = false; - this.data.joinCheckbox = false; - this.data.maxAttendeesCheckbox = false; - }, - async submitForm() { - this.submitting = true; - this.errors = []; - const formData = new FormData(); - for (const key in this.data) { - if (this.data.hasOwnProperty(key)) { - formData.append(key, this.data[key]); - } - } - formData.append("imageUpload", this.$refs.eventImageUpload.files[0]); - try { - const response = await fetch("/event", { - method: "POST", - body: formData, - }); - this.submitting = false; - if (!response.ok) { - if (response.status !== 400) { - this.errors = [ - { - message: "An unexpected error has occurred. Please try again later.", - } - ]; - return; - } - const json = await response.json(); - this.errors = json.errors; - // Set Bootstrap validation classes using 'field' property - $("input, textarea").removeClass("is-invalid"); - this.errors.forEach((error) => { - $(`#${error.field}`).addClass("is-invalid"); - }); - return; - } - const json = await response.json(); - window.location.assign(json.url); - } catch (error) { - console.log(error); - this.errors = [ - { - message: "An unexpected error has occurred. Please try again later.", - } - ]; - this.submitting = false; - } - }, - } - } -</script>
\ No newline at end of file +<script src="/js/generate-timezones.js"></script> +<script src="/js/modules/new.js"></script>
\ No newline at end of file diff --git a/views/partials/editeventgroupmodal.handlebars b/views/partials/editeventgroupmodal.handlebars index 3b8f55a..2506e26 100644 --- a/views/partials/editeventgroupmodal.handlebars +++ b/views/partials/editeventgroupmodal.handlebars @@ -8,32 +8,10 @@ </button> </div> <div class="modal-body"> - <form id="editEventForm" action="/editeventgroup/{{eventGroupData.id}}/{{eventGroupData.editToken}}" method="post" enctype="multipart/form-data" autocomplete="off"> - <div class="form-group"> - <label for="eventGroupName" >Name</label> - <input type="text" class="form-control" id="eventGroupName" name="eventGroupName" placeholder="Make it snappy." value="{{eventGroupData.name}}" data-validation="required length" data-validation-length="3-120"> - </div> - <div class="form-group"> - <label for="eventGroupDescription" >Description</label> - <textarea class="form-control" id="eventGroupDescription" name="eventGroupDescription" data-validation="required">{{eventGroupData.description}}</textarea> - <small class="form-text"><a href="https://commonmark.org/help/">Markdown</a> formatting supported.</small> - </div> - <div class="form-group"> - <label for="eventGroupURL" >Link</label> - <input type="url" class="form-control" id="eventURL" name="eventGroupURL" value="{{eventGroupData.url}}" placeholder="For tickets or a page with more information (optional)." data-validation="url" data-validation-optional="true"> - </div> - <div class="form-group"> - <label for="hostName" >Host or organisation name</label> - <input type="text" class="form-control" id="hostName" name="hostName" placeholder="Will be shown on the event group page (optional)." value="{{eventGroupData.hostName}}" data-validation="length" data-validation-length="3-120" data-validation-optional="true"> - </div> - <div class="form-group"> - <label>Cover image</label> - <div class="image-preview" id="eventGroupImagePreview"> - <label for="eventGroupImageUpload" id="eventGroupImageLabel">Choose file</label> - <input type="file" name="eventGroupImageUpload" id="eventGroupImageUpload" /> - </div> - <small class="form-text">Recommended dimensions (w x h): 920px by 300px.</small> - </div> + <form id="editEventForm" enctype="multipart/form-data" x-data="editEventGroupForm()" @submit.prevent="submitForm"> + + {{> eventGroupForm }} + <div class="form-group"> <div class="card border-danger mb-3"> <div class="card-header text-danger">Delete this event group</div> @@ -51,3 +29,5 @@ </div> </div> </div> + +<script type="text/javascript" src="/js/modules/group-edit.js"></script>
\ No newline at end of file diff --git a/views/partials/editeventmodal.handlebars b/views/partials/editeventmodal.handlebars index 2572cbb..a36cd98 100644 --- a/views/partials/editeventmodal.handlebars +++ b/views/partials/editeventmodal.handlebars @@ -46,104 +46,4 @@ </script> <script type="text/javascript" src="/js/generate-timezones.js"></script> - -<script> - $(document).ready(function () { - $.uploadPreview({ - input_field: "#image-upload", - preview_box: "#image-preview", - label_field: "#image-label", - label_default: "Choose file", - label_selected: "Change file", - no_label: false - }); - autosize($('textarea')); - $("#image-preview").css("background-image", "url('/events/{{eventData.image}}')"); - $("#image-preview").css("background-size", "cover"); - $("#image-preview").css("background-position", "center center"); - $("#timezone").val('{{eventData.timezone}}').trigger('change'); - }); - - function editEventForm() { - return { - data: { - eventName: `{{{eventData.name}}}`, - eventLocation: `{{{ eventData.location }}}`, - eventStart: `{{{ parsedStartForDateInput }}}`, - eventEnd: `{{{ parsedEndForDateInput }}}`, - timezone: `{{{ eventData.timezone }}}`, - eventDescription: `{{{ eventData.description }}}`, - eventURL: `{{{ eventData.url }}}`, - hostName: `{{{ eventData.hostName }}}`, - creatorEmail: `{{{ eventData.creatorEmail }}}`, - eventGroupID: `{{{ eventData.eventGroupID }}}`, - eventGroupEditToken: `{{{ eventData.eventGroupEditToken }}}`, - interactionCheckbox: {{{ eventData.usersCanComment }}}, - joinCheckbox: {{{ eventData.usersCanAttend }}}, - maxAttendeesCheckbox: {{#if eventData.maxAttendees}}true{{else}}false{{/if}}, - maxAttendees: `{{{ eventData.maxAttendees }}}`, - }, - errors: [], - submitting: false, - init() { - // Set up Select2 - this.select2 = $(this.$refs.timezone).select2(); - this.select2.on("select2:select", (event) => { - this.data.timezone = event.target.value; - }); - this.data.timezone = this.select2.val(); - /* Set up checkboxes */ - this.data.eventGroupCheckbox = {{#if eventData.eventGroupID}}true{{else}}false{{/if}}; - this.data.interactionCheckbox = {{eventData.usersCanComment}}; - this.data.joinCheckbox = {{eventData.usersCanAttend}}; - this.data.maxAttendeesCheckbox = {{#if eventData.maxAttendees}}true{{else}}false{{/if}}; - }, - async submitForm() { - this.submitting = true; - this.errors = []; - const formData = new FormData(); - for (const key in this.data) { - if (this.data.hasOwnProperty(key)) { - formData.append(key, this.data[key]); - } - } - formData.append("imageUpload", this.$refs.eventImageUpload.files[0]); - formData.append("editToken", '{{eventData.editToken}}'); - try { - const response = await fetch("/event/{{eventData.id}}", { - method: "PUT", - body: formData, - }); - this.submitting = false; - if (!response.ok) { - if (response.status !== 400) { - this.errors = [ - { - message: "An unexpected error has occurred. Please try again later.", - } - ]; - return; - } - const json = await response.json(); - this.errors = json.errors; - // Set Bootstrap validation classes using 'field' property - $("input, textarea").removeClass("is-invalid"); - this.errors.forEach((error) => { - $(`#${error.field}`).addClass("is-invalid"); - }); - return; - } - window.location.reload(); - } catch (error) { - console.log(error); - this.errors = [ - { - message: "An unexpected error has occurred. Please try again later.", - } - ]; - this.submitting = false; - } - }, - } - } -</script>
\ No newline at end of file +<script type="text/javascript" src="/js/modules/event-edit.js"></script> diff --git a/views/partials/eventForm.handlebars b/views/partials/eventForm.handlebars index 36da7b8..93d679d 100755 --- a/views/partials/eventForm.handlebars +++ b/views/partials/eventForm.handlebars @@ -1,52 +1,52 @@ -<div class="form-group row"> - <label for="eventName" class="col-sm-2 col-form-label">Event name</label> - <div class="form-group col-sm-10"> +<div class="form-group"> + <label for="eventName" >Event name</label> + <div class="form-group "> <input type="text" class="form-control" id="eventName" name="eventName" placeholder="Make it snappy." x-model="data.eventName" > </div> </div> -<div class="form-group row"> - <label for="eventLocation" class="col-sm-2 col-form-label">Location</label> - <div class="form-group col-sm-10"> +<div class="form-group"> + <label for="eventLocation" >Location</label> + <div class="form-group "> <input type="text" class="form-control" id="eventLocation" name="eventLocation" placeholder="Be specific." x-model="data.eventLocation"> </div> </div> -<div class="form-group row"> - <label for="eventStart" class="col-sm-2 col-form-label">Starts</label> - <div class="form-group col-sm-4"> +<div class="form-group"> + <label for="eventStart" >Starts</label> + <div class="form-group"> <input type="datetime-local" class="form-control" id="eventStart" name="eventStart" x-model="data.eventStart"> </div> </div> -<div class="form-group row"> - <label for="eventEnd" class="col-sm-2 col-form-label">Ends</label> - <div class="form-group col-sm-4"> +<div class="form-group"> + <label for="eventEnd" >Ends</label> + <div class="form-group "> <input type="datetime-local" class="form-control" id="eventEnd" name="eventEnd" x-model="data.eventEnd"> </div> </div> -<div class="form-group row"> - <label for="timezone" class="col-sm-2 col-form-label">Timezone</label> - <div class="form-group col-sm-10"> +<div class="form-group"> + <label for="timezone" >Timezone</label> + <div class="form-group "> <select class="select2" id="timezone" name="timezone" x-ref="timezone"></select> </div> </div> -<div class="form-group row"> - <label for="eventDescription" class="col-sm-2 col-form-label">Description</label> - <div class="form-group col-sm-10"> +<div class="form-group"> + <label for="eventDescription" >Description</label> + <div class="form-group "> <textarea class="form-control expand" id="eventDescription" name="eventDescription" placeholder="You can always edit it later." x-model="data.eventDescription" ></textarea> <small class="form-text"><a href="https://commonmark.org/help/">Markdown</a> formatting supported.</small> </div> </div> -<div class="form-group row"> - <label for="eventURL" class="col-sm-2 col-form-label">Link</label> - <div class="form-group col-sm-10"> +<div class="form-group"> + <label for="eventURL" >Link</label> + <div class="form-group "> <input type="url" class="form-control" id="eventURL" name="eventURL" placeholder="For tickets or another event page (optional)." x-model="data.eventURL" > </div> </div> -<div class="form-group row"> - <label for="eventImage" class="col-sm-2 col-form-label">Cover image</label> - <div class="form-group col-sm-10"> - <div class="image-preview" id="image-preview"> - <label for="image-upload" id="image-label">Choose file</label> - <input type="file" name="imageUpload" id="image-upload" accept="image/jpeg,image/gif,image/png" x-ref="eventImageUpload" /> +<div class="form-group"> + <label for="eventImage" >Cover image</label> + <div class="form-group "> + <div class="image-preview" id="event-image-preview"> + <label for="image-upload" id="event-image-label">Choose file</label> + <input type="file" name="imageUpload" id="event-image-upload" accept="image/jpeg,image/gif,image/png" x-ref="eventImageUpload" /> </div> <small class="form-text">Recommended dimensions (w x h): 920px by 300px.</small> {{#if eventData.image}} @@ -54,22 +54,22 @@ {{/if}} </div> </div> -<div class="form-group row"> - <label for="hostName" class="col-sm-2 col-form-label">Host name</label> - <div class="form-group col-sm-10"> +<div class="form-group"> + <label for="hostName" >Host name</label> + <div class="form-group "> <input type="text" class="form-control" id="hostName" name="hostName" placeholder="Will be shown on the event page (optional)." x-model="data.hostName" > </div> </div> -<div class="form-group row"> - <label for="creatorEmail" class="col-sm-2 col-form-label">Your email</label> - <div class="form-group col-sm-10"> - <input type="email" class="form-control" id="creatorEmail" name="creatorEmail" placeholder="Will not be shown anywhere (optional)" x-model="data.creatorEmail" > +<div class="form-group"> + <label for="creatorEmail" >Your email</label> + <div class="form-group "> + <input type="email" class="form-control" id="creatorEmail" name="creatorEmail" placeholder="Will not be shown anywhere (optional)." x-model="data.creatorEmail" > <small class="form-text">If you provide your email, we will send your secret editing password here, and use it to notify you of updates to the event.</small> </div> </div> -<div class="form-group row"> - <div class="col-sm-2">Options</div> - <div class="col-sm-10"> +<div class="form-group"> + <label>Options</label> + <div > <div class="form-check"> <input class="form-check-input" type="checkbox" id="eventGroupCheckbox" name="eventGroupCheckbox" x-model="data.eventGroupCheckbox"> <label class="form-check-label" for="eventGroupCheckbox"> @@ -81,14 +81,14 @@ <strong>Link this event to an event group</strong> </div> <div class="card-body"> - <div class="form-group row"> + <div class="form-group"> <label for="eventGroupID" class="col-12">Event group ID</label> <div class="form-group col-12"> <input type="text" class="form-control" id="eventGroupID" name="eventGroupID" placeholder="" x-model="data.eventGroupID" > <small class="form-text">You can find this short string of characters in the event group's link, in your confirmation email, or on the event group's page.</small> </div> </div> - <div class="form-group row"> + <div class="form-group"> <label for="eventGroupEditToken" class="col-12">Event group secret editing code</label> <div class="form-group col-12"> <input type="text" class="form-control" id="eventGroupEditToken" name="eventGroupEditToken" placeholder="" x-model="data.eventGroupEditToken" > @@ -117,13 +117,13 @@ </div> </div> </div> -<div class="form-group row" id="maxAttendeesContainer" x-show="data.maxAttendeesCheckbox && data.joinCheckbox"> - <label for="maxAttendees" class="col-sm-2 col-form-label">Attendee limit</label> - <div class="form-group col-sm-10"> +<div class="form-group" id="maxAttendeesContainer" x-show="data.maxAttendeesCheckbox && data.joinCheckbox"> + <label for="maxAttendees" >Attendee limit</label> + <div class="form-group "> <input type="number" class="form-control" id="maxAttendees" name="maxAttendees" placeholder="Enter a number." x-model="data.maxAttendees" > </div> </div> -<div class="form-group row"> +<div class="form-group"> <div class="col-12"> <div class="alert alert-danger" diff --git a/views/partials/eventGroupForm.handlebars b/views/partials/eventGroupForm.handlebars new file mode 100644 index 0000000..0b18bba --- /dev/null +++ b/views/partials/eventGroupForm.handlebars @@ -0,0 +1,47 @@ +<div class="form-group"> + <label for="eventGroupName">Name</label> + <input type="text" class="form-control" id="eventGroupName" name="eventGroupName" placeholder="Make it snappy." x-model="data.eventGroupName"> +</div> +<div class="form-group"> + <label for="eventGroupDescription">Description</label> + <textarea class="form-control" id="eventGroupDescription" name="eventGroupDescription" x-model="data.eventGroupDescription">{{eventGroupData.description}}</textarea> + <small class="form-text"><a href="https://commonmark.org/help/">Markdown</a> formatting supported.</small> +</div> +<div class="form-group"> + <label for="eventGroupURL">Link</label> + <input type="url" class="form-control" id="eventGroupURL" name="eventGroupURL" placeholder="For tickets or a page with more information (optional)." x-model="data.eventGroupURL"> +</div> +<div class="form-group"> + <label for="hostName">Host or organisation name</label> + <input type="text" class="form-control" id="eventGroupHostName" name="hostName" placeholder="Will be shown on the event group page (optional)." x-model="data.hostName"> +</div> +<div class="form-group"> + <label for="creatorEmail">Your email</label> + <div class="form-group"> + <input type="email" class="form-control" id="eventGroupCreatorEmail" name="creatorEmail" placeholder="Will not be shown anywhere (optional)." x-model="data.creatorEmail"> + <small class="form-text">If you provide your email, we will send your secret editing password here, and use it to notify you of updates to the event.</small> + </div> +</div> +<div class="form-group"> + <label>Cover image</label> + <div class="image-preview" id="group-image-preview"> + <label for="eventGroupImageUpload" id="group-image-label">Choose file</label> + <input type="file" name="imageUpload" id="group-image-upload" accept="image/jpeg,image/gif,image/png" x-ref="eventGroupImageUpload"/> + </div> + <small class="form-text">Recommended dimensions (w x h): 920px by 300px.</small> +</div> +<div class="form-group"> + <div class="col-12"> + <div + class="alert alert-danger" + role="alert" + x-show="errors.length > 0" + > + <p><i class="fas fa-exclamation-triangle"></i> Please fix these errors:</p> + <ul> + <template x-for="error in errors"> + <li x-text="error.message"></li> + </template> + </ul> + </div> + </div>
\ No newline at end of file diff --git a/views/partials/neweventgroupform.handlebars b/views/partials/neweventgroupform.handlebars deleted file mode 100755 index 616b8ca..0000000 --- a/views/partials/neweventgroupform.handlebars +++ /dev/null @@ -1,66 +0,0 @@ -<h4 class="mb-2">Create an event group</h4> -<p>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.</p> -<p>Event groups do not get automatically removed like events do, but events which have been removed from {{siteName}} will of course not show up in an event group.</p> -<form id="newEventForm" action="/neweventgroup" method="post" enctype="multipart/form-data"> - <div class="form-group row"> - <label for="eventGroupName" class="col-sm-2 col-form-label">Event group name</label> - <div class="form-group col-sm-10"> - <input type="text" class="form-control" id="eventGroupName" name="eventGroupName" placeholder="Make it snappy." value="{{data.eventName}}" data-validation="required length" data-validation-length="3-120"> - </div> - </div> - <div class="form-group row"> - <label for="eventGroupDescription" class="col-sm-2 col-form-label">Description</label> - <div class="form-group col-sm-10"> - <textarea class="form-control expand" id="eventGroupDescription" name="eventGroupDescription" data-validation="required" placeholder="You can always edit it later."></textarea> - <small class="form-text"><a href="https://commonmark.org/help/">Markdown</a> formatting supported.</small> - </div> - </div> - <div class="form-group row"> - <label for="eventGroupURL" class="col-sm-2 col-form-label">Link</label> - <div class="form-group col-sm-10"> - <input type="url" class="form-control" id="eventGroupURL" name="eventGroupURL" placeholder="For tickets or a page with more information (optional)." data-validation="url" data-validation-optional="true"> - </div> - </div> - <div class="form-group row"> - <label for="eventGroupImage" class="col-sm-2 col-form-label">Cover image</label> - <div class="form-group col-sm-10"> - <div class="image-preview" id="eventGroupImagePreview"> - <label for="image-upload" id="eventGroupImageLabel">Choose file</label> - <input type="file" name="imageUpload" id="eventGroupImageUpload" /> - </div> - <small class="form-text">Recommended dimensions (w x h): 920px by 300px.</small> - </div> - </div> - <div class="form-group row"> - <label for="hostName" class="col-sm-2 col-form-label">Host or organisation name</label> - <div class="form-group col-sm-10"> - <input type="text" class="form-control" id="hostName" name="hostName" placeholder="Will be shown on the event group page (optional)." data-validation="length" data-validation-length="2-60" data-validation-optional="true"> - </div> - </div> - <div class="form-group row"> - <label for="creatorEmail" class="col-sm-2 col-form-label">Your email</label> - <div class="form-group col-sm-10"> - <input type="email" class="form-control" id="creatorEmail" name="creatorEmail" placeholder="We won't spam you <3 (optional)" data-validation="email" data-validation-optional="true"> - <small class="form-text">If you provide your email, we will send your secret editing password here, and use it to notify you of updates to the event.</small> - </div> - </div> - <div class="form-group row"> - <div class="col-sm-12 pt-3 pb-3 text-center"> - <button type="submit" class="btn btn-primary w-50">Create</button> - </div> - </div> -</form> - -<script> - $(document).ready(function() { - $.uploadPreview({ - input_field: "#eventGroupImageUpload", - preview_box: "#eventGroupImagePreview", - label_field: "#eventGroupImageLabel", - label_default: "Choose file", - label_selected: "Change file", - no_label: false - }); - autosize($('textarea')); - }); -</script> diff --git a/views/partials/sidebar.handlebars b/views/partials/sidebar.handlebars index 980e699..5d8e847 100755 --- a/views/partials/sidebar.handlebars +++ b/views/partials/sidebar.handlebars @@ -3,5 +3,5 @@ <p class="lead text-center mb-4">Nicer events</p> - <a class="btn btn-success mb-2 btn-block" href="/new"><i class="far fa-calendar-plus"></i> New event</a> + <a class="btn btn-success mb-2 btn-block" href="/new"><i class="far fa-calendar-plus"></i> New</a> </div> |