mirror of
https://github.com/frappe/lms.git
synced 2026-05-02 13:39:31 +03:00
feat: demo review and course progress
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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()"
|
||||
/>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user