Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
quantum-kittens
GitHub Repository: quantum-kittens/platypus
Path: blob/main/frontend/vue/components/Quiz/Quiz.vue
3375 views
<template>
  <div class="quiz">
    <div
      ref="configRef"
      class="quiz__config-container"
    >
      <slot />
    </div>
    <div v-if="question" class="quiz__question" v-html="question" />
    <ol class="quiz__answer-container">
      <li v-for="(html, i) in answers" :key="i - 1" class="quiz__answer-container__element">
        <input
          :id="`${i}-${uid}`"
          v-model="selected"
          type="radio"
          :name="`quiz-${uid}`"
          :value="`${i}`"
          :disabled="solved()"
          class="quiz__answer-container__element__radio"
        >
        <label
          :for="`${i}-${uid}`"
          class="quiz__answer-container__element__label"
          :class="{
            'quiz__answer-container__element__label_correct': solved(),
            'quiz__answer-container__element__label_wrong': !solved(),
          }"
          :style="`--list-item-text: '${listIndexText(i)}'`"
          v-html="html"
        />
        <SolutionStateIndicator class="quiz__answer-container__element__state-indicator" :state="solutionState()" />
      </li>
    </ol>
    <SolutionStateIndicator v-if="selected" class="quiz__general-state-indicator" :state="solutionState()" />
  </div>
</template>

<script lang="ts">
import { Options, Vue, prop } from 'vue-class-component'
import { ref } from 'vue'
import SolutionStateIndicator, { SolutionState } from '../common/SolutionStateIndicator.vue'

class Props {
  goal = prop<String>({ default: 'quiz-solved', required: true });
}

@Options({
  components: {
    SolutionStateIndicator
  }
})
export default class Quiz extends Vue.with(Props) {
  configRef = ref<HTMLElement | null>(null)
  get config () { return (this.configRef as unknown as HTMLElement) }

  answers: string[] = []
  question: string = ''
  correctIndex: string = ''
  selected: string = ''
  isScored: boolean = false

  resultInfo: string = ''
  codeLineLastId: number = 0

  mounted () {
    const questionElement = this.config.querySelector('.question')
    this.question = questionElement?.innerHTML || ''
    const optionElements = Array.from(this.config.querySelectorAll('.option'))
    const optionElementsShuffled = this.shuffle(optionElements)
    this.answers = optionElementsShuffled.map(element => element.innerHTML)
    let correctElementIdx = optionElementsShuffled.findIndex(element => element.hasAttribute('x') || element.hasAttribute('correct'))
    if (correctElementIdx === -1) {
      correctElementIdx = optionElementsShuffled.findIndex(element => element === optionElements[0])
    }
    this.correctIndex = `${correctElementIdx}`

    while (this.config.hasChildNodes()) {
      this.config.childNodes[0].remove()
    }
  }

  shuffle<T> (input: T[]) : T[] {
    const array = Array.from(input)

    for (let i = 0; i < array.length - 1; i++) {
      const randomChoiceIndex: number = this.random(0, array.length - 1);
      [array[i], array[randomChoiceIndex]] = [array[randomChoiceIndex], array[i]]
    }

    return array
  }

  random (min: number, max: number) : number {
    const r = Math.random() * (max - min + 1)
    return Math.floor(r) + min
  }

  solved () : boolean {
    const isSolved = this.selected === this.correctIndex
    if (isSolved && !this.isScored) {
      this.isScored = true
      this.$step?.score(this.goal as string)
    }
    return isSolved
  }

  solutionState () : SolutionState {
    const quizTitle = this.goal
    const sectionElement = document.getElementsByTagName('x-course')[0]
    const sectionTitle = sectionElement.getAttribute('data-section')
    const quizLocation = `${sectionTitle} > ${quizTitle}`
    const windowInstance = (window as any)

    if (this.solved()) {
      // Segment tracking correct
      windowInstance.textbook.trackClickEvent('correct', quizLocation)
      return SolutionState.CORRECT
    }

    // Segment tracking incorrect responses
    if (windowInstance?.textbook?.trackClickEvent) {
      windowInstance.textbook.trackClickEvent('incorrect', quizLocation)
    }

    return SolutionState.WRONG
  }

  listIndexText (index: number) {
    return String.fromCharCode(65 + index)
  }

  uid = Math.random().toString().replace('.', '')
}
</script>
<style scoped lang="scss">
@import 'carbon-components/scss/globals/scss/typography';
@import 'carbon-components/scss/globals/scss/layout';
@import '../../../scss/variables/colors.scss';
@import '~/../scss/variables/mq.scss';

.quiz {
  display: flex;
  flex-direction: row;

  @include mq($until: medium) {
    flex-direction: column;
  }

  &__config-container {
    display: none;
  }
  &__question {
    @include type-style('body-long-01');
    flex: 0 0 160px;
    margin-right: $spacing-05;

    @include mq($until: medium) {
      flex: 1;
      margin-right: 0;
      margin-bottom: $spacing-05;
    }
  }
  &__answer-container {
    display: flex;
    flex: 3;
    flex-direction: column;
    list-style-type: upper-alpha;
    list-style-position: inside;

    @include mq($until: medium) {
      flex: 1;
    }

    &__element {
      display: flex;
      flex-direction: row;
      margin: 0 0 $spacing-03 0;

      &__radio {
        transition: border 0.2s ease-out;
        display: none;
      }
      &__label {
        @include type-style('body-long-01');
        flex: 1;
        border: 1px solid transparent;
        background-color: $background-color-white;
        padding: $spacing-02 $spacing-03;
        display: flex;
        flex-direction: row;
        min-height: 50px;
        cursor: pointer;

        &:hover {
          border: 1px solid $border-color;
        }
        &::before {
          @include type-style('heading-01');
          content: var(--list-item-text);
          margin-right: $spacing-05;
          flex: 0 0 auto;
        }
        &::v-deep(ol) {
          margin: 0;
        }
        &::v-deep(ol li) {
          display: inline;
          margin: 0;
        }
      }
      &__state-indicator {
        flex: 0 0 auto;
        opacity: 0;
        height: auto;

        @include mq($until: medium) {
          display: none;
        }
      }
      &__radio:checked ~ &__state-indicator {
        transition: opacity 0.2s ease-out;
        opacity: 1;
      }
      &__radio:checked ~ &__label_correct {
        border: 1px solid $status-color-correct;
      }
      &__radio:checked ~ &__label_wrong {
        border: 1px solid $status-color-wrong;
      }
    }
  }
  &__general-state-indicator {
    width: 100%;

    @include mq($from: medium) {
      display: none;
    }
  }
}
</style>