Compare commits

...

156 Commits

Author SHA1 Message Date
Jannat Patel 57c1a6b540 Merge pull request #1676 from pateljannat/issues-127
fix: misc issues
2025-08-08 12:08:04 +05:30
Jannat Patel 8dba0e8242 Merge pull request #1677 from frappe/l10n_develop2
chore: sync translations from crowdin
2025-08-08 12:06:59 +05:30
Jannat Patel ee715f6387 chore: fixed linters 2025-08-08 10:47:21 +05:30
Jannat Patel b770b30334 chore: Italian translations 2025-08-08 06:54:22 +05:30
Jannat Patel d61abac126 fix: validate is uploaded svg is malicious 2025-08-07 17:33:32 +05:30
Jannat Patel ccf28b8012 refactor: bring course title down from the gradient in course cards 2025-08-07 17:23:14 +05:30
Jannat Patel 3762cb06bb Merge pull request #1671 from pateljannat/notes
feat: notes and highlights in lesson
2025-08-07 17:11:46 +05:30
Jannat Patel 15400f2a3e test: open community tab before testing discussions 2025-08-07 17:01:55 +05:30
Jannat Patel 20d1b1fe83 Merge pull request #1675 from frappe/l10n_develop2
chore: sync translations from crowdin
2025-08-07 16:05:03 +05:30
Jannat Patel 73844f8813 fix: minor changes with visibility and change 2025-08-07 16:04:36 +05:30
Jannat Patel 2187553625 chore: Serbian (Latin) translations 2025-08-07 06:36:14 +05:30
Jannat Patel 984b2a5dea chore: Serbian (Cyrillic) translations 2025-08-07 06:36:13 +05:30
Jannat Patel 9098d9454f Merge pull request #1674 from pateljannat/issues-126
fix: video statistics scroll and time format
2025-08-06 11:28:15 +05:30
Jannat Patel 027dd93fb5 fix: reload notes when moving to another lesson 2025-08-06 11:13:16 +05:30
Jannat Patel a005adc89a Merge pull request #1672 from frappe/l10n_develop2
chore: sync translations from crowdin
2025-08-06 10:32:09 +05:30
Jannat Patel 866ef04fbf chore: Esperanto translations 2025-08-06 05:34:46 +05:30
Jannat Patel 00b6f97e3a chore: Serbian (Latin) translations 2025-08-06 05:34:45 +05:30
Jannat Patel a1d21b1a2a chore: Bosnian translations 2025-08-06 05:34:43 +05:30
Jannat Patel 7358ea43d8 chore: Croatian translations 2025-08-06 05:34:42 +05:30
Jannat Patel 88c69311eb chore: Thai translations 2025-08-06 05:34:40 +05:30
Jannat Patel c1e45e5d0d chore: Persian translations 2025-08-06 05:34:39 +05:30
Jannat Patel fe78de2417 chore: Indonesian translations 2025-08-06 05:34:37 +05:30
Jannat Patel 4c1fc201e6 chore: Portuguese, Brazilian translations 2025-08-06 05:34:36 +05:30
Jannat Patel 3f5d270915 chore: Vietnamese translations 2025-08-06 05:34:34 +05:30
Jannat Patel a452fbeb07 chore: Chinese Simplified translations 2025-08-06 05:34:33 +05:30
Jannat Patel a6f02c245f chore: Turkish translations 2025-08-06 05:34:31 +05:30
Jannat Patel cb4f9129d6 chore: Swedish translations 2025-08-06 05:34:30 +05:30
Jannat Patel 9c5d64c211 chore: Serbian (Cyrillic) translations 2025-08-06 05:34:28 +05:30
Jannat Patel 41dc0ecc60 chore: Russian translations 2025-08-06 05:34:26 +05:30
Jannat Patel 6b9409b889 chore: Portuguese translations 2025-08-06 05:34:25 +05:30
Jannat Patel ea66eeed6c chore: Polish translations 2025-08-06 05:34:24 +05:30
Jannat Patel a419d28ef1 chore: Dutch translations 2025-08-06 05:34:22 +05:30
Jannat Patel 481dfc24fd chore: Italian translations 2025-08-06 05:34:20 +05:30
Jannat Patel ed686a7d52 chore: Hungarian translations 2025-08-06 05:34:19 +05:30
Jannat Patel b4c5a07800 chore: German translations 2025-08-06 05:34:17 +05:30
Jannat Patel 6ae16f7fef chore: Czech translations 2025-08-06 05:34:16 +05:30
Jannat Patel 4aae2ed3b8 chore: Arabic translations 2025-08-06 05:34:14 +05:30
Jannat Patel 81d4137b20 chore: Spanish translations 2025-08-06 05:34:13 +05:30
Jannat Patel 77ecb02a17 feat: notes in lesson 2025-08-05 20:00:09 +05:30
Jannat Patel 4a375f92ed Merge pull request #1668 from frappe/pot_develop_2025-08-01
chore: update POT file
2025-08-04 20:05:22 +05:30
Jannat Patel 7caf91460a Merge pull request #1669 from frappe/l10n_develop2
chore: sync translations from crowdin
2025-08-04 20:05:12 +05:30
Jannat Patel 0e015c8b97 chore: Indonesian translations 2025-08-03 04:35:30 +05:30
frappe-pr-bot 7b69ddb14d chore: update POT file 2025-08-01 16:04:44 +00:00
Jannat Patel 2271eb270e Merge pull request #1667 from harshpwctech/develop
refactor: Announcement mail being sent to students in BCC
2025-08-01 17:32:41 +05:30
CA Harsh Agrawal 7e5b2e4e79 refactor: Announcement mail being sent to students in BCC 2025-08-01 17:02:48 +05:30
Jannat Patel 124b9d9ea5 fix: video statistics scroll 2025-07-30 17:31:33 +05:30
Jannat Patel 36076068ec fix: text padding on card gradient 2025-07-30 12:30:12 +05:30
Frappe PR Bot c868354b5b chore(release): Bumped to Version 2.33.0 2025-07-30 06:14:36 +00:00
Jannat Patel db91f0b2a0 Merge pull request #1663 from pateljannat/issues-125
fix: show video statistics watch time in minutes
2025-07-30 11:43:01 +05:30
Jannat Patel d7e83bb78e fix: show video statistics watch time in minutes 2025-07-30 11:30:17 +05:30
Jannat Patel feb2a39e05 Merge pull request #1661 from frappe/l10n_develop2
chore: sync translations from crowdin
2025-07-30 10:53:15 +05:30
Jannat Patel a6cf910d05 chore: Esperanto translations 2025-07-30 04:23:56 +05:30
Jannat Patel b891b44ac6 chore: Spanish translations 2025-07-30 04:23:54 +05:30
Jannat Patel 026a3ebb81 chore: Serbian (Latin) translations 2025-07-30 04:23:53 +05:30
Jannat Patel 71ba246011 chore: Bosnian translations 2025-07-30 04:23:51 +05:30
Jannat Patel a391204fa6 chore: Croatian translations 2025-07-30 04:23:49 +05:30
Jannat Patel 9c773399a8 chore: Thai translations 2025-07-30 04:23:48 +05:30
Jannat Patel 528b85352a chore: Persian translations 2025-07-30 04:23:47 +05:30
Jannat Patel 249c369c14 chore: Indonesian translations 2025-07-30 04:23:45 +05:30
Jannat Patel 9803fc1031 chore: Portuguese, Brazilian translations 2025-07-30 04:23:44 +05:30
Jannat Patel 299fde1c98 chore: Vietnamese translations 2025-07-30 04:23:43 +05:30
Jannat Patel 7f55734fbb chore: Chinese Simplified translations 2025-07-30 04:23:41 +05:30
Jannat Patel efe230865a chore: Turkish translations 2025-07-30 04:23:40 +05:30
Jannat Patel 6e52e684c8 chore: Swedish translations 2025-07-30 04:23:38 +05:30
Jannat Patel 99d880297a chore: Serbian (Cyrillic) translations 2025-07-30 04:23:36 +05:30
Jannat Patel dec706ae72 chore: Russian translations 2025-07-30 04:23:35 +05:30
Jannat Patel 2e60f0a0c2 chore: Portuguese translations 2025-07-30 04:23:34 +05:30
Jannat Patel ef612f86e5 chore: Polish translations 2025-07-30 04:23:32 +05:30
Jannat Patel 9c16e03ea7 chore: Dutch translations 2025-07-30 04:23:31 +05:30
Jannat Patel 7780c0310e chore: Italian translations 2025-07-30 04:23:29 +05:30
Jannat Patel b0a23c0d1a chore: Hungarian translations 2025-07-30 04:23:27 +05:30
Jannat Patel 05c85cea08 chore: German translations 2025-07-30 04:23:26 +05:30
Jannat Patel 1ffae0a1de chore: Czech translations 2025-07-30 04:23:24 +05:30
Jannat Patel 15cbccd15f chore: Arabic translations 2025-07-30 04:23:23 +05:30
Jannat Patel 266b2f2ac8 chore: French translations 2025-07-30 04:23:21 +05:30
Jannat Patel 26f9fb4199 Merge pull request #1658 from frappe/pot_develop_2025-07-25
chore: update POT file
2025-07-29 12:05:37 +05:30
frappe-pr-bot 67887fb6ef chore: update POT file 2025-07-25 16:04:39 +00:00
Jannat Patel 3d102e39ff Merge pull request #1657 from pateljannat/course-card-gradient
feat: course card gradient
2025-07-25 18:56:50 +05:30
Jannat Patel ddd9089130 fix: color swatch input style 2025-07-25 18:31:46 +05:30
Jannat Patel d8ce88ab57 fix: color swatch input style 2025-07-25 18:30:58 +05:30
Jannat Patel 01794a47c6 feat: set a random color is no color or image is present 2025-07-25 17:46:50 +05:30
Jannat Patel 17626dbbdb feat: course card gradient 2025-07-25 17:29:48 +05:30
Jannat Patel e5bd86658d Merge pull request #1655 from frappe/l10n_develop2
chore: sync translations from crowdin
2025-07-24 10:41:13 +05:30
Jannat Patel e911dc1353 chore: Thai translations 2025-07-24 02:45:56 +05:30
Jannat Patel 27e3e5aa6a chore: Indonesian translations 2025-07-24 02:45:53 +05:30
Jannat Patel 5b65525bf1 chore: Portuguese translations 2025-07-24 02:45:46 +05:30
Jannat Patel 277804f8b1 chore: Hungarian translations 2025-07-24 02:45:42 +05:30
Jannat Patel 4c77802e3c Merge pull request #1653 from pateljannat/issues-124
fix: progress timer in lessons
2025-07-23 11:32:51 +05:30
Jannat Patel aacfea6ea5 fix: progress timer in lessons 2025-07-23 11:31:41 +05:30
Frappe PR Bot 6d55040e43 chore(release): Bumped to Version 2.32.2 2025-07-23 05:31:05 +00:00
Jannat Patel 290f785a47 Merge pull request #1651 from pateljannat/issues-123
fix: vimeo video embed with plyr
2025-07-23 11:00:03 +05:30
Jannat Patel 39ef187f6b fix: vimeo video embed with plyr 2025-07-23 10:44:53 +05:30
Frappe PR Bot a7a475e763 chore(release): Bumped to Version 2.32.1 2025-07-22 13:31:38 +00:00
Jannat Patel 6eb380ea38 Merge pull request #1648 from pateljannat/issues-122
fix: play embed videos on Lesson Form
2025-07-22 14:20:42 +05:30
Jannat Patel 4d150cb323 fix: play embed videos on Lesson Form 2025-07-22 14:11:29 +05:30
Jannat Patel 09d6d99b14 Merge pull request #1647 from frappe/l10n_develop2
chore: sync translations from crowdin
2025-07-22 10:24:07 +05:30
Jannat Patel 5e7fd8baff chore: Esperanto translations 2025-07-22 02:06:41 +05:30
Jannat Patel 52c159e2e8 chore: French translations 2025-07-22 02:06:40 +05:30
Jannat Patel 67e8feb879 chore: Serbian (Latin) translations 2025-07-22 02:06:38 +05:30
Jannat Patel a5b61d5244 chore: Bosnian translations 2025-07-22 02:06:37 +05:30
Jannat Patel decc3a16ed chore: Croatian translations 2025-07-22 02:06:35 +05:30
Jannat Patel 7f39e9f0cc chore: Thai translations 2025-07-22 02:06:34 +05:30
Jannat Patel 95afa1a6ad chore: Persian translations 2025-07-22 02:06:32 +05:30
Jannat Patel 0d0bb5f9e2 chore: Portuguese, Brazilian translations 2025-07-22 02:06:31 +05:30
Jannat Patel 3dd5ce5035 chore: Vietnamese translations 2025-07-22 02:06:29 +05:30
Jannat Patel 549e56d551 chore: Chinese Simplified translations 2025-07-22 02:06:28 +05:30
Jannat Patel 50b6215d1e chore: Turkish translations 2025-07-22 02:06:27 +05:30
Jannat Patel ff69bfdce7 chore: Swedish translations 2025-07-22 02:06:25 +05:30
Jannat Patel c04cc8ec0f chore: Serbian (Cyrillic) translations 2025-07-22 02:06:24 +05:30
Jannat Patel f324de2254 chore: Russian translations 2025-07-22 02:06:22 +05:30
Jannat Patel 40af4e6f34 chore: Portuguese translations 2025-07-22 02:06:21 +05:30
Jannat Patel 5d9b66b5cb chore: Polish translations 2025-07-22 02:06:20 +05:30
Jannat Patel d2a8277c13 chore: Dutch translations 2025-07-22 02:06:18 +05:30
Jannat Patel ada85fc0f3 chore: Italian translations 2025-07-22 02:06:17 +05:30
Jannat Patel 505345eff7 chore: Hungarian translations 2025-07-22 02:06:15 +05:30
Jannat Patel 2911ade880 chore: German translations 2025-07-22 02:06:14 +05:30
Jannat Patel 8980dc8f9c chore: Czech translations 2025-07-22 02:06:12 +05:30
Jannat Patel d94a1c47c0 chore: Arabic translations 2025-07-22 02:06:11 +05:30
Jannat Patel 99c3e5182d chore: Spanish translations 2025-07-22 02:06:09 +05:30
Jannat Patel 70e39fee40 Merge pull request #1646 from frappe/pot_develop_2025-07-18
chore: update POT file
2025-07-21 19:30:03 +05:30
frappe-pr-bot 26d6bec8a0 chore: update POT file 2025-07-18 16:05:11 +00:00
Jannat Patel c9ac1a1402 Merge pull request #1645 from frappe/l10n_develop2
chore: sync translations from crowdin
2025-07-18 10:38:18 +05:30
Jannat Patel 6949c1092c chore: Persian translations 2025-07-18 01:39:51 +05:30
Jannat Patel aae8a54481 Merge pull request #1644 from frappe/l10n_develop2
chore: sync translations from crowdin
2025-07-17 10:26:39 +05:30
Jannat Patel e1d93bf670 chore: Serbian (Latin) translations 2025-07-17 01:24:47 +05:30
Jannat Patel fea0533cb1 chore: Chinese Simplified translations 2025-07-17 01:24:40 +05:30
Jannat Patel 5cd991f02a chore: Swedish translations 2025-07-17 01:24:37 +05:30
Jannat Patel 50a8a605d5 chore: Serbian (Cyrillic) translations 2025-07-17 01:24:36 +05:30
Jannat Patel 9ce7d8f5d6 Merge pull request #1641 from pateljannat/issues-121
chore: upgraded frappe-ui
2025-07-15 15:39:48 +05:30
Jannat Patel eae2587e4c chore: upgraded frappe-ui 2025-07-15 15:08:32 +05:30
Jannat Patel 323097f201 Merge pull request #1639 from frappe/l10n_develop2
chore: sync translations from crowdin
2025-07-15 09:44:54 +05:30
Jannat Patel 014499888a chore: Esperanto translations 2025-07-15 01:33:53 +05:30
Jannat Patel 5662de21ae chore: Serbian (Latin) translations 2025-07-15 01:33:52 +05:30
Jannat Patel 17c2eba455 chore: Bosnian translations 2025-07-15 01:33:50 +05:30
Jannat Patel 1f2c986e8f chore: Croatian translations 2025-07-15 01:33:49 +05:30
Jannat Patel 12040b5f6d chore: Thai translations 2025-07-15 01:33:47 +05:30
Jannat Patel 20a985848f chore: Portuguese, Brazilian translations 2025-07-15 01:33:46 +05:30
Jannat Patel c06c6169e5 chore: Vietnamese translations 2025-07-15 01:33:44 +05:30
Jannat Patel 917aeb79ef chore: Turkish translations 2025-07-15 01:33:43 +05:30
Jannat Patel c4f36a39fe chore: Swedish translations 2025-07-15 01:33:42 +05:30
Jannat Patel befedc30ad chore: Serbian (Cyrillic) translations 2025-07-15 01:33:40 +05:30
Jannat Patel d3bc67daa2 chore: Russian translations 2025-07-15 01:33:39 +05:30
Jannat Patel 5d7e211367 chore: Polish translations 2025-07-15 01:33:37 +05:30
Jannat Patel fa9daa01ec chore: Dutch translations 2025-07-15 01:33:36 +05:30
Jannat Patel 0ed9dc63b8 chore: Italian translations 2025-07-15 01:33:34 +05:30
Jannat Patel 5dd6b33eb2 chore: Hungarian translations 2025-07-15 01:33:33 +05:30
Jannat Patel 1210b823c7 chore: German translations 2025-07-15 01:33:32 +05:30
Jannat Patel 04240b4b3d chore: Czech translations 2025-07-15 01:33:30 +05:30
Jannat Patel 787f592a1a chore: Arabic translations 2025-07-15 01:33:29 +05:30
Jannat Patel e7363fbd40 chore: Spanish translations 2025-07-15 01:33:27 +05:30
Jannat Patel e2762825e5 chore: French translations 2025-07-15 01:33:25 +05:30
Jannat Patel bbbca70c71 chore: Chinese Simplified translations 2025-07-15 01:33:24 +05:30
Jannat Patel 8dde423866 chore: Persian translations 2025-07-15 01:33:23 +05:30
Jannat Patel fc4c1c2b7e chore: Portuguese translations 2025-07-15 01:33:21 +05:30
Jannat Patel bf02e2de3f Merge pull request #1637 from pateljannat/issues-120
fix: increase pageLength for evaluation schedule
2025-07-14 16:57:09 +05:30
Jannat Patel a26ba4dc6e fix: increase pageLength for evaluation schedule 2025-07-14 16:33:11 +05:30
65 changed files with 20622 additions and 4887 deletions
+1 -1
View File
@@ -1,7 +1,7 @@
name: Create weekly release name: Create weekly release
on: on:
schedule: schedule:
- cron: '30 4 15 * *' - cron: '30 3 * * 3'
workflow_dispatch: workflow_dispatch:
jobs: jobs:
+2 -1
View File
@@ -107,7 +107,7 @@ describe("Course Creation", () => {
cy.get("div").contains( cy.get("div").contains(
"Test Course Short Introduction to test the UI" "Test Course Short Introduction to test the UI"
); );
cy.get(".course-image") cy.get(".bg-cover")
.invoke("css", "background-image") .invoke("css", "background-image")
.should("include", "/files/profile"); .should("include", "/files/profile");
}); });
@@ -140,6 +140,7 @@ describe("Course Creation", () => {
); );
// Add Discussion // Add Discussion
cy.get("span").contains("Community").click();
cy.button("New Question").click(); cy.button("New Question").click();
cy.wait(500); cy.wait(500);
cy.get("[id^=headlessui-dialog-panel-").within(() => { cy.get("[id^=headlessui-dialog-panel-").within(() => {
+10
View 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
View File
@@ -40,6 +40,7 @@ declare module 'vue' {
Code: typeof import('./src/components/Controls/Code.vue')['default'] Code: typeof import('./src/components/Controls/Code.vue')['default']
CodeEditor: typeof import('./src/components/Controls/CodeEditor.vue')['default'] CodeEditor: typeof import('./src/components/Controls/CodeEditor.vue')['default']
CollapseSidebar: typeof import('./src/components/Icons/CollapseSidebar.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'] CourseCard: typeof import('./src/components/CourseCard.vue')['default']
CourseCardOverlay: typeof import('./src/components/CourseCardOverlay.vue')['default'] CourseCardOverlay: typeof import('./src/components/CourseCardOverlay.vue')['default']
CourseInstructors: typeof import('./src/components/CourseInstructors.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'] FrappeCloudIcon: typeof import('./src/components/Icons/FrappeCloudIcon.vue')['default']
IconPicker: typeof import('./src/components/Controls/IconPicker.vue')['default'] IconPicker: typeof import('./src/components/Controls/IconPicker.vue')['default']
IndicatorIcon: typeof import('./src/components/Icons/IndicatorIcon.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'] InviteIcon: typeof import('./src/components/Icons/InviteIcon.vue')['default']
JobApplicationModal: typeof import('./src/components/Modals/JobApplicationModal.vue')['default'] JobApplicationModal: typeof import('./src/components/Modals/JobApplicationModal.vue')['default']
JobCard: typeof import('./src/components/JobCard.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'] MultiSelect: typeof import('./src/components/Controls/MultiSelect.vue')['default']
NoPermission: typeof import('./src/components/NoPermission.vue')['default'] NoPermission: typeof import('./src/components/NoPermission.vue')['default']
NoSidebarLayout: typeof import('./src/components/NoSidebarLayout.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'] NotPermitted: typeof import('./src/components/NotPermitted.vue')['default']
PageModal: typeof import('./src/components/Modals/PageModal.vue')['default'] PageModal: typeof import('./src/components/Modals/PageModal.vue')['default']
PaymentSettings: typeof import('./src/components/Settings/PaymentSettings.vue')['default'] PaymentSettings: typeof import('./src/components/Settings/PaymentSettings.vue')['default']
+1 -1
View File
@@ -31,7 +31,7 @@
"codemirror": "^6.0.1", "codemirror": "^6.0.1",
"dayjs": "^1.11.6", "dayjs": "^1.11.6",
"feather-icons": "^4.28.0", "feather-icons": "^4.28.0",
"frappe-ui": "^0.1.163", "frappe-ui": "0.1.173",
"highlight.js": "^11.11.1", "highlight.js": "^11.11.1",
"lucide-vue-next": "^0.383.0", "lucide-vue-next": "^0.383.0",
"markdown-it": "^14.0.0", "markdown-it": "^14.0.0",
+17
View File
@@ -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 = () => { const addPrograms = () => {
let activeFor = ['Programs', 'ProgramForm'] let activeFor = ['Programs', 'ProgramForm']
let index = 1 let index = 1
@@ -627,6 +643,7 @@ watch(userResource, () => {
isModerator.value = userResource.data.is_moderator isModerator.value = userResource.data.is_moderator
isInstructor.value = userResource.data.is_instructor isInstructor.value = userResource.data.is_instructor
addPrograms() addPrograms()
addProgrammingExercises()
addQuizzes() addQuizzes()
addAssignments() addAssignments()
setUpOnboarding() setUpOnboarding()
@@ -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>
+61 -66
View File
@@ -1,41 +1,51 @@
<template> <template>
<div <div
v-if="course.title" 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" style="min-height: 350px"
> >
<div <div
class="course-image" class="w-[100%] h-[168px] bg-cover bg-center bg-no-repeat"
:class="{ 'default-image': !course.image }" :style="
:style="{ backgroundImage: 'url(\'' + encodeURI(course.image) + '\')' }" 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"> <div class="flex items-center flex-wrap relative top-4 px-2 w-fit">
<Badge <div
v-if="course.featured" v-if="course.featured"
variant="subtle" 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"
theme="green"
size="md"
class="mb-1 mr-1"
> >
{{ __('Featured') }} <Star class="size-3 stroke-2" />
</Badge> <span>
{{ __('Featured') }}
</span>
</div>
<div <div
v-if="course.tags" v-if="course.tags"
v-for="tag in course.tags?.split(', ')" 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 }} {{ tag }}
</div> </div>
</div> </div>
<div v-if="!course.image" class="image-placeholder"> <!-- <div
{{ course.title[0] }} v-if="!course.image"
</div> 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>
<div class="flex flex-col flex-auto p-4"> <div class="flex flex-col flex-auto p-4">
<div class="flex items-center justify-between mb-2"> <div class="flex items-center justify-between mb-2">
<div v-if="course.lessons"> <div v-if="course.lessons">
<Tooltip :text="__('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" /> <BookOpen class="h-4 w-4 stroke-1.5 mr-1" />
{{ course.lessons }} {{ course.lessons }}
</span> </span>
@@ -44,8 +54,8 @@
<div v-if="course.enrollments"> <div v-if="course.enrollments">
<Tooltip :text="__('Enrolled Students')"> <Tooltip :text="__('Enrolled Students')">
<span class="flex items-center text-ink-gray-7"> <span class="flex items-center">
<Users class="h-4 w-4 stroke-1. mr-1" /> <Users class="h-4 w-4 stroke-1.5 mr-1" />
{{ course.enrollments }} {{ course.enrollments }}
</span> </span>
</Tooltip> </Tooltip>
@@ -53,29 +63,22 @@
<div v-if="course.rating"> <div v-if="course.rating">
<Tooltip :text="__('Average 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" /> <Star class="h-4 w-4 stroke-1.5 mr-1" />
{{ course.rating }} {{ course.rating }}
</span> </span>
</Tooltip> </Tooltip>
</div> </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>
<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 }} {{ course.title }}
</div> </div>
<div class="short-introduction text-ink-gray-7 text-sm"> <div class="short-introduction text-sm">
{{ course.short_introduction }} {{ course.short_introduction }}
</div> </div>
@@ -84,11 +87,8 @@
:progress="course.membership.progress" :progress="course.membership.progress"
/> />
<div <div v-if="user && course.membership" class="text-sm mt-2 mb-4">
v-if="user && course.membership" {{ Math.ceil(course.membership.progress) }}% {{ __('completed') }}
class="text-sm text-ink-gray-7 mt-2 mb-4"
>
{{ Math.ceil(course.membership.progress) }}% completed
</div> </div>
<div class="flex items-center justify-between mt-auto"> <div class="flex items-center justify-between mt-auto">
@@ -108,21 +108,23 @@
<div v-if="course.paid_course" class="font-semibold"> <div v-if="course.paid_course" class="font-semibold">
{{ course.price }} {{ course.price }}
</div> </div>
<div
<Tooltip
v-if="course.paid_certificate || course.enable_certification" 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') }} <GraduationCap class="size-5 stroke-1.5 text-ink-gray-7" />
</div> </Tooltip>
</div> </div>
</div> </div>
</div> </div>
</template> </template>
<script setup> <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 UserAvatar from '@/components/UserAvatar.vue'
import { sessionStore } from '@/stores/session' 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 CourseInstructors from '@/components/CourseInstructors.vue'
import ProgressBar from '@/components/ProgressBar.vue' import ProgressBar from '@/components/ProgressBar.vue'
@@ -134,16 +136,24 @@ const props = defineProps({
default: null, 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> </script>
<style> <style>
.course-image {
height: 168px;
width: 100%;
background-size: cover;
background-position: center;
background-repeat: no-repeat;
}
.course-card-pills { .course-card-pills {
background: #ffffff; background: #ffffff;
margin-left: 0; margin-left: 0;
@@ -157,14 +167,6 @@ const props = defineProps({
width: fit-content; 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 { .avatar-group {
display: inline-flex; display: inline-flex;
align-items: center; align-items: center;
@@ -173,14 +175,7 @@ const props = defineProps({
.avatar-group .avatar { .avatar-group .avatar {
transition: margin 0.1s ease-in-out; 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 { .avatar-group.overlap .avatar + .avatar {
margin-left: calc(-8px); margin-left: calc(-8px);
} }
@@ -1,5 +1,5 @@
<template> <template>
<div class="text-ink-gray-7"> <div class="">
<span v-if="instructors?.length == 1"> <span v-if="instructors?.length == 1">
<router-link <router-link
:to="{ :to="{
@@ -19,7 +19,7 @@
> >
{{ instructors[0].first_name }} {{ instructors[0].first_name }}
</router-link> </router-link>
and {{ __('and') }}
<router-link <router-link
:to="{ :to="{
name: 'Profile', name: 'Profile',
@@ -38,7 +38,7 @@
> >
{{ instructors[0].first_name }} {{ instructors[0].first_name }}
</router-link> </router-link>
and {{ instructors?.length - 1 }} others {{ __('and') }} {{ instructors?.length - 1 }} {{ __('others') }}
</span> </span>
</div> </div>
</template> </template>
@@ -32,13 +32,13 @@
" "
:options="[ :options="[
{ {
label: 'Edit', label: __('Edit'),
onClick() { onClick() {
reply.editable = true reply.editable = true
}, },
}, },
{ {
label: 'Delete', label: __('Delete'),
onClick() { onClick() {
deleteReply(reply) deleteReply(reply)
}, },
+6 -3
View File
@@ -5,6 +5,9 @@
class="float-right" class="float-right"
@click="openTopicModal()" @click="openTopicModal()"
> >
<template #prefix>
<Plus class="size-4" />
</template>
{{ __('New {0}').format(singularize(title)) }} {{ __('New {0}').format(singularize(title)) }}
</Button> </Button>
<div class="text-xl font-semibold text-ink-gray-9"> <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" 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" /> <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"> <div v-if="emptyStateTitle" class="font-medium mb-2">
{{ __(emptyStateTitle) }} {{ __(emptyStateTitle) }}
</div> </div>
@@ -73,7 +76,7 @@ import { singularize, timeAgo } from '@/utils'
import { ref, onMounted, inject, onUnmounted } from 'vue' import { ref, onMounted, inject, onUnmounted } from 'vue'
import DiscussionReplies from '@/components/DiscussionReplies.vue' import DiscussionReplies from '@/components/DiscussionReplies.vue'
import DiscussionModal from '@/components/Modals/DiscussionModal.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' import { getScrollContainer } from '@/utils/scrollContainer'
const showTopics = ref(true) const showTopics = ref(true)
@@ -102,7 +105,7 @@ const props = defineProps({
}, },
emptyStateText: { emptyStateText: {
type: String, type: String,
default: 'Start a discussion', default: 'Start a Discussion',
}, },
singleThread: { singleThread: {
type: Boolean, type: Boolean,
@@ -25,6 +25,7 @@
<div class=""> <div class="">
<div class="mb-1.5 text-sm text-ink-gray-5"> <div class="mb-1.5 text-sm text-ink-gray-5">
{{ __('Reply To') }} {{ __('Reply To') }}
<span class="text-ink-red-3">*</span>
</div> </div>
<Input type="text" v-model="announcement.replyTo" /> <Input type="text" v-model="announcement.replyTo" />
</div> </div>
@@ -70,8 +71,8 @@ const announcementResource = createResource({
url: 'frappe.core.doctype.communication.email.make', url: 'frappe.core.doctype.communication.email.make',
makeParams(values) { makeParams(values) {
return { return {
recipients: props.students.join(', '), recipients: announcement.replyTo,
cc: announcement.replyTo, bcc: props.students.join(', '),
subject: announcement.subject, subject: announcement.subject,
content: announcement.announcement, content: announcement.announcement,
doctype: 'LMS Batch', doctype: 'LMS Batch',
@@ -95,6 +96,9 @@ const makeAnnouncement = (close) => {
if (!announcement.announcement) { if (!announcement.announcement) {
return __('Announcement is required') return __('Announcement is required')
} }
if (!announcement.replyTo) {
return __('Reply To is required')
}
}, },
onSuccess() { onSuccess() {
close() close()
@@ -15,7 +15,7 @@
</div> --> </div> -->
<FormControl <FormControl
v-model="searchFilter" v-model="searchFilter"
:placeholder="__('Search by Member Name')" :placeholder="__('Search by Member')"
type="text" type="text"
class="w-full" class="w-full"
/> />
@@ -149,6 +149,10 @@ import { theme } from '@/utils/theme'
const show = defineModel<boolean | undefined>() const show = defineModel<boolean | undefined>()
const searchFilter = ref<string | null>(null) const searchFilter = ref<string | null>(null)
type Filters = {
course: string | undefined
member_name?: string[]
}
const props = defineProps<{ const props = defineProps<{
courseName?: string courseName?: string
@@ -184,10 +188,6 @@ const progressList = createListResource({
watch([searchFilter], () => { watch([searchFilter], () => {
let filterApplied = false let filterApplied = false
type Filters = {
course: string | undefined
member_name?: string[]
}
let filters: Filters = { let filters: Filters = {
course: props.courseName, course: props.courseName,
} }
@@ -8,15 +8,24 @@
> >
<template #body-content> <template #body-content>
<div class="text-base"> <div class="text-base">
<TabButtons <div class="flex items-center justify-between">
v-if="tabs.length > 1" <TabButtons
:buttons="tabs" v-if="tabs.length > 1"
v-model="currentTab" :buttons="tabs"
class="w-fit" v-model="currentTab"
/> class="w-fit"
<div v-if="currentTab" class="mt-8"> />
<!-- <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="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="grid grid-cols-[70%,30%] text-sm text-ink-gray-5">
<div class="px-4"> <div class="px-4">
{{ __('Member') }} {{ __('Member') }}
@@ -52,7 +61,7 @@
</div> </div>
</div> </div>
<div class="text-center text-sm"> <div class="text-center text-sm">
{{ parseFloat(row.watch_time).toFixed(2) }} {{ formatTimestamp(row.watch_time) }}
</div> </div>
</div> </div>
</router-link> </router-link>
@@ -62,7 +71,7 @@
<NumberChart <NumberChart
class="border rounded-md" class="border rounded-md"
:config="{ :config="{
title: __('Average Watch Time (seconds)'), title: __('Average Watch Time'),
value: averageWatchTime, value: averageWatchTime,
}" }"
/> />
@@ -73,6 +82,9 @@
</div> </div>
</div> </div>
</div> </div>
<div v-else class="text-sm text-ink-gray-5">
{{ __('No statistics available for this video.') }}
</div>
</div> </div>
</template> </template>
</Dialog> </Dialog>
@@ -82,15 +94,21 @@ import {
Avatar, Avatar,
createListResource, createListResource,
Dialog, Dialog,
FormControl,
NumberChart, NumberChart,
TabButtons, TabButtons,
} from 'frappe-ui' } from 'frappe-ui'
import { computed, ref, watch } from 'vue' import { computed, ref, watch } from 'vue'
import { enablePlyr } from '@/utils' import { enablePlyr, formatTimestamp } from '@/utils'
import VideoBlock from '@/components/VideoBlock.vue' import VideoBlock from '@/components/VideoBlock.vue'
const show = defineModel<boolean | undefined>() const show = defineModel<boolean | undefined>()
const currentTab = ref<string>('') const currentTab = ref<string>('')
const searchText = ref<string>('')
type Filters = {
lesson: string | undefined
member_name?: string[]
}
const props = defineProps<{ const props = defineProps<{
lessonName?: string 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, () => { watch(show, () => {
if (show.value) { if (show.value) {
enablePlyr() enablePlyr()
@@ -151,7 +187,7 @@ const averageWatchTime = computed(() => {
totalWatchTime += parseFloat(item.watch_time) totalWatchTime += parseFloat(item.watch_time)
}) })
return totalWatchTime / currentTabData.value.length return formatTimestamp(totalWatchTime / currentTabData.value.length)
}) })
const currentTabData = computed(() => { const currentTabData = computed(() => {
@@ -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
View 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
View 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', doctype: 'LMS Certificate',
url: 'lms.lms.api.get_certified_participants', url: 'lms.lms.api.get_certified_participants',
start: 0, start: 0,
cache: ['certified_participants'],
pageLength: 100, pageLength: 100,
}) })
+32 -25
View File
@@ -47,9 +47,16 @@
:required="true" :required="true"
/> />
<div> <div>
<div class="mb-1.5 text-xs text-ink-gray-5"> <div class="text-xs text-ink-gray-5">
{{ __('Tags') }} {{ __('Tags') }}
</div> </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>
<div class="flex items-center flex-wrap gap-2"> <div class="flex items-center flex-wrap gap-2">
<div <div
@@ -64,37 +71,13 @@
/> />
</div> </div>
</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>
</div> </div>
<div class="grid grid-cols-2 gap-5"> <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="mb-4">
<div class="text-xs text-ink-gray-5 mb-2"> <div class="text-xs text-ink-gray-5 mb-2">
{{ __('Course Image') }} {{ __('Course Image') }}
<span class="text-ink-red-3">*</span>
</div> </div>
<FileUploader <FileUploader
v-if="!course.course_image" v-if="!course.course_image"
@@ -144,6 +127,13 @@
</div> </div>
</div> </div>
</div> </div>
<ColorSwatches
v-model="course.card_gradient"
:label="__('Color')"
:description="__('Choose a color for the course card')"
class="w-full"
/>
</div> </div>
</div> </div>
@@ -185,6 +175,21 @@
</div> </div>
<div class="px-10 pb-5 mb-5 space-y-5 border-b"> <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="">
<div class="mb-1.5 text-sm text-ink-gray-5"> <div class="mb-1.5 text-sm text-ink-gray-5">
{{ __('Course Description') }} {{ __('Course Description') }}
@@ -342,6 +347,7 @@ import {
import Link from '@/components/Controls/Link.vue' import Link from '@/components/Controls/Link.vue'
import CourseOutline from '@/components/CourseOutline.vue' import CourseOutline from '@/components/CourseOutline.vue'
import MultiSelect from '@/components/Controls/MultiSelect.vue' import MultiSelect from '@/components/Controls/MultiSelect.vue'
import ColorSwatches from '@/components/Controls/ColorSwatches.vue'
const user = inject('$user') const user = inject('$user')
const newTag = ref('') const newTag = ref('')
@@ -365,6 +371,7 @@ const course = reactive({
description: '', description: '',
video_link: '', video_link: '',
course_image: null, course_image: null,
card_gradient: '',
tags: '', tags: '',
category: '', category: '',
published: false, published: false,
+1 -1
View File
@@ -57,7 +57,7 @@
</div> </div>
<div <div
v-if="courses.data?.length" 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 <router-link
v-for="course in courses.data" v-for="course in courses.data"
+254 -135
View File
@@ -69,155 +69,180 @@
}" }"
> >
<div <div
class="border-r container pt-5 pb-10 px-5 h-full" class="border-r pt-5 pb-10 h-full"
:class="{ :class="{
'w-full md:w-3/5 mx-auto border-none !pt-10': zenModeEnabled, 'w-full md:w-3/5 mx-auto border-none !pt-10': zenModeEnabled,
}" }"
> >
<div <div class="px-5">
class="flex flex-col md:flex-row md:items-center justify-between" <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"> <div class="flex flex-col">
{{ lesson.data.title }} <div class="text-3xl font-semibold text-ink-gray-9">
</div> {{ 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 <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) }}% <span>
{{ __('completed') }} {{ 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> </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>
<div class="flex items-center space-x-2 mt-2 md:mt-0"> <div v-if="!zenModeEnabled" class="flex items-center mt-2">
<Button v-if="zenModeEnabled" @click="showDiscussionsInZenMode()"> <span
<template #icon> class="h-6 mr-1"
<MessageCircleQuestion class="w-4 h-4 stroke-1.5" /> :class="{
</template> 'avatar-group overlap': lesson.data.instructors?.length > 1,
</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> <UserAvatar
{{ __('Edit') }} v-for="instructor in lesson.data.instructors"
</Button> :user="instructor"
</router-link> />
</span>
<Button v-if="lesson.data.next" @click="switchLesson('next')"> <CourseInstructors
<template #suffix> v-if="lesson.data?.instructors"
<ChevronRight class="w-4 h-4 stroke-1" /> :instructors="lesson.data.instructors"
</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"
/> />
</span> </div>
<CourseInstructors
v-if="lesson.data?.instructors"
:instructors="lesson.data.instructors"
/>
</div>
<div <div
v-if=" v-if="
lesson.data.instructor_content && lesson.data.instructor_content &&
JSON.parse(lesson.data.instructor_content)?.blocks?.length > 1 && JSON.parse(lesson.data.instructor_content)?.blocks?.length >
allowInstructorContent() 1 &&
" allowInstructorContent()
class="bg-surface-gray-2 p-3 rounded-md mt-6" "
> class="bg-surface-gray-2 p-3 rounded-md mt-6"
<div class="text-ink-gray-5 font-medium"> >
{{ __('Instructor Notes') }} <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>
<div <div
id="instructor-content" 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" 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> >
<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>
<div <div
v-else-if="lesson.data.instructor_notes" v-if="lesson.data"
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" class="mt-10 pt-5 border-t px-5"
ref="discussionsContainer"
> >
<LessonContent :content="lesson.data.instructor_notes" /> <TabButtons
</div> :buttons="tabs"
<div v-model="currentTab"
v-if="lesson.data.content" class="w-fit mb-10"
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" />
> <Notes
<div id="editor"></div> v-if="currentTab === 'Notes'"
</div> :lesson="lesson.data?.name"
<div v-model:notes="notes"
v-else @updateNotes="updateNotes"
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 class="mt-20" ref="discussionsContainer">
<Discussions <Discussions
v-if="allowDiscussions" v-else-if="allowDiscussions"
:title="'Questions'" :title="'Questions'"
:doctype="'Course Lesson'" :doctype="'Course Lesson'"
:docname="lesson.data.name" :docname="lesson.data.name"
:key="lesson.data.name" :key="lesson.data.name"
:emptyStateText="
__('Ask a question to get help from the community.')
"
/> />
</div> </div>
</div> </div>
@@ -247,6 +272,13 @@
</div> </div>
</div> </div>
</div> </div>
<InlineLessonMenu
v-if="lesson.data"
v-model="showInlineMenu"
:lesson="lesson.data?.name"
v-model:notes="notes"
@updateNotes="updateNotes"
/>
<VideoStatistics <VideoStatistics
v-model="showStatsDialog" v-model="showStatsDialog"
:lessonName="lesson.data?.name" :lessonName="lesson.data?.name"
@@ -259,7 +291,9 @@ import {
Breadcrumbs, Breadcrumbs,
Button, Button,
call, call,
createListResource,
createResource, createResource,
TabButtons,
Tooltip, Tooltip,
usePageMeta, usePageMeta,
} from 'frappe-ui' } from 'frappe-ui'
@@ -272,8 +306,6 @@ import {
onBeforeUnmount, onBeforeUnmount,
nextTick, nextTick,
} from 'vue' } from 'vue'
import CourseOutline from '@/components/CourseOutline.vue'
import UserAvatar from '@/components/UserAvatar.vue'
import { useRouter, useRoute } from 'vue-router' import { useRouter, useRoute } from 'vue-router'
import { import {
ChevronLeft, ChevronLeft,
@@ -285,16 +317,20 @@ import {
MessageCircleQuestion, MessageCircleQuestion,
TrendingUp, TrendingUp,
} from 'lucide-vue-next' } from 'lucide-vue-next'
import Discussions from '@/components/Discussions.vue' import { getEditorTools, enablePlyr, highlightText } from '@/utils'
import { getEditorTools, enablePlyr } from '@/utils'
import { sessionStore } from '@/stores/session' import { sessionStore } from '@/stores/session'
import { useSidebar } from '@/stores/sidebar' import { useSidebar } from '@/stores/sidebar'
import EditorJS from '@editorjs/editorjs' import EditorJS from '@editorjs/editorjs'
import LessonContent from '@/components/LessonContent.vue' import LessonContent from '@/components/LessonContent.vue'
import CourseInstructors from '@/components/CourseInstructors.vue' import CourseInstructors from '@/components/CourseInstructors.vue'
import ProgressBar from '@/components/ProgressBar.vue' import ProgressBar from '@/components/ProgressBar.vue'
import Discussions from '@/components/Discussions.vue'
import CertificationLinks from '@/components/CertificationLinks.vue' import CertificationLinks from '@/components/CertificationLinks.vue'
import VideoStatistics from '@/components/Modals/VideoStatistics.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 user = inject('$user')
const socket = inject('$socket') const socket = inject('$socket')
@@ -313,8 +349,17 @@ const timer = ref(0)
const { brand } = sessionStore() const { brand } = sessionStore()
const sidebarStore = useSidebar() const sidebarStore = useSidebar()
const plyrSources = ref([]) const plyrSources = ref([])
const showInlineMenu = ref(false)
const currentTab = ref('Notes')
let timerInterval let timerInterval
const tabs = ref([
{
label: __('Notes'),
value: 'Notes',
},
])
const props = defineProps({ const props = defineProps({
courseName: { courseName: {
type: String, type: String,
@@ -392,16 +437,22 @@ const setupLesson = (data) => {
editor.value?.isReady.then(() => { editor.value?.isReady.then(() => {
checkIfDiscussionsAllowed() checkIfDiscussionsAllowed()
}) })
checkQuiz()
}
if (!editor.value && data.body) { const checkQuiz = () => {
if (!editor.value && lesson.body) {
const quizRegex = /\{\{ Quiz\(".*"\) \}\}/ const quizRegex = /\{\{ Quiz\(".*"\) \}\}/
hasQuiz.value = quizRegex.test(data.body) hasQuiz.value = quizRegex.test(lesson.body)
if (!hasQuiz.value && !zenModeEnabled) allowDiscussions.value = true if (!hasQuiz.value && !zenModeEnabled) {
allowDiscussions.value = true
} else {
allowDiscussions.value = false
}
} }
} }
const renderEditor = (holder, content) => { const renderEditor = (holder, content) => {
// empty the holder
if (document.getElementById(holder)) if (document.getElementById(holder))
document.getElementById(holder).innerHTML = '' document.getElementById(holder).innerHTML = ''
return new EditorJS({ return new EditorJS({
@@ -409,7 +460,7 @@ const renderEditor = (holder, content) => {
tools: getEditorTools(), tools: getEditorTools(),
data: JSON.parse(content), data: JSON.parse(content),
readOnly: true, 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(() => { const breadcrumbs = computed(() => {
let items = [{ label: 'Courses', route: { name: 'Courses' } }] let items = [{ label: 'Courses', route: { name: 'Courses' } }]
items.push({ items.push({
@@ -480,6 +548,9 @@ watch(
await nextTick() await nextTick()
resetLessonState(newChapterNumber, newLessonNumber) resetLessonState(newChapterNumber, newLessonNumber)
startTimer() startTimer()
updateNotes()
checkIfDiscussionsAllowed()
checkQuiz()
} }
} }
) )
@@ -546,6 +617,7 @@ watch(
async (data) => { async (data) => {
setupLesson(data) setupLesson(data)
getPlyrSource() getPlyrSource()
updateNotes()
if (data.icon == 'icon-youtube') clearInterval(timerInterval) if (data.icon == 'icon-youtube') clearInterval(timerInterval)
} }
) )
@@ -604,7 +676,7 @@ const updateVideoTime = (video) => {
} }
const startTimer = () => { const startTimer = () => {
timerInterval = setInterval(() => { let timerInterval = setInterval(() => {
timer.value++ timer.value++
if (timer.value == 30) { if (timer.value == 30) {
clearInterval(timerInterval) clearInterval(timerInterval)
@@ -618,8 +690,11 @@ onBeforeUnmount(() => {
}) })
const checkIfDiscussionsAllowed = () => { const checkIfDiscussionsAllowed = () => {
hasQuiz.value = false
JSON.parse(lesson.data?.content)?.blocks?.forEach((block) => { JSON.parse(lesson.data?.content)?.blocks?.forEach((block) => {
if (block.type === 'quiz') hasQuiz.value = true if (block.type === 'quiz') {
hasQuiz.value = true
}
}) })
if ( if (
@@ -628,8 +703,11 @@ const checkIfDiscussionsAllowed = () => {
(lesson.data?.membership || (lesson.data?.membership ||
user.data?.is_moderator || user.data?.is_moderator ||
user.data?.is_instructor) user.data?.is_instructor)
) ) {
allowDiscussions.value = true allowDiscussions.value = true
} else {
allowDiscussions.value = false
}
} }
const allowEdit = () => { 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 = () => { const canSeeStats = () => {
if (user.data?.is_moderator || user.data?.is_instructor) return true if (user.data?.is_moderator || user.data?.is_instructor) return true
return false 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 = () => { const redirectToLogin = () => {
window.location.href = `/login?redirect-to=/lms/courses/${props.courseName}` window.location.href = `/login?redirect-to=/lms/courses/${props.courseName}`
} }
-11
View File
@@ -714,17 +714,6 @@ iframe {
width: 100%; width: 100%;
} }
.cdx-block.embed-tool::after {
content: '';
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: transparent;
z-index: 1000;
}
:root { :root {
--plyr-range-fill-background: white; --plyr-range-fill-background: white;
--plyr-video-control-background-hover: transparent; --plyr-video-control-background-hover: transparent;
@@ -77,7 +77,7 @@ const evaluations = createListResource({
], ],
auto: true, auto: true,
orderBy: 'creation desc', orderBy: 'creation desc',
limit: 100, pageLength: 500,
cache: ['schedule', user.data?.name], cache: ['schedule', user.data?.name],
transform(data) { transform(data) {
return data.map((d) => { return data.map((d) => {
+130 -21
View File
@@ -1,5 +1,6 @@
import { call, toast } from 'frappe-ui' import { call, toast } from 'frappe-ui'
import { useTimeAgo } from '@vueuse/core' import { useTimeAgo } from '@vueuse/core'
import { theme } from '@/utils/theme'
import { Quiz } from '@/utils/quiz' import { Quiz } from '@/utils/quiz'
import { Program } from '@/utils/program' import { Program } from '@/utils/program'
import { Assignment } from '@/utils/assignment' import { Assignment } from '@/utils/assignment'
@@ -420,17 +421,6 @@ export function getSidebarLinks() {
to: 'Batches', to: 'Batches',
activeFor: ['Batches', 'BatchDetail', 'Batch', 'BatchForm'], activeFor: ['Batches', 'BatchDetail', 'Batch', 'BatchForm'],
}, },
{
label: 'Programming Exercises',
icon: 'Code',
to: 'ProgrammingExercises',
activeFor: [
'ProgrammingExercises',
'ProgrammingExerciseForm',
'ProgrammingExerciseSubmissions',
'ProgrammingExerciseSubmission',
],
},
{ {
label: 'Certified Members', label: 'Certified Members',
icon: 'GraduationCap', icon: 'GraduationCap',
@@ -497,14 +487,39 @@ export function singularize(word) {
) )
} }
export const validateFile = (file, showToast = true) => { export const validateFile = async (file, showToast = true) => {
if (!file.type.startsWith('image/')) { const error = (msg) => {
const errorMessage = __('Only image file is allowed.') if (showToast) toast.error(msg)
if (showToast) { console.error(msg)
toast.error(errorMessage) return msg
}
return errorMessage
} }
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) => { export const escapeHTML = (text) => {
@@ -551,11 +566,10 @@ export const enablePlyr = async () => {
const wait = (ms) => new Promise((resolve) => setTimeout(resolve, ms)) const wait = (ms) => new Promise((resolve) => setTimeout(resolve, ms))
const setupPlyrForVideo = (video, players) => { const setupPlyrForVideo = (video, players) => {
const src = video.getAttribute('src') || video.getAttribute('data-src') const src = video.getAttribute('src')
if (src) { if (src) {
const videoID = extractYouTubeId(src) const videoID = extractYouTubeId(src)
video.setAttribute('data-plyr-provider', 'youtube')
video.setAttribute('data-plyr-embed-id', videoID) video.setAttribute('data-plyr-embed-id', videoID)
} }
@@ -675,7 +689,102 @@ export const updateMetaInfo = (type, route, meta) => {
export const formatTimestamp = (seconds) => { export const formatTimestamp = (seconds) => {
const date = new Date(seconds * 1000) const date = new Date(seconds * 1000)
const hours = String(date.getUTCHours()).padStart(2, '0')
const minutes = String(date.getUTCMinutes()).padStart(2, '0') const minutes = String(date.getUTCMinutes()).padStart(2, '0')
const secs = String(date.getUTCSeconds()).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)
}
})
})
} }
+3 -1
View File
@@ -25,7 +25,8 @@ export default defineConfig({
}), }),
], ],
server: { server: {
allowedHosts: ['fs', 'per2'], host: '0.0.0.0', // Accept connections from any network interface
allowedHosts: ['ps', 'fs'], // Explicitly allow this host
}, },
resolve: { resolve: {
alias: { alias: {
@@ -39,6 +40,7 @@ export default defineConfig({
'showdown', 'showdown',
'engine.io-client', 'engine.io-client',
'tailwind.config.js', 'tailwind.config.js',
'interactjs',
'highlight.js', 'highlight.js',
'plyr', 'plyr',
], ],
+449 -408
View File
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -1 +1 @@
__version__ = "2.32.0" __version__ = "2.33.0"
+13 -7
View File
@@ -16,13 +16,14 @@
"field_order": [ "field_order": [
"title", "title",
"video_link", "video_link",
"tags",
"column_break_3", "column_break_3",
"instructors", "instructors",
"tags",
"column_break_htgn",
"image",
"category", "category",
"status", "status",
"column_break_htgn",
"image",
"card_gradient",
"section_break_7", "section_break_7",
"published", "published",
"published_on", "published_on",
@@ -98,8 +99,7 @@
{ {
"fieldname": "image", "fieldname": "image",
"fieldtype": "Attach Image", "fieldtype": "Attach Image",
"label": "Preview Image", "label": "Preview Image"
"reqd": 1
}, },
{ {
"fieldname": "tags", "fieldname": "tags",
@@ -272,6 +272,12 @@
"fieldtype": "Link", "fieldtype": "Link",
"label": "Evaluator", "label": "Evaluator",
"options": "Course 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", "is_published_field": "published",
@@ -290,8 +296,8 @@
} }
], ],
"make_attachments_public": 1, "make_attachments_public": 1,
"modified": "2025-05-29 12:38:01.002898", "modified": "2025-07-25 17:50:44.983391",
"modified_by": "Administrator", "modified_by": "sayali@frappe.io",
"module": "LMS", "module": "LMS",
"name": "LMS Course", "name": "LMS Course",
"owner": "Administrator", "owner": "Administrator",
+19
View File
@@ -21,6 +21,7 @@ class LMSCourse(Document):
self.validate_certification() self.validate_certification()
self.validate_amount_and_currency() self.validate_amount_and_currency()
self.image = validate_image(self.image) self.image = validate_image(self.image)
self.validate_card_gradient()
def validate_published(self): def validate_published(self):
if self.published and not self.published_on: 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): if self.paid_certificate and (cint(self.course_price) <= 0 or not self.currency):
frappe.throw(_("Amount and currency are required for paid certificates.")) 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): def on_update(self):
if not self.upcoming and self.has_value_changed("upcoming"): if not self.upcoming and self.has_value_changed("upcoming"):
self.send_email_to_interested_users() self.send_email_to_interested_users()
@@ -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) {
// },
// });
@@ -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"
}
@@ -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
@@ -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", "fieldname": "lesson",
"fieldtype": "Link", "fieldtype": "Link",
"in_list_view": 1, "in_list_view": 1,
"in_standard_filter": 1,
"label": "Lesson", "label": "Lesson",
"options": "Course Lesson", "options": "Course Lesson",
"reqd": 1 "reqd": 1
@@ -39,6 +40,7 @@
"fetch_from": "lesson.course", "fetch_from": "lesson.course",
"fieldname": "course", "fieldname": "course",
"fieldtype": "Link", "fieldtype": "Link",
"in_standard_filter": 1,
"label": "Course", "label": "Course",
"options": "LMS Course", "options": "LMS Course",
"read_only": 1 "read_only": 1
@@ -51,6 +53,7 @@
"fieldname": "member", "fieldname": "member",
"fieldtype": "Link", "fieldtype": "Link",
"in_list_view": 1, "in_list_view": 1,
"in_standard_filter": 1,
"label": "Member", "label": "Member",
"options": "User", "options": "User",
"reqd": 1 "reqd": 1
@@ -99,7 +102,7 @@
"grid_page_length": 50, "grid_page_length": 50,
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"links": [], "links": [],
"modified": "2025-06-30 16:57:10.561660", "modified": "2025-07-30 14:38:52.555010",
"modified_by": "sayali@frappe.io", "modified_by": "sayali@frappe.io",
"module": "LMS", "module": "LMS",
"name": "LMS Video Watch Duration", "name": "LMS Video Watch Duration",
+8 -4
View File
@@ -565,10 +565,13 @@ def get_courses_under_review():
def validate_image(path): def validate_image(path):
if path and "/private" in path: if path and "/private" in path:
file = frappe.get_doc("File", {"file_url": path}) frappe.db.set_value(
file.is_private = 0 "File",
file.save() {"file_url": path},
return file.file_url "is_private",
0,
)
return path.replace("/private", "")
return path return path
@@ -1097,6 +1100,7 @@ def get_course_fields():
"title", "title",
"tags", "tags",
"image", "image",
"card_gradient",
"short_introduction", "short_introduction",
"published", "published",
"upcoming", "upcoming",
+433 -176
View File
File diff suppressed because it is too large Load Diff
+435 -178
View File
File diff suppressed because it is too large Load Diff
+433 -176
View File
File diff suppressed because it is too large Load Diff
+433 -176
View File
File diff suppressed because it is too large Load Diff
+435 -178
View File
File diff suppressed because it is too large Load Diff
+433 -176
View File
File diff suppressed because it is too large Load Diff
+435 -178
View File
File diff suppressed because it is too large Load Diff
+384 -136
View File
File diff suppressed because it is too large Load Diff
+435 -178
View File
File diff suppressed because it is too large Load Diff
+439 -182
View File
File diff suppressed because it is too large Load Diff
+7315
View File
File diff suppressed because it is too large Load Diff
+462 -205
View File
File diff suppressed because it is too large Load Diff
+201 -100
View File
@@ -7,8 +7,8 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: Frappe LMS VERSION\n" "Project-Id-Version: Frappe LMS VERSION\n"
"Report-Msgid-Bugs-To: jannat@frappe.io\n" "Report-Msgid-Bugs-To: jannat@frappe.io\n"
"POT-Creation-Date: 2025-07-11 16:04+0000\n" "POT-Creation-Date: 2025-08-01 16:04+0000\n"
"PO-Revision-Date: 2025-07-11 16:04+0000\n" "PO-Revision-Date: 2025-08-01 16:04+0000\n"
"Last-Translator: jannat@frappe.io\n" "Last-Translator: jannat@frappe.io\n"
"Language-Team: jannat@frappe.io\n" "Language-Team: jannat@frappe.io\n"
"MIME-Version: 1.0\n" "MIME-Version: 1.0\n"
@@ -69,11 +69,11 @@ msgstr ""
msgid "<span style=\"font-size: 18px;\"><b>Statistics</b></span>" msgid "<span style=\"font-size: 18px;\"><b>Statistics</b></span>"
msgstr "" 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." msgid "A course cannot have both paid certificate and certificate of completion."
msgstr "" 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" msgid "A one line introduction to the course that appears on the course card"
msgstr "" msgstr ""
@@ -81,6 +81,10 @@ msgstr ""
msgid "About" msgid "About"
msgstr "" msgstr ""
#: frontend/src/pages/CourseForm.vue:179
msgid "About the Course"
msgstr ""
#: frontend/src/pages/Batch.vue:101 #: frontend/src/pages/Batch.vue:101
msgid "About this batch" msgid "About this batch"
msgstr "" msgstr ""
@@ -178,7 +182,7 @@ msgstr ""
msgid "Add a Student" msgid "Add a Student"
msgstr "" msgstr ""
#: frontend/src/components/AppSidebar.vue:568 #: frontend/src/components/AppSidebar.vue:584
msgid "Add a chapter" msgid "Add a chapter"
msgstr "" msgstr ""
@@ -186,11 +190,11 @@ msgstr ""
msgid "Add a course" msgid "Add a course"
msgstr "" msgstr ""
#: frontend/src/pages/CourseForm.vue:69 #: frontend/src/pages/CourseForm.vue:55
msgid "Add a keyword and then press enter" msgid "Add a keyword and then press enter"
msgstr "" msgstr ""
#: frontend/src/components/AppSidebar.vue:569 #: frontend/src/components/AppSidebar.vue:585
msgid "Add a lesson" msgid "Add a lesson"
msgstr "" msgstr ""
@@ -223,7 +227,7 @@ msgstr ""
msgid "Add at least one possible answer for this question: {0}" msgid "Add at least one possible answer for this question: {0}"
msgstr "" msgstr ""
#: frontend/src/components/AppSidebar.vue:532 #: frontend/src/components/AppSidebar.vue:548
msgid "Add courses to your batch" msgid "Add courses to your batch"
msgstr "" msgstr ""
@@ -231,7 +235,7 @@ msgstr ""
msgid "Add quiz to this video" msgid "Add quiz to this video"
msgstr "" msgstr ""
#: frontend/src/components/AppSidebar.vue:511 #: frontend/src/components/AppSidebar.vue:527
msgid "Add students to your batch" msgid "Add students to your batch"
msgstr "" msgstr ""
@@ -243,11 +247,11 @@ msgstr ""
msgid "Add your assignment as {0}" msgid "Add your assignment as {0}"
msgstr "" msgstr ""
#: frontend/src/components/AppSidebar.vue:444 #: frontend/src/components/AppSidebar.vue:460
msgid "Add your first chapter" msgid "Add your first chapter"
msgstr "" msgstr ""
#: frontend/src/components/AppSidebar.vue:460 #: frontend/src/components/AppSidebar.vue:476
msgid "Add your first lesson" msgid "Add your first lesson"
msgstr "" msgstr ""
@@ -323,10 +327,15 @@ msgstr ""
msgid "Already Registered" msgid "Already Registered"
msgstr "" 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 amount (Currency) field in DocType 'LMS Batch'
#. Label of the course_price (Currency) field in DocType 'LMS Course' #. Label of the course_price (Currency) field in DocType 'LMS Course'
#. Label of the amount (Currency) field in DocType 'LMS Payment' #. 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_batch/lms_batch.json
#: lms/lms/doctype/lms_course/lms_course.json #: lms/lms/doctype/lms_course/lms_course.json
#: lms/lms/doctype/lms_payment/lms_payment.json #: lms/lms/doctype/lms_payment/lms_payment.json
@@ -344,11 +353,11 @@ msgstr ""
msgid "Amount and currency are required for paid batches." msgid "Amount and currency are required for paid batches."
msgstr "" 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." msgid "Amount and currency are required for paid certificates."
msgstr "" 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." msgid "Amount and currency are required for paid courses."
msgstr "" msgstr ""
@@ -357,15 +366,15 @@ msgstr ""
msgid "Amount with GST" msgid "Amount with GST"
msgstr "" msgstr ""
#: frontend/src/components/Modals/AnnouncementModal.vue:33 #: frontend/src/components/Modals/AnnouncementModal.vue:34
msgid "Announcement" msgid "Announcement"
msgstr "" msgstr ""
#: frontend/src/components/Modals/AnnouncementModal.vue:101 #: frontend/src/components/Modals/AnnouncementModal.vue:105
msgid "Announcement has been sent successfully" msgid "Announcement has been sent successfully"
msgstr "" msgstr ""
#: frontend/src/components/Modals/AnnouncementModal.vue:96 #: frontend/src/components/Modals/AnnouncementModal.vue:97
msgid "Announcement is required" msgid "Announcement is required"
msgstr "" msgstr ""
@@ -380,7 +389,7 @@ msgstr ""
msgid "Answer" msgid "Answer"
msgstr "" 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" msgid "Appears on the course card in the course list"
msgstr "" msgstr ""
@@ -463,7 +472,7 @@ msgid "Assessment {0} has already been added to this batch."
msgstr "" msgstr ""
#. Label of the show_assessments (Check) field in DocType 'LMS Settings' #. 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/Assessments.vue:5
#: frontend/src/components/BatchStudents.vue:32 #: frontend/src/components/BatchStudents.vue:32
#: lms/lms/doctype/lms_settings/lms_settings.json #: lms/lms/doctype/lms_settings/lms_settings.json
@@ -545,7 +554,7 @@ msgstr ""
msgid "Assignment will appear at the bottom of the lesson." msgid "Assignment will appear at the bottom of the lesson."
msgstr "" msgstr ""
#: frontend/src/components/AppSidebar.vue:585 #: frontend/src/components/AppSidebar.vue:601
#: frontend/src/components/Settings/Badges.vue:163 #: frontend/src/components/Settings/Badges.vue:163
#: frontend/src/pages/Assignments.vue:208 lms/www/lms.py:273 #: frontend/src/pages/Assignments.vue:208 lms/www/lms.py:273
msgid "Assignments" msgid "Assignments"
@@ -604,13 +613,13 @@ msgstr ""
msgid "Average Progress %" msgid "Average Progress %"
msgstr "" msgstr ""
#: frontend/src/components/CourseCard.vue:55 #: frontend/src/components/CourseCard.vue:65
#: frontend/src/pages/CourseDetail.vue:20 #: frontend/src/pages/CourseDetail.vue:20
msgid "Average Rating" msgid "Average Rating"
msgstr "" msgstr ""
#: frontend/src/components/Modals/VideoStatistics.vue:65 #: frontend/src/components/Modals/VideoStatistics.vue:72
msgid "Average Watch Time (seconds)" msgid "Average Watch Time (minutes)"
msgstr "" msgstr ""
#: frontend/src/pages/Lesson.vue:151 #: frontend/src/pages/Lesson.vue:151
@@ -715,6 +724,7 @@ msgstr ""
#. Name of a role #. Name of a role
#: lms/lms/doctype/course_evaluator/course_evaluator.json #: 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_batch/lms_batch.json
#: lms/lms/doctype/lms_category/lms_category.json #: lms/lms/doctype/lms_category/lms_category.json
#: lms/lms/doctype/lms_certificate_evaluation/lms_certificate_evaluation.json #: lms/lms/doctype/lms_certificate_evaluation/lms_certificate_evaluation.json
@@ -822,6 +832,11 @@ msgstr ""
msgid "Bio" msgid "Bio"
msgstr "" 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' #. Label of the body (Markdown Editor) field in DocType 'Course Lesson'
#: lms/lms/doctype/course_lesson/course_lesson.json #: lms/lms/doctype/course_lesson/course_lesson.json
msgid "Body" msgid "Body"
@@ -951,8 +966,7 @@ msgstr ""
#. Enrollment' #. Enrollment'
#. Label of a Card Break in the LMS Workspace #. Label of a Card Break in the LMS Workspace
#. Label of a Link in the LMS Workspace #. Label of a Link in the LMS Workspace
#: frontend/src/components/AppSidebar.vue:589 #: frontend/src/components/AppSidebar.vue:605
#: frontend/src/components/CourseCard.vue:115
#: frontend/src/components/Modals/Event.vue:381 #: frontend/src/components/Modals/Event.vue:381
#: frontend/src/pages/BatchForm.vue:69 frontend/src/pages/Batches.vue:38 #: frontend/src/pages/BatchForm.vue:69 frontend/src/pages/Batches.vue:38
#: frontend/src/pages/CourseCertification.vue:10 #: frontend/src/pages/CourseCertification.vue:10
@@ -980,8 +994,8 @@ msgid "Certified"
msgstr "" msgstr ""
#. Label of the certified_members (Check) field in DocType 'LMS Settings' #. Label of the certified_members (Check) field in DocType 'LMS Settings'
#: frontend/src/pages/CertifiedParticipants.vue:182 #: frontend/src/pages/CertifiedParticipants.vue:183
#: frontend/src/pages/CertifiedParticipants.vue:189 #: frontend/src/pages/CertifiedParticipants.vue:190
#: frontend/src/pages/Statistics.vue:40 #: frontend/src/pages/Statistics.vue:40
#: lms/lms/doctype/lms_settings/lms_settings.json #: lms/lms/doctype/lms_settings/lms_settings.json
msgid "Certified Members" msgid "Certified Members"
@@ -1073,6 +1087,10 @@ msgstr ""
msgid "Choices" msgid "Choices"
msgstr "" 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 #: frontend/src/components/Quiz.vue:644 lms/templates/quiz/quiz.html:53
msgid "Choose all answers that apply" msgid "Choose all answers that apply"
msgstr "" msgstr ""
@@ -1210,12 +1228,15 @@ msgstr ""
msgid "College Name" msgid "College Name"
msgstr "" msgstr ""
#. Label of the card_gradient (Select) field in DocType 'LMS Course'
#. Label of the color (Color) field in DocType 'LMS Timetable Legend' #. 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 #: lms/lms/doctype/lms_timetable_legend/lms_timetable_legend.json
msgid "Color" msgid "Color"
msgstr "" 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" msgid "Comma separated keywords for SEO"
msgstr "" msgstr ""
@@ -1324,7 +1345,7 @@ msgid "Completed"
msgstr "" msgstr ""
#. Label of the enable_certification (Check) field in DocType 'LMS Course' #. 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 #: lms/lms/doctype/lms_course/lms_course.json
msgid "Completion Certificate" msgid "Completion Certificate"
msgstr "" msgstr ""
@@ -1517,6 +1538,7 @@ msgstr ""
#: frontend/src/pages/ProfileRoles.vue:26 #: frontend/src/pages/ProfileRoles.vue:26
#: lms/lms/doctype/course_chapter/course_chapter.json #: lms/lms/doctype/course_chapter/course_chapter.json
#: lms/lms/doctype/course_lesson/course_lesson.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_category/lms_category.json
#: lms/lms/doctype/lms_course/lms_course.json #: lms/lms/doctype/lms_course/lms_course.json
#: lms/lms/doctype/lms_program/lms_program.json #: lms/lms/doctype/lms_program/lms_program.json
@@ -1533,7 +1555,7 @@ msgstr ""
msgid "Course Data" msgid "Course Data"
msgstr "" msgstr ""
#: frontend/src/pages/CourseForm.vue:190 #: frontend/src/pages/CourseForm.vue:195
msgid "Course Description" msgid "Course Description"
msgstr "" msgstr ""
@@ -1551,7 +1573,7 @@ msgstr ""
msgid "Course Evaluator" msgid "Course Evaluator"
msgstr "" msgstr ""
#: frontend/src/pages/CourseForm.vue:96 #: frontend/src/pages/CourseForm.vue:80
msgid "Course Image" msgid "Course Image"
msgstr "" msgstr ""
@@ -1573,7 +1595,7 @@ msgstr ""
msgid "Course Name" msgid "Course Name"
msgstr "" 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" msgid "Course Outline"
msgstr "" msgstr ""
@@ -1610,11 +1632,11 @@ msgstr ""
msgid "Course added to program" msgid "Course added to program"
msgstr "" msgstr ""
#: frontend/src/pages/CourseForm.vue:537 #: frontend/src/pages/CourseForm.vue:544
msgid "Course created successfully" msgid "Course created successfully"
msgstr "" msgstr ""
#: frontend/src/pages/CourseForm.vue:574 #: frontend/src/pages/CourseForm.vue:581
msgid "Course deleted successfully" msgid "Course deleted successfully"
msgstr "" msgstr ""
@@ -1622,7 +1644,7 @@ msgstr ""
msgid "Course moved successfully" msgid "Course moved successfully"
msgstr "" msgstr ""
#: frontend/src/pages/CourseForm.vue:557 #: frontend/src/pages/CourseForm.vue:564
msgid "Course updated successfully" msgid "Course updated successfully"
msgstr "" msgstr ""
@@ -1693,15 +1715,15 @@ msgstr ""
msgid "Create a Quiz" msgid "Create a Quiz"
msgstr "" msgstr ""
#: frontend/src/components/AppSidebar.vue:576 #: frontend/src/components/AppSidebar.vue:592
msgid "Create a batch" msgid "Create a batch"
msgstr "" msgstr ""
#: frontend/src/components/AppSidebar.vue:567 #: frontend/src/components/AppSidebar.vue:583
msgid "Create a course" msgid "Create a course"
msgstr "" msgstr ""
#: frontend/src/components/AppSidebar.vue:577 #: frontend/src/components/AppSidebar.vue:593
msgid "Create a live class" msgid "Create a live class"
msgstr "" msgstr ""
@@ -1713,15 +1735,15 @@ msgstr ""
msgid "Create an Assignment" msgid "Create an Assignment"
msgstr "" msgstr ""
#: frontend/src/components/AppSidebar.vue:501 #: frontend/src/components/AppSidebar.vue:517
msgid "Create your first batch" msgid "Create your first batch"
msgstr "" msgstr ""
#: frontend/src/components/AppSidebar.vue:432 #: frontend/src/components/AppSidebar.vue:448
msgid "Create your first course" msgid "Create your first course"
msgstr "" msgstr ""
#: frontend/src/components/AppSidebar.vue:479 #: frontend/src/components/AppSidebar.vue:495
msgid "Create your first quiz" msgid "Create your first quiz"
msgstr "" msgstr ""
@@ -1729,18 +1751,18 @@ msgstr ""
msgid "Created" msgid "Created"
msgstr "" msgstr ""
#: frontend/src/components/AppSidebar.vue:573 #: frontend/src/components/AppSidebar.vue:589
msgid "Creating a batch" msgid "Creating a batch"
msgstr "" msgstr ""
#: frontend/src/components/AppSidebar.vue:564 #: frontend/src/components/AppSidebar.vue:580
msgid "Creating a course" msgid "Creating a course"
msgstr "" msgstr ""
#. Label of the currency (Link) field in DocType 'LMS Batch' #. 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 Course'
#. Label of the currency (Link) field in DocType 'LMS Payment' #. 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_batch/lms_batch.json
#: lms/lms/doctype/lms_course/lms_course.json #: lms/lms/doctype/lms_course/lms_course.json
#: lms/lms/doctype/lms_payment/lms_payment.json #: lms/lms/doctype/lms_payment/lms_payment.json
@@ -1752,7 +1774,7 @@ msgstr ""
msgid "Current Lesson" msgid "Current Lesson"
msgstr "" msgstr ""
#: frontend/src/components/AppSidebar.vue:595 #: frontend/src/components/AppSidebar.vue:611
msgid "Custom Certificate Templates" msgid "Custom Certificate Templates"
msgstr "" msgstr ""
@@ -1777,6 +1799,11 @@ msgstr ""
msgid "Customisations" msgid "Customisations"
msgstr "" 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' #. Label of the show_dashboard (Check) field in DocType 'LMS Settings'
#: lms/lms/doctype/lms_settings/lms_settings.json #: lms/lms/doctype/lms_settings/lms_settings.json
msgid "Dashboard" msgid "Dashboard"
@@ -1847,7 +1874,7 @@ msgstr ""
#: frontend/src/components/CourseOutline.vue:283 #: frontend/src/components/CourseOutline.vue:283
#: frontend/src/components/CourseOutline.vue:349 #: frontend/src/components/CourseOutline.vue:349
#: frontend/src/components/Settings/Badges.vue:171 #: 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 #: frontend/src/pages/ProgrammingExercises/ProgrammingExerciseForm.vue:67
msgid "Delete" msgid "Delete"
msgstr "" msgstr ""
@@ -1856,7 +1883,7 @@ msgstr ""
msgid "Delete Chapter" msgid "Delete Chapter"
msgstr "" msgstr ""
#: frontend/src/pages/CourseForm.vue:581 #: frontend/src/pages/CourseForm.vue:588
msgid "Delete Course" msgid "Delete Course"
msgstr "" msgstr ""
@@ -1868,7 +1895,7 @@ msgstr ""
msgid "Delete this lesson?" msgid "Delete this lesson?"
msgstr "" 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?" msgid "Deleting the course will also delete all its chapters and lessons. Are you sure you want to delete this course?"
msgstr "" msgstr ""
@@ -1923,7 +1950,7 @@ msgstr ""
msgid "Details" msgid "Details"
msgstr "" msgstr ""
#: frontend/src/pages/CourseForm.vue:181 #: frontend/src/pages/CourseForm.vue:171
msgid "Disable Self Enrollment" msgid "Disable Self Enrollment"
msgstr "" msgstr ""
@@ -2197,7 +2224,7 @@ msgstr ""
msgid "Enrolled" msgid "Enrolled"
msgstr "" msgstr ""
#: frontend/src/components/CourseCard.vue:46 #: frontend/src/components/CourseCard.vue:56
#: frontend/src/components/CourseCardOverlay.vue:138 #: frontend/src/components/CourseCardOverlay.vue:138
#: frontend/src/pages/CourseDetail.vue:33 #: frontend/src/pages/CourseDetail.vue:33
msgid "Enrolled Students" msgid "Enrolled Students"
@@ -2212,7 +2239,7 @@ msgstr ""
msgid "Enrollment Count" msgid "Enrollment Count"
msgstr "" msgstr ""
#: lms/lms/utils.py:1943 #: lms/lms/utils.py:1948
msgid "Enrollment Failed" msgid "Enrollment Failed"
msgstr "" msgstr ""
@@ -2313,7 +2340,7 @@ msgstr ""
#. Label of the evaluator (Link) field in DocType 'LMS Course' #. Label of the evaluator (Link) field in DocType 'LMS Course'
#: frontend/src/components/Modals/BatchCourseModal.vue:37 #: frontend/src/components/Modals/BatchCourseModal.vue:37
#: frontend/src/components/Modals/BulkCertificates.vue:22 #: 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/batch_course/batch_course.json
#: lms/lms/doctype/course_evaluator/course_evaluator.json #: lms/lms/doctype/course_evaluator/course_evaluator.json
#: lms/lms/doctype/lms_assignment_submission/lms_assignment_submission.json #: lms/lms/doctype/lms_assignment_submission/lms_assignment_submission.json
@@ -2353,7 +2380,7 @@ msgstr ""
msgid "Evaluator does not exist." msgid "Evaluator does not exist."
msgstr "" 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." msgid "Evaluator is required for paid certificates."
msgstr "" msgstr ""
@@ -2479,13 +2506,13 @@ msgstr ""
msgid "Failed to update badge assignment: " msgid "Failed to update badge assignment: "
msgstr "" msgstr ""
#: frontend/src/utils/index.js:671 #: frontend/src/utils/index.js:659
msgid "Failed to update meta tags {0}" msgid "Failed to update meta tags {0}"
msgstr "" msgstr ""
#. Label of the featured (Check) field in DocType 'LMS Course' #. Label of the featured (Check) field in DocType 'LMS Course'
#: frontend/src/components/CourseCard.vue:20 #: frontend/src/components/CourseCard.vue:25
#: frontend/src/pages/CourseForm.vue:176 #: frontend/src/pages/CourseForm.vue:166
#: lms/lms/doctype/lms_course/lms_course.json #: lms/lms/doctype/lms_course/lms_course.json
msgid "Featured" msgid "Featured"
msgstr "" msgstr ""
@@ -2643,6 +2670,7 @@ msgstr ""
#: frontend/src/components/CertificationLinks.vue:34 #: frontend/src/components/CertificationLinks.vue:34
#: frontend/src/components/CertificationLinks.vue:50 #: frontend/src/components/CertificationLinks.vue:50
#: frontend/src/components/CourseCard.vue:121
#: frontend/src/pages/CertifiedParticipants.vue:11 #: frontend/src/pages/CertifiedParticipants.vue:11
msgid "Get Certified" msgid "Get Certified"
msgstr "" msgstr ""
@@ -2692,6 +2720,16 @@ msgstr ""
msgid "Grant only once" msgid "Grant only once"
msgstr "" 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 #: lms/templates/signup-form.html:56
msgid "Have an account? Login" msgid "Have an account? Login"
msgstr "" msgstr ""
@@ -2967,8 +3005,8 @@ msgstr ""
msgid "Interest" msgid "Interest"
msgstr "" msgstr ""
#: frontend/src/components/AppSidebar.vue:556 #: frontend/src/components/AppSidebar.vue:572
#: frontend/src/components/AppSidebar.vue:559 #: frontend/src/components/AppSidebar.vue:575
msgid "Introduction" msgid "Introduction"
msgstr "" msgstr ""
@@ -3006,7 +3044,7 @@ msgstr ""
msgid "Invite Request" msgid "Invite Request"
msgstr "" msgstr ""
#: frontend/src/components/AppSidebar.vue:490 #: frontend/src/components/AppSidebar.vue:506
msgid "Invite your team and students" msgid "Invite your team and students"
msgstr "" msgstr ""
@@ -3038,7 +3076,7 @@ msgstr ""
msgid "Issue Date" msgid "Issue Date"
msgstr "" msgstr ""
#: frontend/src/components/AppSidebar.vue:592 #: frontend/src/components/AppSidebar.vue:608
msgid "Issue a Certificate" msgid "Issue a Certificate"
msgstr "" msgstr ""
@@ -3510,7 +3548,7 @@ msgstr ""
#. Label of the lessons (Table) field in DocType 'Course Chapter' #. Label of the lessons (Table) field in DocType 'Course Chapter'
#. Group in Course Chapter's connections #. Group in Course Chapter's connections
#. Label of the lessons (Int) field in DocType 'LMS Course' #. 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 #: frontend/src/components/CourseCardOverlay.vue:131
#: lms/lms/doctype/course_chapter/course_chapter.json #: lms/lms/doctype/course_chapter/course_chapter.json
#: lms/lms/doctype/lms_course/lms_course.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' #. Label of the member (Link) field in DocType 'LMS Zoom Settings'
#: frontend/src/components/Modals/CourseProgressSummary.vue:216 #: frontend/src/components/Modals/CourseProgressSummary.vue:216
#: frontend/src/components/Modals/LiveClassAttendance.vue:14 #: 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/Modals/ZoomAccountModal.vue:42
#: frontend/src/components/Settings/BadgeAssignmentForm.vue:26 #: frontend/src/components/Settings/BadgeAssignmentForm.vue:26
#: frontend/src/components/Settings/BadgeAssignments.vue:179 #: frontend/src/components/Settings/BadgeAssignments.vue:179
@@ -3938,7 +3976,7 @@ msgid "Mentor Request Status Update Template"
msgstr "" msgstr ""
#. Label of the meta_description (Small Text) field in DocType 'LMS Settings' #. 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 #: lms/lms/doctype/lms_settings/lms_settings.json
msgid "Meta Description" msgid "Meta Description"
msgstr "" msgstr ""
@@ -3952,12 +3990,12 @@ msgid "Meta Image"
msgstr "" msgstr ""
#. Label of the meta_keywords (Small Text) field in DocType 'LMS Settings' #. 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 #: lms/lms/doctype/lms_settings/lms_settings.json
msgid "Meta Keywords" msgid "Meta Keywords"
msgstr "" 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" msgid "Meta Tags"
msgstr "" msgstr ""
@@ -3980,6 +4018,7 @@ msgstr ""
#: lms/lms/doctype/course_evaluator/course_evaluator.json #: lms/lms/doctype/course_evaluator/course_evaluator.json
#: lms/lms/doctype/course_lesson/course_lesson.json #: lms/lms/doctype/course_lesson/course_lesson.json
#: lms/lms/doctype/lms_assignment/lms_assignment.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_badge_assignment/lms_badge_assignment.json
#: lms/lms/doctype/lms_batch/lms_batch.json #: lms/lms/doctype/lms_batch/lms_batch.json
#: lms/lms/doctype/lms_batch_enrollment/lms_batch_enrollment.json #: lms/lms/doctype/lms_batch_enrollment/lms_batch_enrollment.json
@@ -4026,7 +4065,7 @@ msgstr ""
msgid "Monday" msgid "Monday"
msgstr "" msgstr ""
#: frontend/src/components/AppSidebar.vue:600 #: frontend/src/components/AppSidebar.vue:616
msgid "Monetization" msgid "Monetization"
msgstr "" msgstr ""
@@ -4068,7 +4107,7 @@ msgstr ""
msgid "New Batch" msgid "New Batch"
msgstr "" 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" msgid "New Course"
msgstr "" msgstr ""
@@ -4112,11 +4151,11 @@ msgstr ""
msgid "New Zoom Account" msgid "New Zoom Account"
msgstr "" msgstr ""
#: lms/lms/utils.py:609 #: lms/lms/utils.py:612
msgid "New comment in batch {0}" msgid "New comment in batch {0}"
msgstr "" msgstr ""
#: lms/lms/utils.py:602 #: lms/lms/utils.py:605
msgid "New reply on the topic {0} in course {1}" msgid "New reply on the topic {0} in course {1}"
msgstr "" msgstr ""
@@ -4214,7 +4253,11 @@ msgstr ""
msgid "No slots available for this date." msgid "No slots available for this date."
msgstr "" 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" msgid "No students in this batch"
msgstr "" msgstr ""
@@ -4323,7 +4366,7 @@ msgstr ""
msgid "Only files of type {0} will be accepted." msgid "Only files of type {0} will be accepted."
msgstr "" msgstr ""
#: frontend/src/utils/index.js:502 #: frontend/src/utils/index.js:491
msgid "Only image file is allowed." msgid "Only image file is allowed."
msgstr "" msgstr ""
@@ -4383,6 +4426,11 @@ msgstr ""
msgid "Options" msgid "Options"
msgstr "" 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' #. Label of the order_id (Data) field in DocType 'LMS Payment'
#: lms/lms/doctype/lms_payment/lms_payment.json #: lms/lms/doctype/lms_payment/lms_payment.json
msgid "Order ID" msgid "Order ID"
@@ -4438,7 +4486,7 @@ msgid "Paid Batch"
msgstr "" msgstr ""
#. Label of the paid_certificate (Check) field in DocType 'LMS Course' #. 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 #: lms/lms/doctype/lms_course/lms_course.json
msgid "Paid Certificate" msgid "Paid Certificate"
msgstr "" msgstr ""
@@ -4448,7 +4496,7 @@ msgid "Paid Certificate after Evaluation"
msgstr "" msgstr ""
#. Label of the paid_course (Check) field in DocType 'LMS Course' #. 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 #: lms/lms/doctype/lms_course/lms_course.json
msgid "Paid Course" msgid "Paid Course"
msgstr "" msgstr ""
@@ -4500,7 +4548,7 @@ msgstr ""
msgid "Password" msgid "Password"
msgstr "" 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" msgid "Paste the youtube link of a short video introducing the course"
msgstr "" msgstr ""
@@ -4614,6 +4662,11 @@ msgstr ""
msgid "Phone Number" msgid "Phone Number"
msgstr "" 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 #: 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." msgid "Please add <a href='{0}'>{1}</a> for <a href='{2}'>{3}</a> to send calendar invites for evaluations."
msgstr "" msgstr ""
@@ -4634,7 +4687,7 @@ msgstr ""
msgid "Please click on the following button to set your new password" msgid "Please click on the following button to set your new password"
msgstr "" 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." msgid "Please complete the previous courses in the program to enroll in this course."
msgstr "" 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}" msgid "Please install the Payments App to create a paid batch. Refer to the documentation for more details. {0}"
msgstr "" 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}" msgid "Please install the Payments App to create a paid course. Refer to the documentation for more details. {0}"
msgstr "" msgstr ""
@@ -4834,7 +4887,7 @@ msgstr ""
msgid "Preview Image" msgid "Preview Image"
msgstr "" msgstr ""
#: frontend/src/pages/CourseForm.vue:204 #: frontend/src/pages/CourseForm.vue:209
msgid "Preview Video" msgid "Preview Video"
msgstr "" msgstr ""
@@ -4849,7 +4902,7 @@ msgid "Pricing"
msgstr "" msgstr ""
#. Label of the pricing_tab (Tab Break) field in DocType 'LMS Course' #. 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 #: lms/lms/doctype/lms_course/lms_course.json
msgid "Pricing and Certification" msgid "Pricing and Certification"
msgstr "" msgstr ""
@@ -5002,7 +5055,7 @@ msgstr ""
#. Label of the published (Check) field in DocType 'LMS Course' #. Label of the published (Check) field in DocType 'LMS Course'
#: frontend/src/components/Modals/BulkCertificates.vue:51 #: frontend/src/components/Modals/BulkCertificates.vue:51
#: frontend/src/components/Modals/Event.vue:108 #: 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_batch/lms_batch.json
#: lms/lms/doctype/lms_course/lms_course.json #: lms/lms/doctype/lms_course/lms_course.json
msgid "Published" msgid "Published"
@@ -5015,7 +5068,7 @@ msgid "Published Courses"
msgstr "" msgstr ""
#. Label of the published_on (Date) field in DocType 'LMS Course' #. 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 #: lms/lms/doctype/lms_course/lms_course.json
msgid "Published On" msgid "Published On"
msgstr "" msgstr ""
@@ -5025,6 +5078,11 @@ msgstr ""
msgid "Purchased Certificate" msgid "Purchased Certificate"
msgstr "" 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 #. Option for the 'Language' (Select) field in DocType 'LMS Programming
#. Exercise' #. Exercise'
#: lms/lms/doctype/lms_programming_exercise/lms_programming_exercise.json #: 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." msgid "Quiz will appear at the bottom of the lesson."
msgstr "" 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/QuizForm.vue:396 frontend/src/pages/Quizzes.vue:275
#: frontend/src/pages/Quizzes.vue:285 lms/www/lms.py:251 #: frontend/src/pages/Quizzes.vue:285 lms/www/lms.py:251
msgid "Quizzes" msgid "Quizzes"
@@ -5182,6 +5240,11 @@ msgstr ""
msgid "Ready" msgid "Ready"
msgstr "" 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 #. Label of the reference_docname (Dynamic Link) field in DocType 'LMS Batch
#. Timetable' #. Timetable'
#: lms/lms/doctype/lms_batch_timetable/lms_batch_timetable.json #: 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' #. Label of the related_courses (Table) field in DocType 'LMS Course'
#. Name of a DocType #. Name of a DocType
#: frontend/src/components/RelatedCourses.vue:5 #: 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/lms_course/lms_course.json
#: lms/lms/doctype/related_courses/related_courses.json #: lms/lms/doctype/related_courses/related_courses.json
msgid "Related Courses" msgid "Related Courses"
msgstr "" msgstr ""
#: frontend/src/components/Controls/Uploader.vue:34 #: 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" msgid "Remove"
msgstr "" msgstr ""
@@ -5245,6 +5308,10 @@ msgstr ""
msgid "Reply To" msgid "Reply To"
msgstr "" msgstr ""
#: frontend/src/components/Modals/AnnouncementModal.vue:100
msgid "Reply To is required"
msgstr ""
#: lms/lms/widgets/RequestInvite.html:7 #: lms/lms/widgets/RequestInvite.html:7
msgid "Request Invite" msgid "Request Invite"
msgstr "" msgstr ""
@@ -5314,7 +5381,7 @@ msgstr ""
msgid "Role updated successfully" msgid "Role updated successfully"
msgstr "" msgstr ""
#: frontend/src/components/AppSidebar.vue:612 #: frontend/src/components/AppSidebar.vue:628
msgid "Roles" msgid "Roles"
msgstr "" msgstr ""
@@ -5438,7 +5505,8 @@ msgid "Search"
msgstr "" msgstr ""
#: frontend/src/components/Modals/CourseProgressSummary.vue:18 #: frontend/src/components/Modals/CourseProgressSummary.vue:18
msgid "Search by Member Name" #: frontend/src/components/Modals/VideoStatistics.vue:20
msgid "Search by Member"
msgstr "" msgstr ""
#: frontend/src/pages/CertifiedParticipants.vue:23 #: frontend/src/pages/CertifiedParticipants.vue:23
@@ -5517,21 +5585,25 @@ msgstr ""
msgid "Sessions On Days" msgid "Sessions On Days"
msgstr "" msgstr ""
#: frontend/src/components/Controls/ColorSwatches.vue:13
msgid "Set Color"
msgstr ""
#: lms/templates/emails/community_course_membership.html:1 #: lms/templates/emails/community_course_membership.html:1
msgid "Set your Password" msgid "Set your Password"
msgstr "" msgstr ""
#: frontend/src/components/AppSidebar.vue:560 #: frontend/src/components/AppSidebar.vue:576
msgid "Setting up" msgid "Setting up"
msgstr "" msgstr ""
#: frontend/src/components/AppSidebar.vue:605 #: frontend/src/components/AppSidebar.vue:621
msgid "Setting up payment gateway" msgid "Setting up payment gateway"
msgstr "" msgstr ""
#: frontend/src/components/AppSidebar.vue:610 #: frontend/src/components/AppSidebar.vue:626
#: frontend/src/components/Settings/Settings.vue:7 #: 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/ProfileRoles.vue:4
#: frontend/src/pages/ProgrammingExercises/ProgrammingExerciseSubmission.vue:19 #: frontend/src/pages/ProgrammingExercises/ProgrammingExerciseSubmission.vue:19
#: frontend/src/pages/QuizForm.vue:86 #: frontend/src/pages/QuizForm.vue:86
@@ -5547,7 +5619,7 @@ msgid "Short Description"
msgstr "" msgstr ""
#. Label of the short_introduction (Small Text) field in DocType 'LMS Course' #. 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 #: lms/lms/doctype/lms_course/lms_course.json
msgid "Short Introduction" msgid "Short Introduction"
msgstr "" msgstr ""
@@ -5874,7 +5946,7 @@ msgstr ""
msgid "Subject" msgid "Subject"
msgstr "" msgstr ""
#: frontend/src/components/Modals/AnnouncementModal.vue:93 #: frontend/src/components/Modals/AnnouncementModal.vue:94
msgid "Subject is required" msgid "Subject is required"
msgstr "" msgstr ""
@@ -5944,6 +6016,10 @@ msgstr ""
msgid "Suspicious pattern found in {0}: {1}" msgid "Suspicious pattern found in {0}: {1}"
msgstr "" msgstr ""
#: frontend/src/components/Controls/ColorSwatches.vue:50
msgid "Swatches"
msgstr ""
#. Name of a role #. Name of a role
#: lms/job/doctype/job_opportunity/job_opportunity.json #: lms/job/doctype/job_opportunity/job_opportunity.json
#: lms/job/doctype/job_settings/job_settings.json #: lms/job/doctype/job_settings/job_settings.json
@@ -6006,6 +6082,11 @@ msgstr ""
msgid "Tags" msgid "Tags"
msgstr "" 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/community_course_membership.html:18
#: lms/templates/emails/mentor_request_creation_email.html:8 #: lms/templates/emails/mentor_request_creation_email.html:8
#: lms/templates/emails/mentor_request_status_update_email.html:7 #: lms/templates/emails/mentor_request_status_update_email.html:7
@@ -6088,7 +6169,7 @@ msgstr ""
msgid "Thanks and Regards" msgid "Thanks and Regards"
msgstr "" msgstr ""
#: lms/lms/utils.py:1975 #: lms/lms/utils.py:1980
msgid "The batch is full. Please contact the Administrator." msgid "The batch is full. Please contact the Administrator."
msgstr "" msgstr ""
@@ -6175,7 +6256,7 @@ msgstr ""
msgid "This course has:" msgid "This course has:"
msgstr "" msgstr ""
#: lms/lms/utils.py:1818 #: lms/lms/utils.py:1823
msgid "This course is free." msgid "This course is free."
msgstr "" msgstr ""
@@ -6358,7 +6439,7 @@ msgstr ""
msgid "To Date" msgid "To Date"
msgstr "" msgstr ""
#: lms/lms/utils.py:1829 #: lms/lms/utils.py:1834
msgid "To join this batch, please contact the Administrator." msgid "To join this batch, please contact the Administrator."
msgstr "" msgstr ""
@@ -6495,7 +6576,7 @@ msgstr ""
#. Option for the 'Status' (Select) field in DocType 'Cohort' #. Option for the 'Status' (Select) field in DocType 'Cohort'
#. Option for the 'Status' (Select) field in DocType 'LMS Certificate Request' #. Option for the 'Status' (Select) field in DocType 'LMS Certificate Request'
#. Label of the upcoming (Check) field in DocType 'LMS Course' #. 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 #: 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_certificate_request/lms_certificate_request.json
#: lms/lms/doctype/lms_course/lms_course.json #: lms/lms/doctype/lms_course/lms_course.json
@@ -6522,7 +6603,7 @@ msgid "Update Password"
msgstr "" msgstr ""
#: frontend/src/components/Controls/Uploader.vue:20 #: 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" msgid "Upload"
msgstr "" msgstr ""
@@ -6636,6 +6717,11 @@ msgstr ""
msgid "View all feedback" msgid "View all feedback"
msgstr "" 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' #. Label of the visibility (Select) field in DocType 'LMS Batch Old'
#: lms/lms/doctype/lms_batch_old/lms_batch_old.json #: lms/lms/doctype/lms_batch_old/lms_batch_old.json
msgid "Visibility" msgid "Visibility"
@@ -6664,7 +6750,7 @@ msgid "Volunteering or Internship"
msgstr "" msgstr ""
#. Label of the watch_time (Data) field in DocType 'LMS Video Watch Duration' #. 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 #: lms/lms/doctype/lms_video_watch_duration/lms_video_watch_duration.json
msgid "Watch Time" msgid "Watch Time"
msgstr "" msgstr ""
@@ -6764,6 +6850,11 @@ msgstr ""
msgid "Write your answer here" msgid "Write your answer here"
msgstr "" 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 #: 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}." msgid "You already have an evaluation on {0} at {1} for the course {2}."
msgstr "" msgstr ""
@@ -7000,6 +7091,11 @@ msgstr ""
msgid "activity" msgid "activity"
msgstr "" 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 #: frontend/src/components/JobCard.vue:26 frontend/src/pages/JobDetail.vue:108
msgid "applicant" msgid "applicant"
msgstr "" msgstr ""
@@ -7032,7 +7128,8 @@ msgstr ""
msgid "certified members" msgid "certified members"
msgstr "" 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" msgid "completed"
msgstr "" msgstr ""
@@ -7068,6 +7165,10 @@ msgstr ""
msgid "of" msgid "of"
msgstr "" msgstr ""
#: frontend/src/components/CourseInstructors.vue:41
msgid "others"
msgstr ""
#: lms/templates/quiz/quiz.js:141 #: lms/templates/quiz/quiz.js:141
msgid "out of" msgid "out of"
msgstr "" msgstr ""
@@ -7164,7 +7265,7 @@ msgstr ""
msgid "{0} is your evaluator" msgid "{0} is your evaluator"
msgstr "" msgstr ""
#: lms/lms/utils.py:686 #: lms/lms/utils.py:689
msgid "{0} mentioned you in a comment" msgid "{0} mentioned you in a comment"
msgstr "" msgstr ""
@@ -7172,7 +7273,7 @@ msgstr ""
msgid "{0} mentioned you in a comment in your batch." msgid "{0} mentioned you in a comment in your batch."
msgstr "" 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}" msgid "{0} mentioned you in a comment in {1}"
msgstr "" msgstr ""
+433 -176
View File
File diff suppressed because it is too large Load Diff
+433 -176
View File
File diff suppressed because it is too large Load Diff
+434 -177
View File
File diff suppressed because it is too large Load Diff
+434 -177
View File
File diff suppressed because it is too large Load Diff
+433 -176
View File
File diff suppressed because it is too large Load Diff
+437 -180
View File
File diff suppressed because it is too large Load Diff
+437 -180
View File
File diff suppressed because it is too large Load Diff
+436 -179
View File
File diff suppressed because it is too large Load Diff
+438 -181
View File
File diff suppressed because it is too large Load Diff
+434 -177
View File
File diff suppressed because it is too large Load Diff
+433 -176
View File
File diff suppressed because it is too large Load Diff
+435 -178
View File
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -25,7 +25,7 @@
}, },
"homepage": "https://github.com/frappe/lms#readme", "homepage": "https://github.com/frappe/lms#readme",
"devDependencies": { "devDependencies": {
"cypress": "^13.9.0", "cypress": "^14.5.2",
"cypress-file-upload": "^5.0.8", "cypress-file-upload": "^5.0.8",
"cypress-real-events": "^1.14.0" "cypress-real-events": "^1.14.0"
}, },
+1 -1
View File
@@ -13,7 +13,7 @@ dependencies = [
"markdown~=3.5.1", "markdown~=3.5.1",
"beautifulsoup4~=4.12.2", "beautifulsoup4~=4.12.2",
"lxml~=4.9.3", "lxml~=4.9.3",
"cairocffi~=1.6.1", "cairocffi==1.5.1",
"razorpay~=1.4.1", "razorpay~=1.4.1",
"fuzzywuzzy~=0.18.0", "fuzzywuzzy~=0.18.0",
] ]
+1374
View File
File diff suppressed because it is too large Load Diff