Commit 257bea38 authored by Zac Wood's avatar Zac Wood

Add share to calendar, record time of each update, better closures

parent b7bac51a
Pipeline #4339 failed with stage
in 3 minutes and 27 seconds
......@@ -25,11 +25,11 @@ class SchedulesController < ApplicationController
}
@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
......@@ -32,3 +32,22 @@ 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) {
var canvas = this;
setTimeout(function() {
var binStr = atob(canvas.toDataURL(type, quality).split(',')[1]),
len = binStr.length,
arr = new Uint8Array(len);
for (var i = 0; i < len; i++) {
arr[i] = binStr.charCodeAt(i);
}
callback(new Blob([arr], { type: type || 'image/png' }));
});
},
});
}
import React from 'react';
import ReactDOM from 'react-dom';
import Cart from 'src/Cart';
import { saveAs } from 'file-saver';
import html2canvas from 'html2canvas';
import $ from 'jquery';
import CalendarPage from 'src/CalendarPage';
document.addEventListener('DOMContentLoaded', () => {
ReactDOM.render(<CalendarPage />, document.getElementById('root'));
});
const downloadIcs = async () => {
const response = await fetch(`${window.location.protocol}//${window.location.hostname}/api/schedules?crns=${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 addToSystemCalendar = () => {
window.open(`webcal://${window.location.hostname}/api/schedules?crns=${Cart._courses.join(',')}`);
};
const saveImage = () => {
html2canvas(document.querySelector('#calendar')).then(canvas => {
canvas.toBlob(blob => {
saveAs(blob, 'GMU Schedule.png');
});
});
};
const initListeners = () => {
const items = Array.from(document.querySelectorAll('.section-item'));
items.forEach(item => (item.onclick = () => remove(item)));
// document.getElementById('open-modal-btn').onclick = setUrlInModal;
document.getElementById('download-ics').onclick = downloadIcs;
document.getElementById('add-to-system').onclick = addToSystemCalendar;
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(',')}`;
};
if (!HTMLCanvasElement.prototype.toBlob) {
Object.defineProperty(HTMLCanvasElement.prototype, 'toBlob', {
value: function(callback, type, quality) {
var canvas = this;
setTimeout(function() {
var binStr = atob(canvas.toDataURL(type, quality).split(',')[1]),
len = binStr.length,
arr = new Uint8Array(len);
for (var i = 0; i < len; i++) {
arr[i] = binStr.charCodeAt(i);
}
callback(new Blob([arr], { type: type || 'image/png' }));
});
},
});
}
import Cart from 'src/Cart';
import { saveAs } from 'file-saver';
import html2canvas from 'html2canvas';
import $ from 'jquery';
import 'fullcalendar';
import 'moment';
import 'url-polyfill';
const params = new URLSearchParams(document.location.search);
const crns = params.get('crns');
import React from 'react';
import ReactDOM from 'react-dom';
import ViewCalendarPage from 'src/ViewCalendarPage';
document.addEventListener('DOMContentLoaded', () => {
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();
ReactDOM.render(<ViewCalendarPage />, document.getElementById('root'));
});
const renderEvents = (start, end, timezone, callback) => {
callback(window.events);
};
/**
* 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=${crns}`;
};
const downloadIcs = async () => {
const response = await fetch(`${window.location.protocol}//${window.location.hostname}/api/schedules?crns=${crns}`);
const text = await response.text();
const blob = new Blob([text], { type: 'text/calendar;charset=utf-8' });
saveAs(blob, 'GMU Schedule.ics');
};
const addToSystemCalendar = () => {
window.open(`webcal://${window.location.hostname}/api/schedules?crns=${crns}`);
};
const saveImage = () => {
html2canvas(document.querySelector('#calendar')).then(canvas => {
canvas.toBlob(blob => {
saveAs(blob, 'GMU Schedule.png');
});
});
};
const initListeners = () => {
// document.getElementById('open-modal-btn').onclick = setUrlInModal;
document.getElementById('download-ics').onclick = downloadIcs;
document.getElementById('add-to-system').onclick = addToSystemCalendar;
document.getElementById('save-image').onclick = saveImage;
};
if (!HTMLCanvasElement.prototype.toBlob) {
Object.defineProperty(HTMLCanvasElement.prototype, 'toBlob', {
value: function(callback, type, quality) {
var canvas = this;
setTimeout(function() {
var binStr = atob(canvas.toDataURL(type, quality).split(',')[1]),
len = binStr.length,
arr = new Uint8Array(len);
for (var i = 0; i < len; i++) {
arr[i] = binStr.charCodeAt(i);
}
callback(new Blob([arr], { type: type || 'image/png' }));
});
},
});
}
......@@ -3,6 +3,7 @@ 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';
......@@ -51,7 +52,7 @@ export default class CalendarPage extends React.Component {
</div>
<div className="col-4">
<div className="d-flex justify-content-center">
<button className="btn btn-lg btn-secondary">
<button className="btn btn-lg btn-secondary" data-toggle="modal" data-target="#shareModal">
<i className="fas fa-share-square mr-2" />
Share
</button>
......@@ -70,6 +71,9 @@ export default class CalendarPage extends React.Component {
<SectionList onClick={this.toggleSection} sections={this.state.sections} expanded={true} />
<QuickAdd loadCalendar={this.loadEvents} />
<ExportModal />
<ShareModal
link={`${window.location.protocol}//${window.location.hostname}${window.location.port === '3000' ? ':3000' : ''}/schedule/view?crns=${Cart.crns.join(',')}`}
/>
</div>
);
}
......
This diff is collapsed.
......@@ -20,7 +20,7 @@ export default class Section extends React.Component {
};
render() {
const { name, title, crn, instructor_name, instructor_url, teaching_rating, location, days, start_time, end_time } = this.props;
const { name, title, crn, readOnly, instructor_name, instructor_url, teaching_rating, location, days, start_time, end_time } = this.props;
const { inCart } = this.state;
const percent = teaching_rating ? <Stars percent={(teaching_rating[0] / 5) * 100} /> : null;
......@@ -49,8 +49,12 @@ export default class Section extends React.Component {
{crn})
</em>
</p>
{remove}
{add}
{!readOnly && (
<div>
{remove}
{add}
</div>
)}
<i className="fas fa-chalkboard-teacher" />{' '}
{instructor_name !== 'TBA' ? (
<span>
......
......@@ -14,7 +14,7 @@ export default class SectionList extends React.Component {
{this.props.expanded ? (
<div className="d-flex list-group list-group-flush sections">
{this.props.sections.map(section => (
<Section key={section.id} onClick={this.props.onClick} {...section} />
<Section key={section.id} onClick={this.props.onClick} readOnly={this.props.readOnly} {...section} />
))}
</div>
) : (
......
import React from 'react';
const ShareModal = props => {
return (
<div className="modal fade" id="shareModal" tabindex="-1" role="dialog" aria-labelledby="shareModalLabel" aria-hidden="true">
<div className="modal-dialog" role="document">
<div className="modal-content">
<div className="modal-header">
<h5 className="modal-title">Share your schedule!</h5>
</div>
<div className="modal-body">
The following link contains a copy of your schedule: <br />
<a id="shareLink" href={props.link}>
{props.link}
</a>
</div>
</div>
</div>
</div>
);
};
export default ShareModal;
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 QuickAdd from 'src/QuickAdd';
import moment from 'moment';
import 'url-polyfill';
const params = new URLSearchParams(document.location.search);
const crns = params.get('crns');
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=${crns}`);
const json = await response.json();
this.setState({ ...json });
};
events = () => {
return this.state.events.map(e => ({ ...e, start: moment(e.start).toDate(), end: moment(e.end).toDate() }));
};
render() {
return (
<div className="container">
<Calendar events={this.events()} />
{this.state.sections.length > 0 ? (
<div className="d-flex justify-content-between align-items-end">
<h2 className="mt-4">Schedule</h2>{' '}
</div>
) : null}
<SectionList readOnly={true} sections={this.state.sections} expanded={true} />
</div>
);
}
}
......@@ -10,7 +10,7 @@ class Course < ApplicationRecord
"#{subject} #{course_number}"
end
def rating(question = 1, sections = self.course_sections)
def rating(question = 1, sections = course_sections)
total = 0
resp = 0
sections.each do |s|
......
module Update
FILE_NAME = "db/data/last_update.txt".freeze
def self.last_update_date
begin
File.open(FILE_NAME).first
rescue
"Data has not yet been loaded."
end
end
def self.new_update
File.write(FILE_NAME, Time.now.strftime("%Y-%m-%d %k:%M:%S"))
end
end
......@@ -2,7 +2,7 @@
<h1><i class="fas fa-calendar-alt"></i>&nbsp;SRCT Schedules</h1>
<p class="lead">Version 3.0</p>
<hr />
Last updated: 2:00am, 4/14/19
Last updated: <%= Update::last_update_date %>
</div>
<h3>Thank you to our contributors who make Schedules possible!</h3>
......
<%= javascript_pack_tag 'schedules_view' %>
<%= stylesheet_link_tag 'schedules' %>
<%= stylesheet_link_tag 'fullcalendar.min' %>
<button id="open-modal-btn" type="button" class="btn btn-primary" data-toggle="modal" data-target="#exportModal">
Export Schedule
</button>
<button id="save-image" class="btn btn-secondary" onclick="saveImage()">Save Image</button>
<div id="calendar"></div>
<template id="events" data-events="<%= @events.to_json %>"></template>
<hr />
<h2>Selected Courses</h2>
<%= render partial: 'shared/section', collection: @all, locals: { editable: false } %>
<!-- Export Modal -->
<div class="modal fade" id="exportModal" tabindex="-1" role="dialog" aria-labelledby="exportModalLabel" aria-hidden="true">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="exportModalLabel">Your calendar has been generated!</h5>
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body">
<h5>Apple Calendar</h5>
To add your schedule to Apple Calendar, click the "Add to calendar" button below. If you are on a device
running macOS or iOS, this will open a dialogue which will walk you through adding the calendar.
<hr />
<h5>Google Calendar</h5>
<strong>On desktop:</strong>
<br />
First, download the calendar file using the "Download calendar file" below. Open your <a href="https://calendar.google.com/" target="_blank">Google Calendar</a>. Click the "Settings" button in the top
right, and then click the Settings tab. In the menu on the left, click "Import & export" and "Import". Now, upload the calendar file you downloaded and click "Import".
<br />
<strong>On mobile (Android only):</strong>
<br />
Click the "Download calendar file" button. This will download the calendar file which you may then open and
add to your calendar.
<hr />
<h5>.ics file</h5>
To download a .ics file containing your schedule, click the "Download calendar file" button below.
</div>
<div class="modal-footer flex">
<button id="download-ics" type="button" class="btn btn-secondary">Download calendar file</button>
<button id="add-to-system" type="button" class="btn btn-primary">Add to system calendar</button>
</div>
</div>
</div>
</div>
<div id="root"></div>
......@@ -98,13 +98,13 @@ def wipe_db
end
def load_closures
semesters = YAML.load_file("db/closures.yaml")
semesters = YAML.load_file("db/data/closures.yaml")
semesters.each do |semester, dates|
season, year = semester.split
s = Semester.find_by(season: season, year: year)
next if s.nil?
dates.each do |date|
Closure.create!(date: Date.strptime(date, "%Y-%m-%d"), semester: s)
Closure.find_or_create_by!(date: Date.strptime(date, "%Y-%m-%d"), semester: s)
end
end
end
......@@ -118,7 +118,7 @@ def main
[parser.parse_semesters.first]
else
# expand to include however many semesters you want
parser.parse_semesters[1..7]
parser.parse_semesters[0..7]
end
puts "\tParsing subjects..."
......@@ -145,6 +145,8 @@ def main
end
load_closures
Update.new_update
end
main
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