diff --git a/.prettierrc b/.prettierrc deleted file mode 100644 index 79dfc3282004bda0fa34ec6b7e91adf7d86093a2..0000000000000000000000000000000000000000 --- a/.prettierrc +++ /dev/null @@ -1,8 +0,0 @@ -{ - "printWidth": 120, - "tabWidth": 4, - "singleQuote": true, - "useTabs": false, - "jsxBracketSameLine": true, - "trailingComma": "es5" -} diff --git a/schedules/.babelrc b/schedules/.babelrc index 75e4c04e4a260234418ae9bf8f7e4a913e89064e..ded31c0d80df45c2c6216ecfb83444689acd0b68 100644 --- a/schedules/.babelrc +++ b/schedules/.babelrc @@ -1,26 +1,18 @@ { "presets": [ - [ - "env", - { - "modules": false, - "targets": { - "browsers": "> 1%", - "uglify": true - }, - "useBuiltIns": true - } - ], - "react" + ["env", { + "modules": false, + "targets": { + "browsers": "> 1%", + "uglify": true + }, + "useBuiltIns": true + }] ], + "plugins": [ "syntax-dynamic-import", "transform-object-rest-spread", - [ - "transform-class-properties", - { - "spec": true - } - ] + ["transform-class-properties", { "spec": true }] ] } diff --git a/schedules/Dockerfile b/schedules/Dockerfile index 55846917c352f99cd815eeb4d35b5a38e77fac78..57231d742ecef48e8e8c3108c1a707382fc5cd34 100644 --- a/schedules/Dockerfile +++ b/schedules/Dockerfile @@ -14,4 +14,3 @@ RUN export SECRET_KEY_BASE=$(rails secret) RUN rails assets:precompile RUN rails db:migrate RUN rails db:seed -RUN rails runner db/load_course_ratings.rb diff --git a/schedules/Gemfile b/schedules/Gemfile index 11aebba81a3163d7a482e1bb2b9a326d6922e39a..f480823224a26295dcb89040ead848b5d0b4840e 100644 --- a/schedules/Gemfile +++ b/schedules/Gemfile @@ -21,9 +21,6 @@ gem 'uglifier' gem 'webpacker', '~> 3.5' -# Access Ruby data from JavaScript -gem 'gon' - # Use Redis adapter to run Action Cable in production # gem 'redis', '~> 4.0' # Use ActiveModel has_secure_password @@ -77,3 +74,7 @@ gem 'apipie-rails' # Markdown for API docs gem 'maruku' + +# gem 'jquery-rails' + +# gem 'font-awesome-sass', '~> 5.3.1' diff --git a/schedules/Gemfile.lock b/schedules/Gemfile.lock index 20a4d3e065178367cbc81e4df9ce9fd9a9acfa11..0d19b52140ed78229517d09ac7a34325c8815866 100644 --- a/schedules/Gemfile.lock +++ b/schedules/Gemfile.lock @@ -64,10 +64,6 @@ GEM ffi (1.9.25) globalid (0.4.1) activesupport (>= 4.2.0) - gon (6.2.1) - actionpack (>= 3.0) - multi_json - request_store (>= 1.0) httparty (0.16.3) mime-types (~> 3.0) multi_xml (>= 0.5.2) @@ -148,8 +144,6 @@ GEM rb-fsevent (0.10.3) rb-inotify (0.9.10) ffi (>= 0.5.0, < 2) - request_store (1.4.1) - rack (>= 1.4) rubocop (0.58.2) jaro_winkler (~> 1.5.1) parallel (~> 1.10) @@ -222,7 +216,6 @@ DEPENDENCIES apipie-rails byebug capybara (~> 2.13) - gon httparty icalendar jbuilder (~> 2.5) diff --git a/schedules/app/assets/images/favicon-32x32.png b/schedules/app/assets/images/favicon-32x32.png deleted file mode 100644 index 750133724121963b8510a8029d59ffab679f5d29..0000000000000000000000000000000000000000 Binary files a/schedules/app/assets/images/favicon-32x32.png and /dev/null differ diff --git a/schedules/app/assets/images/favicon.ico b/schedules/app/assets/images/favicon.ico deleted file mode 100644 index 6b44450f301ddd680033bab34095dddd681bf6ba..0000000000000000000000000000000000000000 Binary files a/schedules/app/assets/images/favicon.ico and /dev/null differ diff --git a/schedules/app/assets/javascripts/about.js b/schedules/app/assets/javascripts/about.js deleted file mode 100644 index dee720facdcda703fe5067aef3fcec32c121f50b..0000000000000000000000000000000000000000 --- a/schedules/app/assets/javascripts/about.js +++ /dev/null @@ -1,2 +0,0 @@ -// Place all the behaviors and hooks related to the matching controller here. -// All this logic will automatically be available in application.js. diff --git a/schedules/app/assets/javascripts/course_sections.js b/schedules/app/assets/javascripts/course_sections.js deleted file mode 100644 index dee720facdcda703fe5067aef3fcec32c121f50b..0000000000000000000000000000000000000000 --- a/schedules/app/assets/javascripts/course_sections.js +++ /dev/null @@ -1,2 +0,0 @@ -// Place all the behaviors and hooks related to the matching controller here. -// All this logic will automatically be available in application.js. diff --git a/schedules/app/assets/stylesheets/about.scss b/schedules/app/assets/stylesheets/about.scss deleted file mode 100644 index fda2fc29d42a14a05412f24c046b13e7e71e6bee..0000000000000000000000000000000000000000 --- a/schedules/app/assets/stylesheets/about.scss +++ /dev/null @@ -1,3 +0,0 @@ -// Place all the styles related to the about controller here. -// They will automatically be included in application.css. -// You can use Sass (SCSS) here: http://sass-lang.com/ diff --git a/schedules/app/assets/stylesheets/application.scss b/schedules/app/assets/stylesheets/application.scss index 3cff1e6ff9a6a27699663626b7b0d42440754546..ea208ff5077359852233de618cabdf826a7f2496 100644 --- a/schedules/app/assets/stylesheets/application.scss +++ b/schedules/app/assets/stylesheets/application.scss @@ -20,11 +20,6 @@ body { background-color: #E4E4E4; } -#page { - padding-top: 16px; - padding-bottom: 16px; -} - .card { margin-bottom: 12px; @@ -95,39 +90,3 @@ body { .quick-add-header { margin-top: 32px; } - -%flex-display { - display: inline-flex; -} -.star-rating { - @extend %flex-display; - align-items: center; - font-size: 1em; - justify-content: flex-start; -} -.back-stars { - @extend %flex-display; - position: relative; - text-shadow: 4px 4px 10px #843a3a; -} -.front-stars { - @extend %flex-display; - color: #FFBC0B; - overflow: hidden; - position: absolute; - text-shadow: 2px 2px 5px #d29b09; - top: 0; -} - -.full-width { - width: 90vw; - position: relative; - left: 50%; - right: 50%; - margin-left: -45vw; - margin-right: -45vw; -} - -// .jumbotron { -// color: #FFFFFF -// } diff --git a/schedules/app/assets/stylesheets/course_sections.scss b/schedules/app/assets/stylesheets/course_sections.scss deleted file mode 100644 index db3813be87772b9ba9d14d2093a3ba33928e830b..0000000000000000000000000000000000000000 --- a/schedules/app/assets/stylesheets/course_sections.scss +++ /dev/null @@ -1,3 +0,0 @@ -// Place all the styles related to the course_sections controller here. -// They will automatically be included in application.css. -// You can use Sass (SCSS) here: http://sass-lang.com/ diff --git a/schedules/app/assets/stylesheets/navbar.scss b/schedules/app/assets/stylesheets/navbar.scss index fd9e16ec3cdee6e71845b5e00b6b03c9cbb15def..d5516d52b1471fdf38cf9ccd2a6595753eb77fa7 100644 --- a/schedules/app/assets/stylesheets/navbar.scss +++ b/schedules/app/assets/stylesheets/navbar.scss @@ -18,7 +18,6 @@ #semester-select { min-width: 100px; - margin-right: 8px; } #cart-button { diff --git a/schedules/app/controllers/about_controller.rb b/schedules/app/controllers/about_controller.rb deleted file mode 100644 index 65749b384033e026e458fc119b470695576d5648..0000000000000000000000000000000000000000 --- a/schedules/app/controllers/about_controller.rb +++ /dev/null @@ -1,3 +0,0 @@ -class AboutController < ApplicationController - def index; end -end diff --git a/schedules/app/controllers/api/course_sections_controller.rb b/schedules/app/controllers/api/course_sections_controller.rb index 8f4f75aefa0e2af8f29fdfe1df9762649168a0f6..3e6644de30c0c1cdfde305147a9cbaed9283e5d5 100644 --- a/schedules/app/controllers/api/course_sections_controller.rb +++ b/schedules/app/controllers/api/course_sections_controller.rb @@ -28,6 +28,7 @@ class API::CourseSectionsController < ApplicationController @sections = @sections.where('UPPER(instructors.name) LIKE UPPER(?)', "%#{params[:instructor]}%") end + # @sections = CourseSection.fetch(params).all res = @sections.map do |s| { id: s.id, @@ -37,9 +38,6 @@ class API::CourseSectionsController < ApplicationController crn: s.crn, title: s.title, instructor_name: s.instructor_name, - instructor_url: instructor_url(s.instructor), - teaching_rating: s.instructor.rating, - course_rating: s.course_rating, section_type: s.section_type, start_date: s.start_date, end_date: s.end_date, diff --git a/schedules/app/controllers/application_controller.rb b/schedules/app/controllers/application_controller.rb index 03a6797eb87c899ac7e0d67e3b8f2ae5e7e2eea5..a3d8489cc202486c7fc8715271e1adec64964153 100644 --- a/schedules/app/controllers/application_controller.rb +++ b/schedules/app/controllers/application_controller.rb @@ -1,4 +1,40 @@ # Configures the application. class ApplicationController < ActionController::Base - include BySemester + protect_from_forgery with: :null_session + + # On each request, set the semester and cart. + before_action :set_semester, :set_cart + + # Every page needs to know what semester it should load data from. + # set_semester checks both the semester_id query parameter and the user's cookies + # to look for a semester id and loads whatever it finds into @semester. + # + # By default, load the most recent semester. + def set_semester + if params.key?(:semester_id) + @semester = Semester.find_by_id params[:semester_id] + cookies[:semester_id] = @semester.id + elsif cookies[:semester_id].nil? + @semester = Semester.first + cookies[:semester_id] = @semester.id + else + @semester = Semester.find_by_id cookies[:semester_id] + end + end + + # The user's cart is stored as a JSON-encoded list of CRNs. + # set_cart sets the @cart variable, which is a list of the sections represented by the CRNs. + def set_cart + # set the cart cookie to be empty if it doesn't already exist + cookies.permanent[:cart] = "[]" if cookies.permanent[:cart].nil? + + # decode the JSON list into an array + @cart = JSON.parse(cookies.permanent[:cart]) + + # get rid of any invalid CRNs + @cart = @cart.reject { |crn| CourseSection.find_by_crn(crn).nil? } + + # set the cookie to the JSON-encoded list of valid sections + cookies.permanent[:cart] = @cart.to_json + end end diff --git a/schedules/app/controllers/concerns/by_semester.rb b/schedules/app/controllers/concerns/by_semester.rb deleted file mode 100644 index c890a8ca60e3bbd5d426d067b7730426f61c3725..0000000000000000000000000000000000000000 --- a/schedules/app/controllers/concerns/by_semester.rb +++ /dev/null @@ -1,25 +0,0 @@ -# BySemester contains logic for setting the current request's Semester. -module BySemester - extend ActiveSupport::Concern - - included do - before_action :set_semester - end - - # This page needs to know what semester it should load data from. - # set_semester checks both the semester_id query parameter and the current session - # to look for a semester id and loads whatever it finds into @semester. - # - # By default, load the most recent semester. - def set_semester - if params.key?(:semester_id) - @semester = Semester.find_by_id(params[:semester_id]) - session[:semester_id] = @semester.id - elsif session[:semester_id].nil? - @semester = Semester.first - session[:semester_id] = @semester.id - else - @semester = Semester.find_by_id(session[:semester_id]) - end - end -end diff --git a/schedules/app/controllers/course_sections_controller.rb b/schedules/app/controllers/course_sections_controller.rb deleted file mode 100644 index 87ca5daf14b98572f6bbb807b159fe3dedda074f..0000000000000000000000000000000000000000 --- a/schedules/app/controllers/course_sections_controller.rb +++ /dev/null @@ -1,5 +0,0 @@ -class CourseSectionsController < ApplicationController - def show - @section = CourseSection.find_by_id(params[:id]) - end -end diff --git a/schedules/app/controllers/instructors_controller.rb b/schedules/app/controllers/instructors_controller.rb index 501b3a8283183a1f7521f505c321099186df7cc0..c7e067815d1fd695b490ce6d0aa8506aa85d62b9 100644 --- a/schedules/app/controllers/instructors_controller.rb +++ b/schedules/app/controllers/instructors_controller.rb @@ -7,11 +7,14 @@ class InstructorsController < ApplicationController @instructor = Instructor.find_by_id(params[:id]) # find the courses being taught this semester - sections = CourseSection.where(instructor: @instructor) - @semesters = sections.group_by do |s| - s.semester.to_s - end + sections = CourseSection.where(instructor: @instructor, semester: @semester) + @courses = Course.build_set(sections) - @rating = { teaching: @instructor.rating, respect: @instructor.rating(6) } + # build the list of courses the instructor has taught in the past + @past = [] + @instructor.course_sections.map(&:course).each do |c| + @past << c unless @past.select { |past| past.full_name == c.full_name }.count.positive? + end + @past.sort_by!(&:full_name) end end diff --git a/schedules/app/controllers/schedules_controller.rb b/schedules/app/controllers/schedules_controller.rb index 5e03370b9cee8319c0a923f0a8ae0337f95cc8a7..ab830338ba6f5ffaebf6199c11ca4b35a9ebcb2a 100644 --- a/schedules/app/controllers/schedules_controller.rb +++ b/schedules/app/controllers/schedules_controller.rb @@ -2,32 +2,29 @@ class SchedulesController < ApplicationController include SchedulesHelper - def show; end + def show + valid_crns = @cart.reject { |crn| + s = CourseSection.find_by_crn(crn) + s.nil? + } - def view - @all = params[:crns].split(',').map { |crn| + @all = valid_crns.map { |crn| CourseSection.latest_by_crn(crn) } - @all.reject!(&:nil?) @without_online = @all.reject { |s| s.start_time == "TBA" || s.end_time == "TBA" } @events = generate_fullcalender_events(@without_online) end - def events - @cart = params[:crns].split(',') - .map { |crn| CourseSection.latest_by_crn(crn) } - .reject(&:nil?) - - @without_online = @cart.reject { |s| + def view + @all = params[:crns].split(',').map { |crn| + CourseSection.latest_by_crn(crn) + } + @all.reject!(&:nil?) + @without_online = @all.reject { |s| s.start_time == "TBA" || s.end_time == "TBA" } - @events = generate_fullcalender_events(@without_online) - sections = @cart.map do |s| - s.serializable_hash.merge(instructor_name: s.instructor.name, instructor_url: instructor_url(s.instructor)) - end - render json: { events: @events, sections: sections } end end diff --git a/schedules/app/controllers/search_controller.rb b/schedules/app/controllers/search_controller.rb index 3d2c1523c3a7364cfd8106c3c1a384f45aa196a8..fd76ad657345c7be58fa0f641725e57a85bbda5d 100644 --- a/schedules/app/controllers/search_controller.rb +++ b/schedules/app/controllers/search_controller.rb @@ -2,57 +2,8 @@ class SearchController < ApplicationController def index redirect_to(home_url) unless params[:query].length > 1 - if params[:query].casecmp('god').zero? - bell = Instructor.find_by_name('Jonathan Bell') - redirect_to(instructor_url(bell)) - end - - @instructors = nil - @courses = nil - - /[[:alpha:]]{2,4} \d{3}/.match(params[:query]) do |m| - subj, num = m[0].split(' ') - course = Course.find_by(subject: subj.upcase, course_number: num) - redirect_to(course_url(course)) unless course.nil? - end - - /[[:alpha:]]{2,4}/i.match(params[:query]) do |m| - @courses = Course.where(subject: m[0].upcase) - .joins(:course_sections) - .merge(CourseSection.in_semester(@semester)) - .uniq - - if @courses.empty? - @courses = Course.where("(courses.title LIKE ?)", "%#{params[:query]}%") - .joins(:course_sections) - .merge(CourseSection.in_semester(@semester)) - .uniq - - other = Course.where("(courses.description LIKE ?)", "%#{params[:query]}%") - .joins(:course_sections) - .merge(CourseSection.in_semester(@semester)) - .uniq - - @courses = [*@courses, *other].uniq - - @instructors = Instructor.named(params[:query]) - end - - @courses.map! do |c| - c.serializable_hash.merge(url: course_url(c)) - end - gon.courses = @courses - gon.instructors = @instructors - end - - /[0-9]{5}/.match(params[:query]) do |m| - redirect_to(course_url(CourseSection.latest_by_crn(m[0]).course)) - end - - if @courses&.count == 1 && @instructors&.count&.zero? - redirect_to course_url(@courses.first["id"]) - elsif @courses&.count&.zero? && @instructors&.count == 1 - redirect_to instructor_url(@instructors.first) - end + results = SearchHelper::GenericItem.fetchall(String.new(params[:query]), semester: @semester).group_by(&:type) + @instructors = results[:instructor]&.map(&:data) + @courses = results[:course]&.map(&:data) end end diff --git a/schedules/app/helpers/about_helper.rb b/schedules/app/helpers/about_helper.rb deleted file mode 100644 index 68e69aee147ab5e1e002da1791f688e7e2953236..0000000000000000000000000000000000000000 --- a/schedules/app/helpers/about_helper.rb +++ /dev/null @@ -1,2 +0,0 @@ -module AboutHelper -end diff --git a/schedules/app/helpers/course_sections_helper.rb b/schedules/app/helpers/course_sections_helper.rb deleted file mode 100644 index 92a1c1d273d30b2b524eeb637260d4e51f075c77..0000000000000000000000000000000000000000 --- a/schedules/app/helpers/course_sections_helper.rb +++ /dev/null @@ -1,2 +0,0 @@ -module CourseSectionsHelper -end diff --git a/schedules/app/helpers/schedules_helper.rb b/schedules/app/helpers/schedules_helper.rb index adb10ee279291672c8399e1902e40523948c9370..e3758aefb39eff33f853a43621114dde5396aec8 100644 --- a/schedules/app/helpers/schedules_helper.rb +++ b/schedules/app/helpers/schedules_helper.rb @@ -19,9 +19,7 @@ module SchedulesHelper { title: s.name, start: "#{formatted_date}T#{time}", - end: "#{formatted_date}T#{endtime}", - crn: s.crn, - active: true + end: "#{formatted_date}T#{endtime}" } end end.flatten diff --git a/schedules/app/helpers/search_helper.rb b/schedules/app/helpers/search_helper.rb index e67bdd632e4aca362587966d60a791accccb5f8f..ce0dedf97a557eae7a6693f92287413d8945f7a9 100644 --- a/schedules/app/helpers/search_helper.rb +++ b/schedules/app/helpers/search_helper.rb @@ -1,4 +1,8 @@ module SearchHelper + def in_cart?(crn) + @cart.include? crn.to_s + end + class GenericQueryData attr_reader :semester attr_reader :sort_mode diff --git a/schedules/app/javascript/jsconfig.json b/schedules/app/javascript/jsconfig.json deleted file mode 100644 index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..0000000000000000000000000000000000000000 diff --git a/schedules/app/javascript/packs/application.js b/schedules/app/javascript/packs/application.js index 536d77f4116ac813b6b69b138970944720cdda8e..1c9b6581a7dd77e5357966aa2f10aad1cc23bd2f 100644 --- a/schedules/app/javascript/packs/application.js +++ b/schedules/app/javascript/packs/application.js @@ -10,9 +10,6 @@ import '@babel/polyfill'; import 'url-polyfill'; -import React from 'react'; -import Cart from 'src/Cart'; - const elementFromString = string => { const html = new DOMParser().parseFromString(string, 'text/html'); return html.body.firstChild; diff --git a/schedules/app/javascript/packs/home.js b/schedules/app/javascript/packs/home.js deleted file mode 100644 index 99a2030c793ceebec23311504b1955ac8b4accd3..0000000000000000000000000000000000000000 --- a/schedules/app/javascript/packs/home.js +++ /dev/null @@ -1,17 +0,0 @@ -import React from 'react'; -import ReactDOM from 'react-dom'; -import Cart from 'src/Cart'; -import QuickAdd from 'src/QuickAdd'; - -document.addEventListener('DOMContentLoaded', () => { - //const calendarUrl = `${window.location.protocol}//${window.location.hostname}${window.location.port == 3000 ? ':3000' : ''}/schedule`; - const calendarUrl = '/schedule'; - ReactDOM.render( - { - window.location.href = calendarUrl; - }} - />, - document.getElementById('quick-add') - ); -}); diff --git a/schedules/app/javascript/packs/instructor.js b/schedules/app/javascript/packs/instructor.js deleted file mode 100644 index 759e4799d659f6bae99534110cc096788a4a33fa..0000000000000000000000000000000000000000 --- a/schedules/app/javascript/packs/instructor.js +++ /dev/null @@ -1,47 +0,0 @@ -// /** -// * Either adds or removes a section from the cart depending on -// * if it is currently in the cart. -// */ - -import $ from 'jquery'; -import Cart from 'src/Cart'; - -const addOrRemoveFromCart = async (event, sectionNode) => { - event && event.stopPropagation(); - if (event.target.tagName === 'A') return; - const section = { ...sectionNode.dataset }; - - Cart.toggleCrn(section.crn); - const icon = $(sectionNode.querySelector('.add-remove-btn #icon')); - const text = sectionNode.querySelector('.add-remove-btn .text'); - if (Cart.includesCrn(section.crn)) { - icon.addClass('fa-minus').removeClass('fa-plus'); - text.innerText = 'Remove'; - } else { - icon.addClass('fa-plus').removeClass('fa-minus'); - text.innerText = 'Add'; - } -}; - -const initSearchListeners = () => { - const sectionItems = Array.from(document.querySelectorAll('.section-item')); - sectionItems.forEach(item => { - item.onclick = event => addOrRemoveFromCart(event, item); - }); - - setTimeout(() => { - sectionItems.forEach(item => { - const icon = $(item.querySelector('.add-remove-btn #icon')); - const text = item.querySelector('.add-remove-btn .text'); - if (Cart.includesCrn(item.dataset.crn)) { - icon.addClass('fa-minus').removeClass('fa-plus'); - text.innerText = 'Remove'; - } else { - icon.addClass('fa-plus').removeClass('fa-minus'); - text.innerText = 'Add'; - } - }); - }, 100); -}; - -document.addEventListener('DOMContentLoaded', initSearchListeners); diff --git a/schedules/app/javascript/packs/schedules.js b/schedules/app/javascript/packs/schedules.js index cda21119c55262ff1338acdd4df7cfac63ed594c..1745879cad73acf4282b835664314eccb22a43b2 100644 --- a/schedules/app/javascript/packs/schedules.js +++ b/schedules/app/javascript/packs/schedules.js @@ -1,15 +1,45 @@ -import React from 'react'; -import ReactDOM from 'react-dom'; -import Cart from 'src/Cart'; +import Cart from 'src/cart'; import { saveAs } from 'file-saver'; import html2canvas from 'html2canvas'; import $ from 'jquery'; -import CalendarPage from 'src/CalendarPage'; +import 'fullcalendar'; +import 'moment'; document.addEventListener('DOMContentLoaded', () => { - ReactDOM.render(, document.getElementById('root')); + const eventsTemplate = document.querySelector('#events'); + if (eventsTemplate) { + const eventsJSON = eventsTemplate.dataset.events; + const events = JSON.parse(eventsJSON); + window.events = events; + $('#calendar').fullCalendar({ + defaultDate: new Date(2019, 0, 14), + defaultView: 'agendaWeek', + header: false, + events: renderEvents, + columnHeaderFormat: 'dddd', + allDaySlot: false, + }); + } + initListeners(); }); +const renderEvents = (start, end, timezone, callback) => { + callback(window.events); +}; + +const remove = async item => { + await Cart.toggleSection({ ...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?crns=${Cart._courses.join(',')}`; +}; + const downloadIcs = async () => { const response = await fetch(`${window.location.protocol}//${window.location.hostname}/api/schedules?crns=${Cart._courses.join(',')}`); const text = await response.text(); @@ -39,7 +69,7 @@ const initListeners = () => { document.getElementById('save-image').onclick = saveImage; document.getElementById('share-url').innerText = `${window.location.protocol}//${window.location.hostname}/schedule/view?crns=${Cart._courses.join(',')}`; - document.getElementById('share-url').href = `${window.location.protocol}//${window.location.hostname}/schedule/view?crns=${Cart._courses.join(',')}`; + document.getElementById('share-url').href= `${window.location.protocol}//${window.location.hostname}/schedule/view?crns=${Cart._courses.join(',')}`; }; if (!HTMLCanvasElement.prototype.toBlob) { diff --git a/schedules/app/javascript/packs/schedules_view.js b/schedules/app/javascript/packs/schedules_view.js index 59134a92608d2601f139d4e07701a3b4d85f4533..94c00d1d4c532a4123cd9c629ab9ce1fd5b8e501 100644 --- a/schedules/app/javascript/packs/schedules_view.js +++ b/schedules/app/javascript/packs/schedules_view.js @@ -1,4 +1,4 @@ -import Cart from 'src/Cart'; +import Cart from 'src/cart'; import { saveAs } from 'file-saver'; import html2canvas from 'html2canvas'; import $ from 'jquery'; diff --git a/schedules/app/javascript/packs/search.js b/schedules/app/javascript/packs/search.js index 412ead09be92a6b7a5c7d7ba28a86d74c693145d..aea5ef219f42c684fd5b11b4732b6b0a7ccf1c09 100644 --- a/schedules/app/javascript/packs/search.js +++ b/schedules/app/javascript/packs/search.js @@ -1,30 +1,54 @@ // Place all the behaviors and hooks related to the matching controller here. // All this logic will automatically be available in application.js. -// import Cart from 'src/cart'; +import Cart from 'src/cart'; -// /** -// * Toggles the display of the schedule -// */ -// const toggleSections = course => { -// const sections = course.querySelector('.sections'); -// const chev = $(course.querySelector('#course-chevron')); -// const label = course.querySelector('#chevron-label'); +/** + * Either adds or removes a section from the cart depending on + * if it is currently in the cart. + */ +const addOrRemoveFromCart = async (event, sectionNode) => { + event && event.stopPropagation(); + const section = { ...sectionNode.dataset }; -// if (sections.style.display === 'flex') { -// sections.style.display = 'none'; -// chev.addClass('fa-chevron-down').removeClass('fa-chevron-up'); -// label.innerText = 'Expand'; -// } else { -// sections.style.display = 'flex'; -// chev.addClass('fa-chevron-up').removeClass('fa-chevron-down'); -// label.innerText = 'Minimize'; -// } -// }; + await Cart.toggleSection(section); + const icon = $(sectionNode.querySelector('.add-remove-btn #icon')); + const text = sectionNode.querySelector('.add-remove-btn .text'); + if (Cart.includesSection(section)) { + icon.addClass('fa-minus').removeClass('fa-plus'); + text.innerText = 'Remove'; + } else { + icon.addClass('fa-plus').removeClass('fa-minus'); + text.innerText = 'Add'; + } +}; -import React from 'react'; -import ReactDOM from 'react-dom'; -import SearchList from 'src/SearchList'; +/** + * Toggles the display of the schedule + */ +const toggleSections = course => { + const sections = course.querySelector('.sections'); + const chev = $(course.querySelector('#course-chevron')); + const label = course.querySelector('#chevron-label'); -document.addEventListener('DOMContentLoaded', () => { - ReactDOM.render(, document.getElementById('root')); -}); + if (sections.style.display === 'flex') { + sections.style.display = 'none'; + chev.addClass('fa-chevron-down').removeClass('fa-chevron-up'); + label.innerText = 'Expand'; + } else { + sections.style.display = 'flex'; + chev.addClass('fa-chevron-up').removeClass('fa-chevron-down'); + label.innerText = 'Minimize'; + } +}; + +const initSearchListeners = () => { + const courseCards = Array.from(document.querySelectorAll('.course-card')); + courseCards.forEach(card => { + card.onclick = () => toggleSections(card); + }); + + const sectionItems = Array.from(document.querySelectorAll('.section-item')); + sectionItems.forEach(item => (item.onclick = event => addOrRemoveFromCart(event, item))); +}; + +document.addEventListener('DOMContentLoaded', initSearchListeners); diff --git a/schedules/app/javascript/src/Calendar.jsx b/schedules/app/javascript/src/Calendar.jsx deleted file mode 100644 index 551d837d7899ffc57b48729190d4e1e02d1650f5..0000000000000000000000000000000000000000 --- a/schedules/app/javascript/src/Calendar.jsx +++ /dev/null @@ -1,32 +0,0 @@ -import React from 'react'; -import BigCalendar from 'react-big-calendar'; -import Toolbar from 'src/Toolbar'; -import moment from 'moment'; -import '!style-loader!css-loader!react-big-calendar/lib/css/react-big-calendar.css'; -import withSizes from 'react-sizes'; - -const localizer = BigCalendar.momentLocalizer(moment); - -const Calendar = props => ( -
- localizer.format(date, 'ddd', culture), - dayHeaderFormat: (date, culture, localizer) => localizer.format(date, 'ddd', culture), - dayRangeHeaderFormat: () => '', - }} - style={{ height: '75vh' }} - /> -
-); - -export default Calendar; diff --git a/schedules/app/javascript/src/CalendarPage.jsx b/schedules/app/javascript/src/CalendarPage.jsx deleted file mode 100644 index 07035f820bed9005d00458c685c64c70707b81e9..0000000000000000000000000000000000000000 --- a/schedules/app/javascript/src/CalendarPage.jsx +++ /dev/null @@ -1,54 +0,0 @@ -import React from 'react'; -import Calendar from 'src/Calendar'; -import Cart from 'src/Cart'; -import SectionList from 'src/SectionList'; -import QuickAdd from 'src/QuickAdd'; -import moment from 'moment'; - -export default class CalendarPage extends React.Component { - state = { events: [], sections: [] }; - - constructor(props) { - super(props); - this.loadEvents(); - } - - loadEvents = async () => { - const response = await fetch(`/schedule/events?crns=${Cart.crns.join(',')}`); - const json = await response.json(); - this.setState({ ...json }); - Cart.crns = json.sections.map(s => s.crn); - }; - - events = () => { - return this.state.events.filter(e => e.active).map(e => ({ ...e, start: moment(e.start).toDate(), end: moment(e.end).toDate() })); - }; - - toggleSection = crn => { - const events = this.state.events.map(e => ({ ...e, active: e.crn == crn ? !e.active : e.active })); - this.setState({ events }); - }; - - removeAll = () => { - Cart.crns = []; - location.reload(); - }; - - render() { - return ( -
- - {this.state.sections.length > 0 ? ( -
-

Your Schedule

{' '} - -
- ) : null} - - -
- ); - } -} diff --git a/schedules/app/javascript/src/Chevron.jsx b/schedules/app/javascript/src/Chevron.jsx deleted file mode 100644 index 2b6e4979bd0d748617368e501f4d3fd40f8fd815..0000000000000000000000000000000000000000 --- a/schedules/app/javascript/src/Chevron.jsx +++ /dev/null @@ -1,28 +0,0 @@ -import React from 'react'; - -export default class Chevron extends React.Component { - constructor(props) { - super(props); - } - - render() { - const base = { display: 'block', textAlign: 'center' }; - return ( -
-
-

- Minimize -

- -
- -
-

- Expand -

- -
-
- ); - } -} diff --git a/schedules/app/javascript/src/Course.jsx b/schedules/app/javascript/src/Course.jsx deleted file mode 100644 index 3c60762dc56afeed0041cb87bba178981ab6561f..0000000000000000000000000000000000000000 --- a/schedules/app/javascript/src/Course.jsx +++ /dev/null @@ -1,74 +0,0 @@ -import React from 'react'; - -import SectionList from 'src/SectionList'; - -export default class Course extends React.Component { - constructor(props) { - super(props); - this.state = { expanded: false, sections: [] }; - } - - async onClick() { - if (this.state.sections.length === 0) { - const resp = await fetch(`/api/course_sections?course_id=${this.props.id}`); - const json = await resp.json(); - this.setState({ sections: json }); - } - this.setState({ expanded: !this.state.expanded }); - } - - prereqs = () => { - if (this.props.prereqs) { - const [first, rest] = this.props.prereqs.split(':'); - const [reqs, note] = rest.split('.'); - return ( -

- {first} - {reqs} - {note} -

- ); - } - return
; - }; - - // <% first, rest = course.prereqs.split(':') %> - // <% prereqs, note = rest.split('.') %> - //

<%= first %>: <%= prereqs %> <%= note %>

- render() { - const { id, subject, course_number, title, credits, description, url } = this.props; - - return ( -
this.onClick()}> -
- -
-
- {title} -
-
-
-
- -
- {credits} credits -
-
-
-
-
-

{description}

- {this.prereqs()} -
- -
-
- ); - } -} diff --git a/schedules/app/javascript/src/CourseList.jsx b/schedules/app/javascript/src/CourseList.jsx deleted file mode 100644 index 9962b9d04ec6425faa26141d8dce438f9b23b1ac..0000000000000000000000000000000000000000 --- a/schedules/app/javascript/src/CourseList.jsx +++ /dev/null @@ -1,18 +0,0 @@ -import React from 'react'; -import Course from 'src/Course'; - -export default class CourseList extends React.Component { - constructor(props) { - super(props); - } - - render() { - return ( -
- {this.props.courses.map(course => ( - - ))} -
- ); - } -} diff --git a/schedules/app/javascript/src/InstructorCard.jsx b/schedules/app/javascript/src/InstructorCard.jsx deleted file mode 100644 index 6b16441d6928f52e8f3cb5fa09b1f445d98bca23..0000000000000000000000000000000000000000 --- a/schedules/app/javascript/src/InstructorCard.jsx +++ /dev/null @@ -1,15 +0,0 @@ -import React from 'react'; - -export default class InstructorCard extends React.Component { - render() { - const inst = this.props.instructor; - return ( - - ); - } -} diff --git a/schedules/app/javascript/src/InstructorList.jsx b/schedules/app/javascript/src/InstructorList.jsx deleted file mode 100644 index 2845e453d1bccb49456eed6a2517968769426242..0000000000000000000000000000000000000000 --- a/schedules/app/javascript/src/InstructorList.jsx +++ /dev/null @@ -1,8 +0,0 @@ -import React from 'react'; -import InstructorCard from 'src/InstructorCard'; - -export default class InstructorList extends React.Component { - render() { - return
{this.props.instructors && this.props.instructors.map(i => )}
; - } -} diff --git a/schedules/app/javascript/src/QuickAdd.jsx b/schedules/app/javascript/src/QuickAdd.jsx deleted file mode 100644 index b7e25cc3f08f90ea102c008099aa1b48a477cb9e..0000000000000000000000000000000000000000 --- a/schedules/app/javascript/src/QuickAdd.jsx +++ /dev/null @@ -1,45 +0,0 @@ -import React from 'react'; -import Cart from 'src/Cart'; - -export default class QuickAdd extends React.Component { - constructor(props) { - super(props); - this.state = { crnString: '' }; - } - - add = e => { - e.preventDefault(); - const crns = this.state.crnString.split(','); - crns.forEach(c => c.length === 5 && Cart.addCrn(c)); - this.props.loadCalendar(); - }; - - render() { - return ( -
-

Quick add

-

Want to quickly generate a calendar populated with your semester's classes? Enter the CRNs in a comma separated list below.

-
-
- this.setState({ crnString: e.target.value })} - className="form-control" - placeholder="12345,54321,..." - aria-describedby="basic-addon2" - autoComplete="off" - /> -
- -
-
-
-
- ); - } -} diff --git a/schedules/app/javascript/src/SearchList.jsx b/schedules/app/javascript/src/SearchList.jsx deleted file mode 100644 index a0471fc2e6a5ad3b59439d9d039f49bd1fe93c46..0000000000000000000000000000000000000000 --- a/schedules/app/javascript/src/SearchList.jsx +++ /dev/null @@ -1,14 +0,0 @@ -import React from 'react'; -import CourseList from 'src/CourseList'; -import InstructorList from 'src/InstructorList'; - -export default class SearchList extends React.Component { - render() { - return ( -
- - -
- ); - } -} diff --git a/schedules/app/javascript/src/Section.jsx b/schedules/app/javascript/src/Section.jsx deleted file mode 100644 index 4fe40b653645194b90e151f938cd536de775a07e..0000000000000000000000000000000000000000 --- a/schedules/app/javascript/src/Section.jsx +++ /dev/null @@ -1,61 +0,0 @@ -import React from 'react'; -import Cart from 'src/Cart'; -import Stars from 'src/Stars'; - -export default class Section extends React.Component { - constructor(props) { - super(props); - this.state = { inCart: Cart.includesCrn(this.props.crn) }; - } - - onClick = e => { - e.stopPropagation(); - console.log(e.target.tagName); - if (e.target.tagName === 'A') return; - - Cart.toggleCrn(this.props.crn); - this.setState({ inCart: Cart.includesCrn(this.props.crn) }); - - this.props.onClick && this.props.onClick(this.props.crn); - }; - - render() { - const { name, title, crn, instructor_name, instructor_url, teaching_rating, course_rating, location, days, start_time, end_time } = this.props; - const { inCart } = this.state; - - const percent = teaching_rating ? : null; - - const remove = ( - - -
- Remove -
- ); - const add = ( - - -
- Add -
- ); - - return ( -
  • -

    - {name}: {title}{' '} - - (# - {crn}) - -

    - {remove} - {add} - {instructor_name} {percent} -
    - {location}
    - {days}, {start_time} - {end_time}
    -
  • - ); - } -} diff --git a/schedules/app/javascript/src/SectionList.jsx b/schedules/app/javascript/src/SectionList.jsx deleted file mode 100644 index 0a8ac5b4dbd24a3f0b95c3e4c07117eec32e2db5..0000000000000000000000000000000000000000 --- a/schedules/app/javascript/src/SectionList.jsx +++ /dev/null @@ -1,26 +0,0 @@ -import React from 'react'; -import Chevron from 'src/Chevron'; -import Section from 'src/Section'; - -export default class SectionList extends React.Component { - constructor(props) { - super(props); - } - - render() { - return ( -
    - {this.props.expandable ? : null} - {this.props.expanded ? ( -
    - {this.props.sections.map(section => ( -
    - ))} -
    - ) : ( -
    - )} -
    - ); - } -} diff --git a/schedules/app/javascript/src/Stars.jsx b/schedules/app/javascript/src/Stars.jsx deleted file mode 100644 index d680a955b7c8a15a5ff4af8356b690ea4cdee56a..0000000000000000000000000000000000000000 --- a/schedules/app/javascript/src/Stars.jsx +++ /dev/null @@ -1,25 +0,0 @@ -import React from 'react'; - -export default class Stars extends React.Component { - render() { - return ( -
    -
    -