Compare commits
156 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
57c1a6b540 | ||
|
|
8dba0e8242 | ||
|
|
ee715f6387 | ||
|
|
b770b30334 | ||
|
|
d61abac126 | ||
|
|
ccf28b8012 | ||
|
|
3762cb06bb | ||
|
|
15400f2a3e | ||
|
|
20d1b1fe83 | ||
|
|
73844f8813 | ||
|
|
2187553625 | ||
|
|
984b2a5dea | ||
|
|
9098d9454f | ||
|
|
027dd93fb5 | ||
|
|
a005adc89a | ||
|
|
866ef04fbf | ||
|
|
00b6f97e3a | ||
|
|
a1d21b1a2a | ||
|
|
7358ea43d8 | ||
|
|
88c69311eb | ||
|
|
c1e45e5d0d | ||
|
|
fe78de2417 | ||
|
|
4c1fc201e6 | ||
|
|
3f5d270915 | ||
|
|
a452fbeb07 | ||
|
|
a6f02c245f | ||
|
|
cb4f9129d6 | ||
|
|
9c5d64c211 | ||
|
|
41dc0ecc60 | ||
|
|
6b9409b889 | ||
|
|
ea66eeed6c | ||
|
|
a419d28ef1 | ||
|
|
481dfc24fd | ||
|
|
ed686a7d52 | ||
|
|
b4c5a07800 | ||
|
|
6ae16f7fef | ||
|
|
4aae2ed3b8 | ||
|
|
81d4137b20 | ||
|
|
77ecb02a17 | ||
|
|
4a375f92ed | ||
|
|
7caf91460a | ||
|
|
0e015c8b97 | ||
|
|
7b69ddb14d | ||
|
|
2271eb270e | ||
|
|
7e5b2e4e79 | ||
|
|
124b9d9ea5 | ||
|
|
36076068ec | ||
|
|
c868354b5b | ||
|
|
db91f0b2a0 | ||
|
|
d7e83bb78e | ||
|
|
feb2a39e05 | ||
|
|
a6cf910d05 | ||
|
|
b891b44ac6 | ||
|
|
026a3ebb81 | ||
|
|
71ba246011 | ||
|
|
a391204fa6 | ||
|
|
9c773399a8 | ||
|
|
528b85352a | ||
|
|
249c369c14 | ||
|
|
9803fc1031 | ||
|
|
299fde1c98 | ||
|
|
7f55734fbb | ||
|
|
efe230865a | ||
|
|
6e52e684c8 | ||
|
|
99d880297a | ||
|
|
dec706ae72 | ||
|
|
2e60f0a0c2 | ||
|
|
ef612f86e5 | ||
|
|
9c16e03ea7 | ||
|
|
7780c0310e | ||
|
|
b0a23c0d1a | ||
|
|
05c85cea08 | ||
|
|
1ffae0a1de | ||
|
|
15cbccd15f | ||
|
|
266b2f2ac8 | ||
|
|
26f9fb4199 | ||
|
|
67887fb6ef | ||
|
|
3d102e39ff | ||
|
|
ddd9089130 | ||
|
|
d8ce88ab57 | ||
|
|
01794a47c6 | ||
|
|
17626dbbdb | ||
|
|
e5bd86658d | ||
|
|
e911dc1353 | ||
|
|
27e3e5aa6a | ||
|
|
5b65525bf1 | ||
|
|
277804f8b1 | ||
|
|
4c77802e3c | ||
|
|
aacfea6ea5 | ||
|
|
6d55040e43 | ||
|
|
290f785a47 | ||
|
|
39ef187f6b | ||
|
|
a7a475e763 | ||
|
|
6eb380ea38 | ||
|
|
4d150cb323 | ||
|
|
09d6d99b14 | ||
|
|
5e7fd8baff | ||
|
|
52c159e2e8 | ||
|
|
67e8feb879 | ||
|
|
a5b61d5244 | ||
|
|
decc3a16ed | ||
|
|
7f39e9f0cc | ||
|
|
95afa1a6ad | ||
|
|
0d0bb5f9e2 | ||
|
|
3dd5ce5035 | ||
|
|
549e56d551 | ||
|
|
50b6215d1e | ||
|
|
ff69bfdce7 | ||
|
|
c04cc8ec0f | ||
|
|
f324de2254 | ||
|
|
40af4e6f34 | ||
|
|
5d9b66b5cb | ||
|
|
d2a8277c13 | ||
|
|
ada85fc0f3 | ||
|
|
505345eff7 | ||
|
|
2911ade880 | ||
|
|
8980dc8f9c | ||
|
|
d94a1c47c0 | ||
|
|
99c3e5182d | ||
|
|
70e39fee40 | ||
|
|
26d6bec8a0 | ||
|
|
c9ac1a1402 | ||
|
|
6949c1092c | ||
|
|
aae8a54481 | ||
|
|
e1d93bf670 | ||
|
|
fea0533cb1 | ||
|
|
5cd991f02a | ||
|
|
50a8a605d5 | ||
|
|
9ce7d8f5d6 | ||
|
|
eae2587e4c | ||
|
|
323097f201 | ||
|
|
014499888a | ||
|
|
5662de21ae | ||
|
|
17c2eba455 | ||
|
|
1f2c986e8f | ||
|
|
12040b5f6d | ||
|
|
20a985848f | ||
|
|
c06c6169e5 | ||
|
|
917aeb79ef | ||
|
|
c4f36a39fe | ||
|
|
befedc30ad | ||
|
|
d3bc67daa2 | ||
|
|
5d7e211367 | ||
|
|
fa9daa01ec | ||
|
|
0ed9dc63b8 | ||
|
|
5dd6b33eb2 | ||
|
|
1210b823c7 | ||
|
|
04240b4b3d | ||
|
|
787f592a1a | ||
|
|
e7363fbd40 | ||
|
|
e2762825e5 | ||
|
|
bbbca70c71 | ||
|
|
8dde423866 | ||
|
|
fc4c1c2b7e | ||
|
|
bf02e2de3f | ||
|
|
a26ba4dc6e |
2
.github/workflows/make_release_pr.yml
vendored
2
.github/workflows/make_release_pr.yml
vendored
@@ -1,7 +1,7 @@
|
||||
name: Create weekly release
|
||||
on:
|
||||
schedule:
|
||||
- cron: '30 4 15 * *'
|
||||
- cron: '30 3 * * 3'
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
|
||||
@@ -107,7 +107,7 @@ describe("Course Creation", () => {
|
||||
cy.get("div").contains(
|
||||
"Test Course Short Introduction to test the UI"
|
||||
);
|
||||
cy.get(".course-image")
|
||||
cy.get(".bg-cover")
|
||||
.invoke("css", "background-image")
|
||||
.should("include", "/files/profile");
|
||||
});
|
||||
@@ -140,6 +140,7 @@ describe("Course Creation", () => {
|
||||
);
|
||||
|
||||
// Add Discussion
|
||||
cy.get("span").contains("Community").click();
|
||||
cy.button("New Question").click();
|
||||
cy.wait(500);
|
||||
cy.get("[id^=headlessui-dialog-panel-").within(() => {
|
||||
|
||||
Submodule frappe-ui updated: fd5252663b...333dce1a4d
10
frontend/auto-imports.d.ts
vendored
Normal file
10
frontend/auto-imports.d.ts
vendored
Normal file
@@ -0,0 +1,10 @@
|
||||
/* eslint-disable */
|
||||
/* prettier-ignore */
|
||||
// @ts-nocheck
|
||||
// noinspection JSUnusedGlobalSymbols
|
||||
// Generated by unplugin-auto-import
|
||||
// biome-ignore lint: disable
|
||||
export {}
|
||||
declare global {
|
||||
|
||||
}
|
||||
3
frontend/components.d.ts
vendored
3
frontend/components.d.ts
vendored
@@ -40,6 +40,7 @@ declare module 'vue' {
|
||||
Code: typeof import('./src/components/Controls/Code.vue')['default']
|
||||
CodeEditor: typeof import('./src/components/Controls/CodeEditor.vue')['default']
|
||||
CollapseSidebar: typeof import('./src/components/Icons/CollapseSidebar.vue')['default']
|
||||
ColorSwatches: typeof import('./src/components/Controls/ColorSwatches.vue')['default']
|
||||
CourseCard: typeof import('./src/components/CourseCard.vue')['default']
|
||||
CourseCardOverlay: typeof import('./src/components/CourseCardOverlay.vue')['default']
|
||||
CourseInstructors: typeof import('./src/components/CourseInstructors.vue')['default']
|
||||
@@ -65,6 +66,7 @@ declare module 'vue' {
|
||||
FrappeCloudIcon: typeof import('./src/components/Icons/FrappeCloudIcon.vue')['default']
|
||||
IconPicker: typeof import('./src/components/Controls/IconPicker.vue')['default']
|
||||
IndicatorIcon: typeof import('./src/components/Icons/IndicatorIcon.vue')['default']
|
||||
InlineLessonMenu: typeof import('./src/components/Notes/InlineLessonMenu.vue')['default']
|
||||
InviteIcon: typeof import('./src/components/Icons/InviteIcon.vue')['default']
|
||||
JobApplicationModal: typeof import('./src/components/Modals/JobApplicationModal.vue')['default']
|
||||
JobCard: typeof import('./src/components/JobCard.vue')['default']
|
||||
@@ -80,6 +82,7 @@ declare module 'vue' {
|
||||
MultiSelect: typeof import('./src/components/Controls/MultiSelect.vue')['default']
|
||||
NoPermission: typeof import('./src/components/NoPermission.vue')['default']
|
||||
NoSidebarLayout: typeof import('./src/components/NoSidebarLayout.vue')['default']
|
||||
Notes: typeof import('./src/components/Notes/Notes.vue')['default']
|
||||
NotPermitted: typeof import('./src/components/NotPermitted.vue')['default']
|
||||
PageModal: typeof import('./src/components/Modals/PageModal.vue')['default']
|
||||
PaymentSettings: typeof import('./src/components/Settings/PaymentSettings.vue')['default']
|
||||
|
||||
@@ -31,7 +31,7 @@
|
||||
"codemirror": "^6.0.1",
|
||||
"dayjs": "^1.11.6",
|
||||
"feather-icons": "^4.28.0",
|
||||
"frappe-ui": "^0.1.163",
|
||||
"frappe-ui": "0.1.173",
|
||||
"highlight.js": "^11.11.1",
|
||||
"lucide-vue-next": "^0.383.0",
|
||||
"markdown-it": "^14.0.0",
|
||||
|
||||
@@ -344,6 +344,22 @@ const addAssignments = () => {
|
||||
}
|
||||
}
|
||||
|
||||
const addProgrammingExercises = () => {
|
||||
if (isInstructor.value || isModerator.value) {
|
||||
sidebarLinks.value.splice(3, 0, {
|
||||
label: 'Programming Exercises',
|
||||
icon: 'Code',
|
||||
to: 'ProgrammingExercises',
|
||||
activeFor: [
|
||||
'ProgrammingExercises',
|
||||
'ProgrammingExerciseForm',
|
||||
'ProgrammingExerciseSubmissions',
|
||||
'ProgrammingExerciseSubmission',
|
||||
],
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const addPrograms = () => {
|
||||
let activeFor = ['Programs', 'ProgramForm']
|
||||
let index = 1
|
||||
@@ -627,6 +643,7 @@ watch(userResource, () => {
|
||||
isModerator.value = userResource.data.is_moderator
|
||||
isInstructor.value = userResource.data.is_instructor
|
||||
addPrograms()
|
||||
addProgrammingExercises()
|
||||
addQuizzes()
|
||||
addAssignments()
|
||||
setUpOnboarding()
|
||||
|
||||
108
frontend/src/components/Controls/ColorSwatches.vue
Normal file
108
frontend/src/components/Controls/ColorSwatches.vue
Normal file
@@ -0,0 +1,108 @@
|
||||
<template>
|
||||
<div>
|
||||
<div class="text-xs text-ink-gray-5 mb-1">
|
||||
{{ __(label) }}
|
||||
</div>
|
||||
<Popover placement="bottom" class="!block">
|
||||
<template #target="{ togglePopover, isOpen }">
|
||||
<div class="space-y-2">
|
||||
<FormControl
|
||||
type="text"
|
||||
autocomplete="off"
|
||||
class="w-full"
|
||||
:placeholder="__('Set Color')"
|
||||
@focus="togglePopover"
|
||||
:modelValue="modelValue"
|
||||
@update:modelValue="(val: string) => emit('update:modelValue', val)"
|
||||
>
|
||||
<template #prefix>
|
||||
<div
|
||||
class="size-4 rounded-full"
|
||||
:style="
|
||||
modelValue
|
||||
? {
|
||||
backgroundColor:
|
||||
theme.backgroundColor[modelValue.toLowerCase()][400],
|
||||
}
|
||||
: {}
|
||||
"
|
||||
>
|
||||
<Palette
|
||||
v-if="!modelValue"
|
||||
class="size-4 stroke-1.5 text-ink-gray-5"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
<template #suffix>
|
||||
<Button variant="ghost">
|
||||
<X
|
||||
class="size-3 text-ink-gray-5"
|
||||
@click="emit('update:modelValue', null)"
|
||||
/>
|
||||
</Button>
|
||||
</template>
|
||||
</FormControl>
|
||||
</div>
|
||||
</template>
|
||||
<template #body="{ close }">
|
||||
<div class="rounded-lg bg-surface-white p-3 border w-fit mt-2">
|
||||
<div class="text-xs text-ink-gray-5 mb-1.5">
|
||||
{{ __('Swatches') }}
|
||||
</div>
|
||||
<div class="grid grid-cols-7 gap-2">
|
||||
<div
|
||||
v-for="color in colors"
|
||||
:key="color"
|
||||
class="size-5 rounded-full cursor-pointer"
|
||||
:style="{
|
||||
backgroundColor:
|
||||
theme.backgroundColor[color.toLowerCase()][400],
|
||||
}"
|
||||
@click="
|
||||
(e) => {
|
||||
emit('update:modelValue', color)
|
||||
close()
|
||||
emit('change', color)
|
||||
}
|
||||
"
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</Popover>
|
||||
<div class="text-sm text-ink-gray-5 mt-2">
|
||||
{{ description }}
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import { Button, FormControl, Popover } from 'frappe-ui'
|
||||
import { computed } from 'vue'
|
||||
import { Palette, X } from 'lucide-vue-next'
|
||||
import { theme } from '@/utils/theme'
|
||||
|
||||
const emit = defineEmits(['update:modelValue', 'change'])
|
||||
|
||||
const props = defineProps<{
|
||||
modelValue: string
|
||||
label: string
|
||||
description?: string
|
||||
}>()
|
||||
|
||||
const colors = computed(() => {
|
||||
return [
|
||||
'Red',
|
||||
'Blue',
|
||||
'Green',
|
||||
'Amber',
|
||||
'Purple',
|
||||
'Cyan',
|
||||
'Orange',
|
||||
'Violet',
|
||||
'Pink',
|
||||
'Teal',
|
||||
'Gray',
|
||||
'Yellow',
|
||||
]
|
||||
})
|
||||
</script>
|
||||
@@ -1,41 +1,51 @@
|
||||
<template>
|
||||
<div
|
||||
v-if="course.title"
|
||||
class="flex flex-col h-full rounded-md border-2 overflow-auto"
|
||||
class="flex flex-col h-full rounded-md border-2 overflow-auto text-ink-gray-9"
|
||||
style="min-height: 350px"
|
||||
>
|
||||
<div
|
||||
class="course-image"
|
||||
:class="{ 'default-image': !course.image }"
|
||||
:style="{ backgroundImage: 'url(\'' + encodeURI(course.image) + '\')' }"
|
||||
class="w-[100%] h-[168px] bg-cover bg-center bg-no-repeat"
|
||||
:style="
|
||||
course.image
|
||||
? { backgroundImage: `url('${encodeURI(course.image)}')` }
|
||||
: {
|
||||
backgroundImage: getGradientColor(),
|
||||
backgroundBlendMode: 'screen',
|
||||
}
|
||||
"
|
||||
>
|
||||
<div class="flex items-center flex-wrap relative top-4 px-2 w-fit">
|
||||
<Badge
|
||||
<div
|
||||
v-if="course.featured"
|
||||
variant="subtle"
|
||||
theme="green"
|
||||
size="md"
|
||||
class="mb-1 mr-1"
|
||||
class="flex items-center space-x-1 text-xs text-ink-amber-3 bg-surface-white border border-outline-amber-1 px-2 py-0.5 rounded-md mr-1 mb-1"
|
||||
>
|
||||
{{ __('Featured') }}
|
||||
</Badge>
|
||||
<Star class="size-3 stroke-2" />
|
||||
<span>
|
||||
{{ __('Featured') }}
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
v-if="course.tags"
|
||||
v-for="tag in course.tags?.split(', ')"
|
||||
class="text-xs bg-white text-gray-800 px-2 py-0.5 rounded-md mb-1 mr-1"
|
||||
class="text-xs border bg-surface-white text-ink-gray-9 px-2 py-0.5 rounded-md mb-1 mr-1"
|
||||
>
|
||||
{{ tag }}
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="!course.image" class="image-placeholder">
|
||||
{{ course.title[0] }}
|
||||
</div>
|
||||
<!-- <div
|
||||
v-if="!course.image"
|
||||
class="flex items-center justify-center text-white flex-1 font-extrabold text-2xl my-auto px-5 text-center leading-6"
|
||||
:class="course.tags ? 'h-[80%]' : 'h-full'"
|
||||
>
|
||||
{{ course.title }}
|
||||
</div> -->
|
||||
</div>
|
||||
<div class="flex flex-col flex-auto p-4">
|
||||
<div class="flex items-center justify-between mb-2">
|
||||
<div v-if="course.lessons">
|
||||
<Tooltip :text="__('Lessons')">
|
||||
<span class="flex items-center text-ink-gray-7">
|
||||
<span class="flex items-center">
|
||||
<BookOpen class="h-4 w-4 stroke-1.5 mr-1" />
|
||||
{{ course.lessons }}
|
||||
</span>
|
||||
@@ -44,8 +54,8 @@
|
||||
|
||||
<div v-if="course.enrollments">
|
||||
<Tooltip :text="__('Enrolled Students')">
|
||||
<span class="flex items-center text-ink-gray-7">
|
||||
<Users class="h-4 w-4 stroke-1. mr-1" />
|
||||
<span class="flex items-center">
|
||||
<Users class="h-4 w-4 stroke-1.5 mr-1" />
|
||||
{{ course.enrollments }}
|
||||
</span>
|
||||
</Tooltip>
|
||||
@@ -53,29 +63,22 @@
|
||||
|
||||
<div v-if="course.rating">
|
||||
<Tooltip :text="__('Average Rating')">
|
||||
<span class="flex items-center text-ink-gray-7">
|
||||
<span class="flex items-center">
|
||||
<Star class="h-4 w-4 stroke-1.5 mr-1" />
|
||||
{{ course.rating }}
|
||||
</span>
|
||||
</Tooltip>
|
||||
</div>
|
||||
|
||||
<div v-if="course.status != 'Approved'">
|
||||
<Badge
|
||||
variant="subtle"
|
||||
:theme="course.status === 'Under Review' ? 'orange' : 'blue'"
|
||||
size="sm"
|
||||
>
|
||||
{{ course.status }}
|
||||
</Badge>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="text-xl font-semibold leading-6 text-ink-gray-9">
|
||||
<div
|
||||
class="font-semibold leading-6"
|
||||
:class="course.title.length > 32 ? 'text-lg' : 'text-xl'"
|
||||
>
|
||||
{{ course.title }}
|
||||
</div>
|
||||
|
||||
<div class="short-introduction text-ink-gray-7 text-sm">
|
||||
<div class="short-introduction text-sm">
|
||||
{{ course.short_introduction }}
|
||||
</div>
|
||||
|
||||
@@ -84,11 +87,8 @@
|
||||
:progress="course.membership.progress"
|
||||
/>
|
||||
|
||||
<div
|
||||
v-if="user && course.membership"
|
||||
class="text-sm text-ink-gray-7 mt-2 mb-4"
|
||||
>
|
||||
{{ Math.ceil(course.membership.progress) }}% completed
|
||||
<div v-if="user && course.membership" class="text-sm mt-2 mb-4">
|
||||
{{ Math.ceil(course.membership.progress) }}% {{ __('completed') }}
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-between mt-auto">
|
||||
@@ -108,21 +108,23 @@
|
||||
<div v-if="course.paid_course" class="font-semibold">
|
||||
{{ course.price }}
|
||||
</div>
|
||||
<div
|
||||
|
||||
<Tooltip
|
||||
v-if="course.paid_certificate || course.enable_certification"
|
||||
class="text-xs text-ink-blue-3 bg-surface-blue-1 py-0.5 px-1 rounded-md"
|
||||
:text="__('Get Certified')"
|
||||
>
|
||||
{{ __('Certification') }}
|
||||
</div>
|
||||
<GraduationCap class="size-5 stroke-1.5 text-ink-gray-7" />
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script setup>
|
||||
import { BookOpen, Users, Star } from 'lucide-vue-next'
|
||||
import { BookOpen, GraduationCap, Star, Users } from 'lucide-vue-next'
|
||||
import UserAvatar from '@/components/UserAvatar.vue'
|
||||
import { sessionStore } from '@/stores/session'
|
||||
import { Badge, Tooltip } from 'frappe-ui'
|
||||
import { Tooltip } from 'frappe-ui'
|
||||
import { theme } from '@/utils/theme'
|
||||
import CourseInstructors from '@/components/CourseInstructors.vue'
|
||||
import ProgressBar from '@/components/ProgressBar.vue'
|
||||
|
||||
@@ -134,16 +136,24 @@ const props = defineProps({
|
||||
default: null,
|
||||
},
|
||||
})
|
||||
|
||||
const getGradientColor = () => {
|
||||
let color = props.course.card_gradient?.toLowerCase() || 'blue'
|
||||
let colorMap = theme.backgroundColor[color]
|
||||
return `linear-gradient(to top right, black, ${colorMap[400]})`
|
||||
/* return `bg-gradient-to-br from-${color}-100 via-${color}-200 to-${color}-400` */
|
||||
/* return `linear-gradient(to bottom right, ${colorMap[100]}, ${colorMap[400]})` */
|
||||
/* return `radial-gradient(ellipse at 80% 20%, black 20%, ${colorMap[500]} 100%)` */
|
||||
/* return `radial-gradient(ellipse at 30% 70%, black 50%, ${colorMap[500]} 100%)` */
|
||||
/* return `radial-gradient(ellipse at 80% 20%, ${colorMap[100]} 0%, ${colorMap[300]} 50%, ${colorMap[500]} 100%)` */
|
||||
/* return `conic-gradient(from 180deg at 50% 50%, ${colorMap[100]} 0%, ${colorMap[200]} 50%, ${colorMap[400]} 100%)` */
|
||||
/* return `linear-gradient(135deg, ${colorMap[100]}, ${colorMap[300]}), linear-gradient(120deg, rgba(255,255,255,0.4) 0%, transparent 60%) ` */
|
||||
/* return `radial-gradient(circle at 20% 30%, ${colorMap[100]} 0%, transparent 40%),
|
||||
radial-gradient(circle at 80% 40%, ${colorMap[200]} 0%, transparent 50%),
|
||||
linear-gradient(135deg, ${colorMap[300]} 0%, ${colorMap[400]} 100%);` */
|
||||
}
|
||||
</script>
|
||||
<style>
|
||||
.course-image {
|
||||
height: 168px;
|
||||
width: 100%;
|
||||
background-size: cover;
|
||||
background-position: center;
|
||||
background-repeat: no-repeat;
|
||||
}
|
||||
|
||||
.course-card-pills {
|
||||
background: #ffffff;
|
||||
margin-left: 0;
|
||||
@@ -157,14 +167,6 @@ const props = defineProps({
|
||||
width: fit-content;
|
||||
}
|
||||
|
||||
.default-image {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
background-color: theme('colors.green.100');
|
||||
color: theme('colors.green.600');
|
||||
}
|
||||
|
||||
.avatar-group {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
@@ -173,14 +175,7 @@ const props = defineProps({
|
||||
.avatar-group .avatar {
|
||||
transition: margin 0.1s ease-in-out;
|
||||
}
|
||||
.image-placeholder {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex: 1;
|
||||
font-size: 5rem;
|
||||
color: theme('colors.gray.700');
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.avatar-group.overlap .avatar + .avatar {
|
||||
margin-left: calc(-8px);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<div class="text-ink-gray-7">
|
||||
<div class="">
|
||||
<span v-if="instructors?.length == 1">
|
||||
<router-link
|
||||
:to="{
|
||||
@@ -19,7 +19,7 @@
|
||||
>
|
||||
{{ instructors[0].first_name }}
|
||||
</router-link>
|
||||
and
|
||||
{{ __('and') }}
|
||||
<router-link
|
||||
:to="{
|
||||
name: 'Profile',
|
||||
@@ -38,7 +38,7 @@
|
||||
>
|
||||
{{ instructors[0].first_name }}
|
||||
</router-link>
|
||||
and {{ instructors?.length - 1 }} others
|
||||
{{ __('and') }} {{ instructors?.length - 1 }} {{ __('others') }}
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -32,13 +32,13 @@
|
||||
"
|
||||
:options="[
|
||||
{
|
||||
label: 'Edit',
|
||||
label: __('Edit'),
|
||||
onClick() {
|
||||
reply.editable = true
|
||||
},
|
||||
},
|
||||
{
|
||||
label: 'Delete',
|
||||
label: __('Delete'),
|
||||
onClick() {
|
||||
deleteReply(reply)
|
||||
},
|
||||
|
||||
@@ -5,6 +5,9 @@
|
||||
class="float-right"
|
||||
@click="openTopicModal()"
|
||||
>
|
||||
<template #prefix>
|
||||
<Plus class="size-4" />
|
||||
</template>
|
||||
{{ __('New {0}').format(singularize(title)) }}
|
||||
</Button>
|
||||
<div class="text-xl font-semibold text-ink-gray-9">
|
||||
@@ -49,7 +52,7 @@
|
||||
class="flex flex-col items-center justify-center border-2 border-dashed mt-5 py-8 rounded-md"
|
||||
>
|
||||
<MessageSquareText class="w-7 h-7 text-ink-gray-4 stroke-1.5 mr-2" />
|
||||
<div class="">
|
||||
<div class="mt-2">
|
||||
<div v-if="emptyStateTitle" class="font-medium mb-2">
|
||||
{{ __(emptyStateTitle) }}
|
||||
</div>
|
||||
@@ -73,7 +76,7 @@ import { singularize, timeAgo } from '@/utils'
|
||||
import { ref, onMounted, inject, onUnmounted } from 'vue'
|
||||
import DiscussionReplies from '@/components/DiscussionReplies.vue'
|
||||
import DiscussionModal from '@/components/Modals/DiscussionModal.vue'
|
||||
import { MessageSquareText } from 'lucide-vue-next'
|
||||
import { MessageSquareText, Plus } from 'lucide-vue-next'
|
||||
import { getScrollContainer } from '@/utils/scrollContainer'
|
||||
|
||||
const showTopics = ref(true)
|
||||
@@ -102,7 +105,7 @@ const props = defineProps({
|
||||
},
|
||||
emptyStateText: {
|
||||
type: String,
|
||||
default: 'Start a discussion',
|
||||
default: 'Start a Discussion',
|
||||
},
|
||||
singleThread: {
|
||||
type: Boolean,
|
||||
|
||||
@@ -25,6 +25,7 @@
|
||||
<div class="">
|
||||
<div class="mb-1.5 text-sm text-ink-gray-5">
|
||||
{{ __('Reply To') }}
|
||||
<span class="text-ink-red-3">*</span>
|
||||
</div>
|
||||
<Input type="text" v-model="announcement.replyTo" />
|
||||
</div>
|
||||
@@ -70,8 +71,8 @@ const announcementResource = createResource({
|
||||
url: 'frappe.core.doctype.communication.email.make',
|
||||
makeParams(values) {
|
||||
return {
|
||||
recipients: props.students.join(', '),
|
||||
cc: announcement.replyTo,
|
||||
recipients: announcement.replyTo,
|
||||
bcc: props.students.join(', '),
|
||||
subject: announcement.subject,
|
||||
content: announcement.announcement,
|
||||
doctype: 'LMS Batch',
|
||||
@@ -95,6 +96,9 @@ const makeAnnouncement = (close) => {
|
||||
if (!announcement.announcement) {
|
||||
return __('Announcement is required')
|
||||
}
|
||||
if (!announcement.replyTo) {
|
||||
return __('Reply To is required')
|
||||
}
|
||||
},
|
||||
onSuccess() {
|
||||
close()
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
</div> -->
|
||||
<FormControl
|
||||
v-model="searchFilter"
|
||||
:placeholder="__('Search by Member Name')"
|
||||
:placeholder="__('Search by Member')"
|
||||
type="text"
|
||||
class="w-full"
|
||||
/>
|
||||
@@ -149,6 +149,10 @@ import { theme } from '@/utils/theme'
|
||||
|
||||
const show = defineModel<boolean | undefined>()
|
||||
const searchFilter = ref<string | null>(null)
|
||||
type Filters = {
|
||||
course: string | undefined
|
||||
member_name?: string[]
|
||||
}
|
||||
|
||||
const props = defineProps<{
|
||||
courseName?: string
|
||||
@@ -184,10 +188,6 @@ const progressList = createListResource({
|
||||
|
||||
watch([searchFilter], () => {
|
||||
let filterApplied = false
|
||||
type Filters = {
|
||||
course: string | undefined
|
||||
member_name?: string[]
|
||||
}
|
||||
let filters: Filters = {
|
||||
course: props.courseName,
|
||||
}
|
||||
|
||||
@@ -8,15 +8,24 @@
|
||||
>
|
||||
<template #body-content>
|
||||
<div class="text-base">
|
||||
<TabButtons
|
||||
v-if="tabs.length > 1"
|
||||
:buttons="tabs"
|
||||
v-model="currentTab"
|
||||
class="w-fit"
|
||||
/>
|
||||
<div v-if="currentTab" class="mt-8">
|
||||
<div class="flex items-center justify-between">
|
||||
<TabButtons
|
||||
v-if="tabs.length > 1"
|
||||
:buttons="tabs"
|
||||
v-model="currentTab"
|
||||
class="w-fit"
|
||||
/>
|
||||
<!-- <FormControl
|
||||
v-model="searchText"
|
||||
:placeholder="__('Search by Member')"
|
||||
class="mt-2 mr-5 w-[25%]"
|
||||
/> -->
|
||||
</div>
|
||||
<div v-if="currentTab" class="mt-4">
|
||||
<div class="grid grid-cols-[55%,40%] gap-5">
|
||||
<div class="space-y-5 border rounded-md p-2 pt-4">
|
||||
<div
|
||||
class="space-y-5 border rounded-md p-2 pt-4 h-[70vh] overflow-y-auto"
|
||||
>
|
||||
<div class="grid grid-cols-[70%,30%] text-sm text-ink-gray-5">
|
||||
<div class="px-4">
|
||||
{{ __('Member') }}
|
||||
@@ -52,7 +61,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-center text-sm">
|
||||
{{ parseFloat(row.watch_time).toFixed(2) }}
|
||||
{{ formatTimestamp(row.watch_time) }}
|
||||
</div>
|
||||
</div>
|
||||
</router-link>
|
||||
@@ -62,7 +71,7 @@
|
||||
<NumberChart
|
||||
class="border rounded-md"
|
||||
:config="{
|
||||
title: __('Average Watch Time (seconds)'),
|
||||
title: __('Average Watch Time'),
|
||||
value: averageWatchTime,
|
||||
}"
|
||||
/>
|
||||
@@ -73,6 +82,9 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="text-sm text-ink-gray-5">
|
||||
{{ __('No statistics available for this video.') }}
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</Dialog>
|
||||
@@ -82,15 +94,21 @@ import {
|
||||
Avatar,
|
||||
createListResource,
|
||||
Dialog,
|
||||
FormControl,
|
||||
NumberChart,
|
||||
TabButtons,
|
||||
} from 'frappe-ui'
|
||||
import { computed, ref, watch } from 'vue'
|
||||
import { enablePlyr } from '@/utils'
|
||||
import { enablePlyr, formatTimestamp } from '@/utils'
|
||||
import VideoBlock from '@/components/VideoBlock.vue'
|
||||
|
||||
const show = defineModel<boolean | undefined>()
|
||||
const currentTab = ref<string>('')
|
||||
const searchText = ref<string>('')
|
||||
type Filters = {
|
||||
lesson: string | undefined
|
||||
member_name?: string[]
|
||||
}
|
||||
|
||||
const props = defineProps<{
|
||||
lessonName?: string
|
||||
@@ -127,6 +145,24 @@ watch(
|
||||
}
|
||||
)
|
||||
|
||||
watch(searchText, () => {
|
||||
let filterApplied = false
|
||||
let filters: Filters = {
|
||||
lesson: props.lessonName,
|
||||
}
|
||||
|
||||
if (searchText.value) {
|
||||
filters.member_name = ['like', `%${searchText.value}%`]
|
||||
filterApplied = true
|
||||
}
|
||||
|
||||
statistics.update({
|
||||
filters: filters,
|
||||
})
|
||||
|
||||
statistics.reload({})
|
||||
})
|
||||
|
||||
watch(show, () => {
|
||||
if (show.value) {
|
||||
enablePlyr()
|
||||
@@ -151,7 +187,7 @@ const averageWatchTime = computed(() => {
|
||||
totalWatchTime += parseFloat(item.watch_time)
|
||||
})
|
||||
|
||||
return totalWatchTime / currentTabData.value.length
|
||||
return formatTimestamp(totalWatchTime / currentTabData.value.length)
|
||||
})
|
||||
|
||||
const currentTabData = computed(() => {
|
||||
|
||||
241
frontend/src/components/Notes/InlineLessonMenu.vue
Normal file
241
frontend/src/components/Notes/InlineLessonMenu.vue
Normal file
@@ -0,0 +1,241 @@
|
||||
<template>
|
||||
<div
|
||||
class="text-sm absolute bg-white border rounded-md z-10 w-44"
|
||||
:style="{
|
||||
display: top > 0 ? 'block' : 'none',
|
||||
top: top + 'px',
|
||||
left: left + 'px',
|
||||
}"
|
||||
>
|
||||
<div class="space-y-2 py-2">
|
||||
<div class="text-xs text-ink-gray-5 font-medium px-3">
|
||||
{{ __('Highlight') }}
|
||||
</div>
|
||||
<div class="">
|
||||
<div
|
||||
v-for="color in colors"
|
||||
class="flex items-center space-x-2 px-3 py-2 cursor-pointer hover:bg-surface-gray-2"
|
||||
@click="saveHighLight(color)"
|
||||
>
|
||||
<span
|
||||
class="size-3 rounded-full"
|
||||
:style="{
|
||||
backgroundColor: theme.backgroundColor[color.toLowerCase()][400],
|
||||
}"
|
||||
></span>
|
||||
<span>
|
||||
{{ __(color) }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="border-t">
|
||||
<div
|
||||
@click="addToNotes()"
|
||||
class="flex items-center space-x-2 hover:bg-surface-gray-2 cursor-pointer rounded-b-md py-2 px-3"
|
||||
>
|
||||
<NotepadText class="size-3 stroke-1.5" />
|
||||
<span>
|
||||
{{ __('Add to Notes') }}
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
v-if="highlightExists()"
|
||||
@click="deleteHighlight"
|
||||
class="flex items-center space-x-2 hover:bg-surface-gray-2 cursor-pointer rounded-b-md py-2 px-3"
|
||||
>
|
||||
<Trash2 class="size-3 stroke-1.5" />
|
||||
<span>
|
||||
{{ __('Remove Highlight') }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import { computed, inject, ref, watch } from 'vue'
|
||||
import { NotepadText, Trash2 } from 'lucide-vue-next'
|
||||
import { theme } from '@/utils/theme'
|
||||
import type { Note, Notes } from '@/components/Notes/types'
|
||||
import { blockQuotesClick, highlightText } from '@/utils'
|
||||
|
||||
const user = inject<any>('$user')
|
||||
const show = defineModel()
|
||||
const notes = defineModel<Notes>('notes')
|
||||
const top = ref(0)
|
||||
const left = ref(0)
|
||||
const currentSelection = ref<Selection | null>(null)
|
||||
const selectedText = ref('')
|
||||
const emit = defineEmits<{
|
||||
(e: 'updateNotes'): void
|
||||
}>()
|
||||
|
||||
const props = defineProps<{
|
||||
lesson: string
|
||||
}>()
|
||||
|
||||
watch(show, () => {
|
||||
if (!show.value) {
|
||||
return resetMenuPosition()
|
||||
}
|
||||
|
||||
currentSelection.value = window.getSelection()
|
||||
if (!currentSelection.value?.toString()) {
|
||||
return resetMenuPosition()
|
||||
}
|
||||
|
||||
updateMenuPosition()
|
||||
})
|
||||
|
||||
const updateMenuPosition = () => {
|
||||
selectedText.value = currentSelection.value?.toString() || ''
|
||||
const range = currentSelection.value?.getRangeAt(0)
|
||||
const rect = range?.getBoundingClientRect()
|
||||
if (!rect) return
|
||||
|
||||
const offsetY = window.scrollY
|
||||
const offsetX = window.scrollX
|
||||
|
||||
top.value = Math.floor(rect.top + offsetY - 40)
|
||||
left.value = Math.floor(rect.right + offsetX + 10)
|
||||
}
|
||||
|
||||
const resetMenuPosition = () => {
|
||||
top.value = 0
|
||||
left.value = 0
|
||||
}
|
||||
|
||||
const colors = computed(() => {
|
||||
return ['Red', 'Blue', 'Green', 'Yellow', 'Purple']
|
||||
})
|
||||
|
||||
const highlightExists = () => {
|
||||
return notes.value?.data?.some(
|
||||
(note: Note) => note.highlighted_text === selectedText.value
|
||||
)
|
||||
}
|
||||
|
||||
const saveHighLight = (color: string) => {
|
||||
if (!selectedText.value) return
|
||||
|
||||
notes.value?.insert.submit(
|
||||
{
|
||||
lesson: props.lesson,
|
||||
member: user?.data?.name,
|
||||
highlighted_text: selectedText.value,
|
||||
color: color,
|
||||
name: '',
|
||||
},
|
||||
{
|
||||
onSuccess(data: Note) {
|
||||
highlightText(data)
|
||||
resetStates()
|
||||
emit('updateNotes')
|
||||
},
|
||||
onError(err: any) {
|
||||
console.error('Error saving highlight:', err)
|
||||
resetStates()
|
||||
},
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
const deleteHighlight = () => {
|
||||
let notesToDelete = notes.value?.data.find(
|
||||
(note: Note) => note.highlighted_text === selectedText.value
|
||||
)
|
||||
if (!notesToDelete) return
|
||||
notes.value?.delete.submit(notesToDelete.name, {
|
||||
onSuccess() {
|
||||
resetStates()
|
||||
document.querySelectorAll('.highlighted-text').forEach((el) => {
|
||||
const element = el as HTMLElement
|
||||
if (element.dataset.name === notesToDelete.name) {
|
||||
element.style.backgroundColor = 'transparent'
|
||||
}
|
||||
})
|
||||
},
|
||||
onError(err: any) {
|
||||
console.error('Error deleting highlight:', err)
|
||||
resetStates()
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
const addToNotes = () => {
|
||||
if (!selectedText.value) return
|
||||
let noteToUpdate = notes.value?.data.find((note: Note) => {
|
||||
return !note.highlighted_text && note.note !== ''
|
||||
})
|
||||
if (!noteToUpdate) {
|
||||
createNote()
|
||||
} else {
|
||||
updateNote(noteToUpdate)
|
||||
}
|
||||
}
|
||||
|
||||
const createNote = () => {
|
||||
notes.value?.insert.submit(
|
||||
{
|
||||
lesson: props.lesson,
|
||||
member: user?.data?.name,
|
||||
note: `<blockquote><p>${selectedText.value}</p></blockquote><br>`,
|
||||
color: 'Yellow',
|
||||
name: '',
|
||||
},
|
||||
{
|
||||
onSuccess(data: Note) {
|
||||
emit('updateNotes')
|
||||
setTimeout(() => {
|
||||
scrollToText(selectedText.value)
|
||||
blockQuotesClick()
|
||||
resetStates()
|
||||
}, 100)
|
||||
},
|
||||
onError(err: any) {
|
||||
console.error('Error creating note:', err)
|
||||
resetStates()
|
||||
},
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
const updateNote = (noteToUpdate: Note) => {
|
||||
notes.value?.setValue.submit(
|
||||
{
|
||||
name: noteToUpdate.name,
|
||||
note: `${noteToUpdate.note}\n\n<blockquote><p>${selectedText.value}</p></blockquote><br>`,
|
||||
},
|
||||
{
|
||||
onSuccess(data: Note) {
|
||||
emit('updateNotes')
|
||||
setTimeout(() => {
|
||||
scrollToText(selectedText.value)
|
||||
blockQuotesClick()
|
||||
resetStates()
|
||||
}, 100)
|
||||
},
|
||||
onError(err: any) {
|
||||
console.error('Error updating note:', err)
|
||||
resetStates()
|
||||
},
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
const scrollToText = (text: string) => {
|
||||
const elements = document.querySelectorAll('blockquote p')
|
||||
Array.from(elements).forEach((el) => {
|
||||
const element = el as HTMLElement
|
||||
if (element.textContent?.toLowerCase().includes(text.toLowerCase())) {
|
||||
element.scrollIntoView({ behavior: 'smooth', block: 'center' })
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const resetStates = () => {
|
||||
selectedText.value = ''
|
||||
show.value = false
|
||||
resetMenuPosition()
|
||||
}
|
||||
</script>
|
||||
115
frontend/src/components/Notes/Notes.vue
Normal file
115
frontend/src/components/Notes/Notes.vue
Normal file
@@ -0,0 +1,115 @@
|
||||
<template>
|
||||
<div class="text-lg font-semibold mb-4">
|
||||
{{ __('My Notes') }}
|
||||
</div>
|
||||
<TextEditor
|
||||
:content="note"
|
||||
:placeholder="__('Make notes for quick revision. Press / for menu.')"
|
||||
@change="(val: string) => updateNoteText(val)"
|
||||
:editable="true"
|
||||
editorClass="prose prose-sm min-h-[200px] max-w-none"
|
||||
/>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import { TextEditor } from 'frappe-ui'
|
||||
import { useDebounceFn } from '@vueuse/core'
|
||||
import { inject, ref, onMounted, watch } from 'vue'
|
||||
import type { Note, Notes } from '@/components/Notes/types'
|
||||
import { blockQuotesClick } from '@/utils/'
|
||||
|
||||
const note = ref<string | null>(null)
|
||||
const currentNoteName = ref<string | null>(null)
|
||||
const user = inject<any>('$user')
|
||||
const notes = defineModel<Notes>('notes')
|
||||
const emit = defineEmits<{
|
||||
(e: 'updateNotes'): void
|
||||
}>()
|
||||
|
||||
const props = defineProps<{
|
||||
lesson: string
|
||||
}>()
|
||||
|
||||
onMounted(() => {
|
||||
updateCurrentNote()
|
||||
})
|
||||
|
||||
watch(
|
||||
() => notes.value?.data,
|
||||
() => {
|
||||
updateCurrentNote()
|
||||
blockQuotesClick()
|
||||
}
|
||||
)
|
||||
|
||||
const updateCurrentNote = () => {
|
||||
const currentNote = notes.value?.data?.filter((row: Note) => {
|
||||
return !row.highlighted_text && row.note !== ''
|
||||
})
|
||||
if (currentNote?.length === 0) {
|
||||
note.value = null
|
||||
currentNoteName.value = null
|
||||
return
|
||||
} else if (currentNote && currentNote.length > 0) {
|
||||
currentNoteName.value = currentNote[0].name
|
||||
note.value = currentNote[0].note || null
|
||||
}
|
||||
}
|
||||
|
||||
const updateNoteText = (val: string) => {
|
||||
note.value = val
|
||||
debouncedSave()
|
||||
}
|
||||
|
||||
const debouncedSave = useDebounceFn(() => {
|
||||
saveNotes()
|
||||
}, 2000)
|
||||
|
||||
const saveNotes = () => {
|
||||
if (currentNoteName.value) {
|
||||
updateNote()
|
||||
} else {
|
||||
createNote()
|
||||
}
|
||||
}
|
||||
|
||||
const createNote = () => {
|
||||
notes.value?.insert.submit(
|
||||
{
|
||||
lesson: props.lesson,
|
||||
member: user?.data?.name,
|
||||
note: note.value,
|
||||
color: 'Yellow',
|
||||
name: '',
|
||||
},
|
||||
{
|
||||
onSuccess(data: Note) {
|
||||
currentNoteName.value = data.name || null
|
||||
emit('updateNotes')
|
||||
},
|
||||
onError(err: any) {
|
||||
console.error('Error creating note:', err)
|
||||
},
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
const updateNote = () => {
|
||||
if (!currentNoteName.value) return
|
||||
notes.value?.setValue.submit(
|
||||
{
|
||||
name: currentNoteName.value,
|
||||
lesson: props.lesson,
|
||||
member: user?.data?.name,
|
||||
note: note.value,
|
||||
},
|
||||
{
|
||||
onSuccess(data: Note) {
|
||||
emit('updateNotes')
|
||||
},
|
||||
onError(err: any) {
|
||||
console.error('Error updating note:', err)
|
||||
},
|
||||
}
|
||||
)
|
||||
}
|
||||
</script>
|
||||
32
frontend/src/components/Notes/types.ts
Normal file
32
frontend/src/components/Notes/types.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
|
||||
export type Note = {
|
||||
highlighted_text?: string
|
||||
color?: string
|
||||
name: string
|
||||
note?: string | null
|
||||
lesson?: string
|
||||
member?: string
|
||||
}
|
||||
|
||||
export type Notes = {
|
||||
data: Note[]
|
||||
reload: () => void
|
||||
insert: {
|
||||
submit: (
|
||||
data: Note,
|
||||
options: { onSuccess: (data: Note) => void; onError: (err: any) => void }
|
||||
) => void
|
||||
}
|
||||
setValue: {
|
||||
submit: (
|
||||
data: Note,
|
||||
options: { onSuccess: (data: Note) => void; onError: (err: any) => void }
|
||||
) => void
|
||||
},
|
||||
delete: {
|
||||
submit: (
|
||||
data: Note | string,
|
||||
options?: { onSuccess: () => void; onError: (err: any) => void }
|
||||
) => void
|
||||
}
|
||||
}
|
||||
@@ -132,6 +132,7 @@ const participants = createListResource({
|
||||
doctype: 'LMS Certificate',
|
||||
url: 'lms.lms.api.get_certified_participants',
|
||||
start: 0,
|
||||
cache: ['certified_participants'],
|
||||
pageLength: 100,
|
||||
})
|
||||
|
||||
|
||||
@@ -47,9 +47,16 @@
|
||||
:required="true"
|
||||
/>
|
||||
<div>
|
||||
<div class="mb-1.5 text-xs text-ink-gray-5">
|
||||
<div class="text-xs text-ink-gray-5">
|
||||
{{ __('Tags') }}
|
||||
</div>
|
||||
<FormControl
|
||||
v-model="newTag"
|
||||
:placeholder="__('Add a keyword and then press enter')"
|
||||
:class="['w-full', 'flex-1', 'my-1']"
|
||||
@keyup.enter="updateTags()"
|
||||
id="tags"
|
||||
/>
|
||||
<div>
|
||||
<div class="flex items-center flex-wrap gap-2">
|
||||
<div
|
||||
@@ -64,37 +71,13 @@
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<FormControl
|
||||
v-model="newTag"
|
||||
:placeholder="__('Add a keyword and then press enter')"
|
||||
:class="[
|
||||
'w-full',
|
||||
'flex-1',
|
||||
{ 'mt-2': course.tags?.length },
|
||||
]"
|
||||
@keyup.enter="updateTags()"
|
||||
id="tags"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid grid-cols-2 gap-5">
|
||||
<FormControl
|
||||
v-model="course.short_introduction"
|
||||
type="textarea"
|
||||
:rows="5"
|
||||
:label="__('Short Introduction')"
|
||||
:placeholder="
|
||||
__(
|
||||
'A one line introduction to the course that appears on the course card'
|
||||
)
|
||||
"
|
||||
:required="true"
|
||||
/>
|
||||
<div class="mb-4">
|
||||
<div class="text-xs text-ink-gray-5 mb-2">
|
||||
{{ __('Course Image') }}
|
||||
<span class="text-ink-red-3">*</span>
|
||||
</div>
|
||||
<FileUploader
|
||||
v-if="!course.course_image"
|
||||
@@ -144,6 +127,13 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ColorSwatches
|
||||
v-model="course.card_gradient"
|
||||
:label="__('Color')"
|
||||
:description="__('Choose a color for the course card')"
|
||||
class="w-full"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -185,6 +175,21 @@
|
||||
</div>
|
||||
|
||||
<div class="px-10 pb-5 mb-5 space-y-5 border-b">
|
||||
<div class="text-lg font-semibold">
|
||||
{{ __('About the Course') }}
|
||||
</div>
|
||||
<FormControl
|
||||
v-model="course.short_introduction"
|
||||
type="textarea"
|
||||
:rows="5"
|
||||
:label="__('Short Introduction')"
|
||||
:placeholder="
|
||||
__(
|
||||
'A one line introduction to the course that appears on the course card'
|
||||
)
|
||||
"
|
||||
:required="true"
|
||||
/>
|
||||
<div class="">
|
||||
<div class="mb-1.5 text-sm text-ink-gray-5">
|
||||
{{ __('Course Description') }}
|
||||
@@ -342,6 +347,7 @@ import {
|
||||
import Link from '@/components/Controls/Link.vue'
|
||||
import CourseOutline from '@/components/CourseOutline.vue'
|
||||
import MultiSelect from '@/components/Controls/MultiSelect.vue'
|
||||
import ColorSwatches from '@/components/Controls/ColorSwatches.vue'
|
||||
|
||||
const user = inject('$user')
|
||||
const newTag = ref('')
|
||||
@@ -365,6 +371,7 @@ const course = reactive({
|
||||
description: '',
|
||||
video_link: '',
|
||||
course_image: null,
|
||||
card_gradient: '',
|
||||
tags: '',
|
||||
category: '',
|
||||
published: false,
|
||||
|
||||
@@ -57,7 +57,7 @@
|
||||
</div>
|
||||
<div
|
||||
v-if="courses.data?.length"
|
||||
class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 2xl:grid-cols-4 gap-5"
|
||||
class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 2xl:grid-cols-4 gap-8"
|
||||
>
|
||||
<router-link
|
||||
v-for="course in courses.data"
|
||||
|
||||
@@ -69,155 +69,180 @@
|
||||
}"
|
||||
>
|
||||
<div
|
||||
class="border-r container pt-5 pb-10 px-5 h-full"
|
||||
class="border-r pt-5 pb-10 h-full"
|
||||
:class="{
|
||||
'w-full md:w-3/5 mx-auto border-none !pt-10': zenModeEnabled,
|
||||
}"
|
||||
>
|
||||
<div
|
||||
class="flex flex-col md:flex-row md:items-center justify-between"
|
||||
>
|
||||
<div class="flex flex-col">
|
||||
<div class="text-3xl font-semibold text-ink-gray-9">
|
||||
{{ lesson.data.title }}
|
||||
</div>
|
||||
<div class="px-5">
|
||||
<div
|
||||
class="flex flex-col md:flex-row md:items-center justify-between"
|
||||
>
|
||||
<div class="flex flex-col">
|
||||
<div class="text-3xl font-semibold text-ink-gray-9">
|
||||
{{ lesson.data.title }}
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="zenModeEnabled"
|
||||
class="relative flex items-center space-x-2 text-sm mt-1 text-ink-gray-7 group w-fit mt-2"
|
||||
>
|
||||
<span>
|
||||
{{ lesson.data.chapter_title }} -
|
||||
{{ lesson.data.course_title }}
|
||||
</span>
|
||||
<Info class="size-3" />
|
||||
<div
|
||||
class="hidden group-hover:block rounded bg-gray-900 px-2 py-1 text-xs text-white shadow-xl absolute left-0 top-full mt-2"
|
||||
v-if="zenModeEnabled"
|
||||
class="relative flex items-center space-x-2 text-sm mt-1 text-ink-gray-7 group w-fit mt-2"
|
||||
>
|
||||
{{ Math.ceil(lesson.data.membership.progress) }}%
|
||||
{{ __('completed') }}
|
||||
<span>
|
||||
{{ lesson.data.chapter_title }} -
|
||||
{{ lesson.data.course_title }}
|
||||
</span>
|
||||
<Info class="size-3" />
|
||||
<div
|
||||
class="hidden group-hover:block rounded bg-gray-900 px-2 py-1 text-xs text-white shadow-xl absolute left-0 top-full mt-2"
|
||||
>
|
||||
{{ Math.ceil(lesson.data.membership.progress) }}%
|
||||
{{ __('completed') }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center space-x-2 mt-2 md:mt-0">
|
||||
<Button
|
||||
v-if="zenModeEnabled"
|
||||
@click="showDiscussionsInZenMode()"
|
||||
>
|
||||
<template #icon>
|
||||
<MessageCircleQuestion class="w-4 h-4 stroke-1.5" />
|
||||
</template>
|
||||
</Button>
|
||||
<Button v-if="lesson.data.prev" @click="switchLesson('prev')">
|
||||
<template #prefix>
|
||||
<ChevronLeft class="w-4 h-4 stroke-1" />
|
||||
</template>
|
||||
<span>
|
||||
{{ __('Previous') }}
|
||||
</span>
|
||||
</Button>
|
||||
|
||||
<router-link
|
||||
v-if="allowEdit()"
|
||||
:to="{
|
||||
name: 'LessonForm',
|
||||
params: {
|
||||
courseName: courseName,
|
||||
chapterNumber: props.chapterNumber,
|
||||
lessonNumber: props.lessonNumber,
|
||||
},
|
||||
}"
|
||||
>
|
||||
<Button>
|
||||
{{ __('Edit') }}
|
||||
</Button>
|
||||
</router-link>
|
||||
|
||||
<Button v-if="lesson.data.next" @click="switchLesson('next')">
|
||||
<template #suffix>
|
||||
<ChevronRight class="w-4 h-4 stroke-1" />
|
||||
</template>
|
||||
<span>
|
||||
{{ __('Next') }}
|
||||
</span>
|
||||
</Button>
|
||||
|
||||
<router-link
|
||||
v-else
|
||||
:to="{
|
||||
name: 'CourseDetail',
|
||||
params: { courseName: courseName },
|
||||
}"
|
||||
>
|
||||
<Button>
|
||||
{{ __('Back to Course') }}
|
||||
</Button>
|
||||
</router-link>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center space-x-2 mt-2 md:mt-0">
|
||||
<Button v-if="zenModeEnabled" @click="showDiscussionsInZenMode()">
|
||||
<template #icon>
|
||||
<MessageCircleQuestion class="w-4 h-4 stroke-1.5" />
|
||||
</template>
|
||||
</Button>
|
||||
<Button v-if="lesson.data.prev" @click="switchLesson('prev')">
|
||||
<template #prefix>
|
||||
<ChevronLeft class="w-4 h-4 stroke-1" />
|
||||
</template>
|
||||
<span>
|
||||
{{ __('Previous') }}
|
||||
</span>
|
||||
</Button>
|
||||
|
||||
<router-link
|
||||
v-if="allowEdit()"
|
||||
:to="{
|
||||
name: 'LessonForm',
|
||||
params: {
|
||||
courseName: courseName,
|
||||
chapterNumber: props.chapterNumber,
|
||||
lessonNumber: props.lessonNumber,
|
||||
},
|
||||
<div v-if="!zenModeEnabled" class="flex items-center mt-2">
|
||||
<span
|
||||
class="h-6 mr-1"
|
||||
:class="{
|
||||
'avatar-group overlap': lesson.data.instructors?.length > 1,
|
||||
}"
|
||||
>
|
||||
<Button>
|
||||
{{ __('Edit') }}
|
||||
</Button>
|
||||
</router-link>
|
||||
|
||||
<Button v-if="lesson.data.next" @click="switchLesson('next')">
|
||||
<template #suffix>
|
||||
<ChevronRight class="w-4 h-4 stroke-1" />
|
||||
</template>
|
||||
<span>
|
||||
{{ __('Next') }}
|
||||
</span>
|
||||
</Button>
|
||||
|
||||
<router-link
|
||||
v-else
|
||||
:to="{
|
||||
name: 'CourseDetail',
|
||||
params: { courseName: courseName },
|
||||
}"
|
||||
>
|
||||
<Button>
|
||||
{{ __('Back to Course') }}
|
||||
</Button>
|
||||
</router-link>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="!zenModeEnabled" class="flex items-center mt-2">
|
||||
<span
|
||||
class="h-6 mr-1"
|
||||
:class="{
|
||||
'avatar-group overlap': lesson.data.instructors?.length > 1,
|
||||
}"
|
||||
>
|
||||
<UserAvatar
|
||||
v-for="instructor in lesson.data.instructors"
|
||||
:user="instructor"
|
||||
<UserAvatar
|
||||
v-for="instructor in lesson.data.instructors"
|
||||
:user="instructor"
|
||||
/>
|
||||
</span>
|
||||
<CourseInstructors
|
||||
v-if="lesson.data?.instructors"
|
||||
:instructors="lesson.data.instructors"
|
||||
/>
|
||||
</span>
|
||||
<CourseInstructors
|
||||
v-if="lesson.data?.instructors"
|
||||
:instructors="lesson.data.instructors"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="
|
||||
lesson.data.instructor_content &&
|
||||
JSON.parse(lesson.data.instructor_content)?.blocks?.length > 1 &&
|
||||
allowInstructorContent()
|
||||
"
|
||||
class="bg-surface-gray-2 p-3 rounded-md mt-6"
|
||||
>
|
||||
<div class="text-ink-gray-5 font-medium">
|
||||
{{ __('Instructor Notes') }}
|
||||
<div
|
||||
v-if="
|
||||
lesson.data.instructor_content &&
|
||||
JSON.parse(lesson.data.instructor_content)?.blocks?.length >
|
||||
1 &&
|
||||
allowInstructorContent()
|
||||
"
|
||||
class="bg-surface-gray-2 p-3 rounded-md mt-6"
|
||||
>
|
||||
<div class="text-ink-gray-5 font-medium">
|
||||
{{ __('Instructor Notes') }}
|
||||
</div>
|
||||
<div
|
||||
id="instructor-content"
|
||||
class="ProseMirror prose prose-table:table-fixed prose-td:p-2 prose-th:p-2 prose-td:border prose-th:border prose-td:border-outline-gray-2 prose-th:border-outline-gray-2 prose-td:relative prose-th:relative prose-th:bg-surface-gray-2 prose-sm max-w-none !whitespace-normal"
|
||||
></div>
|
||||
</div>
|
||||
<div
|
||||
id="instructor-content"
|
||||
class="ProseMirror prose prose-table:table-fixed prose-td:p-2 prose-th:p-2 prose-td:border prose-th:border prose-td:border-outline-gray-2 prose-th:border-outline-gray-2 prose-td:relative prose-th:relative prose-th:bg-surface-gray-2 prose-sm max-w-none !whitespace-normal"
|
||||
></div>
|
||||
v-else-if="lesson.data.instructor_notes"
|
||||
class="ProseMirror prose prose-table:table-fixed prose-td:p-2 prose-th:p-2 prose-td:border prose-th:border prose-td:border-outline-gray-2 prose-th:border-outline-gray-2 prose-td:relative prose-th:relative prose-th:bg-surface-gray-2 prose-sm max-w-none !whitespace-normal mt-8"
|
||||
>
|
||||
<LessonContent :content="lesson.data.instructor_notes" />
|
||||
</div>
|
||||
<div
|
||||
v-if="lesson.data.content"
|
||||
@mouseup="toggleInlineMenu"
|
||||
class="ProseMirror prose prose-table:table-fixed prose-td:p-2 prose-th:p-2 prose-td:border prose-th:border prose-td:border-outline-gray-2 prose-th:border-outline-gray-2 prose-td:relative prose-th:relative prose-th:bg-surface-gray-2 prose-sm max-w-none !whitespace-normal mt-8"
|
||||
>
|
||||
<div id="editor"></div>
|
||||
</div>
|
||||
<div
|
||||
v-else
|
||||
class="ProseMirror prose prose-table:table-fixed prose-td:p-2 prose-th:p-2 prose-td:border prose-th:border prose-td:border-outline-gray-2 prose-th:border-outline-gray-2 prose-td:relative prose-th:relative prose-th:bg-surface-gray-2 prose-sm max-w-none !whitespace-normal mt-8"
|
||||
>
|
||||
<LessonContent
|
||||
v-if="lesson.data?.body"
|
||||
:content="lesson.data.body"
|
||||
:youtube="lesson.data.youtube"
|
||||
:quizId="lesson.data.quiz_id"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-else-if="lesson.data.instructor_notes"
|
||||
class="ProseMirror prose prose-table:table-fixed prose-td:p-2 prose-th:p-2 prose-td:border prose-th:border prose-td:border-outline-gray-2 prose-th:border-outline-gray-2 prose-td:relative prose-th:relative prose-th:bg-surface-gray-2 prose-sm max-w-none !whitespace-normal mt-8"
|
||||
v-if="lesson.data"
|
||||
class="mt-10 pt-5 border-t px-5"
|
||||
ref="discussionsContainer"
|
||||
>
|
||||
<LessonContent :content="lesson.data.instructor_notes" />
|
||||
</div>
|
||||
<div
|
||||
v-if="lesson.data.content"
|
||||
class="ProseMirror prose prose-table:table-fixed prose-td:p-2 prose-th:p-2 prose-td:border prose-th:border prose-td:border-outline-gray-2 prose-th:border-outline-gray-2 prose-td:relative prose-th:relative prose-th:bg-surface-gray-2 prose-sm max-w-none !whitespace-normal mt-8"
|
||||
>
|
||||
<div id="editor"></div>
|
||||
</div>
|
||||
<div
|
||||
v-else
|
||||
class="ProseMirror prose prose-table:table-fixed prose-td:p-2 prose-th:p-2 prose-td:border prose-th:border prose-td:border-outline-gray-2 prose-th:border-outline-gray-2 prose-td:relative prose-th:relative prose-th:bg-surface-gray-2 prose-sm max-w-none !whitespace-normal mt-8"
|
||||
>
|
||||
<LessonContent
|
||||
v-if="lesson.data?.body"
|
||||
:content="lesson.data.body"
|
||||
:youtube="lesson.data.youtube"
|
||||
:quizId="lesson.data.quiz_id"
|
||||
<TabButtons
|
||||
:buttons="tabs"
|
||||
v-model="currentTab"
|
||||
class="w-fit mb-10"
|
||||
/>
|
||||
<Notes
|
||||
v-if="currentTab === 'Notes'"
|
||||
:lesson="lesson.data?.name"
|
||||
v-model:notes="notes"
|
||||
@updateNotes="updateNotes"
|
||||
/>
|
||||
</div>
|
||||
<div class="mt-20" ref="discussionsContainer">
|
||||
<Discussions
|
||||
v-if="allowDiscussions"
|
||||
v-else-if="allowDiscussions"
|
||||
:title="'Questions'"
|
||||
:doctype="'Course Lesson'"
|
||||
:docname="lesson.data.name"
|
||||
:key="lesson.data.name"
|
||||
:emptyStateText="
|
||||
__('Ask a question to get help from the community.')
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -247,6 +272,13 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<InlineLessonMenu
|
||||
v-if="lesson.data"
|
||||
v-model="showInlineMenu"
|
||||
:lesson="lesson.data?.name"
|
||||
v-model:notes="notes"
|
||||
@updateNotes="updateNotes"
|
||||
/>
|
||||
<VideoStatistics
|
||||
v-model="showStatsDialog"
|
||||
:lessonName="lesson.data?.name"
|
||||
@@ -259,7 +291,9 @@ import {
|
||||
Breadcrumbs,
|
||||
Button,
|
||||
call,
|
||||
createListResource,
|
||||
createResource,
|
||||
TabButtons,
|
||||
Tooltip,
|
||||
usePageMeta,
|
||||
} from 'frappe-ui'
|
||||
@@ -272,8 +306,6 @@ import {
|
||||
onBeforeUnmount,
|
||||
nextTick,
|
||||
} from 'vue'
|
||||
import CourseOutline from '@/components/CourseOutline.vue'
|
||||
import UserAvatar from '@/components/UserAvatar.vue'
|
||||
import { useRouter, useRoute } from 'vue-router'
|
||||
import {
|
||||
ChevronLeft,
|
||||
@@ -285,16 +317,20 @@ import {
|
||||
MessageCircleQuestion,
|
||||
TrendingUp,
|
||||
} from 'lucide-vue-next'
|
||||
import Discussions from '@/components/Discussions.vue'
|
||||
import { getEditorTools, enablePlyr } from '@/utils'
|
||||
import { getEditorTools, enablePlyr, highlightText } from '@/utils'
|
||||
import { sessionStore } from '@/stores/session'
|
||||
import { useSidebar } from '@/stores/sidebar'
|
||||
import EditorJS from '@editorjs/editorjs'
|
||||
import LessonContent from '@/components/LessonContent.vue'
|
||||
import CourseInstructors from '@/components/CourseInstructors.vue'
|
||||
import ProgressBar from '@/components/ProgressBar.vue'
|
||||
import Discussions from '@/components/Discussions.vue'
|
||||
import CertificationLinks from '@/components/CertificationLinks.vue'
|
||||
import VideoStatistics from '@/components/Modals/VideoStatistics.vue'
|
||||
import CourseOutline from '@/components/CourseOutline.vue'
|
||||
import UserAvatar from '@/components/UserAvatar.vue'
|
||||
import Notes from '@/components/Notes/Notes.vue'
|
||||
import InlineLessonMenu from '@/components/Notes/InlineLessonMenu.vue'
|
||||
|
||||
const user = inject('$user')
|
||||
const socket = inject('$socket')
|
||||
@@ -313,8 +349,17 @@ const timer = ref(0)
|
||||
const { brand } = sessionStore()
|
||||
const sidebarStore = useSidebar()
|
||||
const plyrSources = ref([])
|
||||
const showInlineMenu = ref(false)
|
||||
const currentTab = ref('Notes')
|
||||
let timerInterval
|
||||
|
||||
const tabs = ref([
|
||||
{
|
||||
label: __('Notes'),
|
||||
value: 'Notes',
|
||||
},
|
||||
])
|
||||
|
||||
const props = defineProps({
|
||||
courseName: {
|
||||
type: String,
|
||||
@@ -392,16 +437,22 @@ const setupLesson = (data) => {
|
||||
editor.value?.isReady.then(() => {
|
||||
checkIfDiscussionsAllowed()
|
||||
})
|
||||
checkQuiz()
|
||||
}
|
||||
|
||||
if (!editor.value && data.body) {
|
||||
const checkQuiz = () => {
|
||||
if (!editor.value && lesson.body) {
|
||||
const quizRegex = /\{\{ Quiz\(".*"\) \}\}/
|
||||
hasQuiz.value = quizRegex.test(data.body)
|
||||
if (!hasQuiz.value && !zenModeEnabled) allowDiscussions.value = true
|
||||
hasQuiz.value = quizRegex.test(lesson.body)
|
||||
if (!hasQuiz.value && !zenModeEnabled) {
|
||||
allowDiscussions.value = true
|
||||
} else {
|
||||
allowDiscussions.value = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const renderEditor = (holder, content) => {
|
||||
// empty the holder
|
||||
if (document.getElementById(holder))
|
||||
document.getElementById(holder).innerHTML = ''
|
||||
return new EditorJS({
|
||||
@@ -409,7 +460,7 @@ const renderEditor = (holder, content) => {
|
||||
tools: getEditorTools(),
|
||||
data: JSON.parse(content),
|
||||
readOnly: true,
|
||||
defaultBlock: 'embed', // editor adds an empty block at the top, so to avoid that added default block as embed
|
||||
defaultBlock: 'embed',
|
||||
})
|
||||
}
|
||||
|
||||
@@ -432,6 +483,23 @@ const progress = createResource({
|
||||
},
|
||||
})
|
||||
|
||||
const notes = createListResource({
|
||||
doctype: 'LMS Lesson Note',
|
||||
filters: {
|
||||
lesson: lesson.data?.name,
|
||||
member: user.data?.name,
|
||||
},
|
||||
fields: ['name', 'color', 'highlighted_text', 'note'],
|
||||
cache: ['notes', lesson.data?.name, user.data?.name],
|
||||
onSuccess(data) {
|
||||
data.forEach((note) => {
|
||||
setTimeout(() => {
|
||||
highlightText(note)
|
||||
}, 500)
|
||||
})
|
||||
},
|
||||
})
|
||||
|
||||
const breadcrumbs = computed(() => {
|
||||
let items = [{ label: 'Courses', route: { name: 'Courses' } }]
|
||||
items.push({
|
||||
@@ -480,6 +548,9 @@ watch(
|
||||
await nextTick()
|
||||
resetLessonState(newChapterNumber, newLessonNumber)
|
||||
startTimer()
|
||||
updateNotes()
|
||||
checkIfDiscussionsAllowed()
|
||||
checkQuiz()
|
||||
}
|
||||
}
|
||||
)
|
||||
@@ -546,6 +617,7 @@ watch(
|
||||
async (data) => {
|
||||
setupLesson(data)
|
||||
getPlyrSource()
|
||||
updateNotes()
|
||||
if (data.icon == 'icon-youtube') clearInterval(timerInterval)
|
||||
}
|
||||
)
|
||||
@@ -604,7 +676,7 @@ const updateVideoTime = (video) => {
|
||||
}
|
||||
|
||||
const startTimer = () => {
|
||||
timerInterval = setInterval(() => {
|
||||
let timerInterval = setInterval(() => {
|
||||
timer.value++
|
||||
if (timer.value == 30) {
|
||||
clearInterval(timerInterval)
|
||||
@@ -618,8 +690,11 @@ onBeforeUnmount(() => {
|
||||
})
|
||||
|
||||
const checkIfDiscussionsAllowed = () => {
|
||||
hasQuiz.value = false
|
||||
JSON.parse(lesson.data?.content)?.blocks?.forEach((block) => {
|
||||
if (block.type === 'quiz') hasQuiz.value = true
|
||||
if (block.type === 'quiz') {
|
||||
hasQuiz.value = true
|
||||
}
|
||||
})
|
||||
|
||||
if (
|
||||
@@ -628,8 +703,11 @@ const checkIfDiscussionsAllowed = () => {
|
||||
(lesson.data?.membership ||
|
||||
user.data?.is_moderator ||
|
||||
user.data?.is_instructor)
|
||||
)
|
||||
) {
|
||||
allowDiscussions.value = true
|
||||
} else {
|
||||
allowDiscussions.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const allowEdit = () => {
|
||||
@@ -669,6 +747,15 @@ const enrollStudent = () => {
|
||||
)
|
||||
}
|
||||
|
||||
const toggleInlineMenu = async () => {
|
||||
showInlineMenu.value = false
|
||||
await nextTick()
|
||||
let selection = window.getSelection()
|
||||
if (selection.toString()) {
|
||||
showInlineMenu.value = true
|
||||
}
|
||||
}
|
||||
|
||||
const canSeeStats = () => {
|
||||
if (user.data?.is_moderator || user.data?.is_instructor) return true
|
||||
return false
|
||||
@@ -720,6 +807,38 @@ const scrollDiscussionsIntoView = () => {
|
||||
})
|
||||
}
|
||||
|
||||
const updateNotes = () => {
|
||||
notes.update({
|
||||
filters: {
|
||||
lesson: lesson.data?.name,
|
||||
member: user.data?.name,
|
||||
},
|
||||
})
|
||||
notes.reload()
|
||||
}
|
||||
|
||||
watch(allowDiscussions, () => {
|
||||
if (allowDiscussions.value) {
|
||||
tabs.value = [
|
||||
{
|
||||
label: __('Notes'),
|
||||
value: 'Notes',
|
||||
},
|
||||
{
|
||||
label: __('Community'),
|
||||
value: 'Community',
|
||||
},
|
||||
]
|
||||
} else {
|
||||
tabs.value = [
|
||||
{
|
||||
label: __('Notes'),
|
||||
value: 'Notes',
|
||||
},
|
||||
]
|
||||
}
|
||||
})
|
||||
|
||||
const redirectToLogin = () => {
|
||||
window.location.href = `/login?redirect-to=/lms/courses/${props.courseName}`
|
||||
}
|
||||
|
||||
@@ -714,17 +714,6 @@ iframe {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.cdx-block.embed-tool::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: transparent;
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
:root {
|
||||
--plyr-range-fill-background: white;
|
||||
--plyr-video-control-background-hover: transparent;
|
||||
|
||||
@@ -77,7 +77,7 @@ const evaluations = createListResource({
|
||||
],
|
||||
auto: true,
|
||||
orderBy: 'creation desc',
|
||||
limit: 100,
|
||||
pageLength: 500,
|
||||
cache: ['schedule', user.data?.name],
|
||||
transform(data) {
|
||||
return data.map((d) => {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { call, toast } from 'frappe-ui'
|
||||
import { useTimeAgo } from '@vueuse/core'
|
||||
import { theme } from '@/utils/theme'
|
||||
import { Quiz } from '@/utils/quiz'
|
||||
import { Program } from '@/utils/program'
|
||||
import { Assignment } from '@/utils/assignment'
|
||||
@@ -420,17 +421,6 @@ export function getSidebarLinks() {
|
||||
to: 'Batches',
|
||||
activeFor: ['Batches', 'BatchDetail', 'Batch', 'BatchForm'],
|
||||
},
|
||||
{
|
||||
label: 'Programming Exercises',
|
||||
icon: 'Code',
|
||||
to: 'ProgrammingExercises',
|
||||
activeFor: [
|
||||
'ProgrammingExercises',
|
||||
'ProgrammingExerciseForm',
|
||||
'ProgrammingExerciseSubmissions',
|
||||
'ProgrammingExerciseSubmission',
|
||||
],
|
||||
},
|
||||
{
|
||||
label: 'Certified Members',
|
||||
icon: 'GraduationCap',
|
||||
@@ -497,14 +487,39 @@ export function singularize(word) {
|
||||
)
|
||||
}
|
||||
|
||||
export const validateFile = (file, showToast = true) => {
|
||||
if (!file.type.startsWith('image/')) {
|
||||
const errorMessage = __('Only image file is allowed.')
|
||||
if (showToast) {
|
||||
toast.error(errorMessage)
|
||||
}
|
||||
return errorMessage
|
||||
export const validateFile = async (file, showToast = true) => {
|
||||
const error = (msg) => {
|
||||
if (showToast) toast.error(msg)
|
||||
console.error(msg)
|
||||
return msg
|
||||
}
|
||||
|
||||
if (!file.type.startsWith('image/')) {
|
||||
return error(__('Only image file is allowed.'))
|
||||
}
|
||||
|
||||
if (file.type === 'image/svg+xml') {
|
||||
const text = await file.text()
|
||||
|
||||
const blacklist = [
|
||||
/<script[\s>]/i,
|
||||
/on\w+=["']?/i,
|
||||
/javascript:/i,
|
||||
/data:/i,
|
||||
/<iframe[\s>]/i,
|
||||
/<object[\s>]/i,
|
||||
/<embed[\s>]/i,
|
||||
/<link[\s>]/i,
|
||||
]
|
||||
|
||||
for (const pattern of blacklist) {
|
||||
if (pattern.test(text)) {
|
||||
return error(__('SVG contains potentially unsafe content.'))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
export const escapeHTML = (text) => {
|
||||
@@ -551,11 +566,10 @@ export const enablePlyr = async () => {
|
||||
const wait = (ms) => new Promise((resolve) => setTimeout(resolve, ms))
|
||||
|
||||
const setupPlyrForVideo = (video, players) => {
|
||||
const src = video.getAttribute('src') || video.getAttribute('data-src')
|
||||
const src = video.getAttribute('src')
|
||||
|
||||
if (src) {
|
||||
const videoID = extractYouTubeId(src)
|
||||
video.setAttribute('data-plyr-provider', 'youtube')
|
||||
video.setAttribute('data-plyr-embed-id', videoID)
|
||||
}
|
||||
|
||||
@@ -675,7 +689,102 @@ export const updateMetaInfo = (type, route, meta) => {
|
||||
|
||||
export const formatTimestamp = (seconds) => {
|
||||
const date = new Date(seconds * 1000)
|
||||
const hours = String(date.getUTCHours()).padStart(2, '0')
|
||||
const minutes = String(date.getUTCMinutes()).padStart(2, '0')
|
||||
const secs = String(date.getUTCSeconds()).padStart(2, '0')
|
||||
return `${minutes}:${secs}`
|
||||
return `${hours}:${minutes}:${secs}`
|
||||
}
|
||||
|
||||
const getRootNode = (selector = '#editor') => {
|
||||
const root = document.querySelector(selector)
|
||||
if (!root) {
|
||||
console.warn(`Root node not found for selector: ${selector}`)
|
||||
}
|
||||
return root
|
||||
}
|
||||
|
||||
const createTextWalker = (root, phrase) => {
|
||||
return document.createTreeWalker(root, NodeFilter.SHOW_TEXT, {
|
||||
acceptNode(node) {
|
||||
return node.nodeValue.toLowerCase().includes(phrase.toLowerCase())
|
||||
? NodeFilter.FILTER_ACCEPT
|
||||
: NodeFilter.FILTER_SKIP
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
const findMatchingTextNode = (walker, phrase) => {
|
||||
const node = walker.nextNode()
|
||||
if (!node) return null
|
||||
|
||||
const startIndex = node.nodeValue
|
||||
.toLowerCase()
|
||||
.indexOf(phrase.toLowerCase())
|
||||
const endIndex = startIndex + phrase.length
|
||||
|
||||
return { node, startIndex, endIndex }
|
||||
}
|
||||
|
||||
const createHighlightSpan = (color, name) => {
|
||||
const span = document.createElement('span')
|
||||
span.className = 'highlighted-text'
|
||||
span.style.backgroundColor = theme.backgroundColor[color][200]
|
||||
span.dataset.name = name
|
||||
return span
|
||||
}
|
||||
|
||||
const wrapRangeInHighlight = ({ node, startIndex, endIndex }, color, name) => {
|
||||
const range = document.createRange()
|
||||
range.setStart(node, startIndex)
|
||||
range.setEnd(node, endIndex)
|
||||
|
||||
const span = createHighlightSpan(color, name)
|
||||
range.surroundContents(span)
|
||||
}
|
||||
|
||||
export const highlightText = (note, scrollIntoView = false) => {
|
||||
if (!note?.highlighted_text) return
|
||||
|
||||
const root = getRootNode()
|
||||
if (!root) return
|
||||
|
||||
const phrase = note.highlighted_text
|
||||
const color = note.color.toLowerCase()
|
||||
|
||||
const walker = createTextWalker(root, phrase)
|
||||
const match = findMatchingTextNode(walker, phrase)
|
||||
if (!match) return
|
||||
|
||||
wrapRangeInHighlight(match, color, note.name)
|
||||
|
||||
if (scrollIntoView) {
|
||||
match.node.parentElement.scrollIntoView({
|
||||
behavior: 'smooth',
|
||||
block: 'center',
|
||||
})
|
||||
setTimeout(() => {
|
||||
const highlightedElements =
|
||||
document.querySelectorAll('.highlighted-text')
|
||||
highlightedElements.forEach((el) => {
|
||||
if (el.dataset.name === note.name) {
|
||||
el.style.backgroundColor = 'transparent'
|
||||
}
|
||||
})
|
||||
}, 3000)
|
||||
}
|
||||
}
|
||||
|
||||
export const scrollToReference = (text) => {
|
||||
highlightText({ highlighted_text: text, color: 'yellow', name: '' }, true)
|
||||
}
|
||||
|
||||
export const blockQuotesClick = () => {
|
||||
document.querySelectorAll('blockquote').forEach((el) => {
|
||||
el.addEventListener('click', (e) => {
|
||||
const text = e.target.textContent || ''
|
||||
if (text) {
|
||||
scrollToReference(text)
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
@@ -25,7 +25,8 @@ export default defineConfig({
|
||||
}),
|
||||
],
|
||||
server: {
|
||||
allowedHosts: ['fs', 'per2'],
|
||||
host: '0.0.0.0', // Accept connections from any network interface
|
||||
allowedHosts: ['ps', 'fs'], // Explicitly allow this host
|
||||
},
|
||||
resolve: {
|
||||
alias: {
|
||||
@@ -39,6 +40,7 @@ export default defineConfig({
|
||||
'showdown',
|
||||
'engine.io-client',
|
||||
'tailwind.config.js',
|
||||
'interactjs',
|
||||
'highlight.js',
|
||||
'plyr',
|
||||
],
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1 +1 @@
|
||||
__version__ = "2.32.0"
|
||||
__version__ = "2.33.0"
|
||||
|
||||
@@ -16,13 +16,14 @@
|
||||
"field_order": [
|
||||
"title",
|
||||
"video_link",
|
||||
"tags",
|
||||
"column_break_3",
|
||||
"instructors",
|
||||
"tags",
|
||||
"column_break_htgn",
|
||||
"image",
|
||||
"category",
|
||||
"status",
|
||||
"column_break_htgn",
|
||||
"image",
|
||||
"card_gradient",
|
||||
"section_break_7",
|
||||
"published",
|
||||
"published_on",
|
||||
@@ -98,8 +99,7 @@
|
||||
{
|
||||
"fieldname": "image",
|
||||
"fieldtype": "Attach Image",
|
||||
"label": "Preview Image",
|
||||
"reqd": 1
|
||||
"label": "Preview Image"
|
||||
},
|
||||
{
|
||||
"fieldname": "tags",
|
||||
@@ -272,6 +272,12 @@
|
||||
"fieldtype": "Link",
|
||||
"label": "Evaluator",
|
||||
"options": "Course Evaluator"
|
||||
},
|
||||
{
|
||||
"fieldname": "card_gradient",
|
||||
"fieldtype": "Select",
|
||||
"label": "Color",
|
||||
"options": "Red\nBlue\nGreen\nAmber\nCyan\nOrange\nPink\nPurple\nTeal\nViolet\nYellow\nGray"
|
||||
}
|
||||
],
|
||||
"is_published_field": "published",
|
||||
@@ -290,8 +296,8 @@
|
||||
}
|
||||
],
|
||||
"make_attachments_public": 1,
|
||||
"modified": "2025-05-29 12:38:01.002898",
|
||||
"modified_by": "Administrator",
|
||||
"modified": "2025-07-25 17:50:44.983391",
|
||||
"modified_by": "sayali@frappe.io",
|
||||
"module": "LMS",
|
||||
"name": "LMS Course",
|
||||
"owner": "Administrator",
|
||||
|
||||
@@ -21,6 +21,7 @@ class LMSCourse(Document):
|
||||
self.validate_certification()
|
||||
self.validate_amount_and_currency()
|
||||
self.image = validate_image(self.image)
|
||||
self.validate_card_gradient()
|
||||
|
||||
def validate_published(self):
|
||||
if self.published and not self.published_on:
|
||||
@@ -73,6 +74,24 @@ class LMSCourse(Document):
|
||||
if self.paid_certificate and (cint(self.course_price) <= 0 or not self.currency):
|
||||
frappe.throw(_("Amount and currency are required for paid certificates."))
|
||||
|
||||
def validate_card_gradient(self):
|
||||
if not self.image and not self.card_gradient:
|
||||
colors = [
|
||||
"Red",
|
||||
"Blue",
|
||||
"Green",
|
||||
"Yellow",
|
||||
"Orange",
|
||||
"Pink",
|
||||
"Amber",
|
||||
"Violet",
|
||||
"Cyan",
|
||||
"Teal",
|
||||
"Gray",
|
||||
"Purple",
|
||||
]
|
||||
self.card_gradient = random.choice(colors)
|
||||
|
||||
def on_update(self):
|
||||
if not self.upcoming and self.has_value_changed("upcoming"):
|
||||
self.send_email_to_interested_users()
|
||||
|
||||
0
lms/lms/doctype/lms_lesson_note/__init__.py
Normal file
0
lms/lms/doctype/lms_lesson_note/__init__.py
Normal file
8
lms/lms/doctype/lms_lesson_note/lms_lesson_note.js
Normal file
8
lms/lms/doctype/lms_lesson_note/lms_lesson_note.js
Normal file
@@ -0,0 +1,8 @@
|
||||
// Copyright (c) 2025, Frappe and contributors
|
||||
// For license information, please see license.txt
|
||||
|
||||
// frappe.ui.form.on("LMS Lesson Note", {
|
||||
// refresh(frm) {
|
||||
|
||||
// },
|
||||
// });
|
||||
140
lms/lms/doctype/lms_lesson_note/lms_lesson_note.json
Normal file
140
lms/lms/doctype/lms_lesson_note/lms_lesson_note.json
Normal file
@@ -0,0 +1,140 @@
|
||||
{
|
||||
"actions": [],
|
||||
"allow_rename": 1,
|
||||
"autoname": "hash",
|
||||
"creation": "2025-08-04 13:17:19.497483",
|
||||
"doctype": "DocType",
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"lesson",
|
||||
"course",
|
||||
"column_break_qgrb",
|
||||
"member",
|
||||
"color",
|
||||
"section_break_smzm",
|
||||
"highlighted_text",
|
||||
"column_break_zvrs",
|
||||
"note"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"fieldname": "lesson",
|
||||
"fieldtype": "Link",
|
||||
"in_list_view": 1,
|
||||
"in_standard_filter": 1,
|
||||
"label": "Lesson",
|
||||
"options": "Course Lesson",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fetch_from": "lesson.course",
|
||||
"fieldname": "course",
|
||||
"fieldtype": "Link",
|
||||
"label": "Course",
|
||||
"options": "LMS Course"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_qgrb",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "member",
|
||||
"fieldtype": "Link",
|
||||
"in_list_view": 1,
|
||||
"in_standard_filter": 1,
|
||||
"label": "Member",
|
||||
"options": "User",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "color",
|
||||
"fieldtype": "Select",
|
||||
"label": "Color",
|
||||
"options": "Red\nBlue\nGreen\nYellow\nPurple",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "section_break_smzm",
|
||||
"fieldtype": "Section Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "highlighted_text",
|
||||
"fieldtype": "Small Text",
|
||||
"label": "Highlighted Text"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_zvrs",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "note",
|
||||
"fieldtype": "Text Editor",
|
||||
"label": "Note"
|
||||
}
|
||||
],
|
||||
"grid_page_length": 50,
|
||||
"index_web_pages_for_search": 1,
|
||||
"links": [],
|
||||
"modified": "2025-08-05 19:08:47.858172",
|
||||
"modified_by": "Administrator",
|
||||
"module": "LMS",
|
||||
"name": "LMS Lesson Note",
|
||||
"naming_rule": "Random",
|
||||
"owner": "Administrator",
|
||||
"permissions": [
|
||||
{
|
||||
"create": 1,
|
||||
"delete": 1,
|
||||
"email": 1,
|
||||
"export": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "System Manager",
|
||||
"share": 1,
|
||||
"write": 1
|
||||
},
|
||||
{
|
||||
"create": 1,
|
||||
"delete": 1,
|
||||
"email": 1,
|
||||
"export": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "Moderator",
|
||||
"share": 1,
|
||||
"write": 1
|
||||
},
|
||||
{
|
||||
"create": 1,
|
||||
"delete": 1,
|
||||
"email": 1,
|
||||
"export": 1,
|
||||
"if_owner": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "LMS Student",
|
||||
"share": 1,
|
||||
"write": 1
|
||||
},
|
||||
{
|
||||
"create": 1,
|
||||
"delete": 1,
|
||||
"email": 1,
|
||||
"export": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "Course Creator",
|
||||
"share": 1,
|
||||
"write": 1
|
||||
}
|
||||
],
|
||||
"row_format": "Dynamic",
|
||||
"sort_field": "creation",
|
||||
"sort_order": "DESC",
|
||||
"states": [],
|
||||
"title_field": "member"
|
||||
}
|
||||
9
lms/lms/doctype/lms_lesson_note/lms_lesson_note.py
Normal file
9
lms/lms/doctype/lms_lesson_note/lms_lesson_note.py
Normal file
@@ -0,0 +1,9 @@
|
||||
# Copyright (c) 2025, Frappe and contributors
|
||||
# For license information, please see license.txt
|
||||
|
||||
# import frappe
|
||||
from frappe.model.document import Document
|
||||
|
||||
|
||||
class LMSLessonNote(Document):
|
||||
pass
|
||||
21
lms/lms/doctype/lms_lesson_note/test_lms_lesson_note.py
Normal file
21
lms/lms/doctype/lms_lesson_note/test_lms_lesson_note.py
Normal file
@@ -0,0 +1,21 @@
|
||||
# Copyright (c) 2025, Frappe and Contributors
|
||||
# See license.txt
|
||||
|
||||
# import frappe
|
||||
from frappe.tests import IntegrationTestCase
|
||||
|
||||
|
||||
# On IntegrationTestCase, the doctype test records and all
|
||||
# link-field test record dependencies are recursively loaded
|
||||
# Use these module variables to add/remove to/from that list
|
||||
EXTRA_TEST_RECORD_DEPENDENCIES = [] # eg. ["User"]
|
||||
IGNORE_TEST_RECORD_DEPENDENCIES = [] # eg. ["User"]
|
||||
|
||||
|
||||
class IntegrationTestLMSLessonNote(IntegrationTestCase):
|
||||
"""
|
||||
Integration tests for LMSLessonNote.
|
||||
Use this class for testing interactions between multiple components.
|
||||
"""
|
||||
|
||||
pass
|
||||
@@ -23,6 +23,7 @@
|
||||
"fieldname": "lesson",
|
||||
"fieldtype": "Link",
|
||||
"in_list_view": 1,
|
||||
"in_standard_filter": 1,
|
||||
"label": "Lesson",
|
||||
"options": "Course Lesson",
|
||||
"reqd": 1
|
||||
@@ -39,6 +40,7 @@
|
||||
"fetch_from": "lesson.course",
|
||||
"fieldname": "course",
|
||||
"fieldtype": "Link",
|
||||
"in_standard_filter": 1,
|
||||
"label": "Course",
|
||||
"options": "LMS Course",
|
||||
"read_only": 1
|
||||
@@ -51,6 +53,7 @@
|
||||
"fieldname": "member",
|
||||
"fieldtype": "Link",
|
||||
"in_list_view": 1,
|
||||
"in_standard_filter": 1,
|
||||
"label": "Member",
|
||||
"options": "User",
|
||||
"reqd": 1
|
||||
@@ -99,7 +102,7 @@
|
||||
"grid_page_length": 50,
|
||||
"index_web_pages_for_search": 1,
|
||||
"links": [],
|
||||
"modified": "2025-06-30 16:57:10.561660",
|
||||
"modified": "2025-07-30 14:38:52.555010",
|
||||
"modified_by": "sayali@frappe.io",
|
||||
"module": "LMS",
|
||||
"name": "LMS Video Watch Duration",
|
||||
|
||||
@@ -565,10 +565,13 @@ def get_courses_under_review():
|
||||
|
||||
def validate_image(path):
|
||||
if path and "/private" in path:
|
||||
file = frappe.get_doc("File", {"file_url": path})
|
||||
file.is_private = 0
|
||||
file.save()
|
||||
return file.file_url
|
||||
frappe.db.set_value(
|
||||
"File",
|
||||
{"file_url": path},
|
||||
"is_private",
|
||||
0,
|
||||
)
|
||||
return path.replace("/private", "")
|
||||
return path
|
||||
|
||||
|
||||
@@ -1097,6 +1100,7 @@ def get_course_fields():
|
||||
"title",
|
||||
"tags",
|
||||
"image",
|
||||
"card_gradient",
|
||||
"short_introduction",
|
||||
"published",
|
||||
"upcoming",
|
||||
|
||||
609
lms/locale/ar.po
609
lms/locale/ar.po
File diff suppressed because it is too large
Load Diff
613
lms/locale/bs.po
613
lms/locale/bs.po
File diff suppressed because it is too large
Load Diff
609
lms/locale/cs.po
609
lms/locale/cs.po
File diff suppressed because it is too large
Load Diff
609
lms/locale/de.po
609
lms/locale/de.po
File diff suppressed because it is too large
Load Diff
613
lms/locale/eo.po
613
lms/locale/eo.po
File diff suppressed because it is too large
Load Diff
609
lms/locale/es.po
609
lms/locale/es.po
File diff suppressed because it is too large
Load Diff
613
lms/locale/fa.po
613
lms/locale/fa.po
File diff suppressed because it is too large
Load Diff
520
lms/locale/fr.po
520
lms/locale/fr.po
File diff suppressed because it is too large
Load Diff
613
lms/locale/hr.po
613
lms/locale/hr.po
File diff suppressed because it is too large
Load Diff
621
lms/locale/hu.po
621
lms/locale/hu.po
File diff suppressed because it is too large
Load Diff
7315
lms/locale/id.po
Normal file
7315
lms/locale/id.po
Normal file
File diff suppressed because it is too large
Load Diff
667
lms/locale/it.po
667
lms/locale/it.po
File diff suppressed because it is too large
Load Diff
@@ -7,8 +7,8 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: Frappe LMS VERSION\n"
|
||||
"Report-Msgid-Bugs-To: jannat@frappe.io\n"
|
||||
"POT-Creation-Date: 2025-07-11 16:04+0000\n"
|
||||
"PO-Revision-Date: 2025-07-11 16:04+0000\n"
|
||||
"POT-Creation-Date: 2025-08-01 16:04+0000\n"
|
||||
"PO-Revision-Date: 2025-08-01 16:04+0000\n"
|
||||
"Last-Translator: jannat@frappe.io\n"
|
||||
"Language-Team: jannat@frappe.io\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
@@ -69,11 +69,11 @@ msgstr ""
|
||||
msgid "<span style=\"font-size: 18px;\"><b>Statistics</b></span>"
|
||||
msgstr ""
|
||||
|
||||
#: lms/lms/doctype/lms_course/lms_course.py:63
|
||||
#: lms/lms/doctype/lms_course/lms_course.py:64
|
||||
msgid "A course cannot have both paid certificate and certificate of completion."
|
||||
msgstr ""
|
||||
|
||||
#: frontend/src/pages/CourseForm.vue:88
|
||||
#: frontend/src/pages/CourseForm.vue:187
|
||||
msgid "A one line introduction to the course that appears on the course card"
|
||||
msgstr ""
|
||||
|
||||
@@ -81,6 +81,10 @@ msgstr ""
|
||||
msgid "About"
|
||||
msgstr ""
|
||||
|
||||
#: frontend/src/pages/CourseForm.vue:179
|
||||
msgid "About the Course"
|
||||
msgstr ""
|
||||
|
||||
#: frontend/src/pages/Batch.vue:101
|
||||
msgid "About this batch"
|
||||
msgstr ""
|
||||
@@ -178,7 +182,7 @@ msgstr ""
|
||||
msgid "Add a Student"
|
||||
msgstr ""
|
||||
|
||||
#: frontend/src/components/AppSidebar.vue:568
|
||||
#: frontend/src/components/AppSidebar.vue:584
|
||||
msgid "Add a chapter"
|
||||
msgstr ""
|
||||
|
||||
@@ -186,11 +190,11 @@ msgstr ""
|
||||
msgid "Add a course"
|
||||
msgstr ""
|
||||
|
||||
#: frontend/src/pages/CourseForm.vue:69
|
||||
#: frontend/src/pages/CourseForm.vue:55
|
||||
msgid "Add a keyword and then press enter"
|
||||
msgstr ""
|
||||
|
||||
#: frontend/src/components/AppSidebar.vue:569
|
||||
#: frontend/src/components/AppSidebar.vue:585
|
||||
msgid "Add a lesson"
|
||||
msgstr ""
|
||||
|
||||
@@ -223,7 +227,7 @@ msgstr ""
|
||||
msgid "Add at least one possible answer for this question: {0}"
|
||||
msgstr ""
|
||||
|
||||
#: frontend/src/components/AppSidebar.vue:532
|
||||
#: frontend/src/components/AppSidebar.vue:548
|
||||
msgid "Add courses to your batch"
|
||||
msgstr ""
|
||||
|
||||
@@ -231,7 +235,7 @@ msgstr ""
|
||||
msgid "Add quiz to this video"
|
||||
msgstr ""
|
||||
|
||||
#: frontend/src/components/AppSidebar.vue:511
|
||||
#: frontend/src/components/AppSidebar.vue:527
|
||||
msgid "Add students to your batch"
|
||||
msgstr ""
|
||||
|
||||
@@ -243,11 +247,11 @@ msgstr ""
|
||||
msgid "Add your assignment as {0}"
|
||||
msgstr ""
|
||||
|
||||
#: frontend/src/components/AppSidebar.vue:444
|
||||
#: frontend/src/components/AppSidebar.vue:460
|
||||
msgid "Add your first chapter"
|
||||
msgstr ""
|
||||
|
||||
#: frontend/src/components/AppSidebar.vue:460
|
||||
#: frontend/src/components/AppSidebar.vue:476
|
||||
msgid "Add your first lesson"
|
||||
msgstr ""
|
||||
|
||||
@@ -323,10 +327,15 @@ msgstr ""
|
||||
msgid "Already Registered"
|
||||
msgstr ""
|
||||
|
||||
#. Option for the 'Color' (Select) field in DocType 'LMS Course'
|
||||
#: lms/lms/doctype/lms_course/lms_course.json
|
||||
msgid "Amber"
|
||||
msgstr ""
|
||||
|
||||
#. Label of the amount (Currency) field in DocType 'LMS Batch'
|
||||
#. Label of the course_price (Currency) field in DocType 'LMS Course'
|
||||
#. Label of the amount (Currency) field in DocType 'LMS Payment'
|
||||
#: frontend/src/pages/BatchForm.vue:275 frontend/src/pages/CourseForm.vue:254
|
||||
#: frontend/src/pages/BatchForm.vue:275 frontend/src/pages/CourseForm.vue:259
|
||||
#: lms/lms/doctype/lms_batch/lms_batch.json
|
||||
#: lms/lms/doctype/lms_course/lms_course.json
|
||||
#: lms/lms/doctype/lms_payment/lms_payment.json
|
||||
@@ -344,11 +353,11 @@ msgstr ""
|
||||
msgid "Amount and currency are required for paid batches."
|
||||
msgstr ""
|
||||
|
||||
#: lms/lms/doctype/lms_course/lms_course.py:74
|
||||
#: lms/lms/doctype/lms_course/lms_course.py:75
|
||||
msgid "Amount and currency are required for paid certificates."
|
||||
msgstr ""
|
||||
|
||||
#: lms/lms/doctype/lms_course/lms_course.py:71
|
||||
#: lms/lms/doctype/lms_course/lms_course.py:72
|
||||
msgid "Amount and currency are required for paid courses."
|
||||
msgstr ""
|
||||
|
||||
@@ -357,15 +366,15 @@ msgstr ""
|
||||
msgid "Amount with GST"
|
||||
msgstr ""
|
||||
|
||||
#: frontend/src/components/Modals/AnnouncementModal.vue:33
|
||||
#: frontend/src/components/Modals/AnnouncementModal.vue:34
|
||||
msgid "Announcement"
|
||||
msgstr ""
|
||||
|
||||
#: frontend/src/components/Modals/AnnouncementModal.vue:101
|
||||
#: frontend/src/components/Modals/AnnouncementModal.vue:105
|
||||
msgid "Announcement has been sent successfully"
|
||||
msgstr ""
|
||||
|
||||
#: frontend/src/components/Modals/AnnouncementModal.vue:96
|
||||
#: frontend/src/components/Modals/AnnouncementModal.vue:97
|
||||
msgid "Announcement is required"
|
||||
msgstr ""
|
||||
|
||||
@@ -380,7 +389,7 @@ msgstr ""
|
||||
msgid "Answer"
|
||||
msgstr ""
|
||||
|
||||
#: frontend/src/pages/CourseForm.vue:121 frontend/src/pages/CourseForm.vue:140
|
||||
#: frontend/src/pages/CourseForm.vue:104 frontend/src/pages/CourseForm.vue:123
|
||||
msgid "Appears on the course card in the course list"
|
||||
msgstr ""
|
||||
|
||||
@@ -463,7 +472,7 @@ msgid "Assessment {0} has already been added to this batch."
|
||||
msgstr ""
|
||||
|
||||
#. Label of the show_assessments (Check) field in DocType 'LMS Settings'
|
||||
#: frontend/src/components/AppSidebar.vue:581
|
||||
#: frontend/src/components/AppSidebar.vue:597
|
||||
#: frontend/src/components/Assessments.vue:5
|
||||
#: frontend/src/components/BatchStudents.vue:32
|
||||
#: lms/lms/doctype/lms_settings/lms_settings.json
|
||||
@@ -545,7 +554,7 @@ msgstr ""
|
||||
msgid "Assignment will appear at the bottom of the lesson."
|
||||
msgstr ""
|
||||
|
||||
#: frontend/src/components/AppSidebar.vue:585
|
||||
#: frontend/src/components/AppSidebar.vue:601
|
||||
#: frontend/src/components/Settings/Badges.vue:163
|
||||
#: frontend/src/pages/Assignments.vue:208 lms/www/lms.py:273
|
||||
msgid "Assignments"
|
||||
@@ -604,13 +613,13 @@ msgstr ""
|
||||
msgid "Average Progress %"
|
||||
msgstr ""
|
||||
|
||||
#: frontend/src/components/CourseCard.vue:55
|
||||
#: frontend/src/components/CourseCard.vue:65
|
||||
#: frontend/src/pages/CourseDetail.vue:20
|
||||
msgid "Average Rating"
|
||||
msgstr ""
|
||||
|
||||
#: frontend/src/components/Modals/VideoStatistics.vue:65
|
||||
msgid "Average Watch Time (seconds)"
|
||||
#: frontend/src/components/Modals/VideoStatistics.vue:72
|
||||
msgid "Average Watch Time (minutes)"
|
||||
msgstr ""
|
||||
|
||||
#: frontend/src/pages/Lesson.vue:151
|
||||
@@ -715,6 +724,7 @@ msgstr ""
|
||||
|
||||
#. Name of a role
|
||||
#: lms/lms/doctype/course_evaluator/course_evaluator.json
|
||||
#: lms/lms/doctype/lms_assignment_submission/lms_assignment_submission.json
|
||||
#: lms/lms/doctype/lms_batch/lms_batch.json
|
||||
#: lms/lms/doctype/lms_category/lms_category.json
|
||||
#: lms/lms/doctype/lms_certificate_evaluation/lms_certificate_evaluation.json
|
||||
@@ -822,6 +832,11 @@ msgstr ""
|
||||
msgid "Bio"
|
||||
msgstr ""
|
||||
|
||||
#. Option for the 'Color' (Select) field in DocType 'LMS Course'
|
||||
#: lms/lms/doctype/lms_course/lms_course.json
|
||||
msgid "Blue"
|
||||
msgstr ""
|
||||
|
||||
#. Label of the body (Markdown Editor) field in DocType 'Course Lesson'
|
||||
#: lms/lms/doctype/course_lesson/course_lesson.json
|
||||
msgid "Body"
|
||||
@@ -951,8 +966,7 @@ msgstr ""
|
||||
#. Enrollment'
|
||||
#. Label of a Card Break in the LMS Workspace
|
||||
#. Label of a Link in the LMS Workspace
|
||||
#: frontend/src/components/AppSidebar.vue:589
|
||||
#: frontend/src/components/CourseCard.vue:115
|
||||
#: frontend/src/components/AppSidebar.vue:605
|
||||
#: frontend/src/components/Modals/Event.vue:381
|
||||
#: frontend/src/pages/BatchForm.vue:69 frontend/src/pages/Batches.vue:38
|
||||
#: frontend/src/pages/CourseCertification.vue:10
|
||||
@@ -980,8 +994,8 @@ msgid "Certified"
|
||||
msgstr ""
|
||||
|
||||
#. Label of the certified_members (Check) field in DocType 'LMS Settings'
|
||||
#: frontend/src/pages/CertifiedParticipants.vue:182
|
||||
#: frontend/src/pages/CertifiedParticipants.vue:189
|
||||
#: frontend/src/pages/CertifiedParticipants.vue:183
|
||||
#: frontend/src/pages/CertifiedParticipants.vue:190
|
||||
#: frontend/src/pages/Statistics.vue:40
|
||||
#: lms/lms/doctype/lms_settings/lms_settings.json
|
||||
msgid "Certified Members"
|
||||
@@ -1073,6 +1087,10 @@ msgstr ""
|
||||
msgid "Choices"
|
||||
msgstr ""
|
||||
|
||||
#: frontend/src/pages/CourseForm.vue:134
|
||||
msgid "Choose a color for the course card"
|
||||
msgstr ""
|
||||
|
||||
#: frontend/src/components/Quiz.vue:644 lms/templates/quiz/quiz.html:53
|
||||
msgid "Choose all answers that apply"
|
||||
msgstr ""
|
||||
@@ -1210,12 +1228,15 @@ msgstr ""
|
||||
msgid "College Name"
|
||||
msgstr ""
|
||||
|
||||
#. Label of the card_gradient (Select) field in DocType 'LMS Course'
|
||||
#. Label of the color (Color) field in DocType 'LMS Timetable Legend'
|
||||
#: frontend/src/pages/CourseForm.vue:133
|
||||
#: lms/lms/doctype/lms_course/lms_course.json
|
||||
#: lms/lms/doctype/lms_timetable_legend/lms_timetable_legend.json
|
||||
msgid "Color"
|
||||
msgstr ""
|
||||
|
||||
#: frontend/src/pages/BatchForm.vue:303 frontend/src/pages/CourseForm.vue:292
|
||||
#: frontend/src/pages/BatchForm.vue:303 frontend/src/pages/CourseForm.vue:297
|
||||
msgid "Comma separated keywords for SEO"
|
||||
msgstr ""
|
||||
|
||||
@@ -1324,7 +1345,7 @@ msgid "Completed"
|
||||
msgstr ""
|
||||
|
||||
#. Label of the enable_certification (Check) field in DocType 'LMS Course'
|
||||
#: frontend/src/pages/CourseForm.vue:241
|
||||
#: frontend/src/pages/CourseForm.vue:246
|
||||
#: lms/lms/doctype/lms_course/lms_course.json
|
||||
msgid "Completion Certificate"
|
||||
msgstr ""
|
||||
@@ -1517,6 +1538,7 @@ msgstr ""
|
||||
#: frontend/src/pages/ProfileRoles.vue:26
|
||||
#: lms/lms/doctype/course_chapter/course_chapter.json
|
||||
#: lms/lms/doctype/course_lesson/course_lesson.json
|
||||
#: lms/lms/doctype/lms_assignment_submission/lms_assignment_submission.json
|
||||
#: lms/lms/doctype/lms_category/lms_category.json
|
||||
#: lms/lms/doctype/lms_course/lms_course.json
|
||||
#: lms/lms/doctype/lms_program/lms_program.json
|
||||
@@ -1533,7 +1555,7 @@ msgstr ""
|
||||
msgid "Course Data"
|
||||
msgstr ""
|
||||
|
||||
#: frontend/src/pages/CourseForm.vue:190
|
||||
#: frontend/src/pages/CourseForm.vue:195
|
||||
msgid "Course Description"
|
||||
msgstr ""
|
||||
|
||||
@@ -1551,7 +1573,7 @@ msgstr ""
|
||||
msgid "Course Evaluator"
|
||||
msgstr ""
|
||||
|
||||
#: frontend/src/pages/CourseForm.vue:96
|
||||
#: frontend/src/pages/CourseForm.vue:80
|
||||
msgid "Course Image"
|
||||
msgstr ""
|
||||
|
||||
@@ -1573,7 +1595,7 @@ msgstr ""
|
||||
msgid "Course Name"
|
||||
msgstr ""
|
||||
|
||||
#: frontend/src/pages/CourseDetail.vue:78 frontend/src/pages/CourseForm.vue:302
|
||||
#: frontend/src/pages/CourseDetail.vue:78 frontend/src/pages/CourseForm.vue:307
|
||||
msgid "Course Outline"
|
||||
msgstr ""
|
||||
|
||||
@@ -1610,11 +1632,11 @@ msgstr ""
|
||||
msgid "Course added to program"
|
||||
msgstr ""
|
||||
|
||||
#: frontend/src/pages/CourseForm.vue:537
|
||||
#: frontend/src/pages/CourseForm.vue:544
|
||||
msgid "Course created successfully"
|
||||
msgstr ""
|
||||
|
||||
#: frontend/src/pages/CourseForm.vue:574
|
||||
#: frontend/src/pages/CourseForm.vue:581
|
||||
msgid "Course deleted successfully"
|
||||
msgstr ""
|
||||
|
||||
@@ -1622,7 +1644,7 @@ msgstr ""
|
||||
msgid "Course moved successfully"
|
||||
msgstr ""
|
||||
|
||||
#: frontend/src/pages/CourseForm.vue:557
|
||||
#: frontend/src/pages/CourseForm.vue:564
|
||||
msgid "Course updated successfully"
|
||||
msgstr ""
|
||||
|
||||
@@ -1693,15 +1715,15 @@ msgstr ""
|
||||
msgid "Create a Quiz"
|
||||
msgstr ""
|
||||
|
||||
#: frontend/src/components/AppSidebar.vue:576
|
||||
#: frontend/src/components/AppSidebar.vue:592
|
||||
msgid "Create a batch"
|
||||
msgstr ""
|
||||
|
||||
#: frontend/src/components/AppSidebar.vue:567
|
||||
#: frontend/src/components/AppSidebar.vue:583
|
||||
msgid "Create a course"
|
||||
msgstr ""
|
||||
|
||||
#: frontend/src/components/AppSidebar.vue:577
|
||||
#: frontend/src/components/AppSidebar.vue:593
|
||||
msgid "Create a live class"
|
||||
msgstr ""
|
||||
|
||||
@@ -1713,15 +1735,15 @@ msgstr ""
|
||||
msgid "Create an Assignment"
|
||||
msgstr ""
|
||||
|
||||
#: frontend/src/components/AppSidebar.vue:501
|
||||
#: frontend/src/components/AppSidebar.vue:517
|
||||
msgid "Create your first batch"
|
||||
msgstr ""
|
||||
|
||||
#: frontend/src/components/AppSidebar.vue:432
|
||||
#: frontend/src/components/AppSidebar.vue:448
|
||||
msgid "Create your first course"
|
||||
msgstr ""
|
||||
|
||||
#: frontend/src/components/AppSidebar.vue:479
|
||||
#: frontend/src/components/AppSidebar.vue:495
|
||||
msgid "Create your first quiz"
|
||||
msgstr ""
|
||||
|
||||
@@ -1729,18 +1751,18 @@ msgstr ""
|
||||
msgid "Created"
|
||||
msgstr ""
|
||||
|
||||
#: frontend/src/components/AppSidebar.vue:573
|
||||
#: frontend/src/components/AppSidebar.vue:589
|
||||
msgid "Creating a batch"
|
||||
msgstr ""
|
||||
|
||||
#: frontend/src/components/AppSidebar.vue:564
|
||||
#: frontend/src/components/AppSidebar.vue:580
|
||||
msgid "Creating a course"
|
||||
msgstr ""
|
||||
|
||||
#. Label of the currency (Link) field in DocType 'LMS Batch'
|
||||
#. Label of the currency (Link) field in DocType 'LMS Course'
|
||||
#. Label of the currency (Link) field in DocType 'LMS Payment'
|
||||
#: frontend/src/pages/BatchForm.vue:282 frontend/src/pages/CourseForm.vue:271
|
||||
#: frontend/src/pages/BatchForm.vue:282 frontend/src/pages/CourseForm.vue:276
|
||||
#: lms/lms/doctype/lms_batch/lms_batch.json
|
||||
#: lms/lms/doctype/lms_course/lms_course.json
|
||||
#: lms/lms/doctype/lms_payment/lms_payment.json
|
||||
@@ -1752,7 +1774,7 @@ msgstr ""
|
||||
msgid "Current Lesson"
|
||||
msgstr ""
|
||||
|
||||
#: frontend/src/components/AppSidebar.vue:595
|
||||
#: frontend/src/components/AppSidebar.vue:611
|
||||
msgid "Custom Certificate Templates"
|
||||
msgstr ""
|
||||
|
||||
@@ -1777,6 +1799,11 @@ msgstr ""
|
||||
msgid "Customisations"
|
||||
msgstr ""
|
||||
|
||||
#. Option for the 'Color' (Select) field in DocType 'LMS Course'
|
||||
#: lms/lms/doctype/lms_course/lms_course.json
|
||||
msgid "Cyan"
|
||||
msgstr ""
|
||||
|
||||
#. Label of the show_dashboard (Check) field in DocType 'LMS Settings'
|
||||
#: lms/lms/doctype/lms_settings/lms_settings.json
|
||||
msgid "Dashboard"
|
||||
@@ -1847,7 +1874,7 @@ msgstr ""
|
||||
#: frontend/src/components/CourseOutline.vue:283
|
||||
#: frontend/src/components/CourseOutline.vue:349
|
||||
#: frontend/src/components/Settings/Badges.vue:171
|
||||
#: frontend/src/pages/BatchForm.vue:562 frontend/src/pages/CourseForm.vue:587
|
||||
#: frontend/src/pages/BatchForm.vue:562 frontend/src/pages/CourseForm.vue:594
|
||||
#: frontend/src/pages/ProgrammingExercises/ProgrammingExerciseForm.vue:67
|
||||
msgid "Delete"
|
||||
msgstr ""
|
||||
@@ -1856,7 +1883,7 @@ msgstr ""
|
||||
msgid "Delete Chapter"
|
||||
msgstr ""
|
||||
|
||||
#: frontend/src/pages/CourseForm.vue:581
|
||||
#: frontend/src/pages/CourseForm.vue:588
|
||||
msgid "Delete Course"
|
||||
msgstr ""
|
||||
|
||||
@@ -1868,7 +1895,7 @@ msgstr ""
|
||||
msgid "Delete this lesson?"
|
||||
msgstr ""
|
||||
|
||||
#: frontend/src/pages/CourseForm.vue:582
|
||||
#: frontend/src/pages/CourseForm.vue:589
|
||||
msgid "Deleting the course will also delete all its chapters and lessons. Are you sure you want to delete this course?"
|
||||
msgstr ""
|
||||
|
||||
@@ -1923,7 +1950,7 @@ msgstr ""
|
||||
msgid "Details"
|
||||
msgstr ""
|
||||
|
||||
#: frontend/src/pages/CourseForm.vue:181
|
||||
#: frontend/src/pages/CourseForm.vue:171
|
||||
msgid "Disable Self Enrollment"
|
||||
msgstr ""
|
||||
|
||||
@@ -2197,7 +2224,7 @@ msgstr ""
|
||||
msgid "Enrolled"
|
||||
msgstr ""
|
||||
|
||||
#: frontend/src/components/CourseCard.vue:46
|
||||
#: frontend/src/components/CourseCard.vue:56
|
||||
#: frontend/src/components/CourseCardOverlay.vue:138
|
||||
#: frontend/src/pages/CourseDetail.vue:33
|
||||
msgid "Enrolled Students"
|
||||
@@ -2212,7 +2239,7 @@ msgstr ""
|
||||
msgid "Enrollment Count"
|
||||
msgstr ""
|
||||
|
||||
#: lms/lms/utils.py:1943
|
||||
#: lms/lms/utils.py:1948
|
||||
msgid "Enrollment Failed"
|
||||
msgstr ""
|
||||
|
||||
@@ -2313,7 +2340,7 @@ msgstr ""
|
||||
#. Label of the evaluator (Link) field in DocType 'LMS Course'
|
||||
#: frontend/src/components/Modals/BatchCourseModal.vue:37
|
||||
#: frontend/src/components/Modals/BulkCertificates.vue:22
|
||||
#: frontend/src/pages/CourseForm.vue:260 frontend/src/pages/ProfileRoles.vue:32
|
||||
#: frontend/src/pages/CourseForm.vue:265 frontend/src/pages/ProfileRoles.vue:32
|
||||
#: lms/lms/doctype/batch_course/batch_course.json
|
||||
#: lms/lms/doctype/course_evaluator/course_evaluator.json
|
||||
#: lms/lms/doctype/lms_assignment_submission/lms_assignment_submission.json
|
||||
@@ -2353,7 +2380,7 @@ msgstr ""
|
||||
msgid "Evaluator does not exist."
|
||||
msgstr ""
|
||||
|
||||
#: lms/lms/doctype/lms_course/lms_course.py:67
|
||||
#: lms/lms/doctype/lms_course/lms_course.py:68
|
||||
msgid "Evaluator is required for paid certificates."
|
||||
msgstr ""
|
||||
|
||||
@@ -2479,13 +2506,13 @@ msgstr ""
|
||||
msgid "Failed to update badge assignment: "
|
||||
msgstr ""
|
||||
|
||||
#: frontend/src/utils/index.js:671
|
||||
#: frontend/src/utils/index.js:659
|
||||
msgid "Failed to update meta tags {0}"
|
||||
msgstr ""
|
||||
|
||||
#. Label of the featured (Check) field in DocType 'LMS Course'
|
||||
#: frontend/src/components/CourseCard.vue:20
|
||||
#: frontend/src/pages/CourseForm.vue:176
|
||||
#: frontend/src/components/CourseCard.vue:25
|
||||
#: frontend/src/pages/CourseForm.vue:166
|
||||
#: lms/lms/doctype/lms_course/lms_course.json
|
||||
msgid "Featured"
|
||||
msgstr ""
|
||||
@@ -2643,6 +2670,7 @@ msgstr ""
|
||||
|
||||
#: frontend/src/components/CertificationLinks.vue:34
|
||||
#: frontend/src/components/CertificationLinks.vue:50
|
||||
#: frontend/src/components/CourseCard.vue:121
|
||||
#: frontend/src/pages/CertifiedParticipants.vue:11
|
||||
msgid "Get Certified"
|
||||
msgstr ""
|
||||
@@ -2692,6 +2720,16 @@ msgstr ""
|
||||
msgid "Grant only once"
|
||||
msgstr ""
|
||||
|
||||
#. Option for the 'Color' (Select) field in DocType 'LMS Course'
|
||||
#: lms/lms/doctype/lms_course/lms_course.json
|
||||
msgid "Gray"
|
||||
msgstr ""
|
||||
|
||||
#. Option for the 'Color' (Select) field in DocType 'LMS Course'
|
||||
#: lms/lms/doctype/lms_course/lms_course.json
|
||||
msgid "Green"
|
||||
msgstr ""
|
||||
|
||||
#: lms/templates/signup-form.html:56
|
||||
msgid "Have an account? Login"
|
||||
msgstr ""
|
||||
@@ -2967,8 +3005,8 @@ msgstr ""
|
||||
msgid "Interest"
|
||||
msgstr ""
|
||||
|
||||
#: frontend/src/components/AppSidebar.vue:556
|
||||
#: frontend/src/components/AppSidebar.vue:559
|
||||
#: frontend/src/components/AppSidebar.vue:572
|
||||
#: frontend/src/components/AppSidebar.vue:575
|
||||
msgid "Introduction"
|
||||
msgstr ""
|
||||
|
||||
@@ -3006,7 +3044,7 @@ msgstr ""
|
||||
msgid "Invite Request"
|
||||
msgstr ""
|
||||
|
||||
#: frontend/src/components/AppSidebar.vue:490
|
||||
#: frontend/src/components/AppSidebar.vue:506
|
||||
msgid "Invite your team and students"
|
||||
msgstr ""
|
||||
|
||||
@@ -3038,7 +3076,7 @@ msgstr ""
|
||||
msgid "Issue Date"
|
||||
msgstr ""
|
||||
|
||||
#: frontend/src/components/AppSidebar.vue:592
|
||||
#: frontend/src/components/AppSidebar.vue:608
|
||||
msgid "Issue a Certificate"
|
||||
msgstr ""
|
||||
|
||||
@@ -3510,7 +3548,7 @@ msgstr ""
|
||||
#. Label of the lessons (Table) field in DocType 'Course Chapter'
|
||||
#. Group in Course Chapter's connections
|
||||
#. Label of the lessons (Int) field in DocType 'LMS Course'
|
||||
#: frontend/src/components/CourseCard.vue:37
|
||||
#: frontend/src/components/CourseCard.vue:47
|
||||
#: frontend/src/components/CourseCardOverlay.vue:131
|
||||
#: lms/lms/doctype/course_chapter/course_chapter.json
|
||||
#: lms/lms/doctype/lms_course/lms_course.json
|
||||
@@ -3747,7 +3785,7 @@ msgstr ""
|
||||
#. Label of the member (Link) field in DocType 'LMS Zoom Settings'
|
||||
#: frontend/src/components/Modals/CourseProgressSummary.vue:216
|
||||
#: frontend/src/components/Modals/LiveClassAttendance.vue:14
|
||||
#: frontend/src/components/Modals/VideoStatistics.vue:22
|
||||
#: frontend/src/components/Modals/VideoStatistics.vue:29
|
||||
#: frontend/src/components/Modals/ZoomAccountModal.vue:42
|
||||
#: frontend/src/components/Settings/BadgeAssignmentForm.vue:26
|
||||
#: frontend/src/components/Settings/BadgeAssignments.vue:179
|
||||
@@ -3938,7 +3976,7 @@ msgid "Mentor Request Status Update Template"
|
||||
msgstr ""
|
||||
|
||||
#. Label of the meta_description (Small Text) field in DocType 'LMS Settings'
|
||||
#: frontend/src/pages/BatchForm.vue:294 frontend/src/pages/CourseForm.vue:283
|
||||
#: frontend/src/pages/BatchForm.vue:294 frontend/src/pages/CourseForm.vue:288
|
||||
#: lms/lms/doctype/lms_settings/lms_settings.json
|
||||
msgid "Meta Description"
|
||||
msgstr ""
|
||||
@@ -3952,12 +3990,12 @@ msgid "Meta Image"
|
||||
msgstr ""
|
||||
|
||||
#. Label of the meta_keywords (Small Text) field in DocType 'LMS Settings'
|
||||
#: frontend/src/pages/BatchForm.vue:300 frontend/src/pages/CourseForm.vue:289
|
||||
#: frontend/src/pages/BatchForm.vue:300 frontend/src/pages/CourseForm.vue:294
|
||||
#: lms/lms/doctype/lms_settings/lms_settings.json
|
||||
msgid "Meta Keywords"
|
||||
msgstr ""
|
||||
|
||||
#: frontend/src/pages/BatchForm.vue:289 frontend/src/pages/CourseForm.vue:278
|
||||
#: frontend/src/pages/BatchForm.vue:289 frontend/src/pages/CourseForm.vue:283
|
||||
msgid "Meta Tags"
|
||||
msgstr ""
|
||||
|
||||
@@ -3980,6 +4018,7 @@ msgstr ""
|
||||
#: lms/lms/doctype/course_evaluator/course_evaluator.json
|
||||
#: lms/lms/doctype/course_lesson/course_lesson.json
|
||||
#: lms/lms/doctype/lms_assignment/lms_assignment.json
|
||||
#: lms/lms/doctype/lms_assignment_submission/lms_assignment_submission.json
|
||||
#: lms/lms/doctype/lms_badge_assignment/lms_badge_assignment.json
|
||||
#: lms/lms/doctype/lms_batch/lms_batch.json
|
||||
#: lms/lms/doctype/lms_batch_enrollment/lms_batch_enrollment.json
|
||||
@@ -4026,7 +4065,7 @@ msgstr ""
|
||||
msgid "Monday"
|
||||
msgstr ""
|
||||
|
||||
#: frontend/src/components/AppSidebar.vue:600
|
||||
#: frontend/src/components/AppSidebar.vue:616
|
||||
msgid "Monetization"
|
||||
msgstr ""
|
||||
|
||||
@@ -4068,7 +4107,7 @@ msgstr ""
|
||||
msgid "New Batch"
|
||||
msgstr ""
|
||||
|
||||
#: frontend/src/pages/CourseForm.vue:668 lms/www/lms.py:95
|
||||
#: frontend/src/pages/CourseForm.vue:675 lms/www/lms.py:95
|
||||
msgid "New Course"
|
||||
msgstr ""
|
||||
|
||||
@@ -4112,11 +4151,11 @@ msgstr ""
|
||||
msgid "New Zoom Account"
|
||||
msgstr ""
|
||||
|
||||
#: lms/lms/utils.py:609
|
||||
#: lms/lms/utils.py:612
|
||||
msgid "New comment in batch {0}"
|
||||
msgstr ""
|
||||
|
||||
#: lms/lms/utils.py:602
|
||||
#: lms/lms/utils.py:605
|
||||
msgid "New reply on the topic {0} in course {1}"
|
||||
msgstr ""
|
||||
|
||||
@@ -4214,7 +4253,11 @@ msgstr ""
|
||||
msgid "No slots available for this date."
|
||||
msgstr ""
|
||||
|
||||
#: frontend/src/components/Modals/AnnouncementModal.vue:90
|
||||
#: frontend/src/components/Modals/VideoStatistics.vue:84
|
||||
msgid "No statistics available for this video."
|
||||
msgstr ""
|
||||
|
||||
#: frontend/src/components/Modals/AnnouncementModal.vue:91
|
||||
msgid "No students in this batch"
|
||||
msgstr ""
|
||||
|
||||
@@ -4323,7 +4366,7 @@ msgstr ""
|
||||
msgid "Only files of type {0} will be accepted."
|
||||
msgstr ""
|
||||
|
||||
#: frontend/src/utils/index.js:502
|
||||
#: frontend/src/utils/index.js:491
|
||||
msgid "Only image file is allowed."
|
||||
msgstr ""
|
||||
|
||||
@@ -4383,6 +4426,11 @@ msgstr ""
|
||||
msgid "Options"
|
||||
msgstr ""
|
||||
|
||||
#. Option for the 'Color' (Select) field in DocType 'LMS Course'
|
||||
#: lms/lms/doctype/lms_course/lms_course.json
|
||||
msgid "Orange"
|
||||
msgstr ""
|
||||
|
||||
#. Label of the order_id (Data) field in DocType 'LMS Payment'
|
||||
#: lms/lms/doctype/lms_payment/lms_payment.json
|
||||
msgid "Order ID"
|
||||
@@ -4438,7 +4486,7 @@ msgid "Paid Batch"
|
||||
msgstr ""
|
||||
|
||||
#. Label of the paid_certificate (Check) field in DocType 'LMS Course'
|
||||
#: frontend/src/pages/CourseForm.vue:246
|
||||
#: frontend/src/pages/CourseForm.vue:251
|
||||
#: lms/lms/doctype/lms_course/lms_course.json
|
||||
msgid "Paid Certificate"
|
||||
msgstr ""
|
||||
@@ -4448,7 +4496,7 @@ msgid "Paid Certificate after Evaluation"
|
||||
msgstr ""
|
||||
|
||||
#. Label of the paid_course (Check) field in DocType 'LMS Course'
|
||||
#: frontend/src/pages/CourseForm.vue:236
|
||||
#: frontend/src/pages/CourseForm.vue:241
|
||||
#: lms/lms/doctype/lms_course/lms_course.json
|
||||
msgid "Paid Course"
|
||||
msgstr ""
|
||||
@@ -4500,7 +4548,7 @@ msgstr ""
|
||||
msgid "Password"
|
||||
msgstr ""
|
||||
|
||||
#: frontend/src/pages/CourseForm.vue:206
|
||||
#: frontend/src/pages/CourseForm.vue:211
|
||||
msgid "Paste the youtube link of a short video introducing the course"
|
||||
msgstr ""
|
||||
|
||||
@@ -4614,6 +4662,11 @@ msgstr ""
|
||||
msgid "Phone Number"
|
||||
msgstr ""
|
||||
|
||||
#. Option for the 'Color' (Select) field in DocType 'LMS Course'
|
||||
#: lms/lms/doctype/lms_course/lms_course.json
|
||||
msgid "Pink"
|
||||
msgstr ""
|
||||
|
||||
#: lms/lms/doctype/lms_settings/lms_settings.py:34
|
||||
msgid "Please add <a href='{0}'>{1}</a> for <a href='{2}'>{3}</a> to send calendar invites for evaluations."
|
||||
msgstr ""
|
||||
@@ -4634,7 +4687,7 @@ msgstr ""
|
||||
msgid "Please click on the following button to set your new password"
|
||||
msgstr ""
|
||||
|
||||
#: lms/lms/utils.py:2077 lms/lms/utils.py:2081
|
||||
#: lms/lms/utils.py:2082 lms/lms/utils.py:2086
|
||||
msgid "Please complete the previous courses in the program to enroll in this course."
|
||||
msgstr ""
|
||||
|
||||
@@ -4679,7 +4732,7 @@ msgstr ""
|
||||
msgid "Please install the Payments App to create a paid batch. Refer to the documentation for more details. {0}"
|
||||
msgstr ""
|
||||
|
||||
#: lms/lms/doctype/lms_course/lms_course.py:55
|
||||
#: lms/lms/doctype/lms_course/lms_course.py:56
|
||||
msgid "Please install the Payments App to create a paid course. Refer to the documentation for more details. {0}"
|
||||
msgstr ""
|
||||
|
||||
@@ -4834,7 +4887,7 @@ msgstr ""
|
||||
msgid "Preview Image"
|
||||
msgstr ""
|
||||
|
||||
#: frontend/src/pages/CourseForm.vue:204
|
||||
#: frontend/src/pages/CourseForm.vue:209
|
||||
msgid "Preview Video"
|
||||
msgstr ""
|
||||
|
||||
@@ -4849,7 +4902,7 @@ msgid "Pricing"
|
||||
msgstr ""
|
||||
|
||||
#. Label of the pricing_tab (Tab Break) field in DocType 'LMS Course'
|
||||
#: frontend/src/pages/CourseForm.vue:230
|
||||
#: frontend/src/pages/CourseForm.vue:235
|
||||
#: lms/lms/doctype/lms_course/lms_course.json
|
||||
msgid "Pricing and Certification"
|
||||
msgstr ""
|
||||
@@ -5002,7 +5055,7 @@ msgstr ""
|
||||
#. Label of the published (Check) field in DocType 'LMS Course'
|
||||
#: frontend/src/components/Modals/BulkCertificates.vue:51
|
||||
#: frontend/src/components/Modals/Event.vue:108
|
||||
#: frontend/src/pages/BatchForm.vue:59 frontend/src/pages/CourseForm.vue:159
|
||||
#: frontend/src/pages/BatchForm.vue:59 frontend/src/pages/CourseForm.vue:149
|
||||
#: lms/lms/doctype/lms_batch/lms_batch.json
|
||||
#: lms/lms/doctype/lms_course/lms_course.json
|
||||
msgid "Published"
|
||||
@@ -5015,7 +5068,7 @@ msgid "Published Courses"
|
||||
msgstr ""
|
||||
|
||||
#. Label of the published_on (Date) field in DocType 'LMS Course'
|
||||
#: frontend/src/pages/CourseForm.vue:163
|
||||
#: frontend/src/pages/CourseForm.vue:153
|
||||
#: lms/lms/doctype/lms_course/lms_course.json
|
||||
msgid "Published On"
|
||||
msgstr ""
|
||||
@@ -5025,6 +5078,11 @@ msgstr ""
|
||||
msgid "Purchased Certificate"
|
||||
msgstr ""
|
||||
|
||||
#. Option for the 'Color' (Select) field in DocType 'LMS Course'
|
||||
#: lms/lms/doctype/lms_course/lms_course.json
|
||||
msgid "Purple"
|
||||
msgstr ""
|
||||
|
||||
#. Option for the 'Language' (Select) field in DocType 'LMS Programming
|
||||
#. Exercise'
|
||||
#: lms/lms/doctype/lms_programming_exercise/lms_programming_exercise.json
|
||||
@@ -5146,7 +5204,7 @@ msgstr ""
|
||||
msgid "Quiz will appear at the bottom of the lesson."
|
||||
msgstr ""
|
||||
|
||||
#: frontend/src/components/AppSidebar.vue:584
|
||||
#: frontend/src/components/AppSidebar.vue:600
|
||||
#: frontend/src/pages/QuizForm.vue:396 frontend/src/pages/Quizzes.vue:275
|
||||
#: frontend/src/pages/Quizzes.vue:285 lms/www/lms.py:251
|
||||
msgid "Quizzes"
|
||||
@@ -5182,6 +5240,11 @@ msgstr ""
|
||||
msgid "Ready"
|
||||
msgstr ""
|
||||
|
||||
#. Option for the 'Color' (Select) field in DocType 'LMS Course'
|
||||
#: lms/lms/doctype/lms_course/lms_course.json
|
||||
msgid "Red"
|
||||
msgstr ""
|
||||
|
||||
#. Label of the reference_docname (Dynamic Link) field in DocType 'LMS Batch
|
||||
#. Timetable'
|
||||
#: lms/lms/doctype/lms_batch_timetable/lms_batch_timetable.json
|
||||
@@ -5230,14 +5293,14 @@ msgstr ""
|
||||
#. Label of the related_courses (Table) field in DocType 'LMS Course'
|
||||
#. Name of a DocType
|
||||
#: frontend/src/components/RelatedCourses.vue:5
|
||||
#: frontend/src/pages/CourseForm.vue:215
|
||||
#: frontend/src/pages/CourseForm.vue:220
|
||||
#: lms/lms/doctype/lms_course/lms_course.json
|
||||
#: lms/lms/doctype/related_courses/related_courses.json
|
||||
msgid "Related Courses"
|
||||
msgstr ""
|
||||
|
||||
#: frontend/src/components/Controls/Uploader.vue:34
|
||||
#: frontend/src/pages/BatchForm.vue:246 frontend/src/pages/CourseForm.vue:136
|
||||
#: frontend/src/pages/BatchForm.vue:246 frontend/src/pages/CourseForm.vue:119
|
||||
msgid "Remove"
|
||||
msgstr ""
|
||||
|
||||
@@ -5245,6 +5308,10 @@ msgstr ""
|
||||
msgid "Reply To"
|
||||
msgstr ""
|
||||
|
||||
#: frontend/src/components/Modals/AnnouncementModal.vue:100
|
||||
msgid "Reply To is required"
|
||||
msgstr ""
|
||||
|
||||
#: lms/lms/widgets/RequestInvite.html:7
|
||||
msgid "Request Invite"
|
||||
msgstr ""
|
||||
@@ -5314,7 +5381,7 @@ msgstr ""
|
||||
msgid "Role updated successfully"
|
||||
msgstr ""
|
||||
|
||||
#: frontend/src/components/AppSidebar.vue:612
|
||||
#: frontend/src/components/AppSidebar.vue:628
|
||||
msgid "Roles"
|
||||
msgstr ""
|
||||
|
||||
@@ -5438,7 +5505,8 @@ msgid "Search"
|
||||
msgstr ""
|
||||
|
||||
#: frontend/src/components/Modals/CourseProgressSummary.vue:18
|
||||
msgid "Search by Member Name"
|
||||
#: frontend/src/components/Modals/VideoStatistics.vue:20
|
||||
msgid "Search by Member"
|
||||
msgstr ""
|
||||
|
||||
#: frontend/src/pages/CertifiedParticipants.vue:23
|
||||
@@ -5517,21 +5585,25 @@ msgstr ""
|
||||
msgid "Sessions On Days"
|
||||
msgstr ""
|
||||
|
||||
#: frontend/src/components/Controls/ColorSwatches.vue:13
|
||||
msgid "Set Color"
|
||||
msgstr ""
|
||||
|
||||
#: lms/templates/emails/community_course_membership.html:1
|
||||
msgid "Set your Password"
|
||||
msgstr ""
|
||||
|
||||
#: frontend/src/components/AppSidebar.vue:560
|
||||
#: frontend/src/components/AppSidebar.vue:576
|
||||
msgid "Setting up"
|
||||
msgstr ""
|
||||
|
||||
#: frontend/src/components/AppSidebar.vue:605
|
||||
#: frontend/src/components/AppSidebar.vue:621
|
||||
msgid "Setting up payment gateway"
|
||||
msgstr ""
|
||||
|
||||
#: frontend/src/components/AppSidebar.vue:610
|
||||
#: frontend/src/components/AppSidebar.vue:626
|
||||
#: frontend/src/components/Settings/Settings.vue:7
|
||||
#: frontend/src/pages/BatchForm.vue:53 frontend/src/pages/CourseForm.vue:152
|
||||
#: frontend/src/pages/BatchForm.vue:53 frontend/src/pages/CourseForm.vue:142
|
||||
#: frontend/src/pages/ProfileRoles.vue:4
|
||||
#: frontend/src/pages/ProgrammingExercises/ProgrammingExerciseSubmission.vue:19
|
||||
#: frontend/src/pages/QuizForm.vue:86
|
||||
@@ -5547,7 +5619,7 @@ msgid "Short Description"
|
||||
msgstr ""
|
||||
|
||||
#. Label of the short_introduction (Small Text) field in DocType 'LMS Course'
|
||||
#: frontend/src/pages/CourseForm.vue:86
|
||||
#: frontend/src/pages/CourseForm.vue:185
|
||||
#: lms/lms/doctype/lms_course/lms_course.json
|
||||
msgid "Short Introduction"
|
||||
msgstr ""
|
||||
@@ -5874,7 +5946,7 @@ msgstr ""
|
||||
msgid "Subject"
|
||||
msgstr ""
|
||||
|
||||
#: frontend/src/components/Modals/AnnouncementModal.vue:93
|
||||
#: frontend/src/components/Modals/AnnouncementModal.vue:94
|
||||
msgid "Subject is required"
|
||||
msgstr ""
|
||||
|
||||
@@ -5944,6 +6016,10 @@ msgstr ""
|
||||
msgid "Suspicious pattern found in {0}: {1}"
|
||||
msgstr ""
|
||||
|
||||
#: frontend/src/components/Controls/ColorSwatches.vue:50
|
||||
msgid "Swatches"
|
||||
msgstr ""
|
||||
|
||||
#. Name of a role
|
||||
#: lms/job/doctype/job_opportunity/job_opportunity.json
|
||||
#: lms/job/doctype/job_settings/job_settings.json
|
||||
@@ -6006,6 +6082,11 @@ msgstr ""
|
||||
msgid "Tags"
|
||||
msgstr ""
|
||||
|
||||
#. Option for the 'Color' (Select) field in DocType 'LMS Course'
|
||||
#: lms/lms/doctype/lms_course/lms_course.json
|
||||
msgid "Teal"
|
||||
msgstr ""
|
||||
|
||||
#: lms/templates/emails/community_course_membership.html:18
|
||||
#: lms/templates/emails/mentor_request_creation_email.html:8
|
||||
#: lms/templates/emails/mentor_request_status_update_email.html:7
|
||||
@@ -6088,7 +6169,7 @@ msgstr ""
|
||||
msgid "Thanks and Regards"
|
||||
msgstr ""
|
||||
|
||||
#: lms/lms/utils.py:1975
|
||||
#: lms/lms/utils.py:1980
|
||||
msgid "The batch is full. Please contact the Administrator."
|
||||
msgstr ""
|
||||
|
||||
@@ -6175,7 +6256,7 @@ msgstr ""
|
||||
msgid "This course has:"
|
||||
msgstr ""
|
||||
|
||||
#: lms/lms/utils.py:1818
|
||||
#: lms/lms/utils.py:1823
|
||||
msgid "This course is free."
|
||||
msgstr ""
|
||||
|
||||
@@ -6358,7 +6439,7 @@ msgstr ""
|
||||
msgid "To Date"
|
||||
msgstr ""
|
||||
|
||||
#: lms/lms/utils.py:1829
|
||||
#: lms/lms/utils.py:1834
|
||||
msgid "To join this batch, please contact the Administrator."
|
||||
msgstr ""
|
||||
|
||||
@@ -6495,7 +6576,7 @@ msgstr ""
|
||||
#. Option for the 'Status' (Select) field in DocType 'Cohort'
|
||||
#. Option for the 'Status' (Select) field in DocType 'LMS Certificate Request'
|
||||
#. Label of the upcoming (Check) field in DocType 'LMS Course'
|
||||
#: frontend/src/pages/Batches.vue:282 frontend/src/pages/CourseForm.vue:171
|
||||
#: frontend/src/pages/Batches.vue:282 frontend/src/pages/CourseForm.vue:161
|
||||
#: frontend/src/pages/Courses.vue:313 lms/lms/doctype/cohort/cohort.json
|
||||
#: lms/lms/doctype/lms_certificate_request/lms_certificate_request.json
|
||||
#: lms/lms/doctype/lms_course/lms_course.json
|
||||
@@ -6522,7 +6603,7 @@ msgid "Update Password"
|
||||
msgstr ""
|
||||
|
||||
#: frontend/src/components/Controls/Uploader.vue:20
|
||||
#: frontend/src/pages/BatchForm.vue:227 frontend/src/pages/CourseForm.vue:117
|
||||
#: frontend/src/pages/BatchForm.vue:227 frontend/src/pages/CourseForm.vue:100
|
||||
msgid "Upload"
|
||||
msgstr ""
|
||||
|
||||
@@ -6636,6 +6717,11 @@ msgstr ""
|
||||
msgid "View all feedback"
|
||||
msgstr ""
|
||||
|
||||
#. Option for the 'Color' (Select) field in DocType 'LMS Course'
|
||||
#: lms/lms/doctype/lms_course/lms_course.json
|
||||
msgid "Violet"
|
||||
msgstr ""
|
||||
|
||||
#. Label of the visibility (Select) field in DocType 'LMS Batch Old'
|
||||
#: lms/lms/doctype/lms_batch_old/lms_batch_old.json
|
||||
msgid "Visibility"
|
||||
@@ -6664,7 +6750,7 @@ msgid "Volunteering or Internship"
|
||||
msgstr ""
|
||||
|
||||
#. Label of the watch_time (Data) field in DocType 'LMS Video Watch Duration'
|
||||
#: frontend/src/components/Modals/VideoStatistics.vue:25
|
||||
#: frontend/src/components/Modals/VideoStatistics.vue:32
|
||||
#: lms/lms/doctype/lms_video_watch_duration/lms_video_watch_duration.json
|
||||
msgid "Watch Time"
|
||||
msgstr ""
|
||||
@@ -6764,6 +6850,11 @@ msgstr ""
|
||||
msgid "Write your answer here"
|
||||
msgstr ""
|
||||
|
||||
#. Option for the 'Color' (Select) field in DocType 'LMS Course'
|
||||
#: lms/lms/doctype/lms_course/lms_course.json
|
||||
msgid "Yellow"
|
||||
msgstr ""
|
||||
|
||||
#: lms/lms/doctype/lms_certificate_request/lms_certificate_request.py:95
|
||||
msgid "You already have an evaluation on {0} at {1} for the course {2}."
|
||||
msgstr ""
|
||||
@@ -7000,6 +7091,11 @@ msgstr ""
|
||||
msgid "activity"
|
||||
msgstr ""
|
||||
|
||||
#: frontend/src/components/CourseInstructors.vue:22
|
||||
#: frontend/src/components/CourseInstructors.vue:41
|
||||
msgid "and"
|
||||
msgstr ""
|
||||
|
||||
#: frontend/src/components/JobCard.vue:26 frontend/src/pages/JobDetail.vue:108
|
||||
msgid "applicant"
|
||||
msgstr ""
|
||||
@@ -7032,7 +7128,8 @@ msgstr ""
|
||||
msgid "certified members"
|
||||
msgstr ""
|
||||
|
||||
#: frontend/src/pages/Lesson.vue:98 frontend/src/pages/Lesson.vue:234
|
||||
#: frontend/src/components/CourseCard.vue:98 frontend/src/pages/Lesson.vue:98
|
||||
#: frontend/src/pages/Lesson.vue:234
|
||||
msgid "completed"
|
||||
msgstr ""
|
||||
|
||||
@@ -7068,6 +7165,10 @@ msgstr ""
|
||||
msgid "of"
|
||||
msgstr ""
|
||||
|
||||
#: frontend/src/components/CourseInstructors.vue:41
|
||||
msgid "others"
|
||||
msgstr ""
|
||||
|
||||
#: lms/templates/quiz/quiz.js:141
|
||||
msgid "out of"
|
||||
msgstr ""
|
||||
@@ -7164,7 +7265,7 @@ msgstr ""
|
||||
msgid "{0} is your evaluator"
|
||||
msgstr ""
|
||||
|
||||
#: lms/lms/utils.py:686
|
||||
#: lms/lms/utils.py:689
|
||||
msgid "{0} mentioned you in a comment"
|
||||
msgstr ""
|
||||
|
||||
@@ -7172,7 +7273,7 @@ msgstr ""
|
||||
msgid "{0} mentioned you in a comment in your batch."
|
||||
msgstr ""
|
||||
|
||||
#: lms/lms/utils.py:639 lms/lms/utils.py:645
|
||||
#: lms/lms/utils.py:642 lms/lms/utils.py:648
|
||||
msgid "{0} mentioned you in a comment in {1}"
|
||||
msgstr ""
|
||||
|
||||
|
||||
609
lms/locale/nl.po
609
lms/locale/nl.po
File diff suppressed because it is too large
Load Diff
609
lms/locale/pl.po
609
lms/locale/pl.po
File diff suppressed because it is too large
Load Diff
611
lms/locale/pt.po
611
lms/locale/pt.po
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
609
lms/locale/ru.po
609
lms/locale/ru.po
File diff suppressed because it is too large
Load Diff
617
lms/locale/sr.po
617
lms/locale/sr.po
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
615
lms/locale/sv.po
615
lms/locale/sv.po
File diff suppressed because it is too large
Load Diff
619
lms/locale/th.po
619
lms/locale/th.po
File diff suppressed because it is too large
Load Diff
611
lms/locale/tr.po
611
lms/locale/tr.po
File diff suppressed because it is too large
Load Diff
609
lms/locale/vi.po
609
lms/locale/vi.po
File diff suppressed because it is too large
Load Diff
613
lms/locale/zh.po
613
lms/locale/zh.po
File diff suppressed because it is too large
Load Diff
@@ -25,7 +25,7 @@
|
||||
},
|
||||
"homepage": "https://github.com/frappe/lms#readme",
|
||||
"devDependencies": {
|
||||
"cypress": "^13.9.0",
|
||||
"cypress": "^14.5.2",
|
||||
"cypress-file-upload": "^5.0.8",
|
||||
"cypress-real-events": "^1.14.0"
|
||||
},
|
||||
|
||||
@@ -13,7 +13,7 @@ dependencies = [
|
||||
"markdown~=3.5.1",
|
||||
"beautifulsoup4~=4.12.2",
|
||||
"lxml~=4.9.3",
|
||||
"cairocffi~=1.6.1",
|
||||
"cairocffi==1.5.1",
|
||||
"razorpay~=1.4.1",
|
||||
"fuzzywuzzy~=0.18.0",
|
||||
]
|
||||
|
||||
Reference in New Issue
Block a user