This commit is contained in:
Kenneth Jao 2016-10-23 22:12:09 -04:00
commit c7ffbc947c
13 changed files with 308 additions and 119 deletions

View File

@ -6,10 +6,10 @@
meteor-base@1.0.4 # Packages every Meteor app needs to have
mobile-experience@1.0.4 # Packages for a great mobile UX
mongo@1.1.12_1 # The database Meteor supports right now
mongo@1.1.13 # The database Meteor supports right now
blaze-html-templates@1.0.4 # Compile .html files into Meteor Blaze views
reactive-var@1.0.10 # Reactive variable for tracker
jquery # Helpful client-side library
jquery@1.11.9 # Helpful client-side library
tracker@1.1.0 # Meteor's client-side reactive programming library
standard-minifier-js@1.2.0_1 # JS minifier run for production mode
@ -33,7 +33,6 @@ shell-server@0.2.1
http@1.2.9_1
underscore@1.0.9
ahref:dragula
harrison:papa-parse
pfafman:filesaver
natestrauser:select2
juliancwirko:s-alert
@ -42,5 +41,5 @@ aldeed:collection2
dburles:collection-helpers
yogiben:admin-edit
mfactory:admin-lte
standard-minifier-css
mrt:jquery-ui
standard-minifier-css@1.2.0_1

View File

@ -1 +1 @@
METEOR@1.4.1.2
METEOR@1.4.1.3

View File

@ -45,7 +45,6 @@ fastclick@1.0.12
fortawesome:fontawesome@4.5.0
geojson-utils@1.0.9
google@1.1.14
harrison:papa-parse@1.1.1
hot-code-push@1.0.4
html-tools@1.0.11
htmljs@1.0.11
@ -82,11 +81,11 @@ mobile-status-bar@1.0.12
modules@0.7.6_1
modules-runtime@0.7.6_1
momentjs:moment@2.15.1
mongo@1.1.12_1
mongo@1.1.13
mongo-id@1.0.5
mrt:jquery-ui@1.9.2
natestrauser:select2@4.0.3
npm-mongo@1.5.49
npm-mongo@2.2.11_1
oauth@1.1.11
oauth2@1.1.10
observe-sequence@1.0.12

View File

@ -4,13 +4,13 @@ All your work in one place. Finish before your time runs out.
Hourglass is a planner tool specialized for schools.
## Usage
How to use hourglass as an end product.
If you find any bugs or have an idea to make this better, you can press the yield icon in the bottom right of the screen.
If you find any bugs or have an idea to make this better, you can press the question mark icon in the bottom right of the screen.
### Main Page
After logging in with a Google account, you are automatically redirected to this page. Here, you can view work from subscribed classes. Preferences are found in the right sidebar, while the left sidebar contains classes and class management. The left sidebar also has options to allow you to switch between displaying a calendar and displaying a list of classes. Hovering over classes in the left sidebar will highlight works from that class. Clicking on a class will filter works to only be of that class. You can filter more than one class at a time. Clicking on a work, both in calendar and class mode, will open a menu showing more details. Cards are colored according to the type of work they are.
After logging in with a Google account and setting up your profile, you can redirect to this page. Here, you can view work from subscribed classes. Preferences are found in the right sidebar, while the left sidebar contains classes and class management. The left sidebar also has options to allow you to switch between displaying a calendar and displaying a list of classes. Hovering over classes in the left sidebar will highlight works from that class. Clicking on a class will filter works to only be of that class. You can filter more than one class at a time or filter by the type of work. Clicking on a work, both in calendar and class mode, will open a menu showing more details. Cards are colored according to the type of work they are.
##### Calendar Mode
Calendar mode organizes work by due date rather than by class. Pressing on a date will open the left sidebar, allowing you to choose a class to create a piece of work under.
Calendar mode organizes work by due date rather than by class. Pressing on a date will open the left sidebar, allowing you to choose a class in which you want to create the work.
##### Class Mode
Class mode allows you to organize work by the class it belongs to.
Class mode allows you to organize work by the class it belongs to. You can drag classes to reorder them.
##### Work
Every piece of work has a confirmed:reported ratio. This is the ratio of the number of people who confirm the presence of the work to the number of people who believe that this is a false report. The creator of a work can edit fields after creation by clicking on details needed to be changed. Lastly, by marking a work as done, it is hidden from view. To disable this, go to preferences.
##### Filters
@ -21,3 +21,19 @@ On this page, it is possible to edit profile details as well as create, join, an
After filling out class details, class creators must wait for the class to be approved by administrators. Public classes can be viewed and joined by all, while private classes are hidden from others and require a code to join.
#### Joining a class
One can join a public class by searching for and then clicking on the class they want to join. Joining a private class requires that the class administrator gives you a code, which you then provide in the "Join Private Class" button.
#### Managing classes
On this tab, you can see all of your current classes. If you are an admin of the class, clicking on it will allow you to manage details about the class. Otherwise, you can leave the class by clicking the x.
### User Pages
You can go to <-url->/user/<-email-> to vist a user's profile.
##Changelog
### 0.1.2
- First beta version
- Fixed resolution / display issues
- Bug fixes
### 0.1.1
- Added dynamic resizing, zooming and scaling support for CSS.
- Fixed reactive updating on work.
- Added personal work.

View File

@ -0,0 +1,56 @@
.adminUserIcon {
width: 2.7vw;
margin: 1%;
cursor: pointer;
-moz-border-radius: 50%;
-webkit-border-radius: 50%;
border-radius: 50%;
}
.adminUserInfo {
margin-top: 1vh;
padding: 0.5% 1.5% 0.5% 1.5%;
background-color: #fff;
-webkit-filter: drop-shadow(0px -1px 2px #666);
filter: drop-shadow(0px -1px 2px #666);
position: fixed;
display: none;
z-index: 5;
}
.adminUserInfo p {
margin: 5% 0 5% 0;
}
.infoTitle {
font-weight: 600;
}
.infoTab {
width: 0;
height: 0;
border-bottom: 2vh solid #fff;
border-left: 1.5vh solid transparent;
border-right: 1.5vh solid transparent;
position: absolute;
right: 1vh;
top: -1.9vh;
}
.approveStatus {
cursor: pointer;
}
.approveStatus .fa {
font-size: 5vh
}
.approveStatus .fa-toggle-on {
color: #288cd3;
}

View File

@ -0,0 +1,17 @@
<template name="adminUserDisplay">
{{#each info}}
<img class="adminUserIcon" src="{{icon}}">
<div class="adminUserInfo">
<div class="infoTab"></div>
<p><span class="infoTitle">ID: </span><span>{{id}}</span></p>
<p><span class="infoTitle">Email: </span><span>{{email}}</span></p>
<p><span class="infoTitle">Name: </span><span>{{name}}</span></p>
</div>
{{/each}}
</template>
<template name="statusButton">
<div class="approveStatus">
<i class="fa fa-toggle-{{status}}" aria-hidden="true"></i>
</div>
</template>

View File

@ -0,0 +1,66 @@
var inInfo = false;
var openUserDisplay = null;
Template.adminUserDisplay.helpers({
info() {
var ids = (this.value instanceof Array) ? this.value : [this.value];
var userInfo = [];
for(var i = 0; i < ids.length; i++) {
var user = Meteor.users.findOne({_id: ids[i]});
userInfo.push({
name: user.profile.name,
email: user.services.google.email,
id: user._id,
icon: user.services.google.picture
})
}
return userInfo;
}
});
Template.statusButton.helpers({
status() {
console.log(this.value);
return (this.value) ? "on" : "off";
}
});
Template.adminUserDisplay.events({
'click .adminUserIcon' (event) {
var icoCoords = $(event.target)[0].getBoundingClientRect();
var x = window.innerWidth - icoCoords.right;
var y = icoCoords.bottom;
openUserDisplay = $(event.target).next();
$(".adminUserInfo").fadeOut(200);
openUserDisplay
.css({'right': x, 'top': y})
.fadeIn(200);
},
'mouseenter .adminUserInfo' () {
inInfo = true;
},
'mouseleave .adminUserInfo' (event) {
if(inInfo) openUserDisplay.fadeOut(200);
inInfo = false;
openUserDisplay = null;
}
});
Template.AdminLTE.events({
'click' (event) {
if(!event.target.className.includes("adminUserInfo") &&
!event.target.className.includes("adminUserIcon") &&
openUserDisplay !== null) {
openUserDisplay.fadeOut(200);
openUserDisplay = null;
}
}
});
Template.statusButton.events({
'click .approveStatus' () {
console.log(this.doc._id);
Meteor.call("approveClass", this.doc._id);
}
})

View File

@ -639,9 +639,9 @@ Template.main.events({
});
},
'click #exportDiv' (event) {
var events = [];
var events = "BEGIN:VCALENDAR\nVERSION:2.0\nPRODID:-//hacksw/handcal//NONSGML v1.0//EN";
var userClasses = Session.get("calendarClasses");
var timestamp = new Date().toJSON().replace(/-|:|\./gi, "");
for (var i = 0; i < userClasses.length; i++) {
var works = userClasses[i].thisClassWork;
for (var j = 0; j < works.length; j++) {
@ -655,24 +655,27 @@ Template.main.events({
if (workclass === undefined) workclass = {
name: "Personal"
};
events.push([
workclass.name + ": " + work.name,
work.realDate.toLocaleDateString(),
work.description,
"True"
]);
if (work.description === undefined) {
work.description = "";
} else {
work.description = " - " + work.description;
}
var duedate = work.realDate.toJSON().slice(0,10).replace(/-/gi,"");
events += "\nBEGIN:VEVENT" +
"\nUID:" + timestamp + work._id + "@hourglass.tk" +
"\nDTSTAMP:" + timestamp +
"\nDTSTART:" + duedate +
"\nDTEND:" + duedate +
"\nSUMMARY:" + work.name + work.description +
"\nCATEGORIES:" + workclass.name +
"\nEND:VEVENT";
}
events += "\nEND:VCALENDAR";
}
var JSONevents = JSON.stringify(events);
var CSVevents = Papa.unparse({
fields: ["Subject", "Start Date", "Description", "All Day Event"],
data: JSONevents
var eventBlob = new Blob([events], {
type: "data:text/ics;charset=utf-8"
});
var eventBlob = new Blob([CSVevents], {
type: "data:text/csv;charset=utf-8"
});
saveAs(eventBlob, "hourglass.csv");
saveAs(eventBlob, "hourglass.ics");
},
// HANDLING INPUT CHANGING
'focus .clickModify' (event) {

View File

@ -177,7 +177,7 @@
{{#if noclass}}
<h3>No results found...</h3>
{{/if}}
{{else}}
{{else}}
{{#each autocompleteClasses}}
{{> classDisplay}}
{{/each}}

View File

@ -25,17 +25,17 @@ Session.set("noclass", null); // If user doesn't have classes.
Session.set("notfound", null); // If no results for autocomplete.
Template.profile.helpers({
/* themeName() {
var vals = _.values(themeColors);
var curtheme = Session.get("user").preferences.theme;
for (var i = 0; i < vals.length; i++) {
if (_.isEqual(vals[i], curtheme)) {
var name = _.keys(themeColors)[i];
return name.charAt(0).toUpperCase() + name.slice(1);
/* themeName() {
var vals = _.values(themeColors);
var curtheme = Session.get("user").preferences.theme;
for (var i = 0; i < vals.length; i++) {
if (_.isEqual(vals[i], curtheme)) {
var name = _.keys(themeColors)[i];
return name.charAt(0).toUpperCase() + name.slice(1);
}
}
}
return "Custom";
},*/
return "Custom";
},*/
classSettings() { // Returns autocomplete array for classes.
return {
position: "bottom",
@ -50,7 +50,15 @@ Template.profile.helpers({
},
selector: (match) => {
regex = new RegExp(match, 'i');
return {$or: [{'name': regex}, {'teacher': regex}, {'hour': regex}]};
return {
$or: [{
'name': regex
}, {
'teacher': regex
}, {
'hour': regex
}]
};
}
}]
};
@ -143,9 +151,9 @@ Template.profile.helpers({
},
profClassTabColor(status) {  // Change this [Supposed to show the current mode that's selected via color]    
if (Session.equals("profClassTab", status)) {
return Meteor.user().profile.preferences.theme.modeHighlight;
return Meteor.user().profile.preferences.theme.modeHighlight;
} else {
return;
return;
}
},
profClassTab(tab) { // Tells current class
@ -182,22 +190,22 @@ Template.profile.helpers({
Template.profile.events({
'click' (event) { // Whenever a click happens'
var e = event.target.className;
if(modifyingInput !== null && event.target !== document.getElementById(modifyingInput)) {
if (modifyingInput !== null && event.target !== document.getElementById(modifyingInput)) {
if (!(e.includes("optionHolder") || e.includes("optionText"))) {
if(document.getElementById(modifyingInput).className.includes("dropdown")) {
if (document.getElementById(modifyingInput).className.includes("dropdown")) {
$(".optionHolder")
.fadeOut(250, "linear");
.fadeOut(250, "linear");
$(".selectedOption").removeClass("selectedOption");
} else {
if(modifyingInput === "description") {
if (modifyingInput === "description") {
Session.set("restrictText", {});
$("#"+modifyingInput).css('cursor','pointer');
$("#" + modifyingInput).css('cursor', 'pointer');
var newSetting = Session.get("user");
newSetting[modifyingInput] = document.getElementById(modifyingInput).value;
serverData = newSetting;
sendData("editProfile");
}
}
}
modifyingInput = null;
}
@ -225,7 +233,10 @@ Template.profile.events({
'click #mainpage' () {
if (!Meteor.userId() || _.contains([null, undefined, ""], Meteor.user().profile.school)) {
sAlert.closeAll();
sAlert.error('Please fill in your profile!', {effect: 'stackslide', position: 'top'});
sAlert.error('Please fill in your profile!', {
effect: 'stackslide',
position: 'top'
});
} else {
window.location = '/';
}
@ -237,12 +248,12 @@ Template.profile.events({
var div = document.getElementById("profClasses");
div.style.height = "50%";
setTimeout(function() {            
Session.set("profClassTab", "addClass");
Session.set("profClassTab", "addClass");
div.style.height = "70%";          
openDivFade(functionHolder);        
}, 400);
},
    'click .manageClass' () { 
},
    'click .manageClass' () { 
if (Session.equals("profClassTab", "manClass")) return;      
var functionHolder = document.getElementById("profClassInfoHolder");
closeDivFade(functionHolder);
@ -254,16 +265,16 @@ Template.profile.events({
openDivFade(functionHolder);        
}, 400);
},
    'click .createClass' () {
    'click .createClass' () {
if (Session.equals("profClassTab", "creClass")) return;
var functionHolder = document.getElementById("profClassInfoHolder");        
closeDivFade(functionHolder);
var div = document.getElementById("profClasses");
div.style.height = "50%";
setTimeout(function() {
Session.set("profClassTab", "creClass");
div.style.height = "70%";
openDivFade(functionHolder);
Session.set("profClassTab", "creClass");
div.style.height = "70%";
openDivFade(functionHolder);
}, 400);
},
'click .classBox' (event) { // When you click on a box that holds class
@ -293,7 +304,7 @@ Template.profile.events({
} else {
var attribute = event.target.getAttribute("classid");
}
if(attribute === Meteor.userId()) return;
if (attribute === Meteor.userId()) return;
Session.set("selectedClass", null);
var usertype = ["moderators", "banned"];
var array = classes.findOne({
@ -461,21 +472,21 @@ Template.profile.events({
// INPUT HANDLING
'focus .clickModify' (event) {
$(".optionHolder")
.fadeOut(250, "linear");
.fadeOut(250, "linear");
if(modifyingInput !== null) {
if(!$("#"+modifyingInput)[0].className.includes("dropdown")) closeInput(modifyingInput);
}
if (modifyingInput !== null) {
if (!$("#" + modifyingInput)[0].className.includes("dropdown")) closeInput(modifyingInput);
}
modifyingInput = event.target.id;
if(!$("#"+modifyingInput)[0].className.includes("dropdown")) {
if (!$("#" + modifyingInput)[0].className.includes("dropdown")) {
event.target.select();
event.target.style.cursor = "text";
event.target.style.cursor = "text";
}
},
'keydown .dropdown' (event) {
event.preventDefault();
var first = $("#"+modifyingInput).next().children("p:first-child");
var last = $("#"+modifyingInput).next().children("p:last-child");
var first = $("#" + modifyingInput).next().children("p:first-child");
var last = $("#" + modifyingInput).next().children("p:last-child");
var next = $(".selectedOption").next();
var prev = $(".selectedOption").prev();
var lastSel = $(".selectedOption");
@ -487,7 +498,7 @@ Template.profile.events({
} else {
if (prev.length === 0) {
last.addClass("selectedOption");
lastSel.removeClass("selectedOption");
lastSel.removeClass("selectedOption");
} else {
prev.addClass("selectedOption");
lastSel.removeClass("selectedOption");
@ -506,7 +517,7 @@ Template.profile.events({
next.addClass("selectedOption");
lastSel.removeClass("selectedOption");
}
}
}
} else if (event.keyCode === 13) {
lastSel[0].click();
}
@ -515,27 +526,29 @@ Template.profile.events({
$(".selectedOption").removeClass("selectedOption");
$("#" + modifyingInput).next()
.css('opacity',0)
.slideDown(300)
.animate(
{ opacity: 1 },
{ queue: false, duration: 100 }
);
.css('opacity', 0)
.slideDown(300)
.animate({
opacity: 1
}, {
queue: false,
duration: 100
});
},
'click .optionText' (event) { // Click each preferences setting.
var option = event.target.childNodes[0].nodeValue;
var userSettings = ["description","school","grade"];
var userSettings = ["description", "school", "grade"];
var newSetting = Session.get("user");
if(modifyingInput === "privacy" || modifyingInput === "category") {
if (modifyingInput === "privacy" || modifyingInput === "category") {
document.getElementById(modifyingInput).value = option;
$("#" + modifyingInput).next()
.fadeOut(250, "linear");
.fadeOut(250, "linear");
$(".selectedOption").removeClass("selectedOption");
return;
}
if(_.contains(userSettings, modifyingInput)) {
if (_.contains(userSettings, modifyingInput)) {
newSetting[modifyingInput] = (modifyingInput === "grade") ? parseInt(option) : option;
} else {
newSetting.preferences[modifyingInput] = (function() {
@ -547,10 +560,10 @@ Template.profile.events({
}
Session.set("user", newSetting);
serverData = Session.get("user");
sendData("editProfile");
sendData("editProfile");
$("#" + modifyingInput).next()
.fadeOut(250, "linear");
.fadeOut(250, "linear");
$(".selectedOption").removeClass("selectedOption");
},
@ -585,11 +598,13 @@ Template.profile.events({
name: item.childNodes[1].childNodes[0].nodeValue,
teacher: item.childNodes[3].childNodes[0].nodeValue,
hour: item.childNodes[5].childNodes[0].nodeValue,
subscribers: Math.floor(item.childNodes[7].childNodes[0].nodeValue.replace(",","").length / 17),
subscribers: Math.floor(item.childNodes[7].childNodes[0].nodeValue.replace(",", "").length / 17),
_id: item.getAttribute("classid")
});
}
Session.set("autocompleteDivs", divs.sort(function(a,b){return b.subscribers-a.subscribers}));
Session.set("autocompleteDivs", divs.sort(function(a, b) {
return b.subscribers - a.subscribers
}));
} catch (err) {}
}
});
@ -712,8 +727,8 @@ function checkUser(email, classid) { // Checks if user email exists.
return true;
} else {
if (classes.findOne({
_id: classid
}).subscribers)
_id: classid
}).subscribers)
return false;
}
}

View File

@ -48,11 +48,3 @@ schools.attachSchema(schools.schema);
classes.attachSchema(classes.schema);
work.attachSchema(work.schema);
requests.attachSchema(requests.schema);
classes.helpers({
fullUserInfo() {
var user = Meteor.users.findOne({_id: this.admin});
console.log(user);
return this.admin + " | " + user.services.google.email + " | " + user.profile.name;
}
})

View File

@ -131,16 +131,16 @@ AdminConfig = {
{ label: 'Name', name: 'name' },
{ label: 'Hour', name: 'hour' },
{ label: 'Teacher', name: 'teacher' },
{ label: 'Admin', name: 'admin' },
{ label: 'Status', name: 'status' },
{ label: 'Admin', name: 'admin', template: 'adminUserDisplay' },
{ label: 'Status', name: 'status', template: 'statusButton'},
{ label: 'Code', name: 'code' },
{ label: 'Privacy', name: 'privacy' },
{ label: 'Category', name: 'category' },
{ label: 'Moderators', name: 'moderators' },
{ label: 'Banned', name: 'banned' },
{ label: 'Subscribers', name: 'subscribers' }
{ label: 'Moderators', name: 'moderators', template: 'adminUserDisplay' },
{ label: 'Banned', name: 'banned', template: 'adminUserDisplay' },
{ label: 'Subscribers', name: 'subscribers', template: 'adminUserDisplay'}
],
color: 'blue'
color: 'purple'
},
work: {
tableColumns: [
@ -149,11 +149,11 @@ AdminConfig = {
{ label: 'Name', name: 'name' },
{ label: 'Due Date', name: 'dueDate' },
{ label: 'Description', name: 'description' },
{ label: 'Creator', name: 'creator' },
{ label: 'Creator', name: 'creator', template: 'adminUserDisplay' },
{ label: 'Comments', name: 'comments' },
{ label: 'Confirmations', name: 'confirmations' },
{ label: 'Reports', name: 'reports' },
{ label: 'Done', name: 'done' },
{ label: 'Confirmations', name: 'confirmations', template: 'adminUserDisplay' },
{ label: 'Reports', name: 'reports', template: 'adminUserDisplay' },
{ label: 'Done', name: 'done', template: 'adminUserDisplay' },
{ label: 'Type', name: 'type' }
],
color: 'yellow'
@ -161,7 +161,7 @@ AdminConfig = {
requests: {
tableColumns: [
{ label: 'ID', name: '_id' },
{ label: 'User', name: 'requestor' },
{ label: 'User', name: 'requestor', template: 'adminUserDisplay' },
{ label: 'Request', name: 'request' },
{ label: 'Time', name: 'timeRequested' }
],

View File

@ -175,26 +175,52 @@ Meteor.methods({
schools.findOne({
name: input.school
})) {
input.status = Roles.userIsInRole(Meteor.userId(), ['superadmin', 'admin']);
input.admin = Meteor.userId();
Meteor.call('genCode', function(error, result) {
input.code = result;
});
if (input.category != "class" && input.category != "club") {
input.category = "other";
}
input.subscribers = [];
input.moderators = [];
input.banned = [];
if (classes.find({
status: true,
privacy: false,
teacher: input.teacher,
hour: input.hour
}).fetch().length < 1 ||
input.teacher === "" ||
input.hour === "") {
input.status = Roles.userIsInRole(Meteor.userId(), ['superadmin', 'admin']);
input.admin = Meteor.userId();
Meteor.call('genCode', function(error, result) {
input.code = result;
});
if (input.category != "class" && input.category != "club") {
input.category = "other";
}
input.subscribers = [];
input.moderators = [];
input.banned = [];
classes.insert(input, function(err, result) {
Meteor.call('joinClass', [result, input.code]);
});
classes.insert(input, function(err, result) {
Meteor.call('joinClass', [result, input.code]);
});
} else {
throw new Meteor.Error("overlap", "This teacher is already teaching a class elsewhere!");
}
} else {
throw new Meteor.Error("unauthorized", "You are not authorized to complete this action.");
}
},
'approveClass': function(classId) {
if (Roles.userIsInRole(Meteor.userId(), ['superadmin', 'admin'])) {
var currentclass = classes.findOne({
_id: classId
});
classes.update({
_id: classId
}, {
$set: {
status: !currentclass.status
}
});
}
},
// For class admins to get code
'getCode': function(classId) {
var foundclass = classes.findOne({
@ -501,7 +527,7 @@ Meteor.methods({
"hideReport": true
};
if (_.contains(superadmins, currentuser.services.google.email)) {
if (_.contains(superadmins, currentuser.services.google.email)) {
Roles.addUsersToRoles(userId, 'superadmin');
Roles.addUsersToRoles(userId, 'admin');
}