aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/App.svelte5
-rw-r--r--src/App.vue30
-rw-r--r--src/components/ActionBar.vue85
-rw-r--r--src/components/Editor.svelte20
-rw-r--r--src/components/EditorActions.svelte86
-rw-r--r--src/components/EditorLists.svelte118
-rw-r--r--src/components/EditorLists.vue152
-rw-r--r--src/components/FileUpload.svelte11
-rw-r--r--src/components/FileUpload.vue51
-rw-r--r--src/components/List.vue35
-rw-r--r--src/components/UIButton.vue32
-rw-r--r--src/features/parser.ts119
-rw-r--r--src/global.d.ts1
-rw-r--r--src/logic/EditorActions.ts22
-rw-r--r--src/main.ts11
-rw-r--r--src/models/EventInfo.ts79
-rw-r--r--src/models/Lab.ts45
-rw-r--r--src/models/PeerTeacher.ts58
-rw-r--r--src/router/index.ts27
-rw-r--r--src/shims-vue.d.ts6
-rw-r--r--src/store/index.ts36
-rw-r--r--src/stores.ts6
-rw-r--r--src/util/error.ts6
-rw-r--r--src/util/parser.ts107
-rw-r--r--src/views/Editor.vue44
-rw-r--r--src/views/Start.vue52
-rw-r--r--src/vuex-shim.d.ts17
27 files changed, 474 insertions, 787 deletions
diff --git a/src/App.svelte b/src/App.svelte
new file mode 100644
index 0000000..4fbef54
--- /dev/null
+++ b/src/App.svelte
@@ -0,0 +1,5 @@
+<script lang="ts">
+ import Editor from "./components/Editor.svelte";
+</script>
+
+<Editor />
diff --git a/src/App.vue b/src/App.vue
deleted file mode 100644
index 5a1f608..0000000
--- a/src/App.vue
+++ /dev/null
@@ -1,30 +0,0 @@
-<template>
- <router-view/>
-</template>
-
-<style>
-*, *:before, *:after {
- box-sizing: inherit;
- font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
-}
-
-html {
- box-sizing: border-box;
-}
-
-html, body {
- margin: 0;
- padding: 0;
-}
-
-#app {
- height: 100vh;
- max-height: 100vh;
-}
-
-@media (prefers-color-scheme: dark) {
- body {
- background-color: #121212;
- }
-}
-</style>
diff --git a/src/components/ActionBar.vue b/src/components/ActionBar.vue
deleted file mode 100644
index bfe3cdb..0000000
--- a/src/components/ActionBar.vue
+++ /dev/null
@@ -1,85 +0,0 @@
-<template>
- <div id="action-bar">
- <FileUpload
- :accept="'text/plain'"
- :multiple="true"
- @fileChanged="handlePtChange">Upload PT Schedule</FileUpload>
- <FileUpload
- :accept="'application/json'"
- @fileChanged="handleLabChange">Import Lab Schedule</FileUpload>
- <UIButton @click="save">Export</UIButton>
- </div>
-</template>
-
-<script lang="ts">
-import { defineComponent } from 'vue';
-import FileUpload from '@/components/FileUpload.vue';
-import { parseLabFile, parsePtSchedule } from '@/features/parser';
-import UIButton from '@/components/UIButton.vue';
-import PeerTeacher from '@/models/PeerTeacher';
-
-export default defineComponent({
- name: 'ActionBar',
- components: {
- FileUpload,
- UIButton,
- },
- methods: {
- async handleLabChange(files: File[]) {
- const data = await parseLabFile(files[0]);
- this.$store.commit('importLabs', data);
- },
- async handlePtChange(files: File[]) {
- const promises: Promise<PeerTeacher>[] = [];
-
- files.forEach((file) => {
- promises.push(parsePtSchedule(file));
- });
-
- const result = await Promise.all(promises);
- 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>
-
-<style>
-#action-bar {
- max-width: 100vw;
- overflow-x: auto;
- white-space: nowrap;
-}
-
-#action-bar > * {
- margin-left: 0.5rem;
-}
-
-#action-bar > *:first-child {
- margin-left: 0;
-}
-</style>
diff --git a/src/components/Editor.svelte b/src/components/Editor.svelte
new file mode 100644
index 0000000..c8a30c6
--- /dev/null
+++ b/src/components/Editor.svelte
@@ -0,0 +1,20 @@
+<script lang="ts">
+ import EditorActions from "./EditorActions.svelte";
+ import EditorLists from "./EditorLists.svelte";
+</script>
+
+<div id="editor">
+ <EditorActions />
+ <EditorLists />
+</div>
+
+<style>
+ #editor {
+ display: flex;
+ flex-direction: column;
+ }
+
+ :global(#action-bar) {
+ flex-shrink: 0;
+ }
+</style>
diff --git a/src/components/EditorActions.svelte b/src/components/EditorActions.svelte
new file mode 100644
index 0000000..a0372ed
--- /dev/null
+++ b/src/components/EditorActions.svelte
@@ -0,0 +1,86 @@
+<script lang="ts">
+ import Button, { Label } from "@smui/button";
+ import IconButton from "@smui/icon-button";
+ import Snackbar, { Actions } from "@smui/snackbar";
+ import FileUpload from "./FileUpload.svelte";
+ import { parseLabScheduleFile, parsePTFile } from "../logic/EditorActions";
+ import { labStore, ptStore } from "../stores";
+
+ let ptSchedules: FileList | null;
+ let labSchedule: FileList | null;
+ let dbFile: FileList | null;
+ let snackbar;
+ let snackbarText;
+
+ $: {
+ if (ptSchedules?.length) {
+ const promises = [...ptSchedules].map((file) => parsePTFile(file));
+ Promise.allSettled(promises)
+ .then((results) =>
+ results.flatMap((result) => {
+ if (result.status === "fulfilled") {
+ ptStore.update((val) => val.set(result.value.id, result.value));
+ return [];
+ } else {
+ return [result];
+ }
+ })
+ )
+ .then((failed) => {
+ if (failed.length) {
+ snackbarText = `Failed to add ${failed.length} PTs. See console for details.`;
+ snackbar.open();
+ }
+ });
+ }
+ }
+
+ $: {
+ if (labSchedule?.length) {
+ parseLabScheduleFile(labSchedule[0])
+ .then((labs) => {
+ labStore.update(() => new Map(labs.map((lab) => [lab.id, lab])));
+ })
+ .catch(() => {
+ snackbarText =
+ "Failed to import lab schedule. See console for details.";
+ snackbar.open();
+ });
+ }
+ }
+
+ $: {
+ if (dbFile?.length) console.log(dbFile);
+ }
+
+ function exportDB() {}
+</script>
+
+<div id="action-bar">
+ <FileUpload accept="text/plain" multiple={true} bind:files={ptSchedules}>
+ <Label>Add PT</Label>
+ </FileUpload>
+ <FileUpload accept="application/json" bind:files={labSchedule}>
+ <Label>Import Labs</Label>
+ </FileUpload>
+ <FileUpload accept="application/json" bind:files={dbFile}>
+ <Label>Import DB</Label>
+ </FileUpload>
+ <Button variant="raised" ripple={false} on:click={exportDB}>
+ <Label>Export DB</Label>
+ </Button>
+</div>
+<Snackbar bind:this={snackbar} labelText={snackbarText}>
+ <Label />
+ <Actions>
+ <IconButton class="material-icons" title="Dismiss">close</IconButton>
+ </Actions>
+</Snackbar>
+
+<style>
+ #action-bar {
+ max-width: 100vw;
+ overflow-x: auto;
+ white-space: nowrap;
+ }
+</style>
diff --git a/src/components/EditorLists.svelte b/src/components/EditorLists.svelte
new file mode 100644
index 0000000..1fc484b
--- /dev/null
+++ b/src/components/EditorLists.svelte
@@ -0,0 +1,118 @@
+<script lang="ts">
+ import IconButton from "@smui/icon-button";
+ import List, {
+ Item,
+ Meta,
+ PrimaryText,
+ SecondaryText,
+ Text,
+ } from "@smui/list";
+ import { labStore, ptStore } from "../stores";
+
+ $: peerTeachers = [...$ptStore.values()].sort((a, b) =>
+ a.lastname.toUpperCase() === b.lastname.toUpperCase()
+ ? a.firstname.toUpperCase().localeCompare(b.firstname.toUpperCase())
+ : a.lastname.toUpperCase().localeCompare(b.lastname.toUpperCase())
+ );
+
+ $: labs = [...$labStore.values()].sort((a, b) => a.id - b.id);
+
+ const nothing: any[] = [];
+</script>
+
+<div id="editor-lists">
+ <div class="column">
+ <h3 class="col-header">Peer Teachers</h3>
+ <List twoline singleSelection class="editor-list">
+ {#each peerTeachers as pt}
+ <Item>
+ <Text>
+ <PrimaryText>{pt.firstname} {pt.lastname}</PrimaryText>
+ <SecondaryText>{pt.id}</SecondaryText>
+ </Text>
+ <Meta>
+ <IconButton
+ class="material-icons"
+ on:click$stopPropagation={() => {
+ ptStore.update((val) => {
+ val.delete(pt.id);
+ return val;
+ });
+ }}>remove_circle</IconButton
+ >
+ </Meta>
+ </Item>
+ {/each}
+ </List>
+ </div>
+ <div class="column">
+ <h3 class="col-header">Labs</h3>
+ <List twoline singleSelection class="editor-list">
+ {#each labs as lab}
+ <Item>
+ <Text>
+ <PrimaryText>{lab.course}-{lab.section} [{lab.time}]</PrimaryText>
+ <SecondaryText>
+ {lab.location}
+ </SecondaryText>
+ </Text>
+ <Meta class="material-icons"
+ ><IconButton
+ class="material-icons"
+ on:click$stopPropagation={() => {
+ console.log("hello");
+ }}>add_circle</IconButton
+ ></Meta
+ >
+ </Item>
+ {/each}
+ </List>
+ </div>
+ <div class="column">
+ <h3 class="col-header">PT - Assigned Labs</h3>
+ <List twoline singleSelection class="editor-list">
+ {#each nothing as n}
+ <Item>
+ <Text>
+ <PrimaryText>{n.course} {n.section}</PrimaryText>
+ <SecondaryText>
+ {n.event.days}
+ {n.event.start}-{n.event.end}
+ </SecondaryText>
+ </Text>
+ <Meta class="material-icons"
+ ><IconButton
+ class="material-icons"
+ on:click$stopPropagation={() => {
+ console.log("hello");
+ }}>remove_circle</IconButton
+ ></Meta
+ >
+ </Item>
+ {/each}
+ </List>
+ </div>
+</div>
+
+<style>
+ #editor-lists {
+ display: flex;
+ max-height: inherit;
+ min-height: 0;
+ overflow-x: auto;
+ }
+
+ .col-header {
+ font-family: Roboto, sans-serif;
+ }
+
+ .column {
+ flex: 1;
+ min-width: 15em;
+ overflow: auto;
+ }
+
+ * :global(.editor-list) {
+ border: 1px solid black;
+ }
+</style>
diff --git a/src/components/EditorLists.vue b/src/components/EditorLists.vue
deleted file mode 100644
index dff34f4..0000000
--- a/src/components/EditorLists.vue
+++ /dev/null
@@ -1,152 +0,0 @@
-<template>
- <div id="editor-lists">
- <div class="column">
- <h3 class="column-header">Peer Teachers</h3>
- <List
- :items="peerTeachers"
- @selection-changed="handlePtClick"
- #default="{ item: { id, name } }">
- <span class="list-item">
- {{ name }} <button @click.stop="deletePeerTeacher(id)">x</button>
- </span>
- </List>
- </div>
- <div class="column">
- <h3 class="column-header">Labs</h3>
- <List :items="compatibleLabs" #default="{ item: { fullInfo, id } }">
- <span class="list-item">
- {{ fullInfo }} <button @click.stop="assignLab(id)">+</button>
- </span>
- </List>
- </div>
- <div class="column">
- <h3 class="column-header">{{ this.selectedPeerTeacher.name }}</h3>
- <List :items="selectedPeerTeacherAssignments" #default="{ item: { fullInfo, id } }">
- <span class="list-item">
- {{ fullInfo }} <button @click.stop="unassignLab(id)">x</button>
- </span>
- </List>
- </div>
- </div>
-</template>
-
-<script lang="ts">
-import { defineComponent, PropType } from 'vue';
-import List from '@/components/List.vue';
-import PeerTeacher from '@/models/PeerTeacher';
-import Lab from '@/models/Lab';
-
-export default defineComponent({
- name: 'EditorLists',
- components: {
- List,
- },
- props: {
- peerTeachers: {
- type: Array as PropType<PeerTeacher[]>,
- default: () => [],
- },
- labs: {
- type: Array as PropType<Lab[]>,
- default: () => [],
- },
- },
- computed: {
- compatibleLabs(): Lab[] {
- 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) => !currentAssignments
- .some((assignedLab) => lab.event.conflictsWith(assignedLab.event)));
- },
- selectedPeerTeacherAssignments(): Lab[] {
- return Array.from(this.selectedPeerTeacher.assignedLabs.values())
- .flatMap((id) => {
- const lab = this.$store.state.labs.get(id);
- return (lab === undefined) ? [] : [lab];
- }).sort((a, b) => a.course - b.course || a.section - b.section);
- },
- },
- data() {
- return {
- selectedPeerTeacher: new PeerTeacher(),
- };
- },
- methods: {
- handlePtClick(peerTeacher: PeerTeacher) {
- this.selectedPeerTeacher = peerTeacher;
- },
- deletePeerTeacher(id: number) {
- if (this.selectedPeerTeacher.id === id) {
- this.selectedPeerTeacher = new PeerTeacher();
- }
- this.$store.commit('deletePeerTeacher', id);
- },
- assignLab(id: string) {
- if (this.selectedPeerTeacher.id !== 0) {
- this.selectedPeerTeacher.assignedLabs.add(id);
- }
- },
- unassignLab(id: string) {
- this.selectedPeerTeacher.assignedLabs.delete(id);
- },
- },
-});
-</script>
-
-<style>
-#editor-lists {
- display: flex;
- max-height: inherit;
- min-height: 0;
-}
-
-.column-header {
- margin: 0;
-}
-
-.column {
- flex: 1;
- overflow: auto;
-}
-
-.list-item {
- align-items: center;
- background-color: #f0f0f0;
- display: flex;
- justify-content: space-between;
- margin: 0.5em;
- padding: 0.5em;
-}
-
-.list-item:hover {
- background-color: #c0c0c0;
-}
-
-.list-item > button {
- background-color: #500000;
- border: none;
- color: white;
- font-size: 1em;
-}
-
-@media (prefers-color-scheme: dark) {
- .column-header {
- color: white;
- }
-
- .list-item {
- background-color: #303030;
- color: white;
- }
-
- .list-item:hover {
- background-color: #707070;
- }
-
- .list-item > button {
- background-color: #81302b;
- }
-}
-</style>
diff --git a/src/components/FileUpload.svelte b/src/components/FileUpload.svelte
new file mode 100644
index 0000000..d325e32
--- /dev/null
+++ b/src/components/FileUpload.svelte
@@ -0,0 +1,11 @@
+<script lang="ts">
+ export let accept = "";
+ export let multiple = false;
+ export let files: FileList | null = null;
+</script>
+
+<label class="mdc-button mdc-button--raised mdc-ripple-upgraded">
+ <div class="mdc-button__ripple" />
+ <slot>Upload</slot>
+ <input type="file" {accept} {multiple} bind:files hidden />
+</label>
diff --git a/src/components/FileUpload.vue b/src/components/FileUpload.vue
deleted file mode 100644
index 082fc9d..0000000
--- a/src/components/FileUpload.vue
+++ /dev/null
@@ -1,51 +0,0 @@
-<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 lang="ts">
-import { defineComponent } from 'vue';
-
-export default defineComponent({
- name: 'FileUpload',
- props: {
- accept: {
- type: String,
- required: false,
- },
- multiple: {
- type: Boolean,
- default: false,
- },
- },
- emits: {
- fileChanged: null,
- },
-});
-</script>
-
-<style scoped>
-.file-upload-lbl {
- background-color: #500000;
- color: white;
- padding: 0.5em;
- text-align: center;
-}
-
-.file-upload-lbl:hover {
- color: grey;
- cursor: pointer;
-}
-
-@media (prefers-color-scheme: dark) {
- .file-upload-lbl {
- background-color: #81302b;
- }
-}
-</style>
diff --git a/src/components/List.vue b/src/components/List.vue
deleted file mode 100644
index 4732a9e..0000000
--- a/src/components/List.vue
+++ /dev/null
@@ -1,35 +0,0 @@
-<template>
- <ul class="list">
- <li
- v-for="item in items"
- :key="item.id"
- @click="$emit('selectionChanged', item)">
- <slot :item="item">{{ item }}</slot>
- </li>
- </ul>
-</template>
-
-<script lang="ts">
-import { defineComponent } from 'vue';
-
-export default defineComponent({
- name: 'List',
- props: {
- items: {
- type: Array,
- default: [] as any[],
- },
- },
- emits: {
- selectionChanged: null,
- },
-});
-</script>
-
-<style scoped>
-.list {
- list-style-type: none;
- margin: 0;
- padding: 0;
-}
-</style>
diff --git a/src/components/UIButton.vue b/src/components/UIButton.vue
deleted file mode 100644
index 57b155b..0000000
--- a/src/components/UIButton.vue
+++ /dev/null
@@ -1,32 +0,0 @@
-<template>
- <button class="ui-button"><slot></slot></button>
-</template>
-
-<script lang="ts">
-import { defineComponent } from 'vue';
-
-export default defineComponent({
- name: 'UIButton',
-});
-</script>
-
-<style>
-.ui-button {
- background-color: #500000;
- border: none;
- color: white;
- font-size: 1em;
- padding: 0.5em;
-}
-
-.ui-button:hover {
- color: grey;
- cursor: pointer;
-}
-
-@media (prefers-color-scheme: dark) {
- .ui-button {
- background-color: #81302b;
- }
-}
-</style>
diff --git a/src/features/parser.ts b/src/features/parser.ts
deleted file mode 100644
index 9d61946..0000000
--- a/src/features/parser.ts
+++ /dev/null
@@ -1,119 +0,0 @@
-import Lab from '@/models/Lab';
-import PeerTeacher from '@/models/PeerTeacher';
-import EventInfo from '../models/EventInfo';
-
-export async function parseLabFile(file: File): Promise<Lab[]> {
- const validCourses = [
- '110',
- '111',
- '121',
- '206',
- '221',
- // '222',
- '312',
- '313',
- // '314',
- '315',
- ];
- const result: Lab[] = [];
-
- const text = await file.text();
- let jsonData;
- try {
- jsonData = JSON.parse(text);
- } catch (e) {
- throw new Error(e);
- }
- const labs = jsonData.data;
-
- labs.forEach((lab: any) => {
- if (validCourses.includes(lab.courseNumber)) {
- const newLab = new Lab(lab.courseNumber, lab.sequenceNumber);
-
- lab.meetingsFaculty.every((meeting: any) => {
- 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);
- }
- });
-
- return result;
-}
-
-export async function parsePtSchedule(file: File): Promise<PeerTeacher> {
- const text = await file.text();
- const peerTeacher = new PeerTeacher();
- // namePatter: <firstname> <lastname> <uin>
- const namePattern = /^(.*)\s(.*)\s(\d{9})/;
- // eventPattern (24hr time): MTWRF hh:mm - hh:mm
- const eventPattern = /^(M?T?W?R?F?)\s(\d{1,2}:\d{2})\s?-\s?(\d{1,2}:\d{2})/;
- const lines = text.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);
- }
- });
-
- return peerTeacher;
-}
-
-export async function parsePtDatabase(file: File): Promise<{
- labs: Map<string, Lab>,
- peerTeachers: Map<number, PeerTeacher>
-}> {
- const text = await file.text();
- const jsonObj = JSON.parse(text);
- const result = {
- labs: new Map(),
- peerTeachers: new Map(),
- };
-
- Object.keys(jsonObj.labs).forEach((key) => {
- const { course, section, event: { days, start, end } } = jsonObj.labs[key];
- result.labs.set(key, new Lab(course,
- section, new EventInfo(days, start, 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: any) => new EventInfo(eventObj.days,
- eventObj.start, eventObj.end));
- ptObj.assignedLabs = new Set();
- pt.assignedLabs.forEach((labId: string) => {
- ptObj.assignedLabs.add(labId);
- });
- result.peerTeachers.set(key, ptObj);
- });
-
- return result;
-}
diff --git a/src/global.d.ts b/src/global.d.ts
new file mode 100644
index 0000000..0e72969
--- /dev/null
+++ b/src/global.d.ts
@@ -0,0 +1 @@
+/// <reference types="svelte" /> \ No newline at end of file
diff --git a/src/logic/EditorActions.ts b/src/logic/EditorActions.ts
new file mode 100644
index 0000000..875fdc4
--- /dev/null
+++ b/src/logic/EditorActions.ts
@@ -0,0 +1,22 @@
+import { parseLabSchedule, parsePTSchedule } from "../util/parser";
+
+export async function parsePTFile(file: File) {
+ try {
+ const text = await file.text();
+ return parsePTSchedule(text);
+ } catch (error) {
+ console.error(file.name, error);
+ throw error;
+ }
+}
+
+export async function parseLabScheduleFile(file: File) {
+ const text = await file.text();
+ try {
+ const labSchedule = JSON.parse(text);
+ return parseLabSchedule(labSchedule);
+ } catch (error) {
+ console.error(file.name, error);
+ throw error;
+ }
+}
diff --git a/src/main.ts b/src/main.ts
index c673f53..4c473fa 100644
--- a/src/main.ts
+++ b/src/main.ts
@@ -1,6 +1,7 @@
-import { createApp } from 'vue';
-import App from './App.vue';
-import router from './router';
-import store from './store';
+import App from './App.svelte';
-createApp(App).use(store).use(router).mount('#app');
+const app = new App({
+ target: document.body
+});
+
+export default app; \ No newline at end of file
diff --git a/src/models/EventInfo.ts b/src/models/EventInfo.ts
index 962ec70..d20682c 100644
--- a/src/models/EventInfo.ts
+++ b/src/models/EventInfo.ts
@@ -1,46 +1,45 @@
export default class EventInfo {
- days: string;
-
- start: number;
-
- end: number;
-
- constructor(days: string = '', start = 0, end = 0) {
- this.days = days;
- this.start = start;
- this.end = end;
- }
-
- static timeToStr(time: number) {
- let hour = Math.floor(time / 100);
- const minute = time % 100;
- const meridiem = (hour < 12) ? 'AM' : 'PM';
-
- if (hour === 0) {
- hour = 12;
- } else if (hour > 12) {
- hour -= 12;
- }
-
- if (minute < 10) {
- return `${hour}:0${minute} ${meridiem}`;
+ days: string;
+ start: number;
+ end: number;
+
+ constructor(days: string, start: number | string, end: number | string) {
+ if (typeof start === "string") {
+ start = parseInt(start, 10);
+ }
+ if (typeof end === "string") {
+ end = parseInt(end, 10);
+ }
+
+ this.days = days;
+ this.start = start;
+ this.end = end;
}
- return `${hour}:${minute} ${meridiem}`;
- }
-
- conflictsWith(event: EventInfo) {
- const daysConflict = event.days.match(new RegExp(`[${this.days}]`));
- if (daysConflict) {
- return (this.start <= event.end) && (event.start <= this.end);
+ static timeToStr(time: number) {
+ let hour = Math.floor(time / 100);
+ const minute = time % 100;
+ const meridiem = (hour < 12) ? 'AM' : 'PM';
+
+ if (hour === 0) {
+ hour = 12;
+ } else if (hour > 12) {
+ hour -= 12;
+ }
+
+ if (minute < 10) {
+ return `${hour}:0${minute} ${meridiem}`;
+ }
+ return `${hour}:${minute} ${meridiem}`;
}
- return false;
- }
- get info() {
- if (this.days === '') {
- return 'WEB';
+ get info() {
+ if(this.days === "") {
+ return `WEB`;
+ }else if(this.start === -1 || this.end === -1) {
+ return `${this.days}`;
+ } else {
+ return `${this.days} ${EventInfo.timeToStr(this.start)}-${EventInfo.timeToStr(this.end)}`;
+ }
}
- return `${this.days} ${EventInfo.timeToStr(this.start)}-${EventInfo.timeToStr(this.end)}`;
- }
-}
+} \ No newline at end of file
diff --git a/src/models/Lab.ts b/src/models/Lab.ts
index a6972ef..8636f74 100644
--- a/src/models/Lab.ts
+++ b/src/models/Lab.ts
@@ -1,23 +1,34 @@
-import EventInfo from '@/models/EventInfo';
+import type EventInfo from "./EventInfo";
export default class Lab {
- course: number;
+ id: number;
+ course: number;
+ section: number;
+ event: EventInfo;
+ building: string;
+ room: string;
- section: number;
+ constructor(course: number | string, section: number | string, event: EventInfo, building = "", room = "") {
+ if(typeof course === "string") {
+ course = parseInt(course, 10);
+ }
+ if(typeof section === "string") {
+ section = parseInt(section, 10);
+ }
- event: EventInfo;
+ this.id = parseInt(`${course}${section}`, 10);
+ this.course = course;
+ this.section = section;
+ this.event = event;
+ this.building = building;
+ this.room = room;
+ }
- constructor(course = 0, section = 0, event = new EventInfo()) {
- this.course = course;
- this.section = section;
- this.event = event;
- }
+ get time() {
+ return this.event.info;
+ }
- get id() {
- return `${this.course}-${this.section}`;
- }
-
- get fullInfo() {
- return `${this.id} ${this.event.info}`;
- }
-}
+ get location() {
+ return `${this.building}-${this.room}`;
+ }
+} \ No newline at end of file
diff --git a/src/models/PeerTeacher.ts b/src/models/PeerTeacher.ts
index f387431..6518a80 100644
--- a/src/models/PeerTeacher.ts
+++ b/src/models/PeerTeacher.ts
@@ -1,41 +1,21 @@
-import EventInfo from './EventInfo';
+import type EventInfo from "./EventInfo";
export default class PeerTeacher {
- firstname: string;
-
- lastname: string;
-
- uin: number;
-
- events: EventInfo[];
-
- assignedLabs: Set<string>;
-
- constructor(firstname = '', lastname = '', uin = 0) {
- this.firstname = firstname;
- this.lastname = lastname;
- this.uin = uin;
- this.events = [];
- this.assignedLabs = new Set();
- }
-
- conflictsWith(event: EventInfo) {
- 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;
- }
-}
+ id: number;
+ firstname: string;
+ lastname: string;
+ events: EventInfo[];
+ labs: Set<number>;
+
+ constructor(id: number | string, firstname: string, lastname: string) {
+ if(typeof id === "string") {
+ id = parseInt(id, 10);
+ }
+
+ this.id = id;
+ this.firstname = firstname;
+ this.lastname = lastname;
+ this.events = [];
+ this.labs = new Set();
+ }
+} \ No newline at end of file
diff --git a/src/router/index.ts b/src/router/index.ts
deleted file mode 100644
index a36cb7e..0000000
--- a/src/router/index.ts
+++ /dev/null
@@ -1,27 +0,0 @@
-import { createRouter, createWebHashHistory } from 'vue-router';
-import Editor from '../views/Editor.vue';
-import Start from '../views/Start.vue';
-
-const routes = [
- {
- path: '/',
- name: 'Start',
- component: Start,
- },
- {
- 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/Editor.vue'),
- component: Editor,
- },
-];
-
-const router = createRouter({
- history: createWebHashHistory(),
- routes,
-});
-
-export default router;
diff --git a/src/shims-vue.d.ts b/src/shims-vue.d.ts
deleted file mode 100644
index 3804a43..0000000
--- a/src/shims-vue.d.ts
+++ /dev/null
@@ -1,6 +0,0 @@
-/* eslint-disable */
-declare module '*.vue' {
- import type { DefineComponent } from 'vue'
- const component: DefineComponent<{}, {}, any>
- export default component
-}
diff --git a/src/store/index.ts b/src/store/index.ts
deleted file mode 100644
index e079259..0000000
--- a/src/store/index.ts
+++ /dev/null
@@ -1,36 +0,0 @@
-import { createStore } from 'vuex';
-import Lab from '@/models/Lab';
-import PeerTeacher from '@/models/PeerTeacher';
-
-export default createStore({
- state: {
- labs: new Map(),
- peerTeachers: new Map(),
- },
- mutations: {
- setLabs(state, labs: Map<string, Lab>) {
- state.labs = labs;
- },
- setPeerTeachers(state, peerTeachers: Map<number, PeerTeacher>) {
- state.peerTeachers = peerTeachers;
- },
- importLabs(state, labs: Lab[]) {
- state.labs.clear();
- labs.forEach((lab) => {
- state.labs.set(lab.id, lab);
- });
- },
- addPeerTeachers(state, peerTeachers: PeerTeacher[]) {
- peerTeachers.forEach((pt) => {
- state.peerTeachers.set(pt.id, pt);
- });
- },
- deletePeerTeacher(state, id: number) {
- state.peerTeachers.delete(id);
- },
- },
- actions: {
- },
- modules: {
- },
-});
diff --git a/src/stores.ts b/src/stores.ts
new file mode 100644
index 0000000..f6c3a9b
--- /dev/null
+++ b/src/stores.ts
@@ -0,0 +1,6 @@
+import { writable } from "svelte/store";
+import type Lab from "./models/Lab";
+import type PeerTeacher from "./models/PeerTeacher";
+
+export const ptStore = writable(new Map<number, PeerTeacher>());
+export const labStore = writable(new Map<number, Lab>()); \ No newline at end of file
diff --git a/src/util/error.ts b/src/util/error.ts
new file mode 100644
index 0000000..5380849
--- /dev/null
+++ b/src/util/error.ts
@@ -0,0 +1,6 @@
+export class PeerTeacherImportError extends Error {
+ constructor(message) {
+ super(message);
+ this.name = "PeerTeacherImportError";
+ }
+} \ No newline at end of file
diff --git a/src/util/parser.ts b/src/util/parser.ts
new file mode 100644
index 0000000..6d706d7
--- /dev/null
+++ b/src/util/parser.ts
@@ -0,0 +1,107 @@
+import EventInfo from "../models/EventInfo";
+import Lab from "../models/Lab";
+import PeerTeacher from "../models/PeerTeacher";
+import { PeerTeacherImportError } from "./error";
+
+interface LabSchedule {
+ data: {
+ courseNumber: string,
+ sequenceNumber: string,
+ meetingsFaculty: {
+ meetingTime: {
+ beginTime: string | null,
+ building: string,
+ endTime: string | null,
+ friday: boolean,
+ meetingType: string,
+ monday: boolean,
+ room: string,
+ thursday: boolean,
+ tuesday: boolean,
+ wednesday: boolean
+ }
+ }[]
+ }[]
+};
+
+/**
+ * Parses a peer teacher schedule
+ * @param schedule The schedule to parse
+ * @returns A peer teacher
+ */
+export function parsePTSchedule(schedule: string) {
+ // namePatter: <firstname> <lastname> <uin>
+ const namePattern = /^(.*)\s(.*)\s(\d{9})/;
+ // eventPattern (24hr time): MTWRF hh:mm - hh:mm
+ const eventPattern = /^(M?T?W?R?F?)\s(\d{1,2}:\d{2})\s?-\s?(\d{1,2}:\d{2})/;
+ const lines = schedule.split("\n").filter(line => line.trim());
+
+ const nameLine = lines.find(line => line.match(namePattern));
+ if(nameLine === undefined) {
+ throw new PeerTeacherImportError(`No peer teacher in schedule`);
+ }
+
+ const [, firstname, lastname, uin] = nameLine.match(namePattern) as RegExpMatchArray;
+ const peerTeacher = new PeerTeacher(uin, firstname, lastname);
+
+ const events = lines
+ .filter(line => line.match(eventPattern))
+ .map(line => {
+ let [, days, start, end] = line.match(eventPattern) as RegExpMatchArray;
+ start = start.replace(":", "");
+ end = end.replace(":", "");
+ return new EventInfo(days, start, end);
+ });
+
+ peerTeacher.events = events;
+ return peerTeacher;
+}
+
+/**
+ * Parses the course schedule into labs attended by PTs
+ * @param schedule The course schedule object from Howdy
+ * @returns An array of labs
+ */
+export function parseLabSchedule(schedule: LabSchedule) {
+ const taughtCourses = ['110', '111', '121', '206', '221', '312', '313', '315'];
+ const results: Lab[] = [];
+
+ const courses = schedule.data;
+ for(const course of courses) {
+ if(!taughtCourses.includes(course.courseNumber)) {
+ continue;
+ }
+
+ for(const meeting of course.meetingsFaculty) {
+ const { meetingTime } = meeting;
+
+ if(meetingTime.meetingType !== "LAB") {
+ continue;
+ }
+
+ let days = "";
+ days += meetingTime.monday ? 'M' : '';
+ days += meetingTime.tuesday ? 'T' : '';
+ days += meetingTime.wednesday ? 'W' : '';
+ days += meetingTime.thursday ? 'R' : '';
+ days += meetingTime.friday ? 'F' : '';
+
+ const start = meetingTime.beginTime === null ? -1 : meetingTime.beginTime;
+ const end = meetingTime.endTime === null ? -1 : meetingTime.endTime;
+ const { courseNumber, sequenceNumber } = course;
+ const { building, room} = meetingTime;
+
+ results.push(
+ new Lab(
+ courseNumber,
+ sequenceNumber,
+ new EventInfo(days, start, end),
+ building,
+ room
+ )
+ );
+ }
+ }
+
+ return results;
+} \ No newline at end of file
diff --git a/src/views/Editor.vue b/src/views/Editor.vue
deleted file mode 100644
index 795ee9d..0000000
--- a/src/views/Editor.vue
+++ /dev/null
@@ -1,44 +0,0 @@
-<template>
- <div id="editor">
- <ActionBar />
- <EditorLists :peerTeachers="peerTeachers" :labs="labs" />
- </div>
-</template>
-
-<script lang="ts">
-import { defineComponent } from 'vue';
-import ActionBar from '@/components/ActionBar.vue';
-import EditorLists from '@/components/EditorLists.vue';
-import Lab from '@/models/Lab';
-import PeerTeacher from '@/models/PeerTeacher';
-
-export default defineComponent({
- name: 'Editor',
- components: {
- ActionBar,
- EditorLists,
- },
- computed: {
- labs(): Lab[] {
- return Array.from(this.$store.state.labs.values()).sort((a, b) => a.id
- .localeCompare(b.id));
- },
- peerTeachers(): PeerTeacher[] {
- return Array.from(this.$store.state.peerTeachers.values()).sort((a, b) => a.lastname
- .toUpperCase().localeCompare(b.lastname.toUpperCase()));
- },
- },
-});
-</script>
-
-<style>
-#editor {
- display: flex;
- flex-direction: column;
- max-height: inherit;
-}
-
-#action-bar {
- flex-shrink: 0;
-}
-</style>
diff --git a/src/views/Start.vue b/src/views/Start.vue
deleted file mode 100644
index f32c337..0000000
--- a/src/views/Start.vue
+++ /dev/null
@@ -1,52 +0,0 @@
-<template>
- <div id="start">
- <router-link to="/editor">
- <UIButton>Create new database</UIButton>
- </router-link>
- <FileUpload
- :accept="'application/json'"
- @fileChanged="handleDatabaseChange">Use existing database</FileUpload>
- </div>
-</template>
-
-<script lang="ts">
-import { defineComponent } from 'vue';
-import FileUpload from '@/components/FileUpload.vue';
-import { parsePtDatabase } from '@/features/parser';
-import UIButton from '@/components/UIButton.vue';
-
-export default defineComponent({
- name: 'Start',
- components: {
- FileUpload,
- UIButton,
- },
- methods: {
- async handleDatabaseChange(files: File[]) {
- const result = await parsePtDatabase(files[0]);
- this.$store.commit('setLabs', result.labs);
- this.$store.commit('setPeerTeachers', result.peerTeachers);
- this.$router.push({ name: 'Editor' });
- },
- },
-});
-</script>
-
-<style>
-#start {
- align-items: center;
- display: flex;
- flex-direction: column;
- font-size: 1.5rem;
- height: 100vh;
- justify-content: center;
-}
-
-#start > * {
- margin-top: 1em;
-}
-
-#start > *:first-child {
- margin-top: 0;
-}
-</style>
diff --git a/src/vuex-shim.d.ts b/src/vuex-shim.d.ts
deleted file mode 100644
index c8a55f9..0000000
--- a/src/vuex-shim.d.ts
+++ /dev/null
@@ -1,17 +0,0 @@
-import { Store } from 'vuex';
-
-import PeerTeacher from './models/PeerTeacher';
-import Lab from './models/Lab';
-
-declare module '@vue/runtime-core' {
- // declare your own store states
- interface State {
- peerTeachers: Map<number, PeerTeacher>,
- labs: Map<string, Lab>
- }
-
- // provide typings for `this.$store`
- interface ComponentCustomProperties {
- $store: Store<State>
- }
-}