aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorFurkan Sahin <furkan-dev@proton.me>2021-04-10 21:35:13 -0500
committerFurkan Sahin <furkan-dev@proton.me>2021-04-10 21:35:13 -0500
commit74e6cc31e83ad570a9f06765d288e9024736e73f (patch)
treea9757e9ffa099a3d3be5786d20d623e2ce933855 /src
parentea8dcfe3bf1ebe84ac483bb91e37ee4faa0d77ea (diff)
Commit MVP
Diffstat (limited to 'src')
-rw-r--r--src/App.vue19
-rw-r--r--src/assets/logo.pngbin6849 -> 0 bytes
-rw-r--r--src/components/ActionBar.vue75
-rw-r--r--src/components/FileUpload.vue41
-rw-r--r--src/components/HelloWorld.vue60
-rw-r--r--src/components/List.vue41
-rw-r--r--src/features/parser.js138
-rw-r--r--src/models/EventInfo.js41
-rw-r--r--src/models/Lab.js17
-rw-r--r--src/models/PeerTeacher.js29
-rw-r--r--src/router/index.js14
-rw-r--r--src/store/index.js22
-rw-r--r--src/views/About.vue27
-rw-r--r--src/views/Editor.vue123
-rw-r--r--src/views/Home.vue18
15 files changed, 575 insertions, 90 deletions
diff --git a/src/App.vue b/src/App.vue
index b964355..c3a04c9 100644
--- a/src/App.vue
+++ b/src/App.vue
@@ -1,13 +1,22 @@
<template>
- <div id="nav">
- <router-link to="/">Home</router-link> |
- <router-link to="/about">About</router-link>
- </div>
<router-view/>
</template>
<style>
+html {
+ box-sizing: border-box;
+}
+
+*, *:before, *:after {
+ box-sizing: inherit;
+}
+
#app {
+ height: 100vh;
+ max-height: 100vh;
+}
+
+/* #app {
font-family: Avenir, Helvetica, Arial, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
@@ -26,5 +35,5 @@
#nav a.router-link-exact-active {
color: #42b983;
-}
+} */
</style>
diff --git a/src/assets/logo.png b/src/assets/logo.png
deleted file mode 100644
index f3d2503..0000000
--- a/src/assets/logo.png
+++ /dev/null
Binary files differ
diff --git a/src/components/ActionBar.vue b/src/components/ActionBar.vue
new file mode 100644
index 0000000..96dbaae
--- /dev/null
+++ b/src/components/ActionBar.vue
@@ -0,0 +1,75 @@
+<template>
+ <div>
+ <file-upload
+ :accept="'text/plain'"
+ :multiple="true"
+ @file-changed="handlePtChange">Upload PT Schedule</file-upload>
+ <file-upload
+ :accept="'application/json'"
+ @file-changed="handleLabChange">Upload Lab Schedule</file-upload>
+ <button @click="save">Export</button>
+ </div>
+</template>
+
+<script>
+import FileUpload from '@/components/FileUpload.vue';
+
+import { parseLabFile, parsePtSchedule } from '@/features/parser';
+
+export default {
+ name: 'ActionBar',
+ components: {
+ FileUpload,
+ },
+ methods: {
+ handleLabChange(files) {
+ parseLabFile(files[0])
+ .then((data) => {
+ this.$store.commit('importLabs', data);
+ })
+ .catch((error) => {
+ console.error(error);
+ });
+ },
+ handlePtChange(files) {
+ const result = [];
+ const promises = [];
+
+ files.forEach((file) => {
+ const p = parsePtSchedule(file).then((data) => result.push(data));
+ promises.push(p);
+ });
+
+ Promise.all(promises)
+ .then(() => {
+ this.$store.commit('addPeerTeachers', result);
+ });
+ },
+ save() {
+ const database = {
+ labs: Object.fromEntries(this.$store.state.labs),
+ peerTeachers: Object.fromEntries(this.$store.state.peerTeachers),
+ };
+
+ const jsonObj = JSON.stringify(database, (_, value) => {
+ if (typeof value === 'object' && value instanceof Set) {
+ return [...value];
+ }
+ return value;
+ });
+
+ const blob = new Blob([jsonObj], { type: 'text/json' });
+ const anchor = document.createElement('a');
+ const url = window.URL.createObjectURL(blob);
+ anchor.href = url;
+ anchor.download = 'pt-db.json';
+ anchor.style = 'display: none';
+
+ document.body.appendChild(anchor);
+ anchor.click();
+ document.body.removeChild(anchor);
+ window.URL.revokeObjectURL(url);
+ },
+ },
+};
+</script>
diff --git a/src/components/FileUpload.vue b/src/components/FileUpload.vue
new file mode 100644
index 0000000..b2fdb14
--- /dev/null
+++ b/src/components/FileUpload.vue
@@ -0,0 +1,41 @@
+<template>
+ <label class="file-upload-lbl">
+ <input
+ type="file"
+ :accept="accept"
+ @change="$emit('fileChanged', $event.target.files)"
+ :multiple="multiple" hidden />
+ <slot>Upload</slot>
+ </label>
+</template>
+
+<script>
+export default {
+ name: 'FileUpload',
+ props: {
+ accept: String,
+ multiple: {
+ type: Boolean,
+ default: false,
+ },
+ },
+ emits: {
+ selectionChanged: null,
+ },
+};
+</script>
+
+<style scoped>
+.file-upload-lbl {
+ background-color: #500000;
+ color: white;
+ font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
+ padding: 5px;
+ text-align: center;
+}
+
+.file-upload-lbl:hover {
+ color: grey;
+ cursor: pointer;
+}
+</style>
diff --git a/src/components/HelloWorld.vue b/src/components/HelloWorld.vue
deleted file mode 100644
index 94ceb6b..0000000
--- a/src/components/HelloWorld.vue
+++ /dev/null
@@ -1,60 +0,0 @@
-<template>
- <div class="hello">
- <h1>{{ msg }}</h1>
- <p>
- For a guide and recipes on how to configure / customize this project,<br>
- check out the
- <a href="https://cli.vuejs.org" target="_blank" rel="noopener">vue-cli documentation</a>.
- </p>
- <h3>Installed CLI Plugins</h3>
- <ul>
- <li><a href="https://github.com/vuejs/vue-cli/tree/dev/packages/%40vue/cli-plugin-babel" target="_blank" rel="noopener">babel</a></li>
- <li><a href="https://github.com/vuejs/vue-cli/tree/dev/packages/%40vue/cli-plugin-router" target="_blank" rel="noopener">router</a></li>
- <li><a href="https://github.com/vuejs/vue-cli/tree/dev/packages/%40vue/cli-plugin-vuex" target="_blank" rel="noopener">vuex</a></li>
- <li><a href="https://github.com/vuejs/vue-cli/tree/dev/packages/%40vue/cli-plugin-eslint" target="_blank" rel="noopener">eslint</a></li>
- </ul>
- <h3>Essential Links</h3>
- <ul>
- <li><a href="https://vuejs.org" target="_blank" rel="noopener">Core Docs</a></li>
- <li><a href="https://forum.vuejs.org" target="_blank" rel="noopener">Forum</a></li>
- <li><a href="https://chat.vuejs.org" target="_blank" rel="noopener">Community Chat</a></li>
- <li><a href="https://twitter.com/vuejs" target="_blank" rel="noopener">Twitter</a></li>
- <li><a href="https://news.vuejs.org" target="_blank" rel="noopener">News</a></li>
- </ul>
- <h3>Ecosystem</h3>
- <ul>
- <li><a href="https://router.vuejs.org" target="_blank" rel="noopener">vue-router</a></li>
- <li><a href="https://vuex.vuejs.org" target="_blank" rel="noopener">vuex</a></li>
- <li><a href="https://github.com/vuejs/vue-devtools#vue-devtools" target="_blank" rel="noopener">vue-devtools</a></li>
- <li><a href="https://vue-loader.vuejs.org" target="_blank" rel="noopener">vue-loader</a></li>
- <li><a href="https://github.com/vuejs/awesome-vue" target="_blank" rel="noopener">awesome-vue</a></li>
- </ul>
- </div>
-</template>
-
-<script>
-export default {
- name: 'HelloWorld',
- props: {
- msg: String,
- },
-};
-</script>
-
-<!-- Add "scoped" attribute to limit CSS to this component only -->
-<style scoped>
-h3 {
- margin: 40px 0 0;
-}
-ul {
- list-style-type: none;
- padding: 0;
-}
-li {
- display: inline-block;
- margin: 0 10px;
-}
-a {
- color: #42b983;
-}
-</style>
diff --git a/src/components/List.vue b/src/components/List.vue
new file mode 100644
index 0000000..031f708
--- /dev/null
+++ b/src/components/List.vue
@@ -0,0 +1,41 @@
+<template>
+ <ul class="list">
+ <li
+ v-for="item in items"
+ :key="item.id"
+ @click="$emit('selectionChanged', item)"
+ class="list-item">
+ <slot :item="item">{{ item }}</slot>
+ </li>
+ </ul>
+</template>
+
+<script>
+export default {
+ name: 'List',
+ props: {
+ items: Array,
+ },
+ emits: {
+ selectionChanged: null,
+ },
+};
+</script>
+
+<style scoped>
+.list {
+ list-style-type: none;
+}
+
+.list-item {
+ border: 1px solid grey;
+ border-radius: 5px;
+ margin: 5px 0;
+ padding: 0.15em;
+ text-align: center;
+}
+
+.list-item:hover {
+ background-color: rgb(182, 182, 182);
+}
+</style>
diff --git a/src/features/parser.js b/src/features/parser.js
new file mode 100644
index 0000000..f91d926
--- /dev/null
+++ b/src/features/parser.js
@@ -0,0 +1,138 @@
+import Lab from '@/models/Lab';
+import PeerTeacher from '@/models/PeerTeacher';
+import EventInfo from '../models/EventInfo';
+
+export function parseLabFile(file) {
+ const validCourses = [
+ '110',
+ '111',
+ '121',
+ '206',
+ '221',
+ // '222',
+ '312',
+ '313',
+ // '314',
+ '315',
+ ];
+
+ const reader = new FileReader();
+
+ return new Promise((resolve, reject) => {
+ const result = [];
+
+ reader.onload = (event) => {
+ let jsonData;
+ try {
+ jsonData = JSON.parse(event.target.result);
+ } catch (e) {
+ reject(new Error(e));
+ }
+ const labs = jsonData.data;
+
+ labs.forEach((lab) => {
+ if (validCourses.includes(lab.courseNumber)) {
+ const newLab = new Lab(lab.courseNumber, lab.sequenceNumber);
+
+ if (lab.instructionalMethod !== 'Web Based') {
+ lab.meetingsFaculty.every((meeting) => {
+ const { meetingTime } = meeting;
+
+ if (meetingTime.meetingType === 'LAB') {
+ let days = '';
+ days += meetingTime.monday ? 'M' : '';
+ days += meetingTime.tuesday ? 'T' : '';
+ days += meetingTime.wednesday ? 'W' : '';
+ days += meetingTime.thursday ? 'R' : '';
+ days += meetingTime.friday ? 'F' : '';
+
+ newLab.event.days = days;
+ newLab.event.start = parseInt(meetingTime.beginTime, 10);
+ newLab.event.end = parseInt(meetingTime.endTime, 10);
+
+ return false;
+ }
+
+ return true;
+ });
+ }
+
+ result.push(newLab);
+ }
+ });
+
+ resolve(result);
+ };
+
+ reader.readAsText(file);
+ });
+}
+
+export function parsePtSchedule(file) {
+ const reader = new FileReader();
+
+ return new Promise((resolve) => {
+ reader.onload = (event) => {
+ const peerTeacher = new PeerTeacher();
+ const namePattern = /^(.*)\s(.*)\s(\d{9})/;
+ const eventPattern = /^(M?T?W?R?F?)\s(\d{1,2}:\d{2})\s?-\s?(\d{1,2}:\d{2})/;
+ const lines = event.target.result.split('\n').filter((line) => line.trim());
+
+ lines.forEach((line) => {
+ const ptName = line.match(namePattern);
+ if (ptName) {
+ [, peerTeacher.firstname, peerTeacher.lastname] = ptName;
+ peerTeacher.uin = parseInt(ptName[3], 10);
+ }
+
+ const eventMatch = line.match(eventPattern);
+ if (eventMatch) {
+ const newEvent = new EventInfo(eventMatch[1]);
+ newEvent.start = parseInt(eventMatch[2].replace(':', ''), 10);
+ newEvent.end = parseInt(eventMatch[3].replace(':', ''), 10);
+ peerTeacher.events.push(newEvent);
+ }
+ });
+
+ resolve(peerTeacher);
+ };
+
+ reader.readAsText(file);
+ });
+}
+
+export function parsePtDatabase(file) {
+ const reader = new FileReader();
+
+ return new Promise((resolve) => {
+ reader.onload = (event) => {
+ const jsonObj = JSON.parse(event.target.result);
+ const result = {
+ labs: new Map(),
+ peerTeachers: new Map(),
+ };
+
+ Object.keys(jsonObj.labs).forEach((key) => {
+ const lab = jsonObj.labs[key];
+ result.labs.set(key, new Lab(lab.course,
+ lab.section, new EventInfo(lab.event.days, lab.event.start, lab.event.end)));
+ });
+
+ Object.keys(jsonObj.peerTeachers).forEach((key) => {
+ const pt = jsonObj.peerTeachers[key];
+ const ptObj = new PeerTeacher(pt.firstname, pt.lastname, pt.uin);
+ ptObj.events = pt.events.map((eventObj) => new EventInfo(eventObj.days,
+ eventObj.start, eventObj.end));
+ ptObj.assignedLabs = new Set();
+ pt.assignedLabs.forEach((labId) => {
+ ptObj.assignedLabs.add(labId);
+ });
+ result.peerTeachers.set(key, ptObj);
+ });
+
+ resolve(result);
+ };
+
+ reader.readAsText(file);
+ });
+}
diff --git a/src/models/EventInfo.js b/src/models/EventInfo.js
new file mode 100644
index 0000000..c1624a8
--- /dev/null
+++ b/src/models/EventInfo.js
@@ -0,0 +1,41 @@
+export default class EventInfo {
+ constructor(days = '', start = 0, end = 0) {
+ this.days = days;
+ this.start = start;
+ this.end = end;
+ }
+
+ static timeToStr(time) {
+ let hour = Math.floor(time / 100);
+ let minute = time % 100;
+ const meridiem = (hour < 12) ? 'AM' : 'PM';
+
+ if (hour === 0) {
+ hour = 12;
+ } else if (hour > 12) {
+ hour -= 12;
+ }
+
+ if (minute < 10) {
+ minute = `0${minute}`;
+ }
+
+ return `${hour}:${minute} ${meridiem}`;
+ }
+
+ conflictsWith(event) {
+ const daysConflict = event.days.match(new RegExp(`[${this.days}]`));
+
+ if (daysConflict) {
+ return (this.start <= event.end) && (event.start <= this.end);
+ }
+ return false;
+ }
+
+ get info() {
+ if (this.days === '') {
+ return 'ONLINE';
+ }
+ return `${this.days} ${EventInfo.timeToStr(this.start)}-${EventInfo.timeToStr(this.end)}`;
+ }
+}
diff --git a/src/models/Lab.js b/src/models/Lab.js
new file mode 100644
index 0000000..2e4412d
--- /dev/null
+++ b/src/models/Lab.js
@@ -0,0 +1,17 @@
+import EventInfo from '@/models/EventInfo';
+
+export default class Lab {
+ constructor(course = 0, section = 0, event = new EventInfo()) {
+ this.course = course;
+ this.section = section;
+ this.event = event;
+ }
+
+ get id() {
+ return `${this.course}-${this.section}`;
+ }
+
+ get fullInfo() {
+ return `${this.id} ${this.event.info}`;
+ }
+}
diff --git a/src/models/PeerTeacher.js b/src/models/PeerTeacher.js
new file mode 100644
index 0000000..00a2f0d
--- /dev/null
+++ b/src/models/PeerTeacher.js
@@ -0,0 +1,29 @@
+export default class PeerTeacher {
+ constructor(firstname = '', lastname = '', uin = 0) {
+ this.firstname = firstname;
+ this.lastname = lastname;
+ this.uin = uin;
+ this.events = [];
+ this.assignedLabs = new Set();
+ }
+
+ conflictsWith(event) {
+ let conflicts = false;
+ this.events.every((item) => {
+ if (item.conflictsWith(event)) {
+ conflicts = true;
+ return false;
+ }
+ return true;
+ });
+ return conflicts;
+ }
+
+ get name() {
+ return `${this.firstname} ${this.lastname}`;
+ }
+
+ get id() {
+ return this.uin;
+ }
+}
diff --git a/src/router/index.js b/src/router/index.js
index 8f52523..7a598b3 100644
--- a/src/router/index.js
+++ b/src/router/index.js
@@ -1,19 +1,21 @@
import { createRouter, createWebHashHistory } from 'vue-router';
-import Home from '../views/Home.vue';
+import Editor from '../views/Editor.vue';
+import About from '../views/About.vue';
const routes = [
{
path: '/',
- name: 'Home',
- component: Home,
+ name: 'About',
+ component: About,
},
{
- path: '/about',
- name: 'About',
+ path: '/editor',
+ name: 'Editor',
// route level code-splitting
// this generates a separate chunk (about.[hash].js) for this route
// which is lazy-loaded when the route is visited.
- component: () => import(/* webpackChunkName: "about" */ '../views/About.vue'),
+ // component: () => import(/* webpackChunkName: "about" */ '../views/Editor.vue'),
+ component: Editor,
},
];
diff --git a/src/store/index.js b/src/store/index.js
index af6cee0..0c3a5c9 100644
--- a/src/store/index.js
+++ b/src/store/index.js
@@ -2,8 +2,30 @@ import { createStore } from 'vuex';
export default createStore({
state: {
+ labs: new Map(),
+ peerTeachers: new Map(),
},
mutations: {
+ setLabs(state, labs) {
+ state.labs = labs;
+ },
+ setPeerTeachers(state, peerTeachers) {
+ state.peerTeachers = peerTeachers;
+ },
+ importLabs(state, labs) {
+ state.labs.clear();
+ labs.forEach((lab) => {
+ state.labs.set(lab.id, lab);
+ });
+ },
+ addPeerTeachers(state, peerTeachers) {
+ peerTeachers.forEach((pt) => {
+ state.peerTeachers.set(pt.id, pt);
+ });
+ },
+ deletePeerTeacher(state, id) {
+ state.peerTeachers.delete(id);
+ },
},
actions: {
},
diff --git a/src/views/About.vue b/src/views/About.vue
index 3fa2807..ab4725e 100644
--- a/src/views/About.vue
+++ b/src/views/About.vue
@@ -1,5 +1,30 @@
<template>
<div class="about">
- <h1>This is an about page</h1>
+ <router-link to="/editor">Create new database</router-link>
+ <file-upload
+ :accept="'application/json'"
+ @file-changed="handleDatabaseChange">Use existing database</file-upload>
</div>
</template>
+
+<script>
+import FileUpload from '../components/FileUpload.vue';
+
+import { parsePtDatabase } from '../features/parser';
+
+export default {
+ name: 'About',
+ components: {
+ FileUpload,
+ },
+ methods: {
+ handleDatabaseChange(files) {
+ parsePtDatabase(files[0]).then((result) => {
+ this.$store.commit('setLabs', result.labs);
+ this.$store.commit('setPeerTeachers', result.peerTeachers);
+ this.$router.push({ name: 'Editor' });
+ });
+ },
+ },
+};
+</script>
diff --git a/src/views/Editor.vue b/src/views/Editor.vue
new file mode 100644
index 0000000..5ef0131
--- /dev/null
+++ b/src/views/Editor.vue
@@ -0,0 +1,123 @@
+<template>
+ <div id="home">
+ <action-bar />
+ <div id="editor">
+ <div class="column">
+ <list :items="peerTeachers" @selection-changed="handlePtClick" #default="pt">
+ {{ pt.item.name }} <button @click.stop="deletePeerTeacher(pt.item.id)">X</button>
+ </list>
+ </div>
+ <div class="column">
+ <list :items="compatibleLabs" #default="lab">
+ {{ lab.item.fullInfo }} <button @click.stop="assignLab(lab.item.id)">+</button>
+ </list>
+ </div>
+ <div class="column">
+ <list :items="selectedPeerTeacherAssignments" #default="lab">
+ {{ lab.item.fullInfo }} <button @click.stop="unassignLab(lab.item.id)">X</button>
+ </list>
+ </div>
+ </div>
+ </div>
+</template>
+
+<script>
+import ActionBar from '@/components/ActionBar.vue';
+import List from '@/components/List.vue';
+import PeerTeacher from '@/models/PeerTeacher';
+
+export default {
+ name: 'Editor',
+ components: {
+ ActionBar,
+ List,
+ },
+ computed: {
+ labs() {
+ return Array.from(this.$store.state.labs.values()).sort((a, b) => a.id
+ .localeCompare(b.id));
+ },
+ peerTeachers() {
+ return Array.from(this.$store.state.peerTeachers.values()).sort((a, b) => a.lastname
+ .toUpperCase().localeCompare(b.lastname.toUpperCase()));
+ },
+ compatibleLabs() {
+ const temp = this.labs.filter((lab) => (!this.selectedPeerTeacher.assignedLabs.has(lab.id)
+ && !this.selectedPeerTeacher.conflictsWith(lab.event)));
+
+ const currentAssignments = this.selectedPeerTeacherAssignments;
+ return temp.filter((lab) => {
+ let compatible = true;
+ currentAssignments.every((assignedLab) => {
+ if (lab.event.conflictsWith(assignedLab.event)) {
+ compatible = false;
+ return false;
+ }
+ return true;
+ });
+ return compatible;
+ });
+ },
+ selectedPeerTeacherAssignments() {
+ return [...this.selectedPeerTeacher.assignedLabs.values()].map((id) => this.$store.state.labs
+ .get(id)).sort((a, b) => {
+ if (a.course < b.course) {
+ return -1;
+ }
+ if (b.course < a.course) {
+ return 1;
+ }
+
+ if (a.section < b.section) {
+ return -1;
+ }
+ if (b.section < a.section) {
+ return 1;
+ }
+
+ return 0;
+ });
+ },
+ },
+ data() {
+ return {
+ selectedPeerTeacher: new PeerTeacher(),
+ };
+ },
+ methods: {
+ handlePtClick(peerTeacher) {
+ this.selectedPeerTeacher = peerTeacher;
+ },
+ deletePeerTeacher(id) {
+ if (this.selectedPeerTeacher.id === id) {
+ this.selectedPeerTeacher = new PeerTeacher();
+ }
+ this.$store.commit('deletePeerTeacher', id);
+ },
+ assignLab(id) {
+ if (this.selectedPeerTeacher.id !== 0) {
+ this.selectedPeerTeacher.assignedLabs.add(id);
+ }
+ },
+ unassignLab(id) {
+ this.selectedPeerTeacher.assignedLabs.delete(id);
+ },
+ },
+};
+</script>
+
+<style>
+#home {
+ max-height: inherit;
+}
+
+#editor {
+ display: flex;
+ max-height: inherit;
+}
+
+.column {
+ flex: 1;
+ overflow: auto;
+}
+</style>
diff --git a/src/views/Home.vue b/src/views/Home.vue
deleted file mode 100644
index e91ef23..0000000
--- a/src/views/Home.vue
+++ /dev/null
@@ -1,18 +0,0 @@
-<template>
- <div class="home">
- <img alt="Vue logo" src="../assets/logo.png">
- <HelloWorld msg="Welcome to Your Vue.js App"/>
- </div>
-</template>
-
-<script>
-// @ is an alias to /src
-import HelloWorld from '@/components/HelloWorld.vue';
-
-export default {
- name: 'Home',
- components: {
- HelloWorld,
- },
-};
-</script>