Commit a4018412 authored by Zac Wood's avatar Zac Wood

Calendar view

parent a09028af
......@@ -27,12 +27,12 @@ document.addEventListener('DOMContentLoaded', () => {
this.schedule = new Schedule();
});
/** Loads FontAwesome icons on load; fixes weird flickering */
document.addEventListener('turbolinks:load', () => {
FontAwesome.dom.i2svg();
});
const setSemester = async select => {
const resp = await fetch(`/sessions/update?semester_id=${select.value}`);
location.reload(true);
};
/** Loads FontAwesome icons on load; fixes weird flickering */
document.addEventListener('turbolinks:load', () => {
FontAwesome.dom.i2svg();
});
class Schedule {
constructor() {
this.isOpen = false;
this._courses = {}; // {title, id, sections: {id, crn}}
const cartData = document.querySelector('#cart-data');
const courses = Array.from(cartData.content.children);
for (const course of courses) {
const { id, title } = course.dataset;
const sections = Array.from(course.children).map(node => ({ ...node.dataset }));
this._courses[id] = { id, title, sections };
}
document.getElementById('course-counter').innerText = Object.keys(this._courses).length;
this._ids = Array.from(document.getElementById('schedule').children).map(e => e.dataset.crn);
}
get ids() {
return this._ids;
get crns() {
return Object.keys(this._courses)
.map(cid => this._courses[cid].sections.map(s => s.crn))
.reduce((prev, curr) => [...prev, ...curr], []);
}
set ids(ids) {
this._ids = ids;
document.getElementById('course-counter').innerText = ids.length;
fetch('/sessions/update?crns=' + ids.join(','), { cache: 'no-store' });
get ids() {
return Object.keys(this._courses)
.map(cid => this._courses[cid].sections.map(s => s.id))
.reduce((prev, curr) => [...prev, ...curr], []);
}
toggle() {
......@@ -30,36 +42,111 @@ class Schedule {
this.isOpen = !this.isOpen;
}
addToSchedule(section) {
if (this.ids.includes(section.dataset.crn)) return;
addCourse(course) {
this._courses[course.id] = course;
const parent = document.querySelector('#schedule');
const current = parent.querySelector(`#schedule-${course.id}`);
const newNode = this._constructCourseNode(course);
if (current !== null) parent.replaceChild(newNode, current);
else parent.appendChild(newNode);
document.getElementById('course-counter').innerText = Object.keys(this._courses).length;
fetch(`/sessions/update?section_ids=${this.ids.join(',')}`, { cache: 'no-store' });
}
removeCourse(id) {
const sectionIds = this._courses[id].sections.map(s => s.id);
for (const sectionId of sectionIds) {
const sectionCard = document.querySelector(`#section-${sectionId}`);
sectionCard && sectionCard.classList.remove('selected');
}
delete this._courses[id];
const parent = document.querySelector('#schedule');
const current = parent.querySelector(`#schedule-${id}`);
parent.removeChild(current);
document.getElementById('course-counter').innerText = Object.keys(this._courses).length;
fetch(`/sessions/update?section_ids=${this.ids.join(',')}`, { cache: 'no-store' });
}
courseContainingSection(id) {
for (const courseId in this._courses) {
const course = this._courses[courseId];
for (const section of course.sections) {
if (section.id == id) return course;
}
}
return undefined;
}
includesSection(id) {
return !!this.courseContainingSection(id);
}
// section: { id, crn }
addSection(section) {
const course = this._courses[section.cid];
if (course) {
course.sections.push(section);
this.ids = [...this.ids, section.dataset.crn];
const courseNode = document.querySelector('#schedule').querySelector(`#schedule-${course.id}`);
const crnList = courseNode.querySelector('.crns');
crnList.innerText = course.sections.map(s => `#${s.crn}`);
section.classList.remove('section-item');
section.classList.remove('selected');
section.classList.add('schedule-section-card');
section.onclick = () => removeFromSchedule(section);
fetch(`/sessions/update?section_ids=${this.ids.join(',')}`, { cache: 'no-store' });
} else {
const courseCard = document.getElementById(`course-${section.cid}`);
const title = courseCard.querySelector('#title').innerText;
document.getElementById('schedule').appendChild(section);
this.addCourse({ title, id: section.cid, sections: [section] });
}
}
removeFromSchedule(id) {
const cart = document.getElementById('schedule');
const section = cart.querySelector(`#section-${id}`);
cart.removeChild(section);
removeSection(section) {
const course = this.courseContainingSection(section.id);
course.sections = course.sections.filter(s => s.id !== section.id);
const schedule = document.querySelector('#schedule');
const courseNode = schedule.querySelector(`#schedule-${course.id}`);
const crnList = courseNode.querySelector('.crns');
if (course.sections.length === 0) {
this.removeCourse(section.cid);
} else {
crnList.innerText = course.sections.map(s => `#${s.crn}`);
}
this.ids = this.ids.filter(_id => _id != id);
fetch(`/sessions/update?section_ids=${this.ids.join(',')}`, { cache: 'no-store' });
}
async downloadIcs() {
const cal = await fetch(`/api/schedules?crns=${this.ids.join(',')}`);
const cal = await fetch(`/api/schedules?crns=${this.crns.join(',')}`);
const text = await cal.text();
var blob = new Blob([text], { type: 'text/calendar;charset=utf-8' });
saveAs(blob, 'test.ics');
}
async addToSystemCalendar() {
const url = `webcal://${window.location.hostname}/api/schedule?crns=${this.ids.join(',')}`;
const url = `webcal://${window.location.hostname}/api/schedule?crns=${this.crns.join(',')}`;
window.open(url, '_self');
}
_constructCourseNode(course) {
let html = `<li id="schedule-${course.id}" class="list-group-item schedule-section-card" onclick="removeCourse(${course.id})">`;
html += `<div style="display: flex; justify-content: space-between;">`;
html += `<b style="min-width: 15%">${course.title}</b>`;
html += `<span class="crns" style="color: gray; font-size: 10pt;">`;
html += course.sections.map(s => `#${s.crn}`).join(', ');
html += `</span>`;
html += `</div>`;
html += `</li>`;
return elementFromString(html);
}
}
const removeCourse = id => {
this.schedule.removeCourse(id);
};
......@@ -3,17 +3,30 @@
const sectionWithCrn = crn => document.getElementById('search-list').querySelector(`[data-crn="${crn}"]`);
const addCourse = (event, id) => {
const courseCard = document.getElementById(`course-${id}`);
const title = courseCard.querySelector('#title').innerText;
const sectionsItems = Array.from(courseCard.querySelectorAll('li'));
const sections = sectionsItems.map(li => ({ ...li.dataset }));
this.schedule.addCourse({ title, id, sections });
sectionsItems.forEach(s => s.classList.add('selected'));
event.stopPropagation();
};
/**
* Either adds or removes a section from the schedule depending on
* if it is currently in the schedule.
*/
const addOrRemoveFromSchedule = (event, section) => {
if (this.schedule.ids.includes(section.dataset.crn)) {
this.schedule.removeFromSchedule(section.dataset.crn);
section.classList.remove('selected');
const addOrRemoveFromSchedule = (event, sectionNode) => {
const section = { ...sectionNode.dataset };
if (this.schedule.includesSection(section.id)) {
this.schedule.removeSection(section);
sectionNode.classList.remove('selected');
} else {
this.schedule.addToSchedule(section.cloneNode(true));
section.classList.add('selected');
this.schedule.addSection(section);
sectionNode.classList.add('selected');
}
event.stopPropagation();
......
......@@ -99,3 +99,12 @@ body {
margin-bottom: 16px;
padding: 12px;
}
#add-course-btn:hover {
background-color: rgba(0,0,0,0.2);
}
#calendar {
background-color: white;
padding: 16px;
}
......@@ -12,15 +12,17 @@ class ApplicationController < ActionController::Base
end
def set_cart
@cart = cookies[:crns].split(',').map do |crn|
s = CourseSection.find_by_crn(crn)
s if s.course.semester == @semester
sections = cookies[:section_ids].split(',').map do |id|
CourseSection.find_by_id(id)
end
@cart.compact!
@cart = sections.group_by do |s|
s.course.id
end
end
def set_cookies
cookies[:crns] = "" if cookies[:crns].nil?
cookies[:section_ids] = "" if cookies[:section_ids].nil?
end
end
......@@ -15,4 +15,19 @@ class SchedulesController < ApplicationController
@schedule = Schedule.new crns
render plain: @schedule.to_ical # render a plaintext iCal file
end
def show
@events = @cart.map do |_cid, sections|
s = sections.first
formatted_date = Date.today.to_s.tr('-', '')
formatted_time = Time.parse(s.start_time).strftime("%H%M%S")
formatted_endtime = Time.parse(s.end_time).strftime("%H%M%S")
{
title: s.name,
start: "#{formatted_date}T#{formatted_time}",
end: "#{formatted_date} #{formatted_endtime}"
}
end
end
end
class SessionsController < ApplicationController
def update
update_cookie :crns
update_cookie :section_ids
update_cookie :semester_id
head :ok
......
module ApplicationHelper
def in_cart?(id)
@cart.select { |_cid, sections| sections.select { |s| s.id == id }.count.positive? }.count.positive?
end
end
......@@ -5,10 +5,13 @@
<%= csrf_meta_tags %>
<%= javascript_include_tag 'masonstrap.min' %>
<%= javascript_include_tag 'moment.min' %>
<%= javascript_include_tag 'fullcalendar.min' %>
<%= javascript_include_tag 'FileSaver' %>
<%= javascript_include_tag 'application' %>
<%= stylesheet_link_tag 'masonstrap.min' %>
<%= stylesheet_link_tag 'masonstrap.min' %>
<%= stylesheet_link_tag 'fullcalendar.min' %>
<%= stylesheet_link_tag 'application' %>
</head>
......
<div id="calendar"></div>
<template id="events" data-events="<%= @events.to_json %>"></template>
<script>
/* const cal = document.querySelector('#calendar');
* var calendar = new Calendar(cal, {
* defaultView: 'agendaWeek'
* }); */
document.addEventListener('DOMContentLoaded', () => {
const eventsJSON = document.querySelector('#events').dataset.events;
const events = JSON.parse(eventsJSON);
console.log(events);
$('#calendar').fullCalendar({
defaultView: 'agendaWeek',
header: { right: 'next'},
events: events
});
});
</script>
......@@ -5,8 +5,3 @@
<p>Please try again!</p>
<% end %>
<%= javascript_tag do %>
document.addEventListener('DOMContentLoaded', () => {
this.search = new Search();
});
<% end %>
......@@ -4,10 +4,20 @@
<div class="col order-1 order-lg-1" id="cart">
<div class="card">
<div class="card-body">
<h3 class="card-title">Your Schedule</h3>
<h3 class="card-title"><%= link_to 'Your Schedule', schedule_path %></h3>
</div>
<ul class="list-group list-group-flush" id="schedule">
<%= render partial: 'shared/section', collection: @cart, locals: { in_cart: true } %>
<% @cart.each do |cid, sections| %>
<% course = Course.find_by_id(cid) %>
<li id="schedule-<%= cid %>" class="list-group-item schedule-section-card" onclick="removeCourse(<%= cid %>)">
<div style="display: flex; justify-content: space-between;">
<b style="min-width: 15%"><%= "#{course.subject} #{course.course_number}" %></b>
<span class="crns" style="color: gray; font-size: 10pt;">
<%= sections.map { |s| "##{s.crn}" }.join(', ') %>
</span>
</div>
</li>
<% end %>
</ul>
<div class="card-body">
<button type="button" class="btn btn-primary" data-toggle="modal" data-target="#exportModal" onclick="setUrlInModal()">
......@@ -17,6 +27,16 @@
</div>
</div>
<template id="cart-data">
<% @cart.each do |cid, sections| %>
<% course = Course.find_by_id cid %>
<div data-id="<%= cid %>" data-title="<%= "#{course.subject} #{course.course_number}" %>">
<% sections.each do |s| %>
<div data-id="<%= s.id %>" data-crn="<%= s.crn %>"></div>
<% end %>
</div>
<% end %>
</template>
</div>
</div>
......
<% expanded = false unless defined? expanded %>
<div class="card" id="course-<%= course.id %>" onclick="toggleSections(this)">
<div class="card-body">
<div>
<h3 style="float: left"><%= "#{course.subject} #{course.course_number}" %></h3>
<h5 style="float: right"><em><%= course.title %></em>. <%= course.credits %> credits.</h5>
</div>
<div class="card-body">
<div style="display: flex; justify-content: space-between">
<h3 id="title"><%= "#{course.subject} #{course.course_number}" %></h3>
<div style="display: flex; flex-direction: column; justify-content: center;">
<div style="display: flex">
<h5><em><%= course.title %></em>. <%= course.credits %> credits.</h5>
&nbsp;&nbsp;&nbsp;
<h4 id="add-course-btn" onclick="addCourse(event, '<%= course.id %>');">
<i class="fas fa-plus" style="color: green"></i>
</h4>
</div>
</div>
</div>
<div style="clear: both"> </div>
<div style="clear: both"> </div>
<p class="description"><%= course.description %></p>
<p class="description"><%= course.description %></p>
<% unless course.prereqs.nil? || course.prereqs.empty? %>
<% first, rest = course.prereqs.split(':') %>
<% prereqs, note = rest.split('.') %>
<p><strong><%= first %>:</strong> <%= prereqs %> <sub><%= note %></sub></p>
<% end %>
<% unless course.prereqs.nil? || course.prereqs.empty? %>
<% first, rest = course.prereqs.split(':') %>
<% prereqs, note = rest.split('.') %>
<p><strong><%= first %>:</strong> <%= prereqs %> <sub><%= note %></sub></p>
<% end %>
<div class="d-block" style="text-align: center">
<p style="margin-bottom:-4px; font-size: 10px;">Expand</p>
<i class="fas fa-chevron-down"></i>
</div>
<div class="d-block" style="text-align: center">
<p style="margin-bottom:-4px; font-size: 10px;">Expand</p>
<i class="fas fa-chevron-down"></i>
</div>
<!-- List of Course Sections -->
<ul class="list-group list-group-flush" id="sections" style="display: <%= expanded ? "block" : "none" %>">
<% if defined?(@instructor) %>
<%= render partial: 'shared/section', collection: course.course_sections.where(instructor: @instructor), locals: { in_cart: false } %>
<% else %>
<%= render partial: 'shared/section', collection: course.course_sections, locals: { in_cart: false } %>
<% end %>
</ul>
</div>
<!-- List of Course Sections -->
<ul class="list-group list-group-flush" id="sections" style="display: <%= expanded ? "block" : "none" %>">
<% if defined?(@instructor) %>
<%= render partial: 'shared/section', collection: course.course_sections.where(instructor: @instructor), locals: {course: course} %>
<% else %>
<%= render partial: 'shared/section', collection: course.course_sections, locals: {course: course} %>
<% end %>
</ul>
</div>
</div>
......@@ -7,7 +7,7 @@
Schedules
</a>
<select onchange="setSemester(this)">
<% for semester in Semester.all %>
<% Semester.all.each do |semester| %>
<option value="<%= semester.id %>" <% if @semester == semester %> selected <% end %> >
<%= "#{semester.season} #{semester.year}" %>
</option>
......
<% if in_cart %>
<li id="section-<%= section.crn %>"
class="list-group-item schedule-section-card"
data-crn="<%= section.crn %>"
onclick="removeFromSchedule(this)"
>
<span style="float:left"><b class="subj"><%= "#{section.name}" %></b>: <%= section.title %></span>
<span style="float:right"><i class="fas fa-map-marker-alt"></i> <%= section.location %></span>
<div style="clear: both"></div>
<span style="float:left"><i class="fas fa-chalkboard-teacher"></i> <%= link_to section.instructor.name, instructor_path(section.instructor) %> </span>
<span style="float:right"><i class="fas fa-clock"></i> <%= "#{section.days}, #{section.start_time}-#{section.end_time}" %></span>
<div style="clear: both"></div>
</li>
<% else %>
<li id="section-<%= section.crn %>"
class="list-group-item section-item <%= "selected" if @cart.include? section %>"
data-crn="<%= section.crn %>"
onclick="addOrRemoveFromSchedule(event, this)"
>
<span style="float:left"><b class="subj"><%= "#{section.name}" %></b>: <%= section.title %></span>
<span style="float:right"><i class="fas fa-map-marker-alt"></i> <%= section.location %></span>
<div style="clear: both"></div>
<span style="float:left"><i class="fas fa-chalkboard-teacher"></i> <%= link_to section.instructor.name, instructor_path(section.instructor) %></span>
<span style="float:right"><i class="fas fa-clock"></i> <%= "#{section.days}, #{section.start_time}-#{section.end_time}" %></span>
<div style="clear: both"></div>
</li>
<% end %>
<li id="section-<%= section.id %>"
class="list-group-item section-item <%= "selected" if in_cart? section.id %>"
data-crn="<%= section.crn %>"
data-id="<%= section.id %>"
data-cid="<%= course.id %>"
onclick="addOrRemoveFromSchedule(event, this)"
>
<span style="float:left"><b class="subj"><%= "#{section.name}" %></b>: <%= section.title %></span>
<span style="float:right"><i class="fas fa-map-marker-alt"></i> <%= section.location %></span>
<div style="clear: both"></div>
<span style="float:left"><i class="fas fa-chalkboard-teacher"></i> <%= link_to section.instructor.name, instructor_path(section.instructor) %></span>
<span style="float:right"><i class="fas fa-clock"></i> <%= "#{section.days}, #{section.start_time}-#{section.end_time}" %></span>
<div style="clear: both"></div>
</li>
......@@ -16,4 +16,7 @@ Rails.application.config.assets.precompile += %w(
schedule.js
masonstrap.min.css
masonstrap.min.js
moment.min.js
fullcalendar.min.js
fullcalendar.min.css
)
......@@ -4,6 +4,7 @@ Rails.application.routes.draw do
get 'sessions/update', as: 'update_session'
resources :instructors, only: [:index, :show]
get 'schedule', to: 'schedules#show', as: 'schedule'
scope :api do # Register /api routes
resources :courses, only: [:index, :show]
......
This source diff could not be displayed because it is too large. You can view the blob instead.
This diff is collapsed.
This diff is collapsed.
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment