feat: demo review and course progress

This commit is contained in:
Jannat Patel
2026-03-05 21:41:34 +05:30
parent f1014e7452
commit 607103e40e
24 changed files with 387 additions and 145 deletions
+1 -1
View File
@@ -1,6 +1,6 @@
<template>
<FrappeUIProvider>
<Layout class="isolate text-base">
<Layout class="isolate text-p-base">
<router-view />
</Layout>
<InstallPrompt v-if="isMobile && !settings.data?.disable_pwa" />
@@ -90,21 +90,26 @@
{{ __('Get Certificate') }}
</Button>
</div>
<div class="space-y-4">
<div class="space-y-3">
<div class="font-medium text-ink-gray-9">
{{ __('This course has:') }}
</div>
<div class="flex items-center text-ink-gray-9">
<BookOpen class="h-4 w-4 stroke-1.5" />
<span class="ml-2">
{{ course.data.lessons }} {{ __('Lessons') }}
{{ course.data.lessons }}
{{ course.data.lessons > 1 ? __('lessons') : __('lesson') }}
</span>
</div>
<div class="flex items-center text-ink-gray-9">
<Users class="h-4 w-4 stroke-1.5" />
<span class="ml-2">
{{ formatAmount(course.data.enrollments) }}
{{ __('Enrolled Students') }}
{{
course.data.enrollments > 1
? __('enrolled students')
: __('enrolled student')
}}
</span>
</div>
<div
@@ -113,7 +118,7 @@
>
<Star class="size-4 stroke-1.5 fill-yellow-500 text-transparent" />
<span class="ml-2">
{{ course.data.rating }} {{ __('Rating') }}
{{ course.data.rating }} {{ __('average rating') }}
</span>
</div>
<div
+1 -3
View File
@@ -4,9 +4,7 @@
<div class="text-lg font-semibold text-ink-gray-7 mb-2.5">
{{ __('No {0}').format(type?.toLowerCase()) }}
</div>
<div
class="leading-5 text-base w-full md:w-2/5 text-base text-center text-ink-gray-7"
>
<div class="text-p-base w-full md:w-2/5 text-center text-ink-gray-7">
{{
__(
'There are no {0} currently. Keep an eye out, fresh learning experiences are on the way!'
@@ -14,7 +14,7 @@
: __('Edit Assignment')
}}
</div>
<div class="space-y-4 max-h-[75vh] overflow-y-auto">
<div class="space-y-4 max-h-[75vh] overflow-y-auto p-1">
<FormControl
v-model="assignment.title"
:label="__('Title')"
@@ -43,7 +43,7 @@
@change="(val) => (assignment.question = val)"
:editable="true"
:fixedMenu="true"
editorClass="prose-sm max-w-none border-b border-x border-outline-gray-modals bg-surface-gray-2 rounded-b-md py-1 px-2 min-h-[7rem] max-h-[18rem] overflow-y-auto"
editorClass="prose-sm max-w-none border-b border-x border-outline-gray-modals bg-surface-gray-2 rounded-b-md py-1 px-2 min-h-[10rem] max-h-[18rem] overflow-y-auto"
/>
</div>
</div>
@@ -73,7 +73,7 @@
import { Button, Dialog, FormControl, TextEditor, toast } from 'frappe-ui'
import { computed, reactive, watch } from 'vue'
import { escapeHTML, sanitizeHTML } from '@/utils'
import { Link } from 'frappe-ui/frappe'
import Link from '@/components/Controls/Link.vue'
const show = defineModel()
const assignments = defineModel<Assignments>('assignments')
+9 -12
View File
@@ -2,7 +2,7 @@
<Dialog
v-model="show"
:options="{
size: '5xl',
size: '3xl',
}"
>
<template #body>
@@ -10,17 +10,14 @@
<div class="text-lg font-semibold text-ink-gray-9 mb-5">
{{ __(props.title) }}
</div>
<div
<Switch
v-if="!editMode"
class="flex items-center text-xs text-ink-gray-7 space-x-5"
>
<Switch
size="sm"
:label="__('Choose an existing question')"
v-model="chooseFromExisting"
class="!p-0"
/>
</div>
size="sm"
:label="__('Choose an existing question')"
:description="__('Select from questions you have already created')"
v-model="chooseFromExisting"
class="!p-0"
/>
<div v-if="!chooseFromExisting || editMode">
<div>
<label class="block text-xs text-ink-gray-5 mb-1">
@@ -164,7 +161,7 @@ populateFields()
const props = defineProps({
title: {
type: String,
default: __('Add a new question'),
default: __('Add new question'),
},
questionDetail: {
type: [Object, null],
@@ -65,7 +65,7 @@
<script setup>
import { sessionStore } from '@/stores/session'
import { Dropdown } from 'frappe-ui'
import { call, Dropdown, toast } from 'frappe-ui'
import { useRouter } from 'vue-router'
import { convertToTitleCase } from '@/utils'
import { usersStore } from '@/stores/user'
@@ -85,7 +85,7 @@ import {
User,
Settings,
Sun,
Zap,
Trash2,
} from 'lucide-vue-next'
const router = useRouter()
@@ -175,6 +175,19 @@ const userDropdownOptions = computed(() => {
return userResource.data?.is_moderator
},
},
{
label: 'Clear Demo Data',
icon: Trash2,
onClick: () => {
clearDemoDataConfirmation()
},
condition: () => {
return (
userResource.data?.is_moderator &&
settingsStore.settings.data?.demo_data_present
)
},
},
{
icon: FrappeCloudIcon,
label: 'Login to Frappe Cloud',
@@ -234,4 +247,36 @@ const loginToFrappeCloud = () => {
let redirect_to = '/dashboard/sites/' + userResource.data.sitename
window.open(`${frappeCloudBaseEndpoint}${redirect_to}`, '_blank')
}
const clearDemoDataConfirmation = () => {
$dialog({
title: __('Confirm clearing demo data?'),
message: __(
'Are you sure you want to clear the demo data? This would delete the course "A guide to Frappe Learning" along with all its associated data. This action cannot be undone.'
),
actions: [
{
label: __('Confirm'),
theme: 'red',
variant: 'solid',
onClick(close) {
clearDemoData()
close()
},
},
],
})
}
const clearDemoData = () => {
call('lms.lms.api.clear_demo_data')
.then(() => {
window.location.href = '/lms'
toast.success(__('Demo data cleared successfully'))
})
.catch((error) => {
toast.error(__(error.message || 'Error clearing demo data'))
console.error('Error clearing demo data:', error)
})
}
</script>
+4 -1
View File
@@ -18,7 +18,7 @@
</Button>
</div>
<Dropdown
v-else-if="isAdmin"
v-else-if="isAdmin && batchMenu.length"
:options="batchMenu"
placement="left"
side="left"
@@ -209,6 +209,9 @@ const canMakeAnnouncement = () => {
}
const batchMenu = computed(() => {
if (!batch.data?.certification && !canMakeAnnouncement()) {
return []
}
let options = [
{
label: __('Generate Certificates'),
@@ -56,7 +56,7 @@
</span>
</div>
<div v-if="!readOnlyMode">
<div v-if="!readOnlyMode && !canAccessBatch">
<router-link
:to="{
name: 'Billing',
@@ -71,7 +71,7 @@
batch.data.accept_enrollments
"
>
<Button v-if="!canAccessBatch" class="w-full mt-4" variant="solid">
<Button class="w-full mt-4" variant="solid">
<template #prefix>
<CreditCard class="size-4 stroke-1.5" />
</template>
@@ -169,14 +169,6 @@ const isEvaluator = computed(() => {
return user.data?.is_evaluator
})
const isInstructor = computed(() => {
return (
props.batch.data?.instructors?.filter(
(instructor) => instructor.name === user.data?.name
).length > 0
)
})
const canAccessBatch = computed(() => {
if (!user.data) {
return false
@@ -184,7 +176,7 @@ const canAccessBatch = computed(() => {
return isModerator.value || isStudent.value || isEvaluator.value
})
const canEditBatch = computed(() => {
return isModerator.value || isInstructor.value
const isAdmin = computed(() => {
return isModerator.value || isEvaluator.value
})
</script>
@@ -13,6 +13,7 @@
v-model="batch.title"
:label="__('Title')"
:required="true"
autocomplete="off"
/>
<FormControl
v-model="batch.start_date"
@@ -42,6 +43,7 @@
v-model="batch.timezone"
:label="__('Timezone')"
:required="true"
autocomplete="off"
/>
<Link
doctype="LMS Category"
@@ -72,6 +74,13 @@
<div class="space-y-5 border-t mt-5 pt-5">
<div class="grid grid-cols-2 gap-5">
<FormControl
v-model="batch.description"
:label="__('Description')"
type="textarea"
:required="true"
:rows="4"
/>
<MultiSelect
v-model="batch.instructors"
doctype="Course Evaluator"
@@ -80,13 +89,6 @@
:onCreate="(close: () => void) => openSettings('Evaluators', close)"
:filters="{ ignore_user_type: 1 }"
/>
<FormControl
v-model="batch.description"
:label="__('Description')"
type="textarea"
:required="true"
:rows="4"
/>
</div>
<div class="">
<div class="mb-1.5 text-sm text-ink-gray-5">
+5 -1
View File
@@ -71,7 +71,11 @@
<ColorSwatches
v-model="courseResource.doc.card_gradient"
:label="__('Color')"
:description="__('Choose a color for the course card')"
:description="
__(
'Select a fallback color for the course card when no image is set.'
)
"
class="w-full"
@update:modelValue="makeFormDirty()"
/>
+18 -28
View File
@@ -65,7 +65,7 @@
</router-link>
</div>
</header>
<div class="grid md:grid-cols-[70%,30%] h-screen">
<div class="grid md:grid-cols-[70%,30%] h-[94vh]">
<div v-if="lesson.data.no_preview" class="border-r">
<div class="shadow rounded-md w-3/4 mt-10 mx-auto text-center p-4">
<div class="flex items-center justify-center mt-4 space-x-2">
@@ -263,7 +263,7 @@
</div>
</div>
<div
v-if="lesson.data"
v-if="lesson.data && (allowDiscussions || tabs.length > 1)"
class="mt-10 pb-20 pt-5 border-t px-5"
ref="discussionsContainer"
>
@@ -399,15 +399,10 @@ const { brand } = sessionStore()
const sidebarStore = useSidebar()
const plyrSources = ref([])
const showInlineMenu = ref(false)
const currentTab = ref('Notes')
const currentTab = ref(null)
let timerInterval = null
const tabs = ref([
{
label: __('Notes'),
value: 'Notes',
},
])
const tabs = ref([])
const props = defineProps({
courseName: {
@@ -627,7 +622,6 @@ const resetLessonState = (newChapterNumber, newLessonNumber) => {
chapter: newChapterNumber,
lesson: newLessonNumber,
})
console.log('resetting lesson state')
clearInterval(timerInterval)
timer.value = 0
}
@@ -742,18 +736,14 @@ const updateVideoTime = (video) => {
}
const startTimer = () => {
console.log(lesson.data?.membership)
if (!lesson.data?.membership) return
console.log('past')
timerInterval = setInterval(() => {
console.log(`Timer: ${timer.value} seconds`)
timer.value++
if (timer.value == 30) {
clearInterval(timerInterval)
markProgress()
}
}, 1000)
console.log(timerInterval)
}
onBeforeUnmount(() => {
@@ -892,24 +882,24 @@ const updateNotes = () => {
}
watch(allowDiscussions, () => {
if (allowDiscussions.value) {
tabs.value = [
{
if (!isAdmin.value) {
if (!tabs.value.find((tab) => tab.value === 'Notes')) {
tabs.value.push({
label: __('Notes'),
value: 'Notes',
},
{
})
}
currentTab.value = 'Notes'
} else {
currentTab.value = allowDiscussions.value ? 'Community' : null
}
if (allowDiscussions.value) {
if (!tabs.value.find((tab) => tab.value === 'Community')) {
tabs.value.push({
label: __('Community'),
value: 'Community',
},
]
} else {
tabs.value = [
{
label: __('Notes'),
value: 'Notes',
},
]
})
}
}
})
@@ -51,7 +51,7 @@
@change="(val: string) => (exercise.problem_statement = val)"
:editable="true"
:fixedMenu="true"
editorClass="prose-sm max-w-none border-b border-x border-outline-gray-modals bg-surface-gray-2 rounded-b-md py-1 px-2 min-h-[7rem] max-h-[21rem] overflow-y-auto"
editorClass="prose-sm max-w-none border-b border-x border-outline-gray-modals bg-surface-gray-2 rounded-b-md py-1 px-2 min-h-[10rem] max-h-[21rem] overflow-y-auto"
/>
</div>
</div>
+1 -5
View File
@@ -194,11 +194,7 @@
v-model="showQuestionModal"
:questionDetail="currentQuestion"
v-model:quiz="quizDetails"
:title="
currentQuestion.question
? __('Edit the question')
: __('Add a new question')
"
:title="currentQuestion.question ? __('Edit Question') : __('Add Question')"
/>
</template>
<script setup>
+3 -6
View File
@@ -12,12 +12,8 @@
</header>
<div class="py-5 mx-5">
<div class="flex items-center justify-between mb-4">
<div class="text-lg font-semibold text-ink-gray-7">
{{
quizzes.data?.length
? __('{0} Quizzes').format(quizzes.data.length)
: __('No Quizzes')
}}
<div class="text-lg font-semibold">
{{ __('{0} Quizzes').format(quizzes.data.length) }}
</div>
<FormControl v-model="search" type="text" placeholder="Search">
<template #prefix>
@@ -116,6 +112,7 @@
v-model="title"
:label="__('Title')"
type="text"
autocomplete="off"
@keydown.enter="insertQuiz(() => (showForm = false))"
/>
</template>