Commit 98c6b181 authored by Zac Wood's avatar Zac Wood

Start redesign

parent 98455010
Pipeline #4989 failed with stage
in 2 minutes and 47 seconds
.vscode
schedules.code-workspace
schedules/config/test.html
......@@ -108,6 +108,7 @@ GEM
parser (2.6.4.1)
ast (~> 2.4.0)
powerpack (0.1.2)
prettier (0.15.0)
pry (0.12.2)
coderay (~> 1.1.0)
method_source (~> 0.9.0)
......@@ -240,6 +241,7 @@ DEPENDENCIES
listen (>= 3.0.5, < 3.2)
maruku
nokogiri
prettier
pry
pry-doc
puma (~> 3.7)
......@@ -258,4 +260,4 @@ DEPENDENCIES
webpacker (~> 3.5)
BUNDLED WITH
1.16.3
1.17.3
// Place all the behaviors and hooks related to the matching controller here.
// All this logic will automatically be available in application.js.
// Action Cable provides the framework to deal with WebSockets in Rails.
// You can generate new channels where WebSocket features live using the `rails generate channel` command.
//
//= require action_cable
//= require_self
//= require_tree ./channels
(function() {
this.App || (this.App = {});
App.cable = ActionCable.createConsumer();
}).call(this);
// Place all the behaviors and hooks related to the matching controller here.
// All this logic will automatically be available in application.js.
......@@ -10,90 +10,146 @@
* 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.
*
* require_tree .
*= require cart
*= require navbar
*= require_tree .
*= require_self
*/
body {
background-color: #E4E4E4;
}
$gray: #4a4a4a;
$gold: #febf10;
$green: #01693f;
#page {
padding-top: 16px;
padding-bottom: 16px;
* {
font-family: 'Roboto', sans-serif;
}
.card {
margin-bottom: 12px;
background-color: white;
border-radius: 8px;
box-shadow: 0 0 5px rgba(0,0,0,0.2);
transition: 0.3s;
h1,
h2,
h3,
h4,
h5,
h6 {
font-family: 'Open Sans', sans-serif;
}
.card-header {
display: flex;
flex-direction: column;
.hero {
margin-top: 30%;
h1 {
margin-top: 1em;
margin-bottom: 1em;
}
}
.attr-list {
#search-container {
margin: 0 auto;
border-radius: 5px;
box-shadow: 0 0 4px $gray;
width: 75%;
border: none;
height: 2em;
outline: none;
display: flex;
flex-direction: row;
.attr {
.icon {
padding-right: 4px;
}
align-items: center;
display: inline-flex;
white-space: nowrap;
& input {
flex-grow: 1;
outline: none;
border: none;
height: 100%;
margin-left: 1em;
margin-right: 1em;
font-family: 'Roboto', sans-serif;
}
}
.unpadded {
padding: 0px;
}
& i {
width: 2em;
margin: auto;
color: #297dc5;
}
/* On mouse-over, add a deeper shadow */
.card:hover {
box-shadow: 0 0 20px rgba(0,0,0,0.4);
& button {
background-color: transparent;
border: none;
}
}
.align-vertical {
nav {
display: flex;
align-items: center;
}
padding-top: 1em;
font-family: 'Open Sans', sans-serif;
.align-left {
display: flex;
justify-content: flex-start;
align-items: center;
margin-bottom: 2em;
// schedules logo
> span:first-child {
white-space: nowrap;
font-size: 1.5em;
a {
color: black;
}
}
}
.align-center {
display: flex;
justify-content: center;
align-items: center;
@media only screen and (max-width: 600px) {
nav {
text-align: center;
flex-direction: column;
> span:first-child {
margin-bottom: 0.5em;
}
}
#search-container {
width: 90%;
}
}
.align-right {
display: flex;
justify-content: flex-end;
align-items: center;
.course {
box-shadow: 0 3px 5px -2px $gray;
border-radius: 5px;
padding: 1em;
margin-bottom: 1em;
border-top: 5px solid $gold;
h4 {
margin-bottom: 0.3em;
a {
color: $green;
font-weight: bolder;
font-family: 'Open Sans', sans-serif;
}
}
h5 {
color: $gray;
font-weight: 400;
}
.description {
display: block;
margin-top: 1em;
line-height: 1.7em;
font-family: 'Roboto', sans-serif;
color: $gray;
font-size: 0.9em;
}
.see-more {
display: block;
text-align: center;
color: $green;
font-weight: bold;
margin-top: 1em;
text-decoration: underline;
}
}
.form-control:focus {
border-color: transparent;
box-shadow: 0px 0px 0px rgba(0, 0, 0, 0.075) inset, 0px 0px 0px rgba(0, 0, 255, 0.5);
.card {
border-radius: 5px;
}
.quick-add-header {
margin-top: 32px;
a {
color: $green;
}
%flex-display {
......@@ -108,26 +164,35 @@ body {
.back-stars {
@extend %flex-display;
position: relative;
text-shadow: 4px 4px 10px #843a3a;
}
.front-stars {
@extend %flex-display;
color: #FFBC0B;
color: $gold;
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;
.details {
margin-top: 1em;
p {
margin-bottom: 0.2em;
}
}
.semester-select {
margin-bottom: 1em;
}
.section-type-label {
margin-top: 1em;
}
// .jumbotron {
// color: #FFFFFF
// }
.section-list {
list-style: none;
margin: 0;
padding: 0;
> * {
margin-bottom: 0.5em;
}
}
......@@ -12,14 +12,10 @@ module BySemester
#
# 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
@semester = if params.key?(:semester_id)
Semester.find_by_id(params[:semester_id])
else
Semester.first
end
end
end
......@@ -3,6 +3,34 @@ class CoursesController < ApplicationController
# Load the course with the id passed in the URL.
@course = Course.find_by_id(params[:id])
@rating = @course.rating
@sections = @course.course_sections.where(semester: @semester)
semester_ids = @course.course_sections
.joins(:semester)
.select("semesters.id")
@semesters = Semester.where(id: semester_ids.map(&:id))
@semesters = Semester.sorted_by_date(@semesters)
@taught_in = Set.new(@semesters.map(&:season))
@taught_in = sort_seasons(@taught_in.to_a).join(", ")
@sections = @course.course_sections.where(semester: @semester).group_by { |s| s.section_type }
end
end
private
def sort_seasons(seasons)
seasons.sort do |s1, s2|
case
when s1 == "Fall"
-1
when s1 == "Summer" && s2 == "Fall"
1
when s1 == "Spring"
1
else
0
end
end
end
end
\ No newline at end of file
......@@ -8,9 +8,14 @@ class InstructorsController < ApplicationController
# find the courses being taught this semester
sections = CourseSection.where(instructor: @instructor)
@semesters = sections.group_by do |s|
s.semester.to_s
end
semester_ids = sections
.joins(:semester)
.select("semesters.id")
@semesters = Semester.where(id: semester_ids.map(&:id))
@semesters = Semester.sorted_by_date(@semesters)
@sections = sections.where(semester: @semester).group_by { |s| s.section_type }
@rating = { teaching: @instructor.rating, respect: @instructor.rating(6) }
end
......
......@@ -5,10 +5,11 @@ class SchedulesController < ApplicationController
def show; end
def view
@all = params[:crns].split(',').map { |crn|
CourseSection.latest_by_crn(crn)
}
@all.reject!(&:nil?)
@all = params[:crns]
.split(',')
.map { |crn| CourseSection.latest_by_crn(crn) }
.reject(&:nil?)
@without_online = @all.reject { |s|
s.start_time == "TBA" || s.end_time == "TBA"
}
......
class SearchController < ApplicationController
def index
params[:query].strip!
redirect_to(home_url) unless params[:query].length > 1
if params[:query].casecmp('god').zero?
......@@ -41,8 +42,6 @@ class SearchController < ApplicationController
@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|
......
......@@ -7,32 +7,13 @@
// To reference this file, add <%= javascript_pack_tag 'application' %> to the appropriate
// layout file, like app/views/layouts/application.html.erb
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;
};
document.addEventListener('DOMContentLoaded', () => {
initGlobalListeners();
});
const setSemester = async select => {
const url = new URL(window.location.href);
url.searchParams.set('semester_id', select.value);
window.open(url.toString(), '_self');
};
const initGlobalListeners = () => {
const semesterSelect = document.getElementById('semester-select');
semesterSelect.onchange = () => setSemester(semesterSelect);
};
if (!HTMLCanvasElement.prototype.toBlob) {
Object.defineProperty(HTMLCanvasElement.prototype, 'toBlob', {
value: function(callback, type, quality) {
......
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(
<QuickAdd
loadCalendar={() => {
window.location.href = calendarUrl;
}}
/>,
document.getElementById('quick-add')
);
});
// /**
// * 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);
import React from 'react';
import ReactDOM from 'react-dom';
import CalendarPage from 'src/CalendarPage';
document.addEventListener('DOMContentLoaded', () => {
ReactDOM.render(<CalendarPage />, document.getElementById('root'));
});
import React from 'react';
import ReactDOM from 'react-dom';
import ViewCalendarPage from 'src/ViewCalendarPage';
document.addEventListener('DOMContentLoaded', () => {
ReactDOM.render(<ViewCalendarPage />, document.getElementById('root'));
});
// 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';
// /**
// * 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');
// 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';
// }
// };
import React from 'react';
import ReactDOM from 'react-dom';
import SearchList from 'src/SearchList';
document.addEventListener('DOMContentLoaded', () => {
ReactDOM.render(<SearchList courses={gon.courses} instructors={gon.instructors} />, document.getElementById('root'));
});
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';
const localizer = BigCalendar.momentLocalizer(moment);
const minTime = new Date();
minTime.setHours(7, 0, 0);
const maxTime = new Date();
maxTime.setHours(23, 0, 0);
const Calendar = props => (
<div className="full-width" style={{ backgroundColor: 'white', padding: '24px' }}>
<BigCalendar
localizer={localizer}
events={props.events}
title=""
components={{ toolbar: Toolbar }}
defaultView="week"
views={['week', 'day']}
startAccessor="start"
endAccessor="end"
defaultDate={moment('2019-01-14').toDate()}
formats={{
dayFormat: (date, culture, localizer) => localizer.format(date, 'ddd', culture),
dayHeaderFormat: (date, culture, localizer) => localizer.format(date, 'ddd', culture),
dayRangeHeaderFormat: () => '',
}}
style={{ height: '75vh' }}
min={minTime}
/>
</div>
);
export default Calendar;
import React from 'react';
import Calendar from 'src/Calendar';
import Cart from 'src/Cart';
import SectionList from 'src/SectionList';
import ExportModal from 'src/ExportModal';
import ShareModal from 'src/ShareModal';
import QuickAdd from 'src/QuickAdd';
import moment from 'moment';
export default class CalendarPage extends React.Component {
state = { events: [], sections: [] };