VIAplanner/via-timetable

View on GitHub
src/components/Timetable/TimetableEvent.vue

Summary

Maintainability
Test Coverage
<template>
  <div style="width: 100%;">
    <div v-if="event.start > 0">
      <v-dialog v-model="dialog" scrollable width="825px" @input="atInput">
        <template v-slot:activator="{ on }">
          <div
            @mouseover="hovered = true"
            @mouseleave="hovered = false"
            v-on="on"
            class="event"
            :style="{
              background: getCourseColor(event.code),
              marginTop: getMarginTop,
              marginBot: getMarginBot,
              height: getHeight,
              color: $vuetify.theme.dark ? 'inherit' : 'white'
            }"
          >
            <h4 class='pb-2'>{{ event.code }}
              <warning
                v-if='getWarningSections.some(x=>event.code==x.code&&event.sectionCode === x.sectionCode)' />
            </h4>

            <div class="lock-button">
              <v-btn small dark @click.stop="lockToggle" v-if="locked" icon>
                <v-icon>mdi-lock</v-icon>
              </v-btn>
              <v-btn
                small
                dark
                @click.stop="lockToggle"
                v-if="!locked && hovered"
                icon
              >
                <v-icon>mdi-lock-open</v-icon>
              </v-btn>
            </div>

            <v-row class="px-3">
              <div>{{ event.sectionCode }} {{ deliveryMethod }}</div>
              <v-spacer />
              <div v-if="locations.length === 2 && duration === 1">
                {{ locations[0] }}
              </div>
            </v-row>

            <v-row class="px-3">
              <div>{{ getFormattedTime(event.start, event.end) }}</div>
              <v-spacer />
              <div v-if="locations.length === 1 || duration != 1">
                {{ locations[0] }}
              </div>
              <div v-else-if="locations.length === 2 && duration === 1">
                {{ locations[1] }}
              </div>
            </v-row>
            <v-row class="px-3" v-if="locations.length === 2 && duration != 1">
              <v-spacer />
              <div>{{ locations[1] }}</div>
            </v-row>
          </div>
        </template>
        <course-section-picker
          v-on:done="dialog = false"
          :code="event.code"
          ref="popUp"
        />,
      </v-dialog>
    </div>
    <div
      v-else-if="checkHalfHour(event.currStart, event.currEnd)"
      class="event empty-event"
      :style="{ height: getHeight }"
    />
    <div
      v-else
      class="event empty-event"
      :style="{ height: getHeight, background: dynamicColor }"
      @mouseover="hovered = true"
      @mouseleave="hovered = false"
    >
      <div v-if='hovered' style='margin: 0;padding: 0;height: 100%;display:flex'
           @click='lockedSectionToggle' v-ripple>
        <v-row>
          <v-col>
            <p class="center unselectable" :style='`color:${this.$vuetify.theme.dark? "#ffffffaa" : "black"}`'>{{ dynamicText }}</p>
          </v-col>
        </v-row>
      </div>
    </div>
  </div>
</template>

<script>
import { mapGetters, mapActions, mapMutations } from 'vuex';
import CourseSectionPicker from '../Popup/CourseSectionPicker.vue';
import Warning from '../SidePanel/Warning.vue';

const convertSecondsToHours = seconds => seconds / 3600;

