aboutsummaryrefslogtreecommitdiff
path: root/src/components
diff options
context:
space:
mode:
authorFurkan Sahin <furkan-dev@proton.me>2021-11-05 09:27:35 -0500
committerFurkan Sahin <furkan-dev@proton.me>2021-11-05 09:27:35 -0500
commit85380b4b60bf74507a01957b29bd6e3808e216db (patch)
treea43784660fdc29e5b95df3e358cf722eec2b092b /src/components
parentffef3a6be19d1139b6378c8119d444082dd0cbac (diff)
parent29fc563863f561cdc707485289c5580b4397a580 (diff)
Merge pull request #10 from cobraguy/rewrite
Update to Svelte app
Diffstat (limited to 'src/components')
-rw-r--r--src/components/ActionBar.vue85
-rw-r--r--src/components/Editor.svelte20
-rw-r--r--src/components/EditorActions.svelte128
-rw-r--r--src/components/EditorLists.svelte160
-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
9 files changed, 319 insertions, 355 deletions
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..fa2a041
--- /dev/null
+++ b/src/components/EditorActions.svelte
@@ -0,0 +1,128 @@
+<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 {
+ parseDatabaseFile,
+ 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) {
+ parseDatabaseFile(dbFile[0])
+ .then((database) => {
+ labStore.set(database.labs);
+ ptStore.set(database.peerTeachers);
+ })
+ .catch(() => {
+ snackbarText = "Failed to import database. See console for details.";
+ snackbar.open();
+ });
+ }
+ }
+
+ function exportDB() {
+ const peerTeachers = [...$ptStore.values()];
+ const labs = [...$labStore.values()];
+ const database = {
+ labs: labs,
+ peerTeachers: peerTeachers,
+ };
+
+ const dbObj = JSON.stringify(database, (_, value) => {
+ // Need to manually convert the PeerTeacher objects'
+ // `labs` set to an array because `JSON.stringify` doesn't
+ // support "stringing" it out of the box
+ if (typeof value === "object" && value instanceof Set) {
+ return [...value];
+ }
+ return value;
+ });
+
+ const blob = new Blob([dbObj], { 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>
+
+<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..1b2084a
--- /dev/null
+++ b/src/components/EditorLists.svelte
@@ -0,0 +1,160 @@
+<script lang="ts">
+ import IconButton from "@smui/icon-button";
+ import List, {
+ Item,
+ Meta,
+ PrimaryText,
+ SecondaryText,
+ Text,
+ } from "@smui/list";
+ import type PeerTeacher from "../models/PeerTeacher";
+ import { labStore, ptStore } from "../stores";
+
+ let selectedPeerTeacher: PeerTeacher | undefined;
+
+ $: 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);
+
+ $: assignedLabs = [...(selectedPeerTeacher?.labs.values() ?? [])]
+ .flatMap((labId) => {
+ const lab = $labStore.get(labId);
+ return lab === undefined ? [] : [lab];
+ })
+ .sort((a, b) => a.id - b.id);
+
+ $: compatibleLabs = labs.filter(
+ (lab) =>
+ !selectedPeerTeacher?.labs.has(lab.id) &&
+ !selectedPeerTeacher?.conflictsWith(lab.event) &&
+ !assignedLabs.some((assignment) =>
+ assignment.event.conflictsWith(lab.event)
+ )
+ );
+
+ function deletePT(id: number) {
+ if (selectedPeerTeacher?.id === id) {
+ selectedPeerTeacher = undefined;
+ }
+
+ ptStore.update((val) => {
+ val.delete(id);
+ return val;
+ });
+ }
+
+ function assignLab(id: number) {
+ selectedPeerTeacher?.labs.add(id);
+ // Self assignemnt to update `assignedLabs` and `compatibleLabs`
+ selectedPeerTeacher = selectedPeerTeacher;
+ }
+
+ function unassignLab(id: number) {
+ selectedPeerTeacher?.labs.delete(id);
+ // Self assignemnt to update `assignedLabs` and `compatibleLabs`
+ selectedPeerTeacher = selectedPeerTeacher;
+ }
+</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 on:SMUI:action={() => (selectedPeerTeacher = pt)}>
+ <Text>
+ <PrimaryText>{pt.name}</PrimaryText>
+ <SecondaryText>{pt.id}</SecondaryText>
+ </Text>
+ <Meta>
+ <IconButton
+ class="material-icons"
+ on:click$stopPropagation={() => {
+ deletePT(pt.id);
+ }}
+ >
+ remove_circle
+ </IconButton>
+ </Meta>
+ </Item>
+ {/each}
+ </List>
+ </div>
+ <div class="column">
+ <h3 class="col-header">Labs</h3>
+ <List threeLine class="editor-list">
+ {#each compatibleLabs as lab}
+ <Item>
+ <Text>
+ <PrimaryText>{lab.course}-{lab.section}</PrimaryText>
+ <SecondaryText>{lab.time}</SecondaryText>
+ <SecondaryText>{lab.location}</SecondaryText>
+ </Text>
+ <Meta class="material-icons">
+ <IconButton
+ class="material-icons"
+ on:click$stopPropagation={() => {
+ assignLab(lab.id);
+ }}
+ >
+ add_circle
+ </IconButton>
+ </Meta>
+ </Item>
+ {/each}
+ </List>
+ </div>
+ <div class="column">
+ <h3 class="col-header">
+ {selectedPeerTeacher?.name ?? "PT"} - Assigned Labs
+ </h3>
+ <List threeLine class="editor-list">
+ {#each assignedLabs as lab}
+ <Item>
+ <Text>
+ <PrimaryText>{lab.course}-{lab.section}</PrimaryText>
+ <SecondaryText>{lab.time}</SecondaryText>
+ <SecondaryText>{lab.location}</SecondaryText>
+ </Text>
+ <Meta class="material-icons">
+ <IconButton
+ class="material-icons"
+ on:click$stopPropagation={() => {
+ unassignLab(lab.id);
+ }}
+ >
+ 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>