feat: quiz base

This commit is contained in:
Jannat Patel
2023-12-28 11:59:44 +05:30
parent e1d61c9eb9
commit 7087fde686
9 changed files with 334 additions and 41 deletions

View File

@@ -1,7 +1,7 @@
<template>
<div class="text-base">
<div class="course-outline text-base">
<div class="mt-4">
<Disclosure v-slot="{ open }" v-for="(chapter, index) in outline.data" :key="chapter.name" :defaultOpen="index == 0">
<Disclosure v-slot="{ open }" v-for="(chapter, index) in outline.data" :key="chapter.name" :defaultOpen="chapter.idx == route.params.chapterNumber">
<DisclosureButton class="flex w-full px-2 pt-2 pb-3">
<ChevronRight
:class="{'rotate-90 transform duration-200' : open, 'duration-200' : !open, 'open': index == 1}"
@@ -11,13 +11,24 @@
{{ chapter.title }}
</div>
</DisclosureButton>
<DisclosurePanel class="px-10 pb-4">
<DisclosurePanel class="pb-2">
<div v-for="lesson in chapter.lessons" :key="lesson.name">
<div class="flex items-center text-base mb-3">
<MonitorPlay v-if="lesson.icon === 'icon-youtube'" class="h-4 w-4 text-gray-900 stroke-1 mr-2"/>
<HelpCircle v-else-if="lesson.icon === 'icon-quiz'" class="h-4 w-4 text-gray-900 stroke-1 mr-2"/>
<FileText v-else-if="lesson.icon === 'icon-list'" class="h-4 w-4 text-gray-900 stroke-1 mr-2"/>
{{ lesson.title }}
<div class="outline-lesson mb-2 px-8">
<router-link :to='{
name: "Lesson",
params: {
courseName: courseName,
chapterNumber: lesson.number.split(".")[0],
lessonNumber: lesson.number.split(".")[1],
}
}'>
<div class="flex items-center text-base">
<MonitorPlay v-if="lesson.icon === 'icon-youtube'" class="h-4 w-4 text-gray-900 stroke-1 mr-2"/>
<HelpCircle v-else-if="lesson.icon === 'icon-quiz'" class="h-4 w-4 text-gray-900 stroke-1 mr-2"/>
<FileText v-else-if="lesson.icon === 'icon-list'" class="h-4 w-4 text-gray-900 stroke-1 mr-2"/>
{{ lesson.title }}
</div>
</router-link>
</div>
</div>
</DisclosurePanel>
@@ -29,7 +40,9 @@
import { createResource } from "frappe-ui";
import { Disclosure, DisclosureButton, DisclosurePanel } from '@headlessui/vue';
import { ChevronRight, MonitorPlay, HelpCircle, FileText } from 'lucide-vue-next';
import { useRoute } from "vue-router";
const route = useRoute();
const props = defineProps({
courseName: {
type: String,
@@ -45,4 +58,10 @@ const outline = createResource({
},
auto: true,
});
</script>
</script>
<style>
.outline-lesson:has(.router-link-active) {
background-color: theme('colors.gray.100');
padding: 0.5rem 0 0.5rem 2rem;
}
</style>

View File

@@ -82,7 +82,7 @@ const props = defineProps({
},
membership: {
type: Object,
required: true,
required: false,
},
});
@@ -98,7 +98,7 @@ const reviews = createResource({
},
auto: true,
});
console.log(reviews)
const rating_percent = computed(() => {
let rating_count = {};
let rating_percent = {};

View File

@@ -0,0 +1,83 @@
<template>
<div v-if="quiz.doc">
<div v-if="activeQuestion == 0">
<div class="bg-blue-100 py-2 px-2 mb-4 rounded-md text-sm text-blue-800">
<div class="leading-relaxed">
{{ __("This quiz consists of {0} questions.").format(quiz.doc.questions.length) }}
</div>
<div v-if="quiz.doc.passing_percentage" class="leading-relaxed">
{{ __("You will have to get {0}% correct answers in order to pass the quiz.").format(quiz.doc.passing_percentage) }}
</div>
<div v-if="quiz.doc.max_attempts" class="leading-relaxed">
{{ __("You can attempt this quiz {0}.").format(quiz.doc.max_attempts == 1 ? "1 time" : `${quiz.doc.max_attempts} times`) }}
</div>
<div v-if="quiz.doc.time" class="leading-relaxed">
{{ __("The quiz has a time limit.For each question you will be given { 0} seconds.").format(quiz.doc.time) }}
</div>
</div>
<div class="border text-center p-20 font-semibold text-lg rounded-md">
<div>
{{ quiz.doc.title }}
</div>
<Button @click="startQuiz" class="mt-2">
<span>
{{ __("Start") }}
</span>
</Button>
</div>
</div>
<div v-else>
<div v-for="index in quiz.doc.questions.length">
<div v-if="index == activeQuestion">
{{ questionDetails }}
</div>
</div>
</div>
</div>
</template>
<script setup>
import { createDocumentResource, Button } from 'frappe-ui';
import { ref, watch, inject } from 'vue';
const user = inject("$user");
const props = defineProps({
quizName: {
type: String,
required: true,
},
})
const activeQuestion = ref(0);
const currentQuestion = ref("");
const quiz = createDocumentResource({
doctype: "LMS Quiz",
name: props.quizName,
cache: ["quiz", props.quizName],
});
console.log(user.data)
if (user.data) {
quiz.reload();
}
const questionDetails = createDocumentResource({
doctype: "LMS Question",
name: currentQuestion.value,
cache: ["question", props.quizName, currentQuestion.value],
});
console.log(questionDetails)
const startQuiz = () => {
activeQuestion.value = 1;
}
watch(activeQuestion, (value) => {
if (value > 0) {
currentQuestion.value = quiz.doc.questions[value - 1];
console.log(currentQuestion.value)
console.log(questionDetails)
}
})
</script>

View File

@@ -32,9 +32,5 @@ app.mount('#app')
const { userResource } = usersStore()
let { isLoggedIn } = sessionStore()
if (isLoggedIn) {
await userResource.reload()
}
app.provide('$user', userResource)
app.config.globalProperties.$user = userResource

View File

@@ -4,10 +4,43 @@
<Breadcrumbs class="h-7" :items="breadcrumbs" />
</header>
<div class="grid grid-cols-[70%,30%] h-full">
<div class="border-r-2 container pt-5 pb-10">
<div class="text-3xl font-semibold">
{{ lesson.data.title }}
<div v-if="lesson.data.no_preview" class="border-r-2 text-center pt-10">
<p class="mb-4">
{{ __("This lesson is not available for preview. Please enroll in the course to access it.") }}
</p>
<router-link :to='{ name: "CourseDetail", params: { courseName: courseName } }'>
<Button variant="solid">
{{ __("Start Learning") }}
</Button>
</router-link>
</div>
<div v-else class="border-r-2 container pt-5 pb-10">
<div class="flex items-center justify-between">
<div class="text-3xl font-semibold">
{{ lesson.data.title }}
</div>
<div>
<router-link v-if="lesson.data.prev" :to='{name: "Lesson", params: {
courseName: courseName,
chapterNumber: lesson.data.prev.split(".")[0],
lessonNumber: lesson.data.prev.split(".")[1]
}}'>
<Button class="mr-2">
<ChevronLeft class="w-4 h-4 stroke-1"/>
</Button>
</router-link>
<router-link v-if="lesson.data.next" :to='{name: "Lesson", params: {
courseName: courseName,
chapterNumber: lesson.data.next.split(".")[0],
lessonNumber: lesson.data.next.split(".")[1]
}}'>
<Button>
<ChevronRight class="w-4 h-4 stroke-1"/>
</Button>
</router-link>
</div>
</div>
<div class="flex items-center mt-2">
<span class="mr-1" :class="{ 'avatar-group overlap': course.data.instructors.length > 1 }">
<UserAvatar v-for="instructor in course.data.instructors" :user="instructor" />
@@ -22,7 +55,46 @@
{{ course.data.instructors[0].first_name }} and {{ course.data.instructors.length - 1 }} others
</span>
</div>
<div v-html="lesson.data.rendered_content" class="lesson-content mt-6"></div>
<!-- <div v-html="lesson.data.rendered_content" class="lesson-content mt-6"></div> -->
<div class="lesson-content mt-6">
<div v-for="block in lesson.data.body.split('\n\n')">
<div v-if='block.includes("{{ YouTubeVideo")'>
<iframe class="youtube-video" :src="getYouTubeVideoSource(block)" width="100%" height="400" frameborder="0" allowfullscreen></iframe>
</div>
<div v-else-if='block.includes("{{ Quiz")'>
<Quiz v-if="user" :quizName="getId(block)"/>
<div v-else>
<div>
{{ __("Please login to access this quiz.") }}
</div>
<Button @click="window.location.href = `/login?redirect-to=/courses/${courseName}`">
<span>
{{ __("Login") }}
</span>
</Button>
</div>
</div>
<div v-else-if='block.includes("{{ Video")'>
<video controls width='100%' controlsList='nodownload'>
<source :src="getId(block)" type='video/mp4'>
</video>
</div>
<div v-else-if='block.includes("{{ PDF")'>
<iframe :src="getPDFSource(block)" width="100%" height="400" frameborder="0" allowfullscreen></iframe>
</div>
<div v-else-if='block.includes("{{ Audio")'>
<audio width='100%' controls controlsList='nodownload'>
<source :src="getId(block)" type='audio/mp3'>
</audio>
</div>
<div v-else-if='block.includes("{{ Embed")'>
<iframe width="100%" height="400" :src="getId(block)" frameborder="0" allowfullscreen>
</iframe>
</div>
<div v-else v-html="markdown.render(block)">
</div>
</div>
</div>
</div>
<div class="sticky top-10">
<div class="bg-gray-50 p-5 border-b-2">
@@ -37,22 +109,30 @@
:style="{ width: Math.ceil(course.data.membership.progress) + '%' }"></div>
</div>
</div>
<CourseOutline :courseName="lesson.data.course" />
<CourseOutline :courseName="courseName" :key="chapterNumber" />
</div>
</div>
</div>
</template>
<script setup>
import { createResource, Breadcrumbs } from "frappe-ui";
import { computed, onMounted, onBeforeMount, onUnmounted, inject } from "vue";
import { createResource, Breadcrumbs, Button } from "frappe-ui";
import { computed, watch, onBeforeMount, onUnmounted, inject } from "vue";
import { useStorage } from '@vueuse/core'
import CourseOutline from '@/components/CourseOutline.vue';
import UserAvatar from '@/components/UserAvatar.vue';
import { useRoute } from "vue-router";
import MarkdownIt from "markdown-it";
import { ChevronLeft, ChevronRight } from "lucide-vue-next";
import Quiz from '@/components/Quiz.vue';
const user = inject("$user");
const route = useRoute();
const markdown = new MarkdownIt({
html: true,
linkify: true,
});
onBeforeMount(() => {
console.log("before mount");
localStorage.setItem("sidebar_is_collapsed", true);
})
@@ -73,11 +153,13 @@ const props = defineProps({
const lesson = createResource({
url: "lms.lms.utils.get_lesson",
cache: ["lesson", props.courseName, props.lessonNumber],
params: {
course: props.courseName,
chapter: props.chapterNumber,
lesson: props.lessonNumber,
cache: ["lesson", props.courseName, props.chapterNumber, props.lessonNumber],
makeParams(values) {
return {
course: props.courseName,
chapter: values ? values.chapter : props.chapterNumber,
lesson: values ? values.lesson : props.lessonNumber,
}
},
auto: true,
});
@@ -104,14 +186,32 @@ const breadcrumbs = computed(() => {
return items
});
onUnmounted(() => {
console.log("unmounted");
useStorage("sidebar_is_collapsed", false);
});
console.log(route.params)
watch(
[() => route.params.chapterNumber, () => route.params.lessonNumber],
([newChapterNumber, newLessonNumber], [oldChapterNumber, oldLessonNumber]) => {
lesson.submit({
chapter: newChapterNumber,
lesson: newLessonNumber,
})
}
);
const getYouTubeVideoSource = (block) => {
return `https://www.youtube.com/embed/${getId(block)}`;
}
const getPDFSource = (block) => {
return `${getId(block)}#toolbar=0`;
}
const getId = (block) => {
return block.match(/\(["']([^"']+?)["']\)/)[1];
}
</script>
<style>
.youtube-video {
border: 1px solid #ddd;
}
.avatar-group {
display: inline-flex;
@@ -123,15 +223,50 @@ onUnmounted(() => {
}
iframe {
border: 1px solid #ddd;
border-radius: 0.5rem;
}
.lesson-content div {
margin-bottom: 1rem;
}
.lesson-content p {
margin-bottom: 1rem;
line-height: 1.7;
}
.lesson-content li {
line-height: 1.7;
}
.lesson-content ol {
list-style: auto;
margin: revert;
padding: 1rem;
}
.lesson-content ul {
list-style: auto;
padding: 1rem;
margin: revert;
}
.lesson-content img {
border: 1px solid theme("colors.gray.200");
border-radius: 0.5rem;
}
.lesson-content code {
display: block;
overflow-x: auto;
padding: 1rem 1.25rem;
background: #011627;
color: #d6deeb;
border-radius: 0.5rem;
margin-bottom: 1rem;
}
.lesson-content a {
color: theme("colors.gray.900");
text-decoration: underline;
font-weight: 500;
}
</style>