diff --git a/.meteor/packages b/.meteor/packages index a4a0d5e..405bbcf 100644 --- a/.meteor/packages +++ b/.meteor/packages @@ -6,19 +6,19 @@ meteor-base@1.4.0 # Packages every Meteor app needs to have mobile-experience@1.0.5 # Packages for a great mobile UX -mongo@1.5.0 # The database Meteor supports right now +mongo@1.6.0 # The database Meteor supports right now blaze-html-templates@1.0.4 # Compile .html files into Meteor Blaze views reactive-var@1.0.11 # Reactive variable for tracker -reactive-dict@1.2.0 # ??? +reactive-dict@1.2.1 # ??? tracker@1.2.0 # Meteor's client-side reactive programming library tomwasd:history-polyfill # Adds IE 8/9 support for HTML5 history. email@1.2.3 # Adds the Meteor/Email package for sending lost password emails -standard-minifier-css@1.4.1 # CSS minifier run for production mode -standard-minifier-js@2.3.4 # JS minifier run for production mode +standard-minifier-css@1.5.2 # CSS minifier run for production mode +standard-minifier-js@2.4.0 # JS minifier run for production mode es5-shim@4.8.0 # ECMAScript 5 compatibility for older browsers. poorvavyas:es6-shim -ecmascript@0.11.1 # Enable ECMAScript2015+ syntax in app code +ecmascript@0.12.3 # Enable ECMAScript2015+ syntax in app code #accounts-ui #accounts-base @@ -31,11 +31,11 @@ alanning:roles # Adds roles to the user mix. https://atmospherejs.com/alanni kadira:flow-router arillo:flow-router-helpers # Provides various template helpers such as {{pathFor 'templateName'}} #tomwasd:flow-router-seo -kadira:blaze-layout +kadira:blaze-layout #TODO: Remove? -shell-server@0.3.1 # ??? +shell-server@0.4.0 # ??? meteortoys:allthings -session@1.1.7 +session@1.2.0 ##browser-policy # Adds support for specifying browser level security rules related to content and what's allowed to laod on the page. check@1.3.1 # Allows for checking the structure and types of arguments passed to Meteor methods and publications. #audit-argument-checks # Used in combination with the Check package for checking the structure and types of arguments passed to Meteor methods and publications. Automatically alerts when a method or publication does not use a check() call. @@ -60,10 +60,16 @@ juliancwirko:s-alert # Client error/alert handling jcbernack:reactive-aggregate # Allows us to create a new client collection (from the server) with the contents being an aggregate of server data. Note that aggregation can only be done on the server currently as mini-mongo does not support it. ostrio:logger ostrio:loggermongo -dynamic-import@0.4.2 +dynamic-import@0.5.0 markdown@1.0.12 wcrisman:jquery-custom-scrollbar +wcrisman:server-side-seo # A custom plugin to take all HTML from the app, and place the templates in a map in a file in the private folder for use at runtime to generate search engine readable html. See code in the /packages folder for this project. stylus@=2.513.14 # This package is no longer supported, but it still works and is available. It provides support for reading .styl files on the client and converting them to CSS on the fly. The alternative would be to compile the styl files to CSS on the server ahead of time. underscore@1.0.10 aldeed:schema-index +ostrio:files +meteorhacks:picker +meteorhacks:ssr +ostrio:meteor-root +manuel:reactivearray diff --git a/.meteor/release b/.meteor/release index 4c71956..2299ae7 100644 --- a/.meteor/release +++ b/.meteor/release @@ -1 +1 @@ -METEOR@1.7.0.5 +METEOR@1.8.0.1 diff --git a/.meteor/versions b/.meteor/versions index 102064c..6d2e0ef 100644 --- a/.meteor/versions +++ b/.meteor/versions @@ -1,21 +1,21 @@ -accounts-base@1.4.2 +accounts-base@1.4.3 accounts-password@1.5.1 alanning:roles@1.2.16 -aldeed:collection2@3.0.0 +aldeed:collection2@3.0.1 aldeed:schema-index@3.0.0 aldeed:template-extension@4.1.0 allow-deny@1.1.0 arillo:flow-router-helpers@0.5.2 -autoupdate@1.4.1 -babel-compiler@7.1.1 -babel-runtime@1.2.5 +autoupdate@1.5.0 +babel-compiler@7.2.3 +babel-runtime@1.3.0 base64@1.0.11 -binary-heap@1.0.10 +binary-heap@1.0.11 blaze@2.3.3 blaze-html-templates@1.1.2 blaze-tools@1.0.10 -boilerplate-generator@1.5.0 -caching-compiler@1.1.12 +boilerplate-generator@1.6.0 +caching-compiler@1.2.1 caching-html-compiler@1.1.3 callback-hook@1.1.0 check@1.3.1 @@ -26,22 +26,24 @@ ddp-common@1.4.0 ddp-rate-limiter@1.0.7 ddp-server@2.2.0 deps@1.0.12 -diff-sequence@1.1.0 -dynamic-import@0.4.2 -ecmascript@0.11.1 +diff-sequence@1.1.1 +dynamic-import@0.5.1 +ecmascript@0.12.3 ecmascript-runtime@0.7.0 -ecmascript-runtime-client@0.7.2 +ecmascript-runtime-client@0.8.0 ecmascript-runtime-server@0.7.1 ejson@1.1.0 email@1.2.3 es5-shim@4.8.0 +fetch@0.1.0 fortawesome:fontawesome@4.7.0 geojson-utils@1.0.10 hot-code-push@1.0.4 html-tools@1.0.11 htmljs@1.0.11 -http@1.4.1 +http@1.4.2 id-map@1.1.0 +inter-process-messaging@0.1.0 jcbernack:reactive-aggregate@1.0.0 jquery@1.11.11 juliancwirko:s-alert@3.2.0 @@ -51,9 +53,12 @@ launch-screen@1.1.1 livedata@1.0.18 localstorage@1.2.0 logging@1.1.20 +manuel:reactivearray@1.0.9 markdown@1.0.12 meteor@1.9.2 meteor-base@1.4.0 +meteorhacks:picker@1.0.3 +meteorhacks:ssr@2.2.0 meteortoys:allthings@7.0.1 meteortoys:authenticate@4.0.0 meteortoys:autopub@4.0.0 @@ -71,27 +76,31 @@ meteortoys:sub@4.0.0 meteortoys:throttle@4.0.0 meteortoys:toggle@4.0.0 meteortoys:toykit@4.0.2 -minifier-css@1.3.1 -minifier-js@2.3.5 -minimongo@1.4.4 +minifier-css@1.4.1 +minifier-js@2.4.0 +minimongo@1.4.5 mizzao:bootboxjs@4.4.0 mobile-experience@1.0.5 mobile-status-bar@1.0.14 -modern-browsers@0.1.2 -modules@0.12.2 -modules-runtime@0.10.2 +modern-browsers@0.1.3 +modules@0.13.0 +modules-runtime@0.10.3 momentjs:moment@2.22.2 -mongo@1.5.1 +mongo@1.6.0 +mongo-decimal@0.1.0 mongo-dev-server@1.1.0 mongo-id@1.0.7 msavin:jetsetter@4.0.0 msavin:mongol@7.0.1 npm-bcrypt@0.9.3 -npm-mongo@3.0.11 +npm-mongo@3.1.1 observe-sequence@1.0.16 ordered-dict@1.1.0 +ostrio:cookies@2.3.0 +ostrio:files@1.10.2 ostrio:logger@2.0.7 ostrio:loggermongo@2.0.4 +ostrio:meteor-root@1.0.7 poorvavyas:es6-shim@0.21.1 promise@0.11.1 raix:eventemitter@0.1.3 @@ -101,18 +110,18 @@ reactive-dict@1.2.1 reactive-var@1.0.11 reload@1.2.0 retry@1.1.0 -routepolicy@1.0.13 +routepolicy@1.1.0 service-configuration@1.0.11 -session@1.1.8 +session@1.2.0 sha@1.0.9 -shell-server@0.3.1 +shell-server@0.4.0 socket-stream-client@0.2.2 softwarerero:accounts-t9n@1.3.11 spacebars@1.0.15 spacebars-compiler@1.1.3 srp@1.0.12 -standard-minifier-css@1.4.1 -standard-minifier-js@2.3.4 +standard-minifier-css@1.5.2 +standard-minifier-js@2.4.0 stylus@2.513.14 templating@1.3.2 templating-compiler@1.3.3 @@ -128,6 +137,7 @@ useraccounts:core@1.14.2 useraccounts:flow-routing@1.14.2 useraccounts:unstyled@1.14.2 wcrisman:jquery-custom-scrollbar@3.0.0 -webapp@1.6.2 +wcrisman:server-side-seo@1.0.0 +webapp@1.7.2 webapp-hashing@1.0.9 zimme:active-route@2.3.2 diff --git a/client/client.js b/client/client.js index e402767..a8e94c6 100644 --- a/client/client.js +++ b/client/client.js @@ -3,7 +3,6 @@ import '/imports/startup/client'; import '/imports/startup/both'; import '/imports/api'; import '/imports/ui/helpers.js'; -// import '/imports/util/normalize.css'; import '/imports/util/validator.js'; import '/imports/util/polyfills/blaze.js'; import '/imports/util/polyfills/regex.js'; @@ -22,17 +21,29 @@ import '/imports/util/select2/select2.full.js'; import 'sweetalert2/dist/sweetalert2.min.css'; import '/imports/util/simplegrid.css'; import 'dragula/dist/dragula.css'; -//import 'malihu-custom-scrollbar-plugin/jquery.mCustomScrollbar.css'; - +//Import all the tinyMCE code, including all the plugins we are using. If one plugin is forgotten it may result in a unexpected '>' error in the console, or it may just not function at all. import '/imports/util/tinymce/tinymce.min.js'; import '/imports/util/tinymce/jquery.tinymce.min.js'; import '/imports/util/tinymce/themes/modern/theme.min.js'; +import '/imports/util/tinymce/themes/inlite/theme.min.js'; +import '/imports/util/tinymce/plugins/autolink/plugin.min.js'; +import '/imports/util/tinymce/plugins/contextmenu/plugin.min.js'; +import '/imports/util/tinymce/plugins/link/plugin.min.js'; +import '/imports/util/tinymce/plugins/lists/plugin.min.js'; +import '/imports/util/tinymce/plugins/table/plugin.min.js'; +import '/imports/util/tinymce/plugins/textcolor/plugin.min.js'; +//Replaces the standard toDataURL() function in canvas to support PNG compression. +import 'canvas-png-compression/dist/bundle.js'; +//Set some tinyMCE basic settings specific to our Meteor project structure. tinymce.baseURL='/tinymce'; tinymce.suddix='.min'; Blaze._allowJavascriptUrls(); +//Turn on the canvas toDataURL() conversion to allow PNG compression. +CanvasPngCompression.replaceToDataURL(); + Meteor.startup(function () { sAlert.config({ diff --git a/client/main.styl b/client/main.styl index ef21330..c1a19de 100644 --- a/client/main.styl +++ b/client/main.styl @@ -44,9 +44,38 @@ h3 .textView border: 1px solid #DDD - padding: 6px 20px + padding: 10px + font-family: Palatino, "Palatino Linotype", "Palatino LT STD", "Book Antiqua", Georgia, serif + font-size: 16px + line-height: 27.4286px + max-width: 960px + ul + list-style-type: disc + margin-block-end: 14px + margin-block-start: 14px + margin-inline-end: 0 + margin-inline-start: 0 + padding-inline-start: 40px + li + font-family: Palatino, "Palatino Linotype", "Palatino LT STD", "Book Antiqua", Georgia, serif + font-size: 16px + line-height: 27.4286px + ol + list-style-type: decimal + margin-block-end: 14px + margin-block-start: 14px + margin-inline-end: 0 + margin-inline-start: 0 + padding-inline-start: 40px + li + font-family: Palatino, "Palatino Linotype", "Palatino LT STD", "Book Antiqua", Georgia, serif + font-size: 16px + line-height: 27.4286px + padding-left: 8px //Standard Stylings +.hidden + display: none .noselect -webkit-touch-callout: none; /* iOS Safari */ -webkit-user-select: none; /* Chrome/Safari/Opera */ @@ -152,6 +181,7 @@ h3 @import "../imports/ui/styles/maxHeightLayout.import.styl" @import "../imports/ui/styles/tabs.import.styl" @import "../imports/ui/styles/forms.import.styl" +@import "../imports/ui/styles/modal.import.styl" @import "../imports/util/de.combo.import.styl" @import "../imports/util/bootstrap-like-btn.import.styl" @@ -162,14 +192,12 @@ h3 @import "../imports/ui/Home.import.styl" -@import "../imports/ui/InternshipJobs.import.styl" @import "../imports/ui/Admin/UserManagement.import.styl" -@import "../imports/ui/Admin/AppreciationEditor.import.styl" -@import "../imports/ui/Admin/DatesEditor.import.styl" -@import "../imports/ui/Admin/InternshipsEditor.import.styl" -@import "../imports/ui/Admin/NewsEditor.import.styl" -@import "../imports/ui/Admin/BoardEditor.import.styl" +@import "../imports/ui/Admin/InternshipEditor.import.styl" +@import "../imports/ui/Admin/PageEditor.import.styl" +@import "../imports/ui/Admin/SlideshowEditor.import.styl" +@import "../imports/ui/InternshipJobs.import.styl" @import "../imports/ui/Grants.import.styl" @import "../imports/ui/Internships.import.styl" @import "../imports/ui/Scholarships.import.styl" @@ -184,3 +212,7 @@ h3 @import "../imports/ui/Contact.import.styl" @import "../imports/ui/Programs.import.styl" @import "../imports/ui/ImportantDates.import.styl" + +@import "../imports/ui/EditablePage.import.styl" + +@import "../imports/ui/dialogs/SelectImageDialog.import.styl" diff --git a/imports/api/Images.js b/imports/api/Images.js new file mode 100644 index 0000000..6e6d77a --- /dev/null +++ b/imports/api/Images.js @@ -0,0 +1,11 @@ +import { Meteor } from 'meteor/meteor'; +import { Mongo } from 'meteor/mongo'; +import { check } from 'meteor/check'; +import SimpleSchema from 'simpl-schema'; +import {FilesCollection} from 'meteor/ostrio:files'; + +const ImageUploads = new FilesCollection({ + storagePath: 'public/uploads' +}); + +export default ImageUploads; diff --git a/imports/api/Internship.js b/imports/api/Internship.js new file mode 100644 index 0000000..0e1435b --- /dev/null +++ b/imports/api/Internship.js @@ -0,0 +1,74 @@ +import { Meteor } from 'meteor/meteor'; +import { Mongo } from 'meteor/mongo'; +import { check } from 'meteor/check'; +import SimpleSchema from 'simpl-schema'; + +let Internship = new Mongo.Collection('Internship'); + +Internship.attachSchema(new SimpleSchema({ + name: { + type: String, + label: "Name", + optional: false, + trim: true, + index: 1, + unique: true + }, + content: { + type: String, + label: "Content", + optional: false, + trim: false + }, + createdAt: { + type: Date, + label: "Created On", + optional: true + }, + updatedAt: { + type: Date, + label: "Updated On", + optional: true + } +})); + +if(Meteor.isServer) Meteor.publish('Internship', function() { + return Internship.find({}); +}); + +if(Meteor.isServer) { + Meteor.methods({ + addInternship: function(name, content) { + check(name, String); + check(content, String); + if(Roles.userIsInRole(this.userId, [Meteor.UserRoles.ROLE_UPDATE])) { + const createdAt = new Date(); + const updatedAt = createdAt; + + //Returns the id of the object created. + return Internship.insert({name, content, createdAt, updatedAt}); + } + else throw new Meteor.Error(403, "Not authorized."); + }, + removeInternship: function(_id) { + check(_id, String); + + if(Roles.userIsInRole(this.userId, [Meteor.UserRoles.ROLE_UPDATE])) { + Internship.remove(_id); + } + else throw new Meteor.Error(403, "Not authorized."); + }, + updateInternship: function(_id, content) { + check(_id, String); + check(content, String); + if(Roles.userIsInRole(this.userId, [Meteor.UserRoles.ROLE_UPDATE])) { + const updatedAt = new Date(); + + return Internship.update(_id, {$set: {content, updatedAt}}); + } + else throw new Meteor.Error(403, "Not authorized."); + } + }); +} + +export default Internship; diff --git a/imports/api/Slideshow.js b/imports/api/Slideshow.js new file mode 100644 index 0000000..ea00a21 --- /dev/null +++ b/imports/api/Slideshow.js @@ -0,0 +1,137 @@ +import { Meteor } from 'meteor/meteor'; +import { Mongo } from 'meteor/mongo'; +import { check } from 'meteor/check'; +import SimpleSchema from 'simpl-schema'; + +let Slideshow = new Mongo.Collection('Slideshow'); +let SlideshowImage = new Mongo.Collection("SlideshowImage"); + +Slideshow.attachSchema(new SimpleSchema({ + name: { + type: String, + label: "Name", + optional: false, + trim: true, + index: 1, + unique: true + }, + images: { //A JSON array of image URL's. + type: Array, + label: "Images", + optional: false, + defaultValue: [] + }, + 'images.$': { + type: String + }, + createdAt: { + type: Date, + label: "Created On", + optional: true + }, + updatedAt: { + type: Date, + label: "Updated On", + optional: true + } +})); + +SlideshowImage.attachSchema(new SimpleSchema({ + image: { + type: String, + label: "Image", + optional: false, + trim: false + } +})); + +if(Meteor.isServer) Meteor.publish('slideshow', function() { + return Slideshow.find({}); +}); + +if(Meteor.isServer) { + WebApp.connectHandlers.use("/slideshow-image/", (req, res, next) => { + try { + let id = req.url.substr(1); + + let slide = SlideshowImage.findOne(id); + + if(slide) { + let i = slide.image.indexOf(";base64,"); + let header = slide.image.substr(5, i); + let content = slide.image.substr(i + 8); + let buffer = new Buffer(content, 'base64'); + + res.writeHead(200, {'Content-Type': header}); + res.write(buffer); + res.end(); + } + else { + //TODO: Write out a bad image picture. + } + } catch(err) { + console.log(err); + res.end(); + } + }); + + Meteor.methods({ + addSlideshowImage: function(slideshowId, image) { + check(slideshowId, String); + check(image, String); + + if(Roles.userIsInRole(this.userId, [Meteor.UserRoles.ROLE_UPDATE])) { + let id = SlideshowImage.insert({image}); + + if(id) { + Slideshow.update(slideshowId, {$push: {images: id}}); + } + else { + throw new Meteor.Error(400, "Image storage failed."); + } + } + else throw new Meteor.Error(403, "Not authorized."); + }, + removeSlideshowImage: function(slideshowId, imageId) { + check(slideshowId, String); + check(imageId, String); + + //Note: there is currently no way in mongo to remove an element at an index. + Slideshow.update(slideshowId, {$pull: {images: imageId}}); + }, + swapSlideshowImages: function(slideshowId, firstIndex, secondIndex) { + check(slideshowId, String); + check(firstIndex, Number); + check(secondIndex, Number); + + let slideshow = Slideshow.findOne(slideshowId); + let temp = slideshow.images[firstIndex]; + + slideshow.images[firstIndex] = slideshow.images[secondIndex]; + slideshow.images[secondIndex] = temp; + + Slideshow.update(slideshowId, {$set: {images: slideshow.images}}); + }, + addSlideshow: function(name) { + check(name, String); + if(Roles.userIsInRole(this.userId, [Meteor.UserRoles.ROLE_UPDATE])) { + const createdAt = new Date(); + const updatedAt = createdAt; + const images = []; + + //Returns the id of the object created. + return Slideshow.insert({name, images, createdAt, updatedAt}); + } + else throw new Meteor.Error(403, "Not authorized."); + }, + removeSlideshow: function(_id) { + check(_id, String); + if(Roles.userIsInRole(this.userId, [Meteor.UserRoles.ROLE_UPDATE])) { + Slideshow.remove({_id}); + } + else throw new Meteor.Error(403, "Not authorized."); + } + }); +} + +export default Slideshow; diff --git a/imports/api/index.js b/imports/api/index.js index d94247c..a7d62e5 100644 --- a/imports/api/index.js +++ b/imports/api/index.js @@ -2,10 +2,13 @@ import Users from "./User.js"; import UserRoles from "./Roles.js"; import Pages from "./Page.js"; +import ImageUploads from "./Images.js"; +import Slideshow from "./Slideshow.js"; +import Internship from "./Internship.js"; import ContactUsMessages from "./ContactUsMessages.js"; //Save the collections in the Meteor.collections property for easy access without name conflicts. -Meteor.collections = {Users, UserRoles, Pages, ContactUsMessages}; +Meteor.collections = {Users, UserRoles, Pages, Slideshow, Internship, ContactUsMessages, ImageUploads}; //If this is the server then setup the default admin user if none exist. if(Meteor.isServer) { diff --git a/imports/startup/both/accounts.js b/imports/startup/both/accounts.js index e64837c..49f82c9 100644 --- a/imports/startup/both/accounts.js +++ b/imports/startup/both/accounts.js @@ -11,7 +11,7 @@ AccountsTemplates.configure({ // defaultLayout: 'Body', // defaultContentRegion: 'content', // defaultLayoutRegions: {} - homeRoutePath: '/Admin/InternshipJobs', + homeRoutePath: '/Admin/InternshipJobs', //The path where the user is taken after logging in successfully. texts: { title: { signIn: "" diff --git a/imports/startup/client/routes.js b/imports/startup/client/routes.js index 2731f01..557dc73 100644 --- a/imports/startup/client/routes.js +++ b/imports/startup/client/routes.js @@ -31,43 +31,57 @@ AccountsTemplates.configureRoute('forgotPwd', { pri.route("/Admin/Internships", { name: "InternshipsEditor", action: function(params, queryParams) { - require("/imports/ui/Admin/InternshipsEditor.js"); - BlazeLayout.render("Admin", {content: "InternshipsEditor"}) + require("/imports/ui/Admin/InternshipEditor.js"); + BlazeLayout.render("Admin", {content: "InternshipEditor"}); } }); pri.route("/Admin/Board", { name: "BoardEditor", action: function(params, queryParams) { - require("/imports/ui/Admin/BoardEditor.js"); - BlazeLayout.render("Admin", {content: "BoardEditor"}) + require("/imports/ui/Admin/PageEditor.js"); + BlazeLayout.render("Admin", {content: "PageEditor"}); } }); pri.route("/Admin/Dates", { name: "DatesEditor", action: function(params, queryParams) { - require("/imports/ui/Admin/DatesEditor.js"); - BlazeLayout.render("Admin", {content: "DatesEditor"}) + require("/imports/ui/Admin/PageEditor.js"); + BlazeLayout.render("Admin", {content: "PageEditor"}); } }); pri.route("/Admin/UserManagement", { name: "UserManagement", action: function(params, queryParams) { require("/imports/ui/Admin/UserManagement.js"); - BlazeLayout.render("Admin", {content: "UserManagement"}) + BlazeLayout.render("Admin", {content: "UserManagement"}); } }); pri.route("/Admin/Appreciation", { name: "AppreciationEditor", action: function(params, queryParams) { - require("/imports/ui/Admin/AppreciationEditor.js"); - BlazeLayout.render("Admin", {content: "AppreciationEditor"}) + require("/imports/ui/Admin/PageEditor.js"); + BlazeLayout.render("Admin", {content: "PageEditor"}); } }); pri.route("/Admin/NewsEditor", { name: "NewsEditor", action: function(params, queryParams) { - require("/imports/ui/Admin/NewsEditor.js"); - BlazeLayout.render("Admin", {content: "NewsEditor"}) + require("/imports/ui/Admin/PageEditor.js"); + BlazeLayout.render("Admin", {content: "PageEditor"}); + } +}); +pri.route("/Admin/SlideshowEditor", { + name: "SlideshowEditor", + action: function(params, queryParams) { + require("/imports/ui/Admin/SlideshowEditor.js"); + BlazeLayout.render("Admin", {content: "SlideshowEditor"}); + } +}); +pri.route("/Admin/SlideshowPageEditor", { + name: "SlideshowPageEditor", + action: function(params, queryParams) { + require("/imports/ui/Admin/PageEditor.js"); + BlazeLayout.render("Admin", {content: "PageEditor"}); } }); @@ -85,8 +99,8 @@ pub.route("/Home", { pub.route("/ImportantDates", { name: 'ImportantDates', action: function(params, queryParams) { - require("/imports/ui/ImportantDates.js"); - BlazeLayout.render("Public", {content: "ImportantDates"}); + require("/imports/ui/EditablePage.js"); + BlazeLayout.render("Public", {content: "EditablePage"}); } }); pub.route("/Support", { @@ -131,6 +145,20 @@ pub.route("/Internships", { BlazeLayout.render("Public", {content: "Internships"}); } }); +pub.route("/Internship-Job-List", { + name: 'Internships Job List', + action: function(params, queryParams) { + require("/imports/ui/InternshipJobs.js"); + BlazeLayout.render("Public", {content: "InternshipJobs"}); + } +}); +pub.route("/Internship-Job/:_id", { + name: 'Internship', + action: function(params, queryParams) { + require("/imports/ui/InternshipJobs.js"); + BlazeLayout.render("Public", {content: "InternshipJob"}); + } +}); pub.route("/Scholarships", { name: 'Scholarships', action: function(params, queryParams) { @@ -145,11 +173,11 @@ pub.route("/Fellowships", { BlazeLayout.render("Public", {content: "Fellowships"}); } }); -pub.route("/News&Notices", { - name: 'News&Notices', +pub.route("/News", { + name: 'News', action: function(params, queryParams) { - require("/imports/ui/News&Notices.js"); - BlazeLayout.render("Public", {content: "NewsNotices"}); + require("/imports/ui/EditablePage.js"); + BlazeLayout.render("Public", {content: "EditablePage"}); } }); pub.route("/PhotoGallery", { @@ -162,8 +190,8 @@ pub.route("/PhotoGallery", { pub.route("/Appreciation", { name: 'Appreciation', action: function(params, queryParams) { - require("/imports/ui/Appreciation.js"); - BlazeLayout.render("Public", {content: "Appreciation"}); + require("/imports/ui/EditablePage.js"); + BlazeLayout.render("Public", {content: "EditablePage"}); } }); pub.route("/HowCanYouHelp", { @@ -176,7 +204,7 @@ pub.route("/HowCanYouHelp", { pub.route("/CurrentBoard", { name: 'CurrentBoard', action: function(params, queryParams) { - require("/imports/ui/CurrentBoard.js"); - BlazeLayout.render("Public", {content: "CurrentBoard"}); + require("/imports/ui/EditablePage.js"); + BlazeLayout.render("Public", {content: "EditablePage"}); } -}); +}); \ No newline at end of file diff --git a/imports/ui/Admin/AppreciationEditor.html b/imports/ui/Admin/AppreciationEditor.html deleted file mode 100644 index 2cee5f1..0000000 --- a/imports/ui/Admin/AppreciationEditor.html +++ /dev/null @@ -1,7 +0,0 @@ - \ No newline at end of file diff --git a/imports/ui/Admin/AppreciationEditor.import.styl b/imports/ui/Admin/AppreciationEditor.import.styl deleted file mode 100644 index 9e60960..0000000 --- a/imports/ui/Admin/AppreciationEditor.import.styl +++ /dev/null @@ -1,6 +0,0 @@ -#appreciationEditor - display: block - .ck.ck-editor__editable_inline - border-color: rgba(0,0,0,.2) - .ck.ck-editor__editable_inline.ck-focused - border-color: rgba(0,0,0,1) \ No newline at end of file diff --git a/imports/ui/Admin/AppreciationEditor.js b/imports/ui/Admin/AppreciationEditor.js deleted file mode 100644 index 4e4bced..0000000 --- a/imports/ui/Admin/AppreciationEditor.js +++ /dev/null @@ -1,87 +0,0 @@ -import './AppreciationEditor.html'; -import swal from "sweetalert2"; - -let originalData = ""; - -Tracker.autorun(function() { - Meteor.subscribe("pages"); -}); - -Template.AppreciationEditor.onRendered(function() { - let _this = this; - - //#appreciationEditor' - // CKEditor.create(document.querySelector('#editor'), {}).then(editor => { - // _this.ckEditor = editor; - // - // Tracker.autorun(function() { - // let doc = Meteor.collections.Pages.findOne({name: 'Appreciation'}); - // - // originalData = (doc === undefined ? "" : doc.html); - // editor.setData(originalData); - // }); - // }).catch(err => { - // console.error(err); - // }); - $('.editor').tinymce({ - inline: true - }); - Tracker.autorun(function() { - let doc = Meteor.collections.Pages.findOne({name: 'Appreciation'}); - - originalData = (doc === undefined ? "" : doc.html); - $('.editor').html(originalData); - }); -}); - -Template.AppreciationEditor.onDestroyed(function() { - // let data = this.ckEditor.getData(); - let data = $('.editor').html(); - - if(data != originalData) { - swal({ - title: "Save Changes", - text: "Would you like to save any changes you have made to this sheet?", - type: "question", - showCancelButton: true, - confirmButtonColor: "#7cdd7f", - confirmButtonText: "Yes", - cancelButtonText: "No" - }).then( - function(isConfirm) { - if(isConfirm) { - Meteor.call('updatePage', 'Appreciation', data, function (error, result) { - if (error) sAlert.error(error); - else sAlert.success("Content Saved Successfully"); - }); - } - }, - function(dismiss) {} - ); - } -}); - -Template.AppreciationEditor.helpers({ - // html: function() { - // let doc = Meteor.collections.Pages.findOne({name: 'Appreciation'}); - // - // return doc == undefined ? "" : doc.html; - // } -}); - -Template.AppreciationEditor.events({ - 'click #save': function(event, template) { - // let data = template.ckEditor.getData(); - let data = $('.editor').html(); - - if(data != originalData) { - Meteor.call('updatePage', 'Appreciation', data, function (error, result) { - if (error) sAlert.error(error); - else sAlert.success("Content Saved Successfully"); - }); - } - else { - sAlert.success("Data has not changed!"); - } - } -}); diff --git a/imports/ui/Admin/BoardEditor.html b/imports/ui/Admin/BoardEditor.html deleted file mode 100644 index daad986..0000000 --- a/imports/ui/Admin/BoardEditor.html +++ /dev/null @@ -1,7 +0,0 @@ - \ No newline at end of file diff --git a/imports/ui/Admin/BoardEditor.js b/imports/ui/Admin/BoardEditor.js deleted file mode 100644 index cf87a4a..0000000 --- a/imports/ui/Admin/BoardEditor.js +++ /dev/null @@ -1,68 +0,0 @@ - -import './BoardEditor.html'; -import swal from "sweetalert2"; - -let originalData = ""; - -Tracker.autorun(function() { - Meteor.subscribe("pages"); -}); - -Template.BoardEditor.onRendered(function() { - let _this = this; - - CKEditor.create(document.querySelector('#editor'), {}).then(editor => { - _this.ckEditor = editor; - - Tracker.autorun(function() { - let doc = Meteor.collections.Pages.findOne({name: 'Board'}); - - originalData = (doc === undefined ? "" : doc.html); - editor.setData(originalData); - }); - }).catch(err => { - console.error(err); - }); -}); - -Template.BoardEditor.onDestroyed(function() { - let data = this.ckEditor.getData(); - - if(data != originalData) { - swal({ - title: "Save Changes", - text: "Would you like to save any changes you have made to this sheet?", - type: "question", - showCancelButton: true, - confirmButtonColor: "#7cdd7f", - confirmButtonText: "Yes", - cancelButtonText: "No" - }).then( - function(isConfirm) { - if(isConfirm) { - Meteor.call('updatePage', 'Board', data, function (error, result) { - if (error) sAlert.error(error); - else sAlert.success("Content Saved Successfully"); - }); - } - }, - function(dismiss) {} - ); - } -}); - -Template.BoardEditor.events({ - 'click #save': function(event, template) { - let data = template.ckEditor.getData(); - - if(data != originalData) { - Meteor.call('updatePage', 'Board', data, function (error, result) { - if (error) sAlert.error(error); - else sAlert.success("Content Saved Successfully"); - }); - } - else { - sAlert.success("Data has not changed!"); - } - } -}); diff --git a/imports/ui/Admin/DatesEditor.html b/imports/ui/Admin/DatesEditor.html deleted file mode 100644 index d516866..0000000 --- a/imports/ui/Admin/DatesEditor.html +++ /dev/null @@ -1,7 +0,0 @@ - \ No newline at end of file diff --git a/imports/ui/Admin/DatesEditor.import.styl b/imports/ui/Admin/DatesEditor.import.styl deleted file mode 100644 index cbacbbc..0000000 --- a/imports/ui/Admin/DatesEditor.import.styl +++ /dev/null @@ -1,6 +0,0 @@ -#datesEditor - display: block - .ck.ck-editor__editable_inline - border-color: rgba(0,0,0,.2) - .ck.ck-editor__editable_inline.ck-focused - border-color: rgba(0,0,0,1) \ No newline at end of file diff --git a/imports/ui/Admin/DatesEditor.js b/imports/ui/Admin/DatesEditor.js deleted file mode 100644 index 8e8a941..0000000 --- a/imports/ui/Admin/DatesEditor.js +++ /dev/null @@ -1,69 +0,0 @@ - -import './DatesEditor.html'; -import swal from "sweetalert2"; - -let originalData = ""; - -Tracker.autorun(function() { - Meteor.subscribe("pages"); -}); - -Template.DatesEditor.onRendered(function() { - let _this = this; - - CKEditor.create(document.querySelector('#editor'), {}).then(editor => { - _this.ckEditor = editor; - - Tracker.autorun(function() { - let doc = Meteor.collections.Pages.findOne({name: 'Dates'}); - - originalData = (doc === undefined ? "" : doc.html); - editor.setData(originalData); - }); - }).catch(err => { - console.error(err); - }); -}); - -Template.DatesEditor.onDestroyed(function() { - let data = this.ckEditor.getData(); - - if(data != originalData) { - swal({ - title: "Save Changes", - text: "Would you like to save any changes you have made to this sheet?", - type: "question", - showCancelButton: true, - confirmButtonColor: "#7cdd7f", - confirmButtonText: "Yes", - cancelButtonText: "No" - }).then( - function(isConfirm) { - if(isConfirm) { - Meteor.call('updatePage', 'Dates', data, function (error, result) { - if (error) sAlert.error(error); - else sAlert.success("Content Saved Successfully"); - }); - } - }, - function(dismiss) {} - ); - } -}); - -Template.DatesEditor.events({ - 'click #save': function(event, template) { - let data = template.ckEditor.getData(); - - if(data != originalData) { - Meteor.call('updatePage', 'Dates', data, function (error, result) { - if (error) sAlert.error(error); - else sAlert.success("Content Saved Successfully"); - }); - } - else { - sAlert.success("Data has not changed!"); - } - } -}); -s \ No newline at end of file diff --git a/imports/ui/Admin/InternshipEditor.html b/imports/ui/Admin/InternshipEditor.html new file mode 100644 index 0000000..0b4f167 --- /dev/null +++ b/imports/ui/Admin/InternshipEditor.html @@ -0,0 +1,33 @@ + + + \ No newline at end of file diff --git a/imports/ui/Admin/InternshipEditor.import.styl b/imports/ui/Admin/InternshipEditor.import.styl new file mode 100644 index 0000000..ce222f8 --- /dev/null +++ b/imports/ui/Admin/InternshipEditor.import.styl @@ -0,0 +1,118 @@ +#internshipEditor + display: block + .internshipNavigation + float: left + clear: none + display: inline-block + min-width: 200px + width: 20% + padding-right: 20px + position: relative + padding-top: 50px + + .addInternship + margin-bottom: 10px + position: absolute + left: 0 + top: 0 + right: 60px + height: 40px + input[name="newInternshipName"] + display: inline-block + width: 0 + transition: all .75s ease + border: 0 + opacity: 0 + font-size: 1.2em + input[name="newInternshipName"].show + opacity: 1 + border: 1px solid #ccc + border-radius: 4px + width: 70% + transform: translateX(4px) + .createInternship + display: inline-block + position: relative + top: 2px + padding: 2px 6px + margin: 0 4px + //width: 33px + text-align: center + font-size: 1.5em + line-height: 1.5em + border-radius: 8px + //border: 1px solid rgba(0, 0, 0, 0) + box-sizing: border-box + .createInternship:hover + //border: 1px inset #b100d1 + //-webkit-box-shadow: inset 0px 0px 20px 0px #de7cff + //-moz-box-shadow: inset 0px 0px 20px 0px #de7cff + //box-shadow: inset 0px 0px 20px 0px #de7cff + color: #5c8744 + .createInternship + transform: translateX(-25px) rotate(0deg) + transition: all .75s ease + .createInternship.move + transform: translateX(6px) rotate(720deg) + .editPageText + position: absolute + right: 0 + top: 0 + width: 60px + font-size: 14px + text-align: center + line-height: 14px + font-weight: 800 + border: 1.5px solid gray + border-radius: 6px + padding: 6px + margin-right: 5px + .editPageText:hover + background-color: #c6c6c6 + .editPageText:active + border-color: black + + .internshipListItem + display: block + background: white + padding: 2px 0 + white-space: nowrap + position: relative + height: 24px + .name + position: absolute + left: 0 + top: 0 + bottom: 0 + right: 24px + overflow: hidden + .deleteInternship + position: absolute + right: 0 + top: 0 + bottom: 0 + width: 24px + padding-top: 0px + padding-left: 2px + color: #7c0000 + line-height: 24px + font-size: 18px + .deleteInternship:hover + font-size: 22px + top: 0 + right: 2px + color: #ab0000 + .deleteInternship:active + color: #613434 + .internshipListItem:hover + background: #e2ddc0 + .internshipListItem:active + background: rgba(254, 255, 0, 0.56) + .internshipListItem.selected + background: rgba(254, 255, 0, 0.56) + + .internshipHtml + display: inline-block + max-width: 80% + min-width: 200px + width: 100% \ No newline at end of file diff --git a/imports/ui/Admin/InternshipEditor.js b/imports/ui/Admin/InternshipEditor.js new file mode 100644 index 0000000..37e44db --- /dev/null +++ b/imports/ui/Admin/InternshipEditor.js @@ -0,0 +1,228 @@ + +import './InternshipEditor.html'; +import '/imports/ui/dialogs/SelectImageDialog.js'; +import swal from "sweetalert2"; + +const PREFIX = "InternshipEditor_"; + +Tracker.autorun(function() { + Meteor.subscribe("Internship"); +}); + +Template.InternshipEditor.onCreated(function() { + this.internships = Meteor.collections.Internship.find({}, {sort: {name: 1}}); + Session.set(PREFIX + 'selectedInternship', null); +}); +Template.InternshipEditor.helpers({ + internships: function() { + return Template.instance().internships; + }, + selectedInternship: function() { + return Session.get(PREFIX + "selectedInternship"); + } +}); +Template.InternshipEditor.events({ + 'click .deleteInternship': function(event, template) { + let $li = template.$(event.target).parent(); + + Meteor.call('removeInternship', $li.data('id'), function(error, id) { + if(error) sAlert.error("Failed to create the internship!\n" + error); + }); + }, + 'click .internshipListItem': function(event, template) { + let $li = template.$(event.currentTarget); + + $li.addClass('selected'); + $li.siblings().removeClass('selected'); + Session.set(PREFIX + "selectedInternship", Meteor.collections.Internship.findOne($li.data('id'))); + }, + 'click .editPageText': function(event,template) { + + }, + "keyup input[name='newInternshipName']" : function(event, template) { + if(event.keyCode === 13) { + event.preventDefault(); + $('.createInternship').trigger('click'); + } + }, + "click .createInternship": function(event, template) { + let $input = template.$('input[name="newInternshipName"]'); + + if($input.hasClass('show')) { + let name = $input.val(); + + name = name ? name.trim() : undefined; + name = name && name.length > 0 ? name : undefined; + + if(name) { + let content = "

Job Title: 

\n

Supervisor / Sponsor: 

\n

Location of Internship: 

\n

Dates & hours: 

\n

Duties & Activities: 

\n

Desirable Qualities / Skills: 

"; + //Meteor.collections.Internship.insert({name, content}); + Meteor.call('addInternship', name, content, function(error, id) { + if(error) sAlert.error("Failed to create the internship!\n" + error); + else { + //Clear the text editor. + $input.val(""); + + //Quick hack to attempt to allow the internship we created to be added to the list before we try to select it and edit it. + let count = 0; + let interval = setInterval(function() { + let selected = Meteor.collections.Internship.findOne(id); + + if(selected) { + //Select the sheet in the drop down. + let $li = template.$('ul.internshipList li[data-id="' + id + '"]'); + + $li.addClass("selected"); + $li.siblings().removeClass("selected"); + Session.set(PREFIX + "selectedInternship", selected); + clearInterval(interval); + } + else count++; + + //Avoid infinite loop that should never happen. + if(count > 100) clearInterval(interval); + }, 100); + } + }); + } + + $input.removeClass('show'); + $(event.target).toggleClass('move'); + } + else { + $input.addClass('show'); + $(event.target).toggleClass('move'); + $input.focus(); + } + } +}); + +Template.InternshipHtmlEditor.onCreated(function() { + let template = this; + + this.showSelectImageDialog = new ReactiveVar(false); + + this.saveChanges = function(ask) { + let data = this.$('.editor').val(); + let template = this; + + //Only ask the user if they want to update their changes if they actually have changes that have not yet been saved. + if(data !== template.currentHtml) { + const changedData = data; + + //Ensure this does not get run twice. + template.currentHtml = changedData; + + if(ask) { + //Ask the user if they want to update the repository with their changes. + swal({ + title: "Save Changes", + text: "Would you like to save any changes you have made to this page?", + type: "question", + showCancelButton: true, + confirmButtonColor: "#7cdd7f", + confirmButtonText: "Yes", + cancelButtonText: "No" + }).then( + function(isConfirm) { + if(isConfirm) { + Meteor.call('updateInternship', template.data.internship._id, changedData, function (error, result) { + if (error) sAlert.error(error); + else sAlert.success("Content Saved Successfully"); + }); + } + }, + function(dismiss) {} + ); + } + else { + Meteor.call('updateInternship', template.data.internship._id, changedData, function (error, result) { + if (error) sAlert.error(error); + else sAlert.success("Content Saved Successfully"); + }); + } + } + }; +}); +Template.InternshipHtmlEditor.onRendered(function() { + let template = this; + + $('.editor').tinymce({ + inline: true, + menubar: false, + theme: 'inlite', + plugins: [ + 'autolink', + 'contextmenu', + 'link', + 'lists', + 'table', + 'textcolor' + ], + toolbar: [ + 'undo redo | bold italic underline | fontselect fontsizeselect', + 'forecolor backcolor | alignleft aligncenter alignright alignfull | link unlink | numlist bullist outdent indent' + ], + table_default_attributes: { + border: 0, + cellpadding: 4 + }, + table_default_styles: { + borderCollapse: "collapse" + }, + insert_toolbar: 'quicktable', //image + selection_toolbar: 'bold italic | h1 h2 h3 | bullist numlist outdent indent | blockquote quicklink', + contextmenu: 'InsertImage | inserttable | cell row column deletetable', //image + contextmenu_never_use_native: false, + + + setup: function(editor) { + editor.addMenuItem('InsertImage', { + text: "Insert Image", + context: 'tools', + onclick: function() { + template.showSelectImageDialog.set(true); + } + }); + } + + }); + + Tracker.autorun(function() { + //let data = Template.currentData().data; //Note: Calling Template.currentData() is a reactive way to get the template parameters. + let data = Blaze.getData(template.view).internship; + + if(data) { + template.currentHtml = data.content; + $('.editor').html(data.content && data.content.length > 0 ? data.content : "Change Me!"); + } + }); +}); +Template.InternshipHtmlEditor.onDestroyed(function() { + this.saveChanges(true); +}); +Template.InternshipHtmlEditor.events({ + 'click #save': function(event, template) { + template.saveChanges(false); + } +}); +Template.InternshipHtmlEditor.helpers({ + showSelectImageDialog() { + return Template.instance().showSelectImageDialog.get(); + }, + selectImageDialogArgs() { + let template = Template.instance(); + return { + onApply(value) { + tinymce.activeEditor.insertContent(''); + template.showSelectImageDialog.set(false); + }, + onClose() { + template.showSelectImageDialog.set(false); + } + }; + }, + showInstructions() { + return !Template.currentData().internship; + } +}); \ No newline at end of file diff --git a/imports/ui/Admin/InternshipsEditor.html b/imports/ui/Admin/InternshipsEditor.html deleted file mode 100644 index 390f2c2..0000000 --- a/imports/ui/Admin/InternshipsEditor.html +++ /dev/null @@ -1,4 +0,0 @@ - \ No newline at end of file diff --git a/imports/ui/Admin/InternshipsEditor.import.styl b/imports/ui/Admin/InternshipsEditor.import.styl deleted file mode 100644 index 6de77c2..0000000 --- a/imports/ui/Admin/InternshipsEditor.import.styl +++ /dev/null @@ -1,2 +0,0 @@ -#internshipsEditor - display: block \ No newline at end of file diff --git a/imports/ui/Admin/InternshipsEditor.js b/imports/ui/Admin/InternshipsEditor.js deleted file mode 100644 index 1451d23..0000000 --- a/imports/ui/Admin/InternshipsEditor.js +++ /dev/null @@ -1,2 +0,0 @@ - -import './InternshipsEditor.html'; \ No newline at end of file diff --git a/imports/ui/Admin/NewsEditor.html b/imports/ui/Admin/NewsEditor.html deleted file mode 100644 index e039ff2..0000000 --- a/imports/ui/Admin/NewsEditor.html +++ /dev/null @@ -1,7 +0,0 @@ - \ No newline at end of file diff --git a/imports/ui/Admin/NewsEditor.import.styl b/imports/ui/Admin/NewsEditor.import.styl deleted file mode 100644 index 3c665ec..0000000 --- a/imports/ui/Admin/NewsEditor.import.styl +++ /dev/null @@ -1,6 +0,0 @@ -#newsEditor - display: block - .ck.ck-editor__editable_inline - border-color: rgba(0,0,0,.2) - .ck.ck-editor__editable_inline.ck-focused - border-color: rgba(0,0,0,1) \ No newline at end of file diff --git a/imports/ui/Admin/NewsEditor.js b/imports/ui/Admin/NewsEditor.js deleted file mode 100644 index 73551d5..0000000 --- a/imports/ui/Admin/NewsEditor.js +++ /dev/null @@ -1,86 +0,0 @@ - -import './NewsEditor.html'; -import swal from "sweetalert2"; - -let originalData = ""; - -Tracker.autorun(function() { - Meteor.subscribe("pages"); -}); - -Template.NewsEditor.onRendered(function() { - let _this = this; - - //#appreciationEditor' - // CKEditor.create(document.querySelector('#editor'), {}).then(editor => { - // _this.ckEditor = editor; - // - // Tracker.autorun(function() { - // let doc = Meteor.collections.Pages.findOne({name: 'Appreciation'}); - // - // originalData = (doc === undefined ? "" : doc.html); - // editor.setData(originalData); - // }); - // }).catch(err => { - // console.error(err); - // }); - $('.editor').tinymce({ - inline: true - }); - Tracker.autorun(function() { - let doc = Meteor.collections.Pages.findOne({name: 'News'}); - - originalData = (doc === undefined ? "" : doc.html); - $('.editor').html(originalData); - }); -}); - -Template.NewsEditor.onDestroyed(function() { - let data = this.ckEditor.getData(); - - if(data != originalData) { - swal({ - title: "Save Changes", - text: "Would you like to save any changes you have made to this sheet?", - type: "question", - showCancelButton: true, - confirmButtonColor: "#7cdd7f", - confirmButtonText: "Yes", - cancelButtonText: "No" - }).then( - function(isConfirm) { - if(isConfirm) { - Meteor.call('updatePage', 'News', data, function (error, result) { - if (error) sAlert.error(error); - else sAlert.success("Content Saved Successfully"); - }); - } - }, - function(dismiss) {} - ); - } -}); - -Template.NewsEditor.helpers({ - // html: function() { - // let doc = Meteor.collections.Pages.findOne({name: 'News'}); - // - // return doc == undefined ? "" : doc.html; - // } -}); - -Template.NewsEditor.events({ - 'click #save': function(event, template) { - let data = template.ckEditor.getData(); - - if(data != originalData) { - Meteor.call('updatePage', 'News', data, function (error, result) { - if (error) sAlert.error(error); - else sAlert.success("Content Saved Successfully"); - }); - } - else { - sAlert.success("Data has not changed!"); - } - } -}); diff --git a/imports/ui/Admin/PageEditor.html b/imports/ui/Admin/PageEditor.html new file mode 100644 index 0000000..24191ba --- /dev/null +++ b/imports/ui/Admin/PageEditor.html @@ -0,0 +1,13 @@ + \ No newline at end of file diff --git a/imports/ui/Admin/BoardEditor.import.styl b/imports/ui/Admin/PageEditor.import.styl similarity index 100% rename from imports/ui/Admin/BoardEditor.import.styl rename to imports/ui/Admin/PageEditor.import.styl diff --git a/imports/ui/Admin/PageEditor.js b/imports/ui/Admin/PageEditor.js new file mode 100644 index 0000000..8099551 --- /dev/null +++ b/imports/ui/Admin/PageEditor.js @@ -0,0 +1,226 @@ +import './PageEditor.html'; +import '/imports/ui/dialogs/SelectImageDialog.js'; +import swal from "sweetalert2"; + +let currentHtml = ""; +let currentPath = ""; + +let routeData = { + AppreciationEditor: {title: "Appreciation", name: "Appreciation"}, + NewsEditor: {title: "News", name: "News"}, + DatesEditor: {title: "Dates", name: "Dates"}, + BoardEditor: {title: "Current Board", name: "Board"}, + SlideshowPageEditor: {title: "Slideshow Page", name: "Slideshow"} +}; + +Tracker.autorun(function() { + //let name = routeData[FlowRouter.getRouteName()] ? routeData[FlowRouter.getRouteName()].name : ""; + + //if(name) { + //TODO: Filter the page by the page name. + Meteor.subscribe("pages"); + //} +}); + +Template.PageEditor.onCreated(function() { + let template = this; + + this.showSelectImageDialog = new ReactiveVar(false); + this.pageName = new ReactiveVar(); + + this.saveChanges = function() { + if(currentPath && currentPath !== FlowRouter.getRouteName()) { + let data = this.$('.editor').val(); + let template = this; + + currentPath = FlowRouter.getRouteName(); + + //Only ask the user if they want to update their changes if they actually have changes that have not yet been saved. + if(data !== currentHtml) { + const changedData = data; + + //Ensure this does not get run twice. + currentHtml = data; + + //Ask the user if they want to update the repository with their changes. + swal({ + title: "Save Changes", + text: "Would you like to save any changes you have made to this page?", + type: "question", + showCancelButton: true, + confirmButtonColor: "#7cdd7f", + confirmButtonText: "Yes", + cancelButtonText: "No" + }).then( + function(isConfirm) { + if(isConfirm) { + Meteor.call('updatePage', template.pageName.get(), changedData, function (error, result) { + if (error) sAlert.error(error); + else sAlert.success("Content Saved Successfully"); + }); + } + }, + function(dismiss) {} + ); + } + } + }; + + Tracker.autorun(function() { + if(routeData[FlowRouter.getRouteName()]) { + template.saveChanges(); + + //Save the page's name (indexes the page HTML in the collection) to the reactive variable so that all the content changes automatically. + template.pageName.set(routeData[FlowRouter.getRouteName()].name); + } + }); +}); + +Template.PageEditor.onRendered(function() { + let template = this; + + $('.editor').tinymce({ +// inline: true, +// menubar: false, +// theme: 'inlite', //inlite +// //skin: 'light', +// plugins: [ +// 'autolink', +// 'contextmenu', +// 'link', +//// 'linkchecker', Broken +// 'lists', +//// 'powerpaste', Broken +// 'table', +// //'image', +// 'textcolor' +// ], +// toolbar: [ +// 'undo redo | bold italic underline | fontselect fontsizeselect', +// 'forecolor backcolor | alignleft aligncenter alignright alignfull | link unlink | numlist bullist outdent indent | InsertImage | inserttable | cell row column deletetable' +// ], +// table_default_attributes: { +// border: 0, +// cellpadding: 4 +// }, +// table_default_styles: { +// borderCollapse: "collapse" +// }, +// insert_toolbar: 'quicktable', //image +// selection_toolbar: 'bold italic | h1 h2 h3 | blockquote quicklink', +// contextmenu: 'InsertImage | inserttable | cell row column deletetable', //image +// contextmenu_never_use_native: false, + + + + //image_advtab: true, + //image_description: true, + //image_dimensions: true, + //image_title: true, + //image_list: function(success) { + // //Expects an array of objects containing title:String and value:String properties where the value is a path to the image. + // Meteor.call('getGeneralImages', function(data, err) { + // if(err) { + // sAlert.error(err); + // } + // else { + // success(data); + // } + // }); + //} + //powerpaste_word_import: 'clean', + //powerpaste_html_import: 'clean' + + + inline: true, + menubar: false, + theme: 'inlite', + plugins: [ + 'autolink', + 'contextmenu', + 'link', + 'lists', + 'table', + 'textcolor' + ], + toolbar: [ + 'undo redo | bold italic underline | fontselect fontsizeselect', + 'forecolor backcolor | alignleft aligncenter alignright alignfull | link unlink | numlist bullist outdent indent' + ], + table_default_attributes: { + border: 0, + cellpadding: 4 + }, + table_default_styles: { + borderCollapse: "collapse" + }, + insert_toolbar: 'quicktable', //image + selection_toolbar: 'bold italic | h1 h2 h3 | blockquote quicklink', + contextmenu: 'InsertImage | inserttable | cell row column deletetable', //image + contextmenu_never_use_native: false, + + + setup: function(editor) { + editor.addMenuItem('InsertImage', { + text: "Insert Image", + context: 'tools', + onclick: function() { + template.showSelectImageDialog.set(true); + } + }); + } + + }); + + Tracker.autorun(function() { + let doc = Meteor.collections.Pages.findOne({name: template.pageName.get()}); + + currentHtml = (doc === undefined ? "" : doc.html); + $('.editor').html(currentHtml); + }); +}); + +Template.PageEditor.onDestroyed(function() { + this.saveChanges(); +}); + +Template.PageEditor.events({ + 'click #save': function(event, template) { + let data = template.$('.editor').val(); + + if(data !== currentHtml) { + Meteor.call('updatePage', template.pageName.get(), data, function (error, result) { + if (error) sAlert.error(error); + else { + sAlert.success("Content Saved Successfully"); + //Ensure we recognize things were saved later. + currentHtml = data; + } + }); + } + else { + sAlert.success("Data has not changed!"); + } + } +}); + +Template.PageEditor.helpers({ + editorName() { + return routeData[FlowRouter.getRouteName()].title; //FlowRouter.getRouteName() is reactive allowing this to trigger when the content changes. + }, + showSelectImageDialog() { + return Template.instance().showSelectImageDialog.get(); + }, + selectImageDialogArgs() { + let template = Template.instance(); + return { + onApply(value) { + tinymce.activeEditor.insertContent(''); + template.showSelectImageDialog.set(false); + }, + onClose() { + template.showSelectImageDialog.set(false); + } + }; + } +}); \ No newline at end of file diff --git a/imports/ui/Admin/SlideshowEditor.html b/imports/ui/Admin/SlideshowEditor.html new file mode 100644 index 0000000..e767a9f --- /dev/null +++ b/imports/ui/Admin/SlideshowEditor.html @@ -0,0 +1,41 @@ + + + + diff --git a/imports/ui/Admin/SlideshowEditor.import.styl b/imports/ui/Admin/SlideshowEditor.import.styl new file mode 100644 index 0000000..18956a6 --- /dev/null +++ b/imports/ui/Admin/SlideshowEditor.import.styl @@ -0,0 +1,108 @@ +#slideshowEditor + display: block + + select[name='slideshows'] + font-size: 1.5em + min-width: 100px + input[name="newSlideshowName"] + display: inline-block + transition: all .75s ease + width: 0 + border: 0 + opacity: 0 + font-size: 1.2em + input[name="newSlideshowName"].show + opacity: 1 + border: 1px solid #ccc + border-radius: 4px + width: 200px + transform: translateX(4px) + .createSlideshow + display: inline-block + padding: 2px 6px + margin: 0 4px + //width: 33px + text-align: center + font-size: 1.5em + line-height: 1.5em + border-radius: 8px + //border: 1px solid rgba(0, 0, 0, 0) + box-sizing: border-box + .createSlideshow:hover + //border: 1px inset #b100d1 + //-webkit-box-shadow: inset 0px 0px 20px 0px #de7cff + //-moz-box-shadow: inset 0px 0px 20px 0px #de7cff + //box-shadow: inset 0px 0px 20px 0px #de7cff + color: #5c8744 + .createSlideshow + transform: translateX(-25px) rotate(0deg) + transition: all .75s ease + .createSlideshow.move + transform: translateX(6px) rotate(720deg) + .deleteSlideshow + display: inline-block + padding: 2px 3px + margin: 0 4px + width: 1.5em + line-height: 1.5em + //width: 33px + text-align: center + font-size: 1.5em + border-radius: 50% + //border: 1px solid #8c0000 + box-sizing: border-box + color: black + .deleteSlideshow:hover + color: #8c2353 + .addSlide + display:inline-block + width: 200px + height: 200px + background-size: contain + background-repeat: no-repeat + border-radius: 6px + border 3px dashed black + .addSlide:hover + border-color: #5c8744 + .slideThumbnail + position: relative + width: 200px + height: 200px + background-position: 50% 50% + background-repeat:no-repeat + background-size: contain + display: inline-block + .controls + position: absolute + bottom: 50% + left: 50% + right: 50% + top: 50% + height: 22px + width: 90px + margin-left: -45px + margin-top: -10px + background-color: rgba(0, 0, 0, .8) + display: none + text-align: center + font-size: 20px + line-height: 20px + border-radius: 8px + border: 1px solid black + .slideThumbnail:hover > .controls, .controls:hover + display: block + .moveLeft, .moveRight, .removeThumbnail + color: white + padding: 0 4px + .moveLeft + padding-right: 14px + .moveRight + padding-left: 14px + .moveLeft:hover, .moveRight:hover + color: #00ff55 + .moveLeft:active, .moveRight:active + color: #006e27 + .removeThumbnail:hover + color: #ff0019 + .removeThumbnail:active + color: #57000c \ No newline at end of file diff --git a/imports/ui/Admin/SlideshowEditor.js b/imports/ui/Admin/SlideshowEditor.js new file mode 100644 index 0000000..a801bb6 --- /dev/null +++ b/imports/ui/Admin/SlideshowEditor.js @@ -0,0 +1,213 @@ + +import './SlideshowEditor.html'; +import '/imports/ui/dialogs/SelectImageDialog.js'; +import swal from "sweetalert2"; + +const PREFIX = "SlideshowEditor_"; + +Tracker.autorun(function() { + Meteor.subscribe("slideshow"); +}); + +Template.SlideshowEditor.onCreated(function() { + this.slideshows = Meteor.collections.Slideshow.find({}, {sort: {name: 1}}); + Session.set(PREFIX + 'selectedSlideshow', null); +}); + +Template.SlideshowEditor.onRendered(function() { + let _this = this; + + Tracker.autorun(function() { + //This is a reactive call, allowing this function to be re-run when the cursor changes. + let slideshows = _this.slideshows.fetch(); + + if(slideshows.length > 0 && !Session.get(PREFIX + "selectedSlideshow")) { + //Mark the first slideshow as selected because it will be selected in the UI by default. + Session.set(PREFIX + "selectedSlideshow", slideshows[0]); + } + }); +}); + +Template.SlideshowEditor.onDestroyed(function() { + +}); + +Template.SlideshowEditor.helpers({ + slideshows: function() { + return Template.instance().slideshows; + }, + selectedSlideshow: function(){ + return Session.get(PREFIX + "selectedSlideshow"); + }, + isSlideshowSelected: function() { + let selectedSlideshow = Session.get(PREFIX + "selectedSlideshow"); + + return selectedSlideshow == this ? "selected" : ""; + }, + hasSelectedSlideshow: function() { + return Session.get(PREFIX + "selectedSlideshow"); + } +}); + +Template.SlideshowEditor.events({ + 'change select[name="slideshows"]': function(event, template) { + let slideshowId = $(event.target).val(); + let slideshow = Meteor.collections.Slideshow.findOne(slideshowId); + + Session.set(PREFIX + 'selectedSlideshow', slideshow); + }, + "keyup input[name='newSlideshowName']" : function(event, template) { + if(event.keyCode === 13) { + event.preventDefault(); + $('.createSlideshow').trigger('click'); + } + }, + "click .createSlideshow": function(event, template) { + let $input = template.$('input[name="newSlideshowName"]'); + + if($input.hasClass('show')) { + let name = $input.val(); + + name = name ? name.trim() : undefined; + name = name && name.length > 0 ? name : undefined; + + if(name) { + Meteor.call('addSlideshow', name, function(error, id) { + if(error) sAlert.error("Failed to create the slideshow!\n" + error); + else { + //Clear the text editor. + $input.val(""); + + //Quick hack to attempt to allow the slideshow we created to be added to the select box before we try to select it and edit it. + let count = 0; + let interval = setInterval(function() { + let selected = Meteor.collections.Slideshow.findOne(id); + + if(selected) { + //Select the sheet in the drop down. + template.$('select[name="slideshows"]').val(id); + Session.set(PREFIX + "selectedSlideshow", selected); + clearInterval(interval); + } + else count++; + + //Avoid infinite loop that should never happen. + if(count > 100) clearInterval(interval); + }, 100); + } + }); + } + + $input.removeClass('show'); + $(event.target).toggleClass('move'); + template.$('.deleteSlideshow').show(); + } + else { + $input.addClass('show'); + $(event.target).toggleClass('move'); + $input.focus(); + template.$('.deleteSlideshow').hide(); + } + }, + "click .deleteSlideshow": function(event, template) { + let $combo = template.$('select[name="slideshows"]'); + let slideshow = Session.get(PREFIX + 'selectedSlideshow'); + + if(slideshow) { + swal({ + title: 'Are you sure?', + text: "You won't be able to revert this!", + type: 'warning', + showCancelButton: true, + confirmButtonColor: '#3085d6', + cancelButtonColor: '#d33', + confirmButtonText: 'Yes, delete it!' + }).then((result) => { + Meteor.call('removeSlideshow', slideshow._id, function(error, id) { + if(error) sAlert.error("Failed to remove the slideshow!\n" + error); + else { + sAlert.success("Slideshow removed."); + //Fire the event so that our selection changes once the slideshow is removed from the combo. + $combo.trigger('change'); + } + }); + }); + } + } +}); + +Template.SlideshowContentEditor.onCreated(function() { + this.showSelectImageDialog = new ReactiveVar(false); +}); +Template.SlideshowContentEditor.helpers({ + showSelectImageDialog: function() { + return Template.instance().showSelectImageDialog.get(); + }, + slides: function() { + return Meteor.collections.Slideshow.findOne(Template.instance().data).images; + }, + selectImageDialogArgs() { + let template = Template.instance(); + + return { + onApply(data) { + //tinymce.activeEditor.insertContent(''); + template.showSelectImageDialog.set(false); + //Send the slide image as a base64 string. + Meteor.call('addSlideshowImage', template.data, data, function(error, id) { + if(error) sAlert.error("Failed to add the slide.\n" + error); + else { + //TODO: ? + } + }); + }, + onClose() { + template.showSelectImageDialog.set(false); + } + }; + } + //getImage(id) { + // Meteor.call('getSlideshowImage', id, function(error, base64)) { + // if(error) sAlert.error("Failed to get the slide image.\n" + error); + // else { + // + // } + // } + //} +}); +Template.SlideshowContentEditor.events({ + "click .addSlide": function(event, template) { + template.showSelectImageDialog.set(true); + }, + 'click .moveLeft': function(event, template) { + let $slideThumbnail = $(event.target).parent().parent(); + let index = $slideThumbnail.index(); + + //If we can move it to the right then do so. + if(index - 1 >= 0) { + Meteor.call('swapSlideshowImages', template.data, index, index - 1, function(error) { + if(error) sAlert.error("Failed to move the slide.\n" + error); + }); + } + }, + 'click .moveRight': function(event, template) { + let $slideThumbnail = $(event.target).parent().parent(); + let index = $slideThumbnail.index(); + let thumbnailCount = $slideThumbnail.siblings().length; //Should give us the number of siblings including the add image button (we don't want to count it), and excluding this slide thumbnail (we do want to count it). + + //If we can move it to the right then do so. + if(index + 1 < thumbnailCount) { + Meteor.call('swapSlideshowImages', template.data, index, index + 1, function(error) { + if(error) sAlert.error("Failed to move the slide.\n" + error); + }); + } + }, + 'click .removeThumbnail': function(event, template) { + let $slideThumbnail = $(event.target).parent().parent(); + let imageId = $slideThumbnail.data("image-id"); + + Meteor.call('removeSlideshowImage', template.data, imageId, function(error) { + if(error) sAlert.error("Failed to remove the slide.\n" + error); + }); + } +}); diff --git a/imports/ui/EditablePage.html b/imports/ui/EditablePage.html new file mode 100644 index 0000000..82c3369 --- /dev/null +++ b/imports/ui/EditablePage.html @@ -0,0 +1,7 @@ + \ No newline at end of file diff --git a/imports/ui/EditablePage.import.styl b/imports/ui/EditablePage.import.styl new file mode 100644 index 0000000..3432cf5 --- /dev/null +++ b/imports/ui/EditablePage.import.styl @@ -0,0 +1,32 @@ +#pageEditor div.editor + max-width: 960px + padding: 20px +#editablePage, #pageEditor div.editor, .editablePage + p + font-family: Palatino, "Palatino Linotype", "Palatino LT STD", "Book Antiqua", Georgia, serif + font-size: 16px + margin: 16px 0 + line-height: 20.8px + h2 + font-family: "Arial Black", "Arial Bold", Gadget, sans-serif + font-size: 19px + text-transform: uppercase + background-color: #EEE + font-weight: bold + padding-left: 40px + line-height: 24px + display: inline-block + h4 + font-family: "Century Gothic", CenturyGothic, AppleGothic, sans-serif + font-size: 16px + font-weight: 800 + clear: left + display: inline-block + line-height: 20.8px + img + padding-right: 10px + ul + margin-top: 10px + font-family: Palatino, "Palatino Linotype", "Palatino LT STD", "Book Antiqua", Georgia, serif + font-size: 16px + line-height: 20.8px \ No newline at end of file diff --git a/imports/ui/EditablePage.js b/imports/ui/EditablePage.js new file mode 100644 index 0000000..ef917f6 --- /dev/null +++ b/imports/ui/EditablePage.js @@ -0,0 +1,33 @@ +import './EditablePage.html'; + +let routeData = { + Appreciation: {name: "Appreciation"}, + News: {name: "News"}, + ImportantDates: {name: "Dates"}, + CurrentBoard: {name: "Board"} +}; + +Tracker.autorun(function() { + Meteor.subscribe("pages"); +}); + +Template.EditablePage.onCreated(function() { + let template = this; + + this.pageName = new ReactiveVar(); + + Tracker.autorun(function() { + if(routeData[FlowRouter.getRouteName()]) { + //Save the page's name (indexes the page HTML in the collection) to the reactive variable so that all the content changes automatically. + template.pageName.set(routeData[FlowRouter.getRouteName()].name); + } + }); +}); + +Template.EditablePage.helpers({ + editableHTML: function() { + let doc = Meteor.collections.Pages.findOne({name: Template.instance().pageName.get()}); + + return doc === undefined ? "" : doc.html; + } +}); \ No newline at end of file diff --git a/imports/ui/Fellowships.html b/imports/ui/Fellowships.html index b15b762..796bead 100644 --- a/imports/ui/Fellowships.html +++ b/imports/ui/Fellowships.html @@ -1,7 +1,7 @@