export default {
  name: 'timetable-event',
  props: {
    event: {
      type: Object,
      default: () => {},
    },
    currDay: {
      type: String,
      default: '',
    },
    semester: String,
  },
  components: {
    Warning,
    CourseSectionPicker,
  },
  data() {
    return {
      hovered: false,
      dialog: false,
      height: window.innerHeight,
    };
  },
  created() {
    window.addEventListener('resize', this.handleResize);
  },
  computed: {
    ...mapGetters([
      'getCourseColor',
      'fallLockedSections',
      'winterLockedSections',
      'getWarningSections',
    ]),
    // Duration of the event in hours
    duration() {
      // Real course
      if (this.event.start > 0) {
        return convertSecondsToHours(this.event.currEnd - this.event.start);
      }
      // Empty or blocked hour
      else {
        return convertSecondsToHours(this.event.currEnd - this.event.currStart);
      }
    },

    getMarginTop() {
      return "olap_start" in this.event ? `${convertSecondsToHours(this.event.start - this.event.olap_start) * this.oneHourHeight}px` : `0`;
    },
    getMarginBot() {
    // If a bottom margin is applicable
    return "olap_end" in this.event ? `${convertSecondsToHours(this.event.olap_end - this.event.currEnd) * this.oneHourHeight}px` : `0`;
    },
    getHeight() {
      return `${this.duration * this.oneHourHeight}px`;
    },
    oneHourHeight() {
      // the height of a timetable event will be at least 65 px
      return (this.height - 168) / 9 > 65 ? (this.height - 168) / 9 : 65;
    },
    deliveryMethod() {
      if (this.event.method === 'INPER') {
        return '(In Person)';
      } else if (this.event.method === 'ROTATE') {
        return '(Rotate)';
      } else {
        return '(Sync)';
      }
    },
    locations() {
      return this.event.location.split('; ');
    },
    dynamicText() {
      return !this.locked ? 'Block This Time' : 'Unblock This Time';
    },
    // change the color in the event so it correct based on hovering or locked
    dynamicColor() {
      const lockedColor = this.$vuetify.theme.dark ? '#212121' : '#d9d9d9';
      const background = this.$vuetify.theme.dark ? '#2C2C2C' : 'white';
      if (this.locked) {
        return lockedColor;
      } else {
        return this.hovered ? lockedColor : background;
      }
    },
    // stores the info of the current section
    currSecData() {
      return {
        name: `Locked Section`,
        courseCode: `Lock${this.semester}${this.currDay}${this.event.currStart}`,
        meeting_sections: [
          {
            sectionCode: 'L0001',
            instructors: ['NA'],
            times: [
              {
                day: this.currDay,
                start: this.event.currStart,
                end: this.event.currStart + 3600,
                location: 'NA',
              },
            ],
          },
        ],
      };
    },
    // lock the status of the current section
    locked() {
      const lockedSections =
        this.semester === 'F'
          ? this.fallLockedSections
          : this.winterLockedSections;
      for (const section of lockedSections) {
        if (
          section === `${this.event.code}${this.event.sectionCode}` ||
          section ===
            `${this.currSecData.courseCode}${this.currSecData.meeting_sections[0].sectionCode}`
        ) {
          return true;
        }
      }
      return false;
    },
  },
  methods: {
    ...mapActions(['deleteCourse']),
    ...mapMutations(['lockSection', 'unlockSection', 'addCourse']),
    handleResize() {
      this.height = window.innerHeight;
    },
    atInput() {
      const courseSectionPicker = this.$refs.popUp;
      if (typeof courseSectionPicker !== 'undefined') {
        courseSectionPicker.resetSelectedMeetingSections();
      }
    },
    checkHalfHour(currStart, end) {
      // if one hour
      if (Number.isInteger((end - currStart) / 3600)) {
        return false;
      }
      // half hour
      return true;
    },
    // Toggle locked status of this TimetableEvent when it is not empty (lock/unlock this section)
    lockToggle() {
      // modifies vuex based on the current section's lock status
      // eslint-disable-next-line no-unused-expressions
      !this.locked
        ? this.lockSection(`${this.event.code}${this.event.sectionCode}`)
        : this.unlockSection(`${this.event.code}${this.event.sectionCode}`);
    },
    getFormattedTime(start, end) {
      let s = (start / 3600) % 12;
      if (s === 0) {
        s = 12;
      }
      let e = (end / 3600) % 12;
      if (e === 0) {
        e = 12;
      }
      const startHalf = Number.isInteger(s) ? '00' : '30';
      const endHalf = Number.isInteger(e) ? '00' : '30';
      return `${s - startHalf / 6 / 10}:${startHalf} - ${e -
        endHalf / 6 / 10}:${endHalf}`;
    },
    // Toggle locked status of this TimetableEvent when it is empty (block/unblock this hour)
    lockedSectionToggle() {
      if (!this.locked) {
        // if the user clicks on an empty timeslot, it will be added as a course in vuex
        this.addCourse({ course: this.currSecData });
        this.lockSection(
          `${this.currSecData.courseCode}${this.currSecData.meeting_sections[0].sectionCode}`,
        );
      } else {
        // if the user clicks on a lock timeslot, it will be removed
        this.deleteCourse({ code: this.currSecData.courseCode });
      }
    },
  },
};
</script>

<style scoped>
@import url('https://fonts.googleapis.com/css?family=Montserrat&display=swap');
* {
  font-family: 'Montserrat', sans-serif;
}
.unselectable {
  -webkit-touch-callout: none;
  -webkit-user-select: none;
  -khtml-user-select: none;
  -moz-user-select: none;
  -ms-user-select: none;
  user-select: none;
}
.center {
  text-align: center;
}
.event {
  border: 1px solid gray;
  color: white;
  position: relative;
  cursor: pointer;
  font-size: small;
}
.empty-event {
  background: white;
  border: 0.2px solid gray;
  cursor: pointer;
}
.course-code {
  margin-left: 3px;
}
.align-left {
  position: absolute;
  left: 0px;
}
.align-right {
  position: absolute;
  right: 0px;
}
.lock-button {
  position: absolute;
  right: 3px;
  top: 3px;
  z-index: 1;
}
</style>