Commit f444af2e authored by Zac Wood's avatar Zac Wood
Browse files

basic courses page

parent fdfb621b
...@@ -10,13 +10,14 @@ ...@@ -10,13 +10,14 @@
// Read Sprockets README (https://github.com/rails/sprockets#sprockets-directives) for details // Read Sprockets README (https://github.com/rails/sprockets#sprockets-directives) for details
// about supported directives. // about supported directives.
// //
//= require rails-ujs
// require turbolinks
//= require FileSaver //= require FileSaver
//= require_tree . //= require cart
// require_tree .
// require jquery3 // require jquery3
// require popper // require popper
// require bootstrap-sprockets // require bootstrap-sprockets
// require rails-ujs
const elementFromString = string => { const elementFromString = string => {
const html = new DOMParser().parseFromString(string, 'text/html'); const html = new DOMParser().parseFromString(string, 'text/html');
...@@ -25,7 +26,7 @@ const elementFromString = string => { ...@@ -25,7 +26,7 @@ const elementFromString = string => {
document.addEventListener('DOMContentLoaded', () => { document.addEventListener('DOMContentLoaded', () => {
this.cart = new Cart(); this.cart = new Cart();
FontAwesome.dom.i2svg(); initGlobalListeners();
}); });
const setSemester = async select => { const setSemester = async select => {
...@@ -33,8 +34,7 @@ const setSemester = async select => { ...@@ -33,8 +34,7 @@ const setSemester = async select => {
location.reload(true); location.reload(true);
}; };
/** Loads FontAwesome icons on load; fixes weird flickering */ const initGlobalListeners = () => {
FontAwesome.dom.watch({ observeMutationsRoot: document }); const semesterSelect = document.getElementById('semester-select');
// document.addEventListener('turbolinks:load', () => { semesterSelect.onchange = () => setSemester(semesterSelect);
// FontAwesome.dom.i2svg(); };
// });
{
"compilerOptions": {
"lib": ["es2015", "dom"]
}
}
...@@ -10,28 +10,42 @@ document.addEventListener('DOMContentLoaded', () => { ...@@ -10,28 +10,42 @@ document.addEventListener('DOMContentLoaded', () => {
header: false, header: false,
events: renderEvents, events: renderEvents,
}); });
// document.getElementById('numSchedules').innerText = window.events.length;
} }
});
// let i = 0; initListeners();
});
const renderEvents = (start, end, timezone, callback) => { const renderEvents = (start, end, timezone, callback) => {
// document.getElementById('currentSchedule').innerText = i + 1;
callback(window.events); callback(window.events);
}; };
// const nextSchedule = () => { const remove = async item => {
// if (i + 1 < window.events.length) i++; await window.cart.addSection({ ...item.dataset });
location.reload(true);
};
/**
* Generates a URL for the current sections in the schedule
* and sets the link in the modal to it.
*/
const setUrlInModal = () => {
document.getElementById('calendar-link').innerText = `${window.location.protocol}//${window.location.hostname}/api/schedules?section_ids=${window.cart._courses.join(',')}`;
};
// $('#calendar').fullCalendar('refetchEvents'); const downloadIcs = async () => {
// }; const response = await fetch(`http://localhost:3000/api/schedules?section_ids=${window.cart._courses.join(',')}`);
const text = await response.text();
const blob = new Blob([text], { type: 'text/calendar;charset=utf-8' });
saveAs(blob, 'GMU Schedule.ics');
};
// const prevSchedule = () => { const addToSystemCalendar = () => {};
// if (i > 0) i--;
// $('#calendar').fullCalendar('refetchEvents'); const initListeners = () => {
const items = Array.from(document.querySelectorAll('.section-item'));
items.forEach(item => (item.onclick = () => remove(item)));
// console.log(window.events[i]); document.getElementById('open-modal-btn').onclick = setUrlInModal;
// }; document.getElementById('download-ics').onclick = downloadIcs;
document.getElementById('add-to-system').onclick = addToSystemCalendar;
};
...@@ -3,20 +3,6 @@ ...@@ -3,20 +3,6 @@
const sectionWithCrn = crn => document.getElementById('search-list').querySelector(`[data-crn="${crn}"]`); const sectionWithCrn = crn => document.getElementById('search-list').querySelector(`[data-crn="${crn}"]`);
// const addCourse = async (event, id) => {
// event && event.stopPropagation();
// const courseCard = document.getElementById(`course-${id}`);
// const title = courseCard.querySelector('.title').innerText;
// const sectionsItems = Array.from(courseCard.querySelectorAll('li'));
// const filtered = sectionsItems.filter(li => {
// return !li.parentNode.classList.contains('pair') || li.dataset.type === 'Lecture';
// });
// for (const section of filtered) {
// await addOrRemoveFromCart(undefined, section);
// }
// }
/** /**
* Either adds or removes a section from the cart depending on * Either adds or removes a section from the cart depending on
* if it is currently in the cart. * if it is currently in the cart.
...@@ -46,10 +32,14 @@ const toggleSections = course => { ...@@ -46,10 +32,14 @@ const toggleSections = course => {
} }
}; };
/** const initSearchListeners = () => {
* Generates a webcal:// URL for the current sections in the schedule const courseCards = Array.from(document.querySelectorAll('.course-card'));
* and sets the link in the modal to it. courseCards.forEach(card => {
*/ card.onclick = () => toggleSections(card);
const setUrlInModal = () => { });
document.getElementById('calendar-link').innerText = `${window.location.protocol}//${window.location.hostname}/api/schedule?crns=${this.cart._courses.join(',')}`;
const sectionItems = Array.from(document.querySelectorAll('.section-item'));
sectionItems.forEach(item => (item.onclick = event => addOrRemoveFromCart(event, item)));
}; };
document.addEventListener('DOMContentLoaded', initSearchListeners);
...@@ -10,7 +10,9 @@ ...@@ -10,7 +10,9 @@
* files in this directory. Styles in this file should be added after the last require_* statement. * files in this directory. Styles in this file should be added after the last require_* statement.
* It is generally better to create a new file per style scope. * It is generally better to create a new file per style scope.
* *
*= require_tree . * require_tree .
*= require cart
*= require navbar
*= require_self *= require_self
*/ */
...@@ -32,26 +34,24 @@ body { ...@@ -32,26 +34,24 @@ body {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
} }
}
.card-body {
.attr-list {
display: flex;
flex-direction: row;
.attr { .attr-list {
.icon { display: flex;
padding-right: 4px; flex-direction: row;
}
align-items: center; .attr {
display: inline-flex; .icon {
white-space: nowrap; padding-right: 4px;
} }
} align-items: center;
display: inline-flex;
white-space: nowrap;
} }
} }
.unpadded { .unpadded {
padding: 0px; padding: 0px;
} }
/* On mouse-over, add a deeper shadow */ /* On mouse-over, add a deeper shadow */
...@@ -59,25 +59,6 @@ body { ...@@ -59,25 +59,6 @@ body {
box-shadow: 0 0 20px rgba(0,0,0,0.4); box-shadow: 0 0 20px rgba(0,0,0,0.4);
} }
.list-group-item:hover {
transition: 0.15s;
background-color: lightgray;
}
.list-group-item.selected {
background-color: lightgreen;
}
.list-group-item.selected:hover {
transition: 0.15s;
background-color: red;
}
.schedule-section-card:hover {
transition: 0.15s;
background-color: red;
}
.align-vertical { .align-vertical {
display: flex; display: flex;
align-items: center; align-items: center;
......
.cart-course {
display: flex; justify-content: space-between;
.title {
min-width: 15%;
}
.crns {
color: gray;
font-size: 10pt;
}
}
#cart { #cart {
display: none; display: none;
} }
......
...@@ -2,3 +2,11 @@ ...@@ -2,3 +2,11 @@
background-color: white; background-color: white;
padding: 16px; padding: 16px;
} }
.section-item.selected {
background-color: white;
}
.section-item.selected:hover {
background-color: red;
}
// Place all the styles related to the search controller here. // Place all the styles related to the search controller here.
// They will automatically be included in application.css. // They will automatically be included in application.css.
// You can use Sass (SCSS) here: http://sass-lang.com/ // You can use Sass (SCSS) here: http://sass-lang.com/
.section-item:hover {
transition: 0.15s;
background-color: lightgray;
}
.section-item.selected {
background-color: lightgreen;
}
.section-item.selected:hover {
transition: 0.15s;
background-color: red;
}
.section-item:hover {
transition: 0.15s;
background-color: red;
}
...@@ -5,10 +5,10 @@ class API::SchedulesController < ApplicationController ...@@ -5,10 +5,10 @@ class API::SchedulesController < ApplicationController
# Render an iCal file containing the schedules of all the # Render an iCal file containing the schedules of all the
# course sections with the given CRNs. # course sections with the given CRNs.
api :GET, '/schedules', 'Generate an iCal file with events for the given CRNs' api :GET, '/schedules', 'Generate an iCal file with events for the given CRNs'
param :crns, String, desc: 'Comma separated list of CRNs to include as events in the calendar', required: true param :section_ids, String, desc: 'Comma separated list of section ids to include as events in the calendar', required: true
def index def index
crns = params["crns"].split ',' ids = params["section_ids"].split ','
@schedule = Schedule.new crns @schedule = Schedule.new ids
render plain: @schedule.to_ical # render a plaintext iCal file render plain: @schedule.to_ical # render a plaintext iCal file
end end
end end
...@@ -7,7 +7,9 @@ class ApplicationController < ActionController::Base ...@@ -7,7 +7,9 @@ class ApplicationController < ActionController::Base
@semester = if cookies.key?(:semester_id) @semester = if cookies.key?(:semester_id)
Semester.find_by(id: cookies[:semester_id]) Semester.find_by(id: cookies[:semester_id])
else else
Semester.find_by(season: 'Spring', year: '2019') sem = Semester.find_by(season: 'Spring', year: '2019')
cookies[:semester_id] = sem.id
sem
end end
end end
......
class CoursesController < ApplicationController
before_action :set_course
def show
end
private
def set_course
@course = Course.find_by_id params[:id]
end
end
...@@ -12,6 +12,10 @@ class Course < ApplicationRecord ...@@ -12,6 +12,10 @@ class Course < ApplicationRecord
validates :subject, presence: true validates :subject, presence: true
validates :semester_id, presence: true validates :semester_id, presence: true
def full_name
"#{subject} #{course_number}"
end
def self.from_subject(base_query, subject) def self.from_subject(base_query, subject)
base_query.where("courses.subject = ?", subject.upcase) base_query.where("courses.subject = ?", subject.upcase)
end end
......
require 'icalendar' require 'icalendar'
require 'time' require 'time'
# Creates a iCal object given a list of CRNs # Creates a iCal object given a list of section ids
class Schedule class Schedule
def initialize(crns) def initialize(ids)
@cal = Icalendar::Calendar.new @cal = Icalendar::Calendar.new
@cal.x_wr_calname = 'GMU Fall 2018' @cal.x_wr_calname = 'GMU Fall 2018'
@course_sections = crns.map do |crn| @course_sections = ids.map { |id| CourseSection.find_by_id id }
CourseSection.find_by crn: crn
end
@course_sections.compact! @course_sections.compact!
load_events load_events
......
<div class="row">
<div class="col-5">
<h1><%= @course.full_name %></h1>
<h4><%= @course.title %></h4>
<div class="d-flex">
<div class="attr-list justify-content-start">
<div class="attr">
<div class="icon">
<i class="fa fa-book"></i>
</div>
3 credits
</div>
&nbsp;&nbsp;&nbsp;
<div class="attr">
<div class="icon">
<i class="fa fa-bars"></i>
</div>
<!-- TODO: FIX THIS -->
<%#= "#{course.section_count}" %> 3 sections
</div>
</div>
</div>
<p><%= @course.description %></p>
</div>
<div class ="col-7">
<%= render partial: 'shared/section', collection: @course.course_sections %>
</div>
</div>
...@@ -5,13 +5,9 @@ ...@@ -5,13 +5,9 @@
<%= csrf_meta_tags %> <%= csrf_meta_tags %>
<%= javascript_include_tag 'masonstrap.min' %> <%= javascript_include_tag 'masonstrap.min' %>
<%= javascript_include_tag 'moment.min' %>
<%= javascript_include_tag 'fullcalendar.min' %>
<%= javascript_include_tag 'FileSaver' %>
<%= javascript_include_tag 'application' %> <%= javascript_include_tag 'application' %>
<%= stylesheet_link_tag 'masonstrap.min' %> <%= stylesheet_link_tag 'masonstrap.min' %>
<%= stylesheet_link_tag 'fullcalendar.min' %>
<%= stylesheet_link_tag 'application' %> <%= stylesheet_link_tag 'application' %>
</head> </head>
......
<!-- <button onclick="nextSchedule()">Next</button> <%= javascript_include_tag 'schedules' %>
<button onclick="prevSchedule()">Prev</button> <%= stylesheet_link_tag 'schedules' %>
<span id="currentSchedule">1</span> / <span id="numSchedules">1</span> -->
<button type="button" class="btn btn-primary" data-toggle="modal" data-target="#exportModal" onclick="setUrlInModal()"> <%= javascript_include_tag 'moment.min' %>
<%= javascript_include_tag 'FileSaver' %>
<%= javascript_include_tag 'fullcalendar.min'%>
<%= stylesheet_link_tag 'fullcalendar.min' %>
<button id="open-modal-btn" type="button" class="btn btn-primary" data-toggle="modal" data-target="#exportModal">
Generate Schedule Generate Schedule
</button> </button>
<div id="calendar"></div> <div id="calendar"></div>
...@@ -46,12 +51,9 @@ ...@@ -46,12 +51,9 @@
To download a .ics file containing your schedule, click the "Download calendar file" button below. To download a .ics file containing your schedule, click the "Download calendar file" button below.
</div> </div>
<div class="modal-footer"> <div class="modal-footer">
<button type="button" class="btn btn-secondary" onclick="downloadIcs()">Download calendar file</button> <button id="download-ics" type="button" class="btn btn-secondary">Download calendar file</button>
<button type="button" class="btn btn-primary" onclick="addToSystemCalendar()">Add to system calendar</button> <button id="add-to-system" type="button" class="btn btn-primary">Add to system calendar</button>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
...@@ -21,3 +21,5 @@ ...@@ -21,3 +21,5 @@
<% end %> <% end %>
<%= javascript_include_tag 'search' %>
<%= stylesheet_link_tag 'search' %>
<% expanded = false unless defined? expanded %> <% expanded = false unless defined? expanded %>
<div class="card" id="course-<%= course.id %>" onclick="toggleSections(this)"> <div class="card course-card" id="course-<%= course.id %>">
<div class="card-header"> <div class="card-header">
<div class="row"> <div class="row">
<div class="col"> <div class="col">
......
...@@ -7,7 +7,7 @@ ...@@ -7,7 +7,7 @@
Schedules Schedules
</a> </a>
&nbsp;&nbsp; &nbsp;&nbsp;
<select onchange="setSemester(this)"> <select id="semester-select">
<% Semester.all.each do |semester| %> <% Semester.all.each do |semester| %>
<option value="<%= semester.id %>" <% if @semester == semester %> selected <% end %> > <option value="<%= semester.id %>" <% if @semester == semester %> selected <% end %> >
<%= "#{semester.season} #{semester.year}" %> <%= "#{semester.season} #{semester.year}" %>
......
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