Compare commits

...

753 Commits

Author SHA1 Message Date
Frappe PR Bot 22b8222c3d chore(release): Bumped to Version 2.34.1 2025-09-03 05:39:26 +00:00
Jannat Patel b3d1b14abd Merge pull request #1708 from pateljannat/issues-130
fix: misc issues
2025-09-03 10:41:52 +05:30
Jannat Patel f1cd0d3dd4 fix: misc issues 2025-09-02 19:57:32 +05:30
Jannat Patel 3d7815d65f Merge pull request #1369 from FahidLatheef/fix/continue-learning
fix: fixed issue with Lesson Render for SCORM Chapters
2025-09-02 09:48:25 +05:30
Fahid Latheef Alungal a3e7d1f981 refactor: derive is_scorm_package from Lesson instead of Chapter 2025-09-02 02:29:19 +05:30
Fahid Latheef Alungal 2e79190977 fix: added Patch to update wrong indexes for SCORM Lesson References 2025-09-02 01:36:18 +05:30
Fahid Latheef Alungal ee7debeef7 fix: added missing index for first lesson for SCORM 2025-09-02 00:28:26 +05:30
Fahid Latheef Alungal 61f547733c fix: Continue Learning button not working for SCORM Chapters 2025-09-01 23:51:46 +05:30
Jannat Patel 5129e6d6ac Merge pull request #1707 from frappe/pot_develop_2025-08-29
chore: update POT file
2025-09-01 10:00:25 +05:30
Hussain Nagaria 0a9c14f8b1 Merge pull request #1706 from vishwajeet-13/fix/mobile-view-for-live-class 2025-08-31 19:55:06 +05:30
vishwajeet a1960489e1 chore: ran pre-commit 2025-08-30 11:34:31 +05:30
frappe-pr-bot 66fe1a83c6 chore: update POT file 2025-08-29 16:04:45 +00:00
vishwajeet a88d384ac7 fix: mobile responsive live class card 2025-08-29 19:04:33 +05:30
Jannat Patel 2dc6b68974 Merge branch 'develop' of https://github.com/frappe/lms into develop 2025-08-29 17:44:03 +05:30
Jannat Patel 0988ecc515 fix: misc issues on home page 2025-08-29 17:43:44 +05:30
Frappe PR Bot 5ff100da27 chore(release): Bumped to Version 2.34.0 2025-08-29 11:16:37 +00:00
Jannat Patel f218846f4a Merge pull request #1700 from vishwajeet-13/fix/ui-text-correction
fix(ui): correct mislabeled subtitle in statistics
2025-08-28 12:28:14 +05:30
Jannat Patel fc0aba60b9 Merge pull request #1704 from frappe/l10n_develop2
chore: sync translations from crowdin
2025-08-28 12:26:29 +05:30
Jannat Patel 82e6a62c54 chore: Norwegian Bokmal translations 2025-08-28 09:48:09 +05:30
Jannat Patel fce8950cc5 chore: Danish translations 2025-08-28 09:48:08 +05:30
Jannat Patel 54247e85e0 Merge pull request #1696 from frappe/l10n_develop2
chore: sync translations from crowdin
2025-08-27 20:39:35 +05:30
vishwajeet cc3f9cd8a4 fix(ui): correct mislabeled subtitle in statistics 2025-08-27 17:43:09 +05:30
Jannat Patel 6dd79dc5d2 chore: Norwegian Bokmal translations 2025-08-27 09:36:52 +05:30
Jannat Patel 6eb53770c3 chore: Danish translations 2025-08-27 09:36:51 +05:30
Jannat Patel 10faeca121 chore: Serbian (Latin) translations 2025-08-27 09:36:49 +05:30
Jannat Patel 38252d9ffc chore: Thai translations 2025-08-27 09:36:46 +05:30
Jannat Patel 86f62aeb99 chore: Portuguese, Brazilian translations 2025-08-27 09:36:43 +05:30
Jannat Patel e3a6bb8781 chore: Turkish translations 2025-08-27 09:36:40 +05:30
Jannat Patel 8ed94ed12f chore: Serbian (Cyrillic) translations 2025-08-27 09:36:37 +05:30
Jannat Patel 95bcfc2ee0 chore: Russian translations 2025-08-27 09:36:36 +05:30
Jannat Patel 313e4811a1 chore: Polish translations 2025-08-27 09:36:33 +05:30
Jannat Patel 5bb9dfeaf8 chore: Spanish translations 2025-08-27 09:36:27 +05:30
Jannat Patel aabb316c7d Merge pull request #1694 from pateljannat/home-page
feat: home page
2025-08-26 12:30:01 +05:30
Jannat Patel ae40e6f41b fix: batch forms labels to add more clarity 2025-08-26 12:17:26 +05:30
Jannat Patel e644d5d20d fix: lesson progress check issue 2025-08-26 11:35:28 +05:30
Jannat Patel ae9abd08ff chore: French translations 2025-08-26 08:58:40 +05:30
Jannat Patel 965128c802 chore: Esperanto translations 2025-08-26 08:58:38 +05:30
Jannat Patel 26324a63df chore: Serbian (Latin) translations 2025-08-26 08:58:37 +05:30
Jannat Patel b9d9754818 chore: Bosnian translations 2025-08-26 08:58:36 +05:30
Jannat Patel 0fb516a86c chore: Croatian translations 2025-08-26 08:58:34 +05:30
Jannat Patel 4b4c7d8927 chore: Thai translations 2025-08-26 08:58:33 +05:30
Jannat Patel e3cae93bd3 chore: Persian translations 2025-08-26 08:58:31 +05:30
Jannat Patel 324f87dc19 chore: Indonesian translations 2025-08-26 08:58:30 +05:30
Jannat Patel 347b5c9411 chore: Portuguese, Brazilian translations 2025-08-26 08:58:29 +05:30
Jannat Patel 7371f8a29d chore: Vietnamese translations 2025-08-26 08:58:27 +05:30
Jannat Patel fb381a30cf chore: Chinese Simplified translations 2025-08-26 08:58:26 +05:30
Jannat Patel 474e3dae65 chore: Turkish translations 2025-08-26 08:58:24 +05:30
Jannat Patel 738ac3f1c9 chore: Swedish translations 2025-08-26 08:58:23 +05:30
Jannat Patel 2fbe69afc5 chore: Serbian (Cyrillic) translations 2025-08-26 08:58:21 +05:30
Jannat Patel 693448cb42 chore: Russian translations 2025-08-26 08:58:20 +05:30
Jannat Patel 17a416d905 chore: Portuguese translations 2025-08-26 08:58:18 +05:30
Jannat Patel 09a6ede925 chore: Polish translations 2025-08-26 08:58:17 +05:30
Jannat Patel 5fc04ae318 chore: Dutch translations 2025-08-26 08:58:15 +05:30
Jannat Patel 900ea0fb5c chore: Italian translations 2025-08-26 08:58:14 +05:30
Jannat Patel 85a6a1d884 chore: Hungarian translations 2025-08-26 08:58:12 +05:30
Jannat Patel 0572009456 chore: German translations 2025-08-26 08:58:11 +05:30
Jannat Patel 9bcd98cb69 chore: Czech translations 2025-08-26 08:58:10 +05:30
Jannat Patel aefce2a842 chore: Arabic translations 2025-08-26 08:58:08 +05:30
Jannat Patel ee4fe99b08 chore: Spanish translations 2025-08-26 08:58:07 +05:30
Jannat Patel 2cb85d47b8 chore: removed unused files 2025-08-25 15:37:00 +05:30
Jannat Patel 0713b6b419 fix: streak logic 2025-08-25 15:33:05 +05:30
Jannat Patel 8f116fddab Merge pull request #1695 from frappe/pot_develop_2025-08-22
chore: update POT file
2025-08-25 10:08:06 +05:30
Jannat Patel 58838cd806 Merge branch 'develop' into pot_develop_2025-08-22 2025-08-25 10:07:59 +05:30
Jannat Patel 7d5918b320 fix: program list heading 2025-08-25 09:57:00 +05:30
Jannat Patel 7eb4ca0fb4 fix: live class end time 2025-08-25 09:22:59 +05:30
Jannat Patel d575bfa0fb feat: streak details 2025-08-25 09:19:03 +05:30
Jannat Patel b60ea3f153 chore: Indonesian translations 2025-08-25 08:59:48 +05:30
Jannat Patel 6e7bc6cfb4 chore: Italian translations 2025-08-25 08:59:39 +05:30
frappe-pr-bot 9b85a0044c chore: update POT file 2025-08-22 16:04:56 +00:00
Jannat Patel b8708382b1 feat: Admin Home 2025-08-22 16:47:25 +05:30
Jannat Patel e0601c7b38 feat: home page 2025-08-21 21:16:21 +05:30
Jannat Patel 0725714144 Merge pull request #1692 from pateljannat/issues-129
fix: misc issues
2025-08-20 18:31:43 +05:30
Jannat Patel ab501e1c6a fix: filter when getting programming exercise title when adding to lesson 2025-08-20 17:18:47 +05:30
Jannat Patel 7cd401327b fix: misc issues 2025-08-20 17:13:23 +05:30
Jannat Patel e886088dff Merge pull request #1686 from pateljannat/program-refactor
refactor: learning path
2025-08-20 14:09:41 +05:30
Jannat Patel ebe7cc32af fix: misc issues 2025-08-20 13:30:31 +05:30
Jannat Patel 5e607c3b8e refactor: sidebar visibility of programs 2025-08-20 13:11:22 +05:30
Jannat Patel 5ec809e3dd feat: program progress summary 2025-08-20 12:06:52 +05:30
Jannat Patel 9d3b6e0556 feat: program self enrollment 2025-08-19 17:33:20 +05:30
Jannat Patel acd003814a refactor: program list for students 2025-08-18 15:51:07 +05:30
Jannat Patel c4991d09f3 Merge pull request #1688 from frappe/l10n_develop2
chore: sync translations from crowdin
2025-08-18 11:26:35 +05:30
Jannat Patel ca9b2dada3 chore: Serbian (Latin) translations 2025-08-18 08:04:07 +05:30
Jannat Patel 6a3004d75d chore: Serbian (Cyrillic) translations 2025-08-18 08:03:56 +05:30
Jannat Patel 9bd624f31a chore: Swedish translations 2025-08-17 07:45:41 +05:30
Jannat Patel acb264da20 chore: French translations 2025-08-16 07:25:45 +05:30
Jannat Patel c23b0fbfe7 chore: Esperanto translations 2025-08-16 07:25:44 +05:30
Jannat Patel 0e78b5e289 chore: Serbian (Latin) translations 2025-08-16 07:25:42 +05:30
Jannat Patel 91ab895a1d chore: Bosnian translations 2025-08-16 07:25:41 +05:30
Jannat Patel 5c37238114 chore: Croatian translations 2025-08-16 07:25:39 +05:30
Jannat Patel 3cf40ba504 chore: Thai translations 2025-08-16 07:25:38 +05:30
Jannat Patel 9229961f72 chore: Persian translations 2025-08-16 07:25:36 +05:30
Jannat Patel 0b35b11909 chore: Indonesian translations 2025-08-16 07:25:35 +05:30
Jannat Patel 77a27d8716 chore: Portuguese, Brazilian translations 2025-08-16 07:25:34 +05:30
Jannat Patel be32939029 chore: Vietnamese translations 2025-08-16 07:25:32 +05:30
Jannat Patel 4f046425aa chore: Chinese Simplified translations 2025-08-16 07:25:31 +05:30
Jannat Patel 89bf6b1fd9 chore: Turkish translations 2025-08-16 07:25:30 +05:30
Jannat Patel d81414cca6 chore: Swedish translations 2025-08-16 07:25:28 +05:30
Jannat Patel d8eb96a37a chore: Serbian (Cyrillic) translations 2025-08-16 07:25:27 +05:30
Jannat Patel d1b3b0ba34 chore: Russian translations 2025-08-16 07:25:25 +05:30
Jannat Patel dca207347f chore: Portuguese translations 2025-08-16 07:25:24 +05:30
Jannat Patel 5a7ebf8027 chore: Polish translations 2025-08-16 07:25:23 +05:30
Jannat Patel f6d877715c chore: Dutch translations 2025-08-16 07:25:22 +05:30
Jannat Patel 754eb32db4 chore: Italian translations 2025-08-16 07:25:20 +05:30
Jannat Patel 67c04c7f59 chore: Hungarian translations 2025-08-16 07:25:19 +05:30
Jannat Patel daebb26ffa chore: German translations 2025-08-16 07:25:18 +05:30
Jannat Patel 3e2993d048 chore: Czech translations 2025-08-16 07:25:16 +05:30
Jannat Patel 2745cf4015 chore: Arabic translations 2025-08-16 07:25:15 +05:30
Jannat Patel 4e99e2960c chore: Spanish translations 2025-08-16 07:25:13 +05:30
Jannat Patel 649028307d Merge pull request #1687 from frappe/pot_develop_2025-08-15
chore: update POT file
2025-08-15 21:57:49 +05:30
frappe-pr-bot 24787c32dd chore: update POT file 2025-08-15 16:04:47 +00:00
Jannat Patel 625ddac65a refactor: learning path 2025-08-14 20:26:46 +05:30
Jannat Patel 78ff2e6d07 Merge pull request #1684 from pateljannat/issues-128
fix: misc ui issues
2025-08-12 15:14:35 +05:30
Jannat Patel 837426d3c5 Merge pull request #1683 from frappe/l10n_develop2
chore: sync translations from crowdin
2025-08-12 15:03:47 +05:30
Jannat Patel 65477a5b2b fix: misc ui issues 2025-08-12 15:03:19 +05:30
Jannat Patel a9fa8be5a2 chore: French translations 2025-08-12 06:48:19 +05:30
Jannat Patel 0bb26150d6 chore: Esperanto translations 2025-08-12 06:48:17 +05:30
Jannat Patel fd8f7ade51 chore: Serbian (Latin) translations 2025-08-12 06:48:16 +05:30
Jannat Patel 3310bc2fed chore: Bosnian translations 2025-08-12 06:48:14 +05:30
Jannat Patel 0cbb640054 chore: Croatian translations 2025-08-12 06:48:13 +05:30
Jannat Patel b4aa88ac33 chore: Thai translations 2025-08-12 06:48:11 +05:30
Jannat Patel f2906e57f0 chore: Persian translations 2025-08-12 06:48:10 +05:30
Jannat Patel 8d381b167e chore: Indonesian translations 2025-08-12 06:48:08 +05:30
Jannat Patel d78e316349 chore: Portuguese, Brazilian translations 2025-08-12 06:48:07 +05:30
Jannat Patel 82d00c6a4a chore: Vietnamese translations 2025-08-12 06:48:05 +05:30
Jannat Patel ba4fd7f1ef chore: Chinese Simplified translations 2025-08-12 06:48:04 +05:30
Jannat Patel 591d265167 chore: Turkish translations 2025-08-12 06:48:02 +05:30
Jannat Patel af9d973a91 chore: Swedish translations 2025-08-12 06:48:01 +05:30
Jannat Patel 15096a3118 chore: Serbian (Cyrillic) translations 2025-08-12 06:47:59 +05:30
Jannat Patel 7c78a9697c chore: Russian translations 2025-08-12 06:47:58 +05:30
Jannat Patel 3f5051f697 chore: Portuguese translations 2025-08-12 06:47:56 +05:30
Jannat Patel 044a66f2e8 chore: Polish translations 2025-08-12 06:47:55 +05:30
Jannat Patel 6fb8775bb7 chore: Dutch translations 2025-08-12 06:47:53 +05:30
Jannat Patel f16d0300ba chore: Italian translations 2025-08-12 06:47:52 +05:30
Jannat Patel 84e12f1eea chore: Hungarian translations 2025-08-12 06:47:50 +05:30
Jannat Patel ba40b7ff15 chore: German translations 2025-08-12 06:47:49 +05:30
Jannat Patel e733326056 chore: Czech translations 2025-08-12 06:47:47 +05:30
Jannat Patel b3d987e933 chore: Arabic translations 2025-08-12 06:47:46 +05:30
Jannat Patel e919874e9e chore: Spanish translations 2025-08-12 06:47:44 +05:30
Jannat Patel a2f51f151a Merge pull request #1681 from pateljannat/pre-commit
chore: formatted files with ruff
2025-08-11 21:30:17 +05:30
Jannat Patel ea288abb27 test: fixed batch overlay test 2025-08-11 20:06:43 +05:30
Jannat Patel b274900ae0 chore: removed profile page renderers 2025-08-11 19:34:22 +05:30
Jannat Patel 10d29d3a3f chore: formatted js with eslint 2025-08-11 17:56:51 +05:30
Jannat Patel a92e04343a chore: formatted files with ruff 2025-08-11 17:48:37 +05:30
Jannat Patel be31c18bfe Merge pull request #1680 from pateljannat/pwa
feat: PWA
2025-08-11 17:26:42 +05:30
Jannat Patel 9d0a5fd8f3 chore: updated pre-commit config 2025-08-11 17:06:35 +05:30
Jannat Patel 5ca577bc0a fix: responsive layout for pages 2025-08-11 17:02:46 +05:30
Jannat Patel 4d25d185c3 feat: PWA 2025-08-11 15:43:34 +05:30
Jannat Patel ea289e02da Merge pull request #1679 from frappe/l10n_develop2
chore: sync translations from crowdin
2025-08-11 11:22:56 +05:30
Jannat Patel a3f16eb7e8 Merge pull request #1678 from frappe/pot_develop_2025-08-08
chore: update POT file
2025-08-11 11:22:41 +05:30
Jannat Patel ddd29abe99 chore: Italian translations 2025-08-11 06:48:11 +05:30
Jannat Patel decd56f2d2 chore: Italian translations 2025-08-10 06:47:20 +05:30
frappe-pr-bot 387401cb44 chore: update POT file 2025-08-08 16:04:31 +00:00
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
Frappe PR Bot f187cc9314 chore(release): Bumped to Version 2.32.0 2025-07-14 06:37:43 +00:00
Jannat Patel c15c6374f9 Merge pull request #1635 from pateljannat/issues-119
fix: quiz progress issue
2025-07-14 11:51:51 +05:30
Jannat Patel acec382dfe fix: quiz progress issue 2025-07-14 11:40:55 +05:30
Jannat Patel fbc078c6b6 Merge pull request #1632 from frappe/pot_develop_2025-07-11
chore: update POT file
2025-07-14 09:37:09 +05:30
frappe-pr-bot 170b20185a chore: update POT file 2025-07-11 16:04:39 +00:00
Jannat Patel 3e8489c13b Merge pull request #1631 from pateljannat/issues-118
fix: misc issues
2025-07-11 11:14:36 +05:30
Jannat Patel 18dfc4c23e test: changed CTA labels 2025-07-11 11:06:29 +05:30
Jannat Patel e6bae3dc77 fix: changed CTA labels on lists to Create 2025-07-11 10:44:04 +05:30
Jannat Patel 6f9f27c030 fix: delete batch and pass fields prop to brand settings 2025-07-10 22:21:26 +05:30
Jannat Patel 874bef74c7 Merge pull request #1623 from JoeBrar/feature/reorder-chapters
feat: added chapter re-ordering functionality for courses
2025-07-10 12:20:04 +05:30
Jannat Patel ad483e0916 Merge pull request #1630 from frappe/l10n_develop2
chore: sync translations from crowdin
2025-07-10 12:19:49 +05:30
Jannat Patel 5b4bbaec20 chore: Portuguese translations 2025-07-10 01:07:19 +05:30
Jannat Patel b8ae0db0bd Merge pull request #1627 from pateljannat/badge-management
feat: badge management from settings
2025-07-09 10:46:58 +05:30
Jannat Patel f2c18fad52 fix: misc improvements to badge flow 2025-07-08 19:41:08 +05:30
Jannat Patel 9716655b94 Merge pull request #1626 from frappe/l10n_develop2
chore: sync translations from crowdin
2025-07-08 14:16:27 +05:30
Jannat Patel efb317191c feat: badge assignment from settings 2025-07-08 14:12:46 +05:30
Jannat Patel a47b5db40c chore: Serbian (Cyrillic) translations 2025-07-08 01:10:50 +05:30
Jannat Patel ec94796b9c chore: Italian translations 2025-07-08 01:10:48 +05:30
Jannat Patel e3e0cd61a2 chore: Vietnamese translations 2025-07-08 01:10:47 +05:30
Jannat Patel a438473279 chore: Dutch translations 2025-07-08 01:10:46 +05:30
Jannat Patel 12b5b8b509 chore: Czech translations 2025-07-08 01:10:44 +05:30
Jannat Patel 22442b47a8 chore: Esperanto translations 2025-07-08 01:10:43 +05:30
Jannat Patel 30c8b7d64f chore: Chinese Simplified translations 2025-07-08 01:10:41 +05:30
Jannat Patel b643575c4f chore: Serbian (Latin) translations 2025-07-08 01:10:40 +05:30
Jannat Patel 7dd7124fac chore: Bosnian translations 2025-07-08 01:10:38 +05:30
Jannat Patel 4b1eebf5bb chore: Croatian translations 2025-07-08 01:10:37 +05:30
Jannat Patel 3257943926 chore: Thai translations 2025-07-08 01:10:36 +05:30
Jannat Patel 24246c83e0 chore: Persian translations 2025-07-08 01:10:34 +05:30
Jannat Patel a26787f478 chore: Portuguese, Brazilian translations 2025-07-08 01:10:33 +05:30
Jannat Patel ec3b88f890 chore: Turkish translations 2025-07-08 01:10:31 +05:30
Jannat Patel 7f5f1dad92 chore: Swedish translations 2025-07-08 01:10:30 +05:30
Jannat Patel b6db128214 chore: Russian translations 2025-07-08 01:10:29 +05:30
Jannat Patel 8831635db2 chore: Portuguese translations 2025-07-08 01:10:28 +05:30
Jannat Patel e19198b720 chore: Polish translations 2025-07-08 01:10:26 +05:30
Jannat Patel f618d9dc1a chore: Hungarian translations 2025-07-08 01:10:25 +05:30
Jannat Patel 66a667a0a3 chore: German translations 2025-07-08 01:10:23 +05:30
Jannat Patel 8a4c67f712 chore: Arabic translations 2025-07-08 01:10:22 +05:30
Jannat Patel fa6ef2e989 chore: Spanish translations 2025-07-08 01:10:21 +05:30
Jannat Patel 7450b99197 chore: French translations 2025-07-08 01:10:19 +05:30
Jannat Patel 023fd272b1 feat: badge list and form 2025-07-07 16:44:48 +05:30
Jannat Patel 84067cb027 Merge pull request #1621 from JoeBrar/fix/image-upload
fix: Changed image extension validation to MIME type validation
2025-07-07 13:01:18 +05:30
Jannat Patel 3087ef70e7 Merge pull request #1619 from JoeBrar/fix/lms-issues
fix: ensure tags wrap correctly to prevent overflow and breaking of layout
2025-07-07 12:59:26 +05:30
Jannat Patel 387385bb1c Merge pull request #1624 from frappe/l10n_develop2
chore: sync translations from crowdin
2025-07-07 11:13:27 +05:30
Jannat Patel 6766d0d08c Merge pull request #1622 from frappe/pot_develop_2025-07-04
chore: update POT file
2025-07-07 11:13:03 +05:30
Jannat Patel 371d890793 chore: Persian translations 2025-07-07 01:14:35 +05:30
Joedeep Singh 57046c1b38 feat: added chapter re-ordering functionality for courses 2025-07-06 17:02:27 +00:00
frappe-pr-bot 2a64144e94 chore: update POT file 2025-07-04 16:04:41 +00:00
Joedeep Singh 9b0320ccf1 chore: fixed the linter issues 2025-07-04 14:02:13 +00:00
Joedeep Singh 23f209131e Merge branch 'develop' into fix/image-upload 2025-07-04 18:57:03 +05:30
Joedeep Singh d71f1c7f9a refactor: updated the validateFile function in utils and reused it 2025-07-04 13:22:25 +00:00
Jannat Patel d21ea2c854 Merge pull request #1618 from pateljannat/member-list-refactor
fix: improved members and evaluators list and form
2025-07-04 12:33:44 +05:30
Jannat Patel cd7f3ba820 Merge pull request #1620 from frappe/l10n_develop2
chore: sync translations from crowdin
2025-07-04 12:28:37 +05:30
Jannat Patel e057d3ed9a fix: improved evaluators list 2025-07-04 12:26:57 +05:30
Joedeep Singh 5f04607a44 fix: Changed image extension validation to MIME type validation 2025-07-03 20:41:23 +00:00
Jannat Patel 9440d13a08 chore: Serbian (Cyrillic) translations 2025-07-04 01:23:16 +05:30
Jannat Patel 85c4f1654e chore: Serbian (Latin) translations 2025-07-04 01:23:15 +05:30
Joedeep Singh eed339cc64 fix: ensure tags wrap correctly to prevent overflow and breaking of layout 2025-07-03 18:25:04 +00:00
Jannat Patel 3d1a23576a fix: repositioned the search bar on members list 2025-07-03 19:40:09 +05:30
Jannat Patel ed0e2e4bb5 fix: issue with roles on profile page 2025-07-03 19:26:07 +05:30
Jannat Patel 954d0a0637 fix: improved members list and form 2025-07-03 18:19:29 +05:30
Jannat Patel f2c8788602 Merge pull request #1613 from Grumbled0rf/patch-1
Update README.md
2025-07-03 13:21:33 +05:30
Jannat Patel 49c63da27c Merge pull request #1614 from mahsem/patch-state
fix: state_translatability
2025-07-03 13:21:06 +05:30
Jannat Patel 24496d1856 Merge pull request #1617 from pateljannat/course-progress-summary
feat: course progress summary report
2025-07-03 13:20:41 +05:30
Jannat Patel 991ebe09a2 chore: linters 2025-07-03 13:09:45 +05:30
Jannat Patel 85da4f6d85 feat: course progress summary report 2025-07-03 13:02:57 +05:30
Jannat Patel 5f065db991 Merge pull request #1616 from pateljannat/issues-117
fix: when logo is updated from brand settings, update the login logo too
2025-07-02 16:56:20 +05:30
Manoj Prabhkaran D ffb40586d7 Merge branch 'develop' into patch-1 2025-07-02 15:10:59 +04:00
Jannat Patel fcfd87fd50 fix: when logo is updated from brand settings, update the login logo too 2025-07-02 16:16:49 +05:30
Jannat Patel eb5b12aa7b Merge pull request #1615 from pateljannat/course-list-fetch
fix: page length issue on course list
2025-07-02 15:41:23 +05:30
Manoj Prabhkaran D f6e2438744 docs(readme): add DNS configuration note to avoid 404 error during self-hosting 2025-07-02 13:43:10 +04:00
Jannat Patel e3c7dc695d fix: page length issue on batch list 2025-07-02 15:01:33 +05:30
Jannat Patel 82d2025e6c fix: page length issue on course list 2025-07-02 15:00:53 +05:30
mahsem 91b82d78b8 fix: state _translatability 2025-07-02 11:03:33 +02:00
Jannat Patel b97e792893 Merge pull request #1609 from pateljannat/track-video-watch-duration
feat: video watch time tracking
2025-07-02 10:53:56 +05:30
Jannat Patel 13ac5ec7dc fix: sidebar settings for programming exercises 2025-07-02 10:47:03 +05:30
Jannat Patel 199f880936 Merge pull request #1612 from addeeandra/fix-sidebar-settings-programming-exercise
feat: sidebar settings to toggle "Programming Exercise" menu
2025-07-02 10:33:47 +05:30
Jannat Patel ed86c207ba Merge pull request #1610 from frappe/l10n_develop2
chore: sync translations from crowdin
2025-07-01 19:18:51 +05:30
Jannat Patel b4cf290f4d fix: allow backward seek but prevent forward seek 2025-07-01 19:16:13 +05:30
Jannat Patel e526a6fd64 fix: moved sirebar settings to settings store 2025-07-01 17:38:15 +05:30
Jannat Patel 94cbbf169a feat: prevent skipping videos 2025-07-01 17:27:43 +05:30
Jannat Patel 2837ed16a7 feat: track watch time for youtube and vimeo 2025-07-01 16:55:55 +05:30
Aditya Chandra 68961deb6b revert: api.py 2025-07-01 12:52:52 +07:00
Aditya Chandra ec54bfee98 fix: programming exercise's sidebar settings 2025-07-01 12:39:50 +07:00
Jannat Patel 385e97b76a chore: Serbian (Cyrillic) translations 2025-07-01 00:39:53 +05:30
Jannat Patel cbd916877f chore: Italian translations 2025-07-01 00:39:51 +05:30
Jannat Patel 38586034cd chore: Vietnamese translations 2025-07-01 00:39:50 +05:30
Jannat Patel 62b3ba2bff chore: Dutch translations 2025-07-01 00:39:49 +05:30
Jannat Patel dd470b61b5 chore: Czech translations 2025-07-01 00:39:47 +05:30
Jannat Patel 4fa92d2327 chore: Esperanto translations 2025-07-01 00:39:46 +05:30
Jannat Patel 6f6c2db66d chore: Chinese Simplified translations 2025-07-01 00:39:44 +05:30
Jannat Patel e6348cfa20 chore: Serbian (Latin) translations 2025-07-01 00:39:43 +05:30
Jannat Patel a006d1000a chore: Bosnian translations 2025-07-01 00:39:42 +05:30
Jannat Patel 4a575e642f chore: Croatian translations 2025-07-01 00:39:40 +05:30
Jannat Patel 93525bc577 chore: Thai translations 2025-07-01 00:39:39 +05:30
Jannat Patel 2cf0e9a723 chore: Persian translations 2025-07-01 00:39:36 +05:30
Jannat Patel c32164bfea chore: Portuguese, Brazilian translations 2025-07-01 00:39:35 +05:30
Jannat Patel 714b0924e7 chore: Turkish translations 2025-07-01 00:39:34 +05:30
Jannat Patel 43079790a8 chore: Swedish translations 2025-07-01 00:39:33 +05:30
Jannat Patel d03e61b625 chore: Russian translations 2025-07-01 00:39:31 +05:30
Jannat Patel 2d760112a3 chore: Portuguese translations 2025-07-01 00:39:30 +05:30
Jannat Patel f46507ec72 chore: Polish translations 2025-07-01 00:39:28 +05:30
Jannat Patel e9e10bdc93 chore: Hungarian translations 2025-07-01 00:39:27 +05:30
Jannat Patel 0386967a32 chore: German translations 2025-07-01 00:39:25 +05:30
Jannat Patel 4900fc8b88 chore: Arabic translations 2025-07-01 00:39:24 +05:30
Jannat Patel 99294b5643 chore: Spanish translations 2025-07-01 00:39:22 +05:30
Jannat Patel eb12bcb83c chore: French translations 2025-07-01 00:39:21 +05:30
Jannat Patel 22a2e57642 fix: fetch stats when lesson is loaded 2025-06-30 20:00:13 +05:30
Jannat Patel 5eaae06ceb feat: video watch time tracking 2025-06-30 19:56:07 +05:30
Jannat Patel ce7fc35349 Merge pull request #1606 from pateljannat/sidebar-toggle-issue
chore: upgraded frappe ui
2025-06-30 11:57:06 +05:30
Jannat Patel 8d4b5c83ae chore: upgraded frappe ui 2025-06-30 11:49:22 +05:30
Jannat Patel cbd3c56ca0 Merge pull request #1605 from frappe/l10n_develop2
chore: sync translations from crowdin
2025-06-30 11:42:20 +05:30
Jannat Patel be6dad1424 Merge pull request #1603 from frappe/pot_develop_2025-06-27
chore: update POT file
2025-06-30 11:42:07 +05:30
Jannat Patel 298452fa7b Merge pull request #1602 from pateljannat/negative-marking-in-quiz
feat: negative marking in quiz
2025-06-30 11:41:53 +05:30
Jannat Patel 4abbd7c35c chore: Bosnian translations 2025-06-30 00:27:08 +05:30
Jannat Patel c2f51c51ab chore: Croatian translations 2025-06-30 00:27:07 +05:30
Jannat Patel 255cff6664 chore: Turkish translations 2025-06-30 00:27:04 +05:30
Jannat Patel 8a9578bb0a chore: German translations 2025-06-30 00:26:59 +05:30
Jannat Patel 8831f6cecc chore: Arabic translations 2025-06-30 00:26:58 +05:30
Jannat Patel f3daa7e48b chore: Spanish translations 2025-06-30 00:26:56 +05:30
Jannat Patel 6163597958 chore: French translations 2025-06-30 00:26:55 +05:30
frappe-pr-bot f9e1222065 chore: update POT file 2025-06-27 16:04:48 +00:00
Jannat Patel 7d85de7c6c fix: set default marks to cut as 1 2025-06-27 20:00:57 +05:30
Jannat Patel cf452c2300 feat: negative marking in quiz 2025-06-27 19:58:35 +05:30
Jannat Patel 72bd1d548d Merge pull request #1600 from frappe/l10n_develop2
chore: sync translations from crowdin
2025-06-27 16:51:01 +05:30
Jannat Patel 4556f4dee6 chore: Thai translations 2025-06-26 23:43:56 +05:30
Jannat Patel 3dfbd3165a chore: German translations 2025-06-26 23:43:54 +05:30
Jannat Patel 02b8e02131 Merge pull request #1599 from pateljannat/issues-116
fix: misc issues
2025-06-26 16:53:06 +05:30
Jannat Patel 087ded9f9e chore: removed console 2025-06-26 16:37:05 +05:30
Jannat Patel 21f122ee82 fix: misc issues 2025-06-26 16:35:03 +05:30
Jannat Patel d60a7e8c94 Merge pull request #1593 from pateljannat/programming-exercises
feat: programming exercises
2025-06-26 13:05:22 +05:30
Jannat Patel b8981c249f feat: livecode settings 2025-06-25 19:57:07 +05:30
Jannat Patel e71275a0dc feat: javascript exercises 2025-06-25 12:15:27 +05:30
Jannat Patel 4fb0db7a1e fix: test case with no input issue 2025-06-24 12:22:02 +05:30
Jannat Patel 1e9beedc77 Merge pull request #1591 from frappe/l10n_develop2
chore: sync translations from crowdin
2025-06-24 09:50:23 +05:30
Jannat Patel 4a4a0653ef chore: Serbian (Cyrillic) translations 2025-06-23 22:25:04 +05:30
Jannat Patel c80a900277 chore: Serbian (Latin) translations 2025-06-23 22:25:02 +05:30
Jannat Patel 6fb0394d96 Merge pull request #1588 from frappe/l10n_develop2
chore: sync translations from crowdin
2025-06-23 18:55:28 +05:30
Jannat Patel a6a7712039 chore: Serbian (Latin) translations 2025-06-22 22:14:14 +05:30
Jannat Patel dd0687ba29 chore: Serbian (Cyrillic) translations 2025-06-21 22:08:07 +05:30
Jannat Patel 9cb87a5333 chore: Italian translations 2025-06-21 22:08:06 +05:30
Jannat Patel 8ec93d84a0 chore: Vietnamese translations 2025-06-21 22:08:04 +05:30
Jannat Patel 1d38715db9 chore: Dutch translations 2025-06-21 22:08:03 +05:30
Jannat Patel 6225c4eb35 chore: Czech translations 2025-06-21 22:08:02 +05:30
Jannat Patel e58ce2fbe6 chore: Esperanto translations 2025-06-21 22:08:00 +05:30
Jannat Patel 8881d62e78 chore: Chinese Simplified translations 2025-06-21 22:07:59 +05:30
Jannat Patel effb2a1265 chore: Serbian (Latin) translations 2025-06-21 22:07:58 +05:30
Jannat Patel ab387473b5 chore: Bosnian translations 2025-06-21 22:07:56 +05:30
Jannat Patel 3cf6079b70 chore: Croatian translations 2025-06-21 22:07:55 +05:30
Jannat Patel 53c655bb53 chore: Thai translations 2025-06-21 22:07:54 +05:30
Jannat Patel 87952463c2 chore: Persian translations 2025-06-21 22:07:52 +05:30
Jannat Patel 3a8a63a49a chore: Portuguese, Brazilian translations 2025-06-21 22:07:51 +05:30
Jannat Patel debe115044 chore: Turkish translations 2025-06-21 22:07:50 +05:30
Jannat Patel 554d2808fd chore: Swedish translations 2025-06-21 22:07:49 +05:30
Jannat Patel 12b2c89a25 chore: Russian translations 2025-06-21 22:07:47 +05:30
Jannat Patel a66fc3a07e chore: Portuguese translations 2025-06-21 22:07:46 +05:30
Jannat Patel 7b3705cab0 chore: Polish translations 2025-06-21 22:07:45 +05:30
Jannat Patel 8e99e5f5e8 chore: Hungarian translations 2025-06-21 22:07:43 +05:30
Jannat Patel c5ba5370bb chore: German translations 2025-06-21 22:07:42 +05:30
Jannat Patel 464dec9810 chore: Arabic translations 2025-06-21 22:07:41 +05:30
Jannat Patel c2e2ec8803 chore: Spanish translations 2025-06-21 22:07:39 +05:30
Jannat Patel 37378e2360 chore: French translations 2025-06-21 22:07:38 +05:30
Jannat Patel 678385d90c Merge pull request #1586 from frappe/pot_develop_2025-06-20
chore: update POT file
2025-06-20 22:25:10 +05:30
frappe-pr-bot 4c461f087f chore: update POT file 2025-06-20 16:04:51 +00:00
Jannat Patel 88a2b69980 feat: exercise form and submission list 2025-06-20 19:59:10 +05:30
Jannat Patel 1f57792da7 Merge pull request #1582 from frappe/l10n_develop2
chore: sync translations from crowdin
2025-06-19 22:30:20 +05:30
Jannat Patel 9bb4c45a23 feat: programming exercise submission 2025-06-19 14:47:52 +05:30
Jannat Patel 75fd19f491 chore: Serbian (Cyrillic) translations 2025-06-18 21:10:25 +05:30
Jannat Patel 0ac16bdeb7 chore: Italian translations 2025-06-18 21:10:23 +05:30
Jannat Patel 223ee41e10 Merge pull request #1578 from frappe/l10n_develop2
chore: sync translations from crowdin
2025-06-18 12:15:27 +05:30
Jannat Patel c126ded82e feat: allow Jammu and Kashmir as state 2025-06-18 11:07:58 +05:30
Jannat Patel 0edf78b7fd feat: programming exercises 2025-06-18 11:06:44 +05:30
Jannat Patel 5af3580987 Merge pull request #1580 from pateljannat/lesson-editor-improvements
fix: lesson editor fixes
2025-06-17 17:04:14 +05:30
Jannat Patel 343cb6f97a fix: removed unused import 2025-06-17 16:46:36 +05:30
Jannat Patel 023c8ac13e fix: lesson editor fixes 2025-06-17 16:38:42 +05:30
Jannat Patel c385eed795 chore: Vietnamese translations 2025-06-16 20:59:31 +05:30
Jannat Patel ee5fdd789f chore: Dutch translations 2025-06-16 20:59:29 +05:30
Jannat Patel df1e400f4e chore: Czech translations 2025-06-16 20:59:28 +05:30
Jannat Patel 6c9c298478 chore: Esperanto translations 2025-06-16 20:59:27 +05:30
Jannat Patel 7106ee150d chore: Chinese Simplified translations 2025-06-16 20:59:26 +05:30
Jannat Patel 03e2287f80 chore: Serbian (Latin) translations 2025-06-16 20:59:24 +05:30
Jannat Patel 2edcd41e24 chore: Bosnian translations 2025-06-16 20:59:23 +05:30
Jannat Patel 0fe043bd99 chore: Croatian translations 2025-06-16 20:59:21 +05:30
Jannat Patel 6686f5240d chore: Thai translations 2025-06-16 20:59:20 +05:30
Jannat Patel 2936facf0f chore: Persian translations 2025-06-16 20:59:18 +05:30
Jannat Patel cc208f2c43 chore: Portuguese, Brazilian translations 2025-06-16 20:59:17 +05:30
Jannat Patel 9a0fc231e5 chore: Turkish translations 2025-06-16 20:59:16 +05:30
Jannat Patel bfc0ae62ec chore: Swedish translations 2025-06-16 20:59:14 +05:30
Jannat Patel 5e7d8d97f2 chore: Russian translations 2025-06-16 20:59:13 +05:30
Jannat Patel 70ceb16ed6 chore: Portuguese translations 2025-06-16 20:59:11 +05:30
Jannat Patel f162fa639f chore: Polish translations 2025-06-16 20:59:10 +05:30
Jannat Patel f000c72546 chore: Hungarian translations 2025-06-16 20:59:08 +05:30
Jannat Patel 32c01f931c chore: German translations 2025-06-16 20:59:07 +05:30
Jannat Patel d0121e2b9d chore: Arabic translations 2025-06-16 20:59:06 +05:30
Jannat Patel 1caab8ce1d chore: Spanish translations 2025-06-16 20:59:04 +05:30
Jannat Patel 878be435a1 chore: French translations 2025-06-16 20:59:02 +05:30
Frappe PR Bot 6a68ae989e chore(release): Bumped to Version 2.31.0 2025-06-16 11:49:38 +00:00
Jannat Patel 00993da781 Merge pull request #1577 from pateljannat/issues-115
fix: misc issues
2025-06-16 17:09:33 +05:30
Jannat Patel e9ef67e402 chore: regenerated yarn lock 2025-06-16 16:55:31 +05:30
Jannat Patel 83ebfececf feat: edit related courses from frontend 2025-06-16 15:13:30 +05:30
Jannat Patel ec8bf6251f Merge branch 'develop' of https://github.com/frappe/lms into issues-115 2025-06-16 13:07:26 +05:30
Jannat Patel 1b2874b3a5 Merge pull request #1565 from OsafAliSayed/related_courses
Feat: Related courses
2025-06-16 13:07:18 +05:30
Jannat Patel 0ac1053a71 Merge pull request #1575 from frappe/pot_develop_2025-06-13
chore: update POT file
2025-06-16 12:55:02 +05:30
Jannat Patel 224d270952 Merge pull request #1572 from harshpwctech/develop
feat: Embedding for Bunny Stream
2025-06-16 12:54:49 +05:30
Jannat Patel c6137545cd ci: verify yarn lock file 2025-06-16 12:53:53 +05:30
Jannat Patel 335417f9f4 fix: persona form role issue 2025-06-16 12:44:29 +05:30
Jannat Patel cb797223ed fix: time markers on video slider for quiz 2025-06-16 12:07:39 +05:30
Jannat Patel 3a2a0313ac fix: show an intermediate dialog informing users of the quiz if its in between video 2025-06-16 11:22:29 +05:30
Jannat Patel e221a5a73a Merge branch 'develop' of https://github.com/frappe/lms into issues-115 2025-06-16 10:47:47 +05:30
frappe-pr-bot 2b7aaf095f chore: update POT file 2025-06-13 16:04:26 +00:00
Jannat Patel 6f01e7b8d8 fix: job count 2025-06-13 20:33:51 +05:30
Jannat Patel d594419200 feat: show live class joining and leaving time in attendance list 2025-06-12 23:18:35 +05:30
Jannat Patel bf50e3f898 Merge pull request #1571 from frappe/l10n_develop2
chore: sync translations from crowdin
2025-06-11 20:05:43 +05:30
safe user d434f1781f feat: Embedding for Bunny Stream 2025-06-11 06:53:26 +00:00
safe user 3f311a45ef feat: Embedding for Bunny Stream 2025-06-11 06:42:29 +00:00
Jannat Patel 9293b7796e chore: Serbian (Latin) translations 2025-06-11 03:33:13 +05:30
OsafAliSayed b1e7883526 fix(relatedCourses): remove loading component 2025-06-10 18:03:43 +00:00
Frappe PR Bot 7fcf6a253d chore(release): Bumped to Version 2.30.0 2025-06-10 10:24:40 +00:00
Jannat Patel be8d985d15 fix: removed duplicate import 2025-06-10 15:34:19 +05:30
Jannat Patel 974c90dddc Merge branch 'main' into develop 2025-06-10 15:29:39 +05:30
Jannat Patel 4811d395d2 Merge pull request #1568 from pateljannat/issues-114
fix: misc evaluation issues
2025-06-10 11:06:45 +05:30
Jannat Patel 132423d577 Merge pull request #1567 from frappe/l10n_develop2
chore: sync translations from crowdin
2025-06-10 10:58:22 +05:30
Jannat Patel 10829e2f00 fix: misc evaluation issues 2025-06-10 10:58:03 +05:30
Jannat Patel 47b908c964 chore: Esperanto translations 2025-06-10 03:35:55 +05:30
Jannat Patel 0f8e471d5d chore: Chinese Simplified translations 2025-06-10 03:35:54 +05:30
Jannat Patel 2537119250 chore: Serbian (Latin) translations 2025-06-10 03:35:52 +05:30
Jannat Patel 977066d114 chore: Bosnian translations 2025-06-10 03:35:51 +05:30
Jannat Patel 46e956dc74 chore: Croatian translations 2025-06-10 03:35:50 +05:30
Jannat Patel 7afdd8d44f chore: Thai translations 2025-06-10 03:35:48 +05:30
Jannat Patel 6daf204b4f chore: Persian translations 2025-06-10 03:35:47 +05:30
Jannat Patel 2f4a550a4a chore: Portuguese, Brazilian translations 2025-06-10 03:35:46 +05:30
Jannat Patel fe214f6b41 chore: Turkish translations 2025-06-10 03:35:44 +05:30
Jannat Patel ca7de81888 chore: Swedish translations 2025-06-10 03:35:43 +05:30
Jannat Patel 17ce20355a chore: Russian translations 2025-06-10 03:35:42 +05:30
Jannat Patel 34981b4765 chore: Portuguese translations 2025-06-10 03:35:41 +05:30
Jannat Patel 21151a2e09 chore: Polish translations 2025-06-10 03:35:39 +05:30
Jannat Patel 1abb7f5b8c chore: Hungarian translations 2025-06-10 03:35:38 +05:30
Jannat Patel 05998549a4 chore: German translations 2025-06-10 03:35:37 +05:30
Jannat Patel 96283a3629 chore: Arabic translations 2025-06-10 03:35:35 +05:30
Jannat Patel 2bfc7abe9c chore: Spanish translations 2025-06-10 03:35:34 +05:30
Jannat Patel 4f389eca8d chore: French translations 2025-06-10 03:35:33 +05:30
Jannat Patel 1789479955 Merge pull request #1564 from frappe/pot_develop_2025-06-06
chore: update POT file
2025-06-09 19:44:09 +05:30
OsafAliSayed 212800155b style(linter): apply linting fixes 2025-06-09 06:13:21 +00:00
OsafAliSayed c241bf2104 feat(related-courses): add related courses component 2025-06-09 06:13:21 +00:00
OsafAliSayed bda61f32f3 feat(related-courses): add related courses frontend 2025-06-09 06:11:40 +00:00
frappe-pr-bot 59316dbaf9 chore: update POT file 2025-06-06 16:04:34 +00:00
Jannat Patel b726073a5b Merge pull request #1562 from frappe/l10n_develop2
chore: sync translations from crowdin
2025-06-06 10:52:56 +05:30
Jannat Patel adf897c812 chore: French translations 2025-06-06 03:03:37 +05:30
Jannat Patel 1fc4c2442c Merge pull request #1561 from pateljannat/evaluator-link-in-batch
fix batch instructor should be linked to evaluator
2025-06-05 15:06:46 +05:30
Jannat Patel 414643ee90 test: link evaluator as batch instructor 2025-06-05 14:46:09 +05:30
Jannat Patel 1a1cbd6ea1 fix: batch instructor should be linked to evaluator 2025-06-05 12:58:12 +05:30
Jannat Patel 9ae809a62f Merge pull request #1559 from pateljannat/issues-113
fix: dont allow enrollment is self learning is disabled from api
2025-06-05 12:53:42 +05:30
Jannat Patel eb9b1c905d Merge pull request #1558 from frappe/l10n_develop2
chore: sync translations from crowdin
2025-06-05 10:46:03 +05:30
Jannat Patel fe9a8f49c1 fix: dont allow enrollment is self learning is disabled from api 2025-06-05 10:43:25 +05:30
Jannat Patel f912c8fce3 chore: French translations 2025-06-05 02:22:54 +05:30
Jannat Patel 1d1ca43c35 chore: Serbian (Latin) translations 2025-06-04 01:58:16 +05:30
Jannat Patel bce45f44e4 Merge pull request #1557 from frappe/l10n_develop2
chore: sync translations from crowdin
2025-06-03 13:23:54 +05:30
Jannat Patel 07583fb563 fix: show error in toast when scheduling evaluations 2025-06-03 12:47:57 +05:30
Jannat Patel 775aa23992 chore: Esperanto translations 2025-06-03 02:00:23 +05:30
Jannat Patel 05ed6b7e73 chore: Chinese Simplified translations 2025-06-03 02:00:22 +05:30
Jannat Patel d602694ea7 chore: Serbian (Latin) translations 2025-06-03 02:00:20 +05:30
Jannat Patel 18d71bc0d4 chore: Bosnian translations 2025-06-03 02:00:19 +05:30
Jannat Patel 3fa68643ba chore: Croatian translations 2025-06-03 02:00:18 +05:30
Jannat Patel 8904525c36 chore: Thai translations 2025-06-03 02:00:16 +05:30
Jannat Patel 3ce09a98f3 chore: Persian translations 2025-06-03 02:00:15 +05:30
Jannat Patel b833768e71 chore: Portuguese, Brazilian translations 2025-06-03 02:00:14 +05:30
Jannat Patel b9a6afd993 chore: Turkish translations 2025-06-03 02:00:12 +05:30
Jannat Patel b5a81ea927 chore: Swedish translations 2025-06-03 02:00:11 +05:30
Jannat Patel 750e92cdde chore: Russian translations 2025-06-03 02:00:09 +05:30
Jannat Patel da45f4c011 chore: Portuguese translations 2025-06-03 02:00:08 +05:30
Jannat Patel 544bb5c11c chore: Polish translations 2025-06-03 02:00:07 +05:30
Jannat Patel 1fc6f62f70 chore: Hungarian translations 2025-06-03 02:00:05 +05:30
Jannat Patel 8751ad27ec chore: German translations 2025-06-03 02:00:04 +05:30
Jannat Patel 159d3d5b87 chore: Arabic translations 2025-06-03 02:00:03 +05:30
Jannat Patel 34d6d99d8c chore: Spanish translations 2025-06-03 02:00:01 +05:30
Jannat Patel 6c46931b1a chore: French translations 2025-06-03 02:00:00 +05:30
Jannat Patel 2c3e2d9d08 Merge pull request #1554 from pateljannat/quiz-in-video
feat: show quiz in between videos
2025-06-02 19:35:55 +05:30
Jannat Patel 7be1562fa4 fix: simplified timestamp label 2025-06-02 19:18:27 +05:30
Jannat Patel 294389e7c7 Merge branch 'develop' of https://github.com/frappe/lms into quiz-in-video 2025-06-02 19:16:27 +05:30
Jannat Patel 2c8ce133f7 fix: quiz and time validation before linking to video 2025-06-02 19:12:13 +05:30
Ankush Menat 4f1d4d90d0 fix: remove invasive configs (#1555) 2025-06-02 19:04:55 +05:30
Jannat Patel 7b7484332b feat: quiz in videos 2025-06-02 18:18:13 +05:30
Jannat Patel 50e94b85aa chore: resolved conflicts 2025-06-02 12:24:16 +05:30
Jannat Patel 9b820594ef Merge pull request #1553 from pateljannat/batch-test
test: batch creation flow
2025-06-02 12:22:58 +05:30
Jannat Patel ddcd45d56d test: don't add course to batch 2025-06-02 12:15:34 +05:30
Jannat Patel c4a4c16516 test: batch creation flow 2025-06-02 10:48:54 +05:30
Jannat Patel 5ae9ad0762 Merge pull request #1552 from frappe/l10n_develop2
chore: sync translations from crowdin
2025-06-02 10:38:49 +05:30
Jannat Patel 405f7d498e Merge pull request #1548 from frappe/pot_develop_2025-05-30
chore: update POT file
2025-06-02 10:38:39 +05:30
Jannat Patel bcd6a5b1e7 chore: Persian translations 2025-06-02 01:08:03 +05:30
Jannat Patel e5e5ac994c Merge pull request #1550 from pateljannat/issues-112
fix: misc issues
2025-05-31 12:49:16 +05:30
Jannat Patel e1f8d6ec49 fix: count of jobs and certified members 2025-05-31 12:41:58 +05:30
Jannat Patel 6f50242f5a fix: misc issues 2025-05-31 11:52:25 +05:30
frappe-pr-bot 036f7ece05 chore: update POT file 2025-05-30 16:04:24 +00:00
Jannat Patel 622a2ff072 feat: display quiz when time is reached 2025-05-30 18:55:26 +05:30
Jannat Patel 60334ca04a feat: show quiz in between videos 2025-05-30 13:00:00 +05:30
Jannat Patel ade47b4e83 Merge pull request #1547 from pateljannat/seo-in-forms
feat: seo tags and keywords for courses and batches
2025-05-30 10:19:29 +05:30
Jannat Patel d7e550dfea feat: seo tags and keywords for courses and batches 2025-05-29 20:13:35 +05:30
Jannat Patel c3cc0b9bf7 Merge pull request #1546 from pateljannat/issues-111
fix: misc issues
2025-05-29 16:29:57 +05:30
Jannat Patel 5ad89189c1 fix: changed certified members count based on filters 2025-05-29 16:09:51 +05:30
Jannat Patel f1bbd4eb13 fix: settings ui cleanup 2025-05-29 16:09:14 +05:30
Jannat Patel fba89dfacb feat: show unpushlished courses to admins on frontend 2025-05-29 12:50:25 +05:30
Jannat Patel b93ed41215 fix: course and chapter permissions to moderators 2025-05-29 12:49:30 +05:30
Jannat Patel 13ff6a7304 chore: record sessions while creating courses and lessons 2025-05-29 12:48:58 +05:30
Jannat Patel ad97405e55 Merge pull request #1544 from Rl0007/fix/edit-profile-escape-html
fix: Edit profile escape html
2025-05-28 20:20:53 +05:30
Rahul Agrawal 376e231d7b chore: remove unwanted line profile.bio = profile.bio 2025-05-28 16:00:14 +05:30
Rahul Agrawal e16d76f6dd fix: remove escapeHtml from edit profile bio on save 2025-05-28 15:44:54 +05:30
Jannat Patel ffd0fd92fc Merge pull request #1542 from pateljannat/zoom-refactor
feat: multiple zoom accounts and zoom attendance
2025-05-28 12:06:21 +05:30
Jannat Patel 933613d730 fix: jobs list header issue 2025-05-28 11:22:03 +05:30
Jannat Patel 9b0673bf92 feat: zoom attendance 2025-05-27 23:01:04 +05:30
Jannat Patel 7cba22aa28 Merge pull request #1539 from frappe/l10n_develop2
chore: sync translations from crowdin
2025-05-27 12:02:06 +05:30
Jannat Patel af05b614a9 feat: delete zoom accounts from settings 2025-05-27 11:40:22 +05:30
Jannat Patel c0fa219a8b chore: Esperanto translations 2025-05-26 23:41:15 +05:30
Jannat Patel 4e3a47b0f4 chore: Chinese Simplified translations 2025-05-26 23:41:14 +05:30
Jannat Patel 161276b58a chore: Serbian (Latin) translations 2025-05-26 23:41:13 +05:30
Jannat Patel 47713019a5 chore: Bosnian translations 2025-05-26 23:41:11 +05:30
Jannat Patel 010632a21d chore: Croatian translations 2025-05-26 23:41:10 +05:30
Jannat Patel e77fe550af chore: Thai translations 2025-05-26 23:41:09 +05:30
Jannat Patel 0a4233da14 chore: Persian translations 2025-05-26 23:41:08 +05:30
Jannat Patel 56fb70ab1e chore: Portuguese, Brazilian translations 2025-05-26 23:41:06 +05:30
Jannat Patel 4a1f2bc01d chore: Turkish translations 2025-05-26 23:41:05 +05:30
Jannat Patel 20292fbf16 chore: Swedish translations 2025-05-26 23:41:04 +05:30
Jannat Patel 1290cf8991 chore: Russian translations 2025-05-26 23:41:02 +05:30
Jannat Patel b8b8af7cf1 chore: Portuguese translations 2025-05-26 23:41:01 +05:30
Jannat Patel 75f4f452d3 chore: Polish translations 2025-05-26 23:41:00 +05:30
Jannat Patel 9de492384f chore: Hungarian translations 2025-05-26 23:40:58 +05:30
Jannat Patel 14c4e161f2 chore: German translations 2025-05-26 23:40:57 +05:30
Jannat Patel c55efbc0ba chore: Arabic translations 2025-05-26 23:40:56 +05:30
Jannat Patel f0610222d9 chore: Spanish translations 2025-05-26 23:40:55 +05:30
Jannat Patel 302ee4a50f chore: French translations 2025-05-26 23:40:53 +05:30
Jannat Patel 2170819159 chore: telemetry fixes 2025-05-26 22:02:46 +05:30
Jannat Patel 0d1fac321a feat: zoom settings on frontend 2025-05-26 21:35:13 +05:30
Jannat Patel dbbc1756dd Merge branch 'develop' of https://github.com/frappe/lms into zoom-refactor 2025-05-26 21:27:18 +05:30
Jannat Patel d5b882d3f8 feat: multiple zoom accounts 2025-05-26 18:08:17 +05:30
Frappe PR Bot 3025ea9a7b chore(release): Bumped to Version 2.29.0 2025-05-26 10:05:36 +00:00
Jannat Patel 5dba4d1384 Merge pull request #1537 from frappe/develop
chore: merge 'develop' into 'main'
2025-05-26 15:33:03 +05:30
Jannat Patel e4f1e7b093 Merge pull request #1536 from pateljannat/telemetry-fixes
chore: fix posthog init condition
2025-05-26 15:21:34 +05:30
Jannat Patel d0a0597087 chore: removed unused imports 2025-05-26 15:08:41 +05:30
Jannat Patel c9ccf9a1b5 chore: fix posthog init condition 2025-05-26 15:02:51 +05:30
Jannat Patel 69107d4441 Merge pull request #1535 from pateljannat/refactor-batch-charts
refactor: use frappe-ui for batch progress charts
2025-05-26 12:43:15 +05:30
Jannat Patel e25afc1ef7 chore: fixed formating 2025-05-26 12:32:34 +05:30
Jannat Patel 9babfd150e fix: course count on batch dashboard 2025-05-26 12:25:16 +05:30
Jannat Patel 532dbbea4a fix: restricted minimum chart interval to 1 2025-05-26 11:34:52 +05:30
Jannat Patel 0d284d05d9 Merge pull request #1534 from pateljannat/issues-110
fix: misc issues
2025-05-26 11:22:16 +05:30
Jannat Patel 28fccae3ac Merge pull request #1532 from frappe/l10n_develop2
chore: sync translations from crowdin
2025-05-26 11:05:01 +05:30
Jannat Patel 3a4a6da69c Merge pull request #1531 from frappe/pot_develop_2025-05-23
chore: update POT file
2025-05-26 11:04:50 +05:30
Jannat Patel 4ea07a95e7 fix: show batch CTA's on mobile 2025-05-26 11:04:00 +05:30
Jannat Patel 80ceb49358 fix: login menu now works on all browsers and devices 2025-05-26 10:58:17 +05:30
Jannat Patel 589337116a fix: added dependencies for onboarding steps 2025-05-26 09:59:05 +05:30
Jannat Patel cb50067223 chore: Chinese Simplified translations 2025-05-24 23:16:12 +05:30
Jannat Patel 4d63266d88 chore: Serbian (Latin) translations 2025-05-24 23:16:11 +05:30
Jannat Patel 90dd33ce21 chore: Serbian (Latin) translations 2025-05-23 23:13:03 +05:30
frappe-pr-bot 763b849ddf chore: update POT file 2025-05-23 16:04:27 +00:00
Jannat Patel 9c76c54283 Merge pull request #1528 from pateljannat/email-template-list
feat: email template in settings
2025-05-23 15:35:11 +05:30
Md Hussain Nagaria 5cb17b3a36 Merge pull request #1529 from frappe/misc-fixes 2025-05-23 11:01:25 +02:00
Hussain Nagaria 2f7b5d1cbb fix: use unavailabilityMessage if set 2025-05-23 14:28:47 +05:30
Hussain Nagaria 4fe14eb2e9 fix: early return cleanup 2025-05-23 14:26:22 +05:30
Jannat Patel eb089f2b58 fix: return payment fields data after transform 2025-05-23 14:25:29 +05:30
Hussain Nagaria 4f0ac98eea fix: toast used but not imported 2025-05-23 14:02:04 +05:30
Hussain Nagaria af19940fa1 fix: some code semantics 2025-05-23 14:00:11 +05:30
Jannat Patel 5635d2a325 feat: email template update and deletion 2025-05-23 13:28:18 +05:30
Jannat Patel 5e2de35693 refactor: layout of payment fields 2025-05-22 21:39:49 +05:30
Jannat Patel ef7180f23f Merge pull request #1523 from pateljannat/issues-109
refactor: misc enhancements
2025-05-22 12:12:57 +05:30
Jannat Patel f939973d4f fix: don't validate number of students if seat count is 0 in batch 2025-05-22 12:03:07 +05:30
Jannat Patel 63f327733e refactor: category list in settings 2025-05-22 11:54:54 +05:30
Jannat Patel c1fb807fe4 fix: show onboarding banner when redirected from other pages 2025-05-21 17:52:24 +05:30
Jannat Patel b7ddf44267 test: close onboaring popover before creating course 2025-05-21 17:35:48 +05:30
Jannat Patel 6d4c72ea5e fix: rating input style on course details page 2025-05-21 16:28:04 +05:30
Jannat Patel 3db11b9372 refactor: moved batch feedback to sidebar 2025-05-21 16:08:49 +05:30
Jannat Patel b8714f4abe refactor: batch progress chart will now use frappe-ui components 2025-05-21 13:13:42 +05:30
Jannat Patel 7ccbe74bbe chore: fixed conflicts 2025-05-20 19:09:19 +05:30
Jannat Patel ea3ae3516b Merge pull request #1513 from frappe/l10n_develop2
chore: sync translations from crowdin
2025-05-20 19:05:38 +05:30
Jannat Patel d33af3ca52 chore: Esperanto translations 2025-05-19 21:33:54 +05:30
Jannat Patel 291c3fa908 chore: Serbian (Latin) translations 2025-05-19 21:33:53 +05:30
Jannat Patel a51fa58122 chore: Bosnian translations 2025-05-19 21:33:52 +05:30
Jannat Patel 65a3967abd chore: Croatian translations 2025-05-19 21:33:50 +05:30
Jannat Patel e1e5c94a43 chore: Thai translations 2025-05-19 21:33:49 +05:30
Jannat Patel f15127eceb chore: Chinese Simplified translations 2025-05-19 21:33:47 +05:30
Jannat Patel 071a238b71 chore: Persian translations 2025-05-19 21:33:46 +05:30
Jannat Patel 050b052156 chore: Portuguese, Brazilian translations 2025-05-19 21:33:44 +05:30
Jannat Patel 8f65cca776 chore: Turkish translations 2025-05-19 21:33:43 +05:30
Jannat Patel 66624a8c47 chore: Swedish translations 2025-05-19 21:33:41 +05:30
Jannat Patel c8b9a415e6 chore: Russian translations 2025-05-19 21:33:40 +05:30
Jannat Patel a1dcb4c203 chore: Portuguese translations 2025-05-19 21:33:39 +05:30
Jannat Patel d4edc3e622 chore: Polish translations 2025-05-19 21:33:37 +05:30
Jannat Patel e2b8c3ee0e chore: Hungarian translations 2025-05-19 21:33:35 +05:30
Jannat Patel c37816e90d chore: German translations 2025-05-19 21:33:34 +05:30
Jannat Patel a35cfcdca7 chore: Arabic translations 2025-05-19 21:33:32 +05:30
Jannat Patel d381646226 chore: Spanish translations 2025-05-19 21:33:31 +05:30
Jannat Patel 285e7afec2 chore: French translations 2025-05-19 21:33:30 +05:30
Jannat Patel df7d678c32 Merge pull request #1510 from frappe/l10n_develop2
chore: sync translations from crowdin
2025-05-19 14:24:21 +05:30
Jannat Patel f36f7e58de Merge pull request #1511 from frappe/pot_develop_2025-05-16
chore: update POT file
2025-05-19 14:24:10 +05:30
Jannat Patel 0e16c834d8 chore: Serbian (Latin) translations 2025-05-18 21:37:27 +05:30
frappe-pr-bot 31a3256128 chore: update POT file 2025-05-16 16:04:20 +00:00
Jannat Patel aa8f70da28 chore: Swedish translations 2025-05-16 21:09:45 +05:30
Jannat Patel de240e40a5 Merge pull request #1507 from frappe/develop
chore: merge 'develop' into 'main'
2025-05-16 12:01:07 +05:30
Jannat Patel 3bbdc828d9 Merge pull request #1506 from frappe/develop
chore: merge 'develop' into 'main'
2025-05-14 17:48:42 +05:30
Jannat Patel 0d41a1ae70 refactor: use frappe-ui for batch progress charts 2025-05-12 10:37:26 +05:30
Jannat Patel b2b92aea31 chore: merged upstream 2025-05-07 22:04:53 +05:30
Jannat Patel e0680d9612 chore: merged upstream 2025-05-07 22:03:57 +05:30
Jannat Patel d286df649e Merge pull request #1490 from frappe/develop
chore: merge 'develop' into 'main'
2025-05-07 12:56:56 +05:30
Jannat Patel e0cbc247b2 Merge pull request #1485 from pateljannat/release-conflicts
chore: merge to 'main'
2025-05-06 13:10:08 +05:30
Jannat Patel a2c8a82559 chore: merged conflicts 2025-05-06 12:59:22 +05:30
Jannat Patel 8b91323705 Merge pull request #1457 from pateljannat/vimeo
fix: allow fullscreen on vimeo
2025-04-21 17:32:12 +05:30
Jannat Patel 89fdbf5660 test: find the course image label and attach course image to its sibling input 2025-04-21 17:10:33 +05:30
Jannat Patel 7ed5dfdb8f fix: allow fullscreen on video and adjust video height on mobile devices 2025-04-21 16:34:34 +05:30
Jannat Patel 824c65eb38 Merge pull request #1440 from frappe/develop
chore: merge 'develop' into 'main'
2025-04-16 18:55:21 +05:30
Jannat Patel e43eeeba4a Merge pull request #1423 from frappe/develop
chore: merge 'develop' into 'main'
2025-04-10 16:01:54 +05:30
Jannat Patel 9e2c7cc145 Merge pull request #1417 from frappe/develop
chore: merge 'develop' into 'main'
2025-04-09 15:17:25 +05:30
Jannat Patel 989598b9cd Merge pull request #1398 from frappe/develop
chore: merge 'develop' into 'main'
2025-03-27 09:44:00 +05:30
Jannat Patel 6a41942de6 Merge pull request #1384 from frappe/develop
chore: merge 'develop' into 'main'
2025-03-20 13:04:16 +05:30
Jannat Patel d263072aca Merge pull request #1373 from frappe/develop
chore: merge 'develop' into 'main'
2025-03-12 11:10:48 +05:30
Jannat Patel 78c8467bf6 Merge pull request #1361 from frappe/develop
chore: merge 'develop' into 'main'
2025-03-05 18:33:33 +05:30
Jannat Patel 084908bd04 Merge pull request #1352 from frappe/develop
chore: merge 'develop' into 'main'
2025-03-03 15:38:08 +05:30
Jannat Patel 039a775ce4 Merge pull request #1340 from frappe/develop
chore: merge 'develop' into 'main'
2025-02-26 10:18:26 +05:30
Jannat Patel dd9e80f067 Merge pull request #1326 from frappe/develop
chore: merge 'develop' into 'main'
2025-02-19 11:06:43 +05:30
Jannat Patel a3a2af948e Merge pull request #1303 from frappe/develop
chore: merge 'develop' into 'main'
2025-02-14 18:07:10 +05:30
Jannat Patel 0bedf3ea59 Merge pull request #1264 from frappe/develop
chore: merge 'develop' into 'main'
2025-01-22 12:59:34 +05:30
Jannat Patel 1775ac4803 Merge pull request #1247 from frappe/develop
chore: merge 'develop' into 'main'
2025-01-15 11:24:49 +05:30
Jannat Patel ae1a615863 Merge pull request #1237 from frappe/develop
chore: merge 'develop' into 'main'
2025-01-09 11:01:37 +05:30
Jannat Patel a6ef1b8902 Merge pull request #1220 from frappe/develop
chore: merge 'develop' into 'main'
2025-01-02 20:24:58 +05:30
Jannat Patel 94d17b81d4 Merge pull request #1197 from frappe/develop
chore: merge 'develop' into 'main'
2024-12-18 20:30:52 +05:30
Jannat Patel 44a63d9cec Merge pull request #1180 from frappe/develop
chore: merge 'develop' into 'main'
2024-12-13 12:27:07 +05:30
Jannat Patel e2b4b5a57e Merge pull request #1164 from frappe/develop
chore: merge 'develop' into 'main'
2024-12-06 11:05:49 +05:30
Jannat Patel ec30aa323e Merge pull request #1155 from frappe/develop
chore: merge 'develop' into 'main'
2024-11-29 17:05:30 +05:30
Jannat Patel 95e9087c6e Merge pull request #1151 from frappe/develop
chore: merge 'develop' into 'main'
2024-11-25 15:06:00 +05:30
Jannat Patel db38099557 Merge pull request #1143 from frappe/develop
chore: merge 'develop' into 'main'
2024-11-20 13:30:06 +05:30
Jannat Patel 164d5cdec9 Merge pull request #1130 from frappe/develop
chore: merge 'develop' into 'main'
2024-11-13 11:53:57 +05:30
Jannat Patel c6b1076092 Merge pull request #1099 from frappe/develop
chore: merge 'develop' into 'main'
2024-11-07 09:51:07 +05:30
Jannat Patel 6aebe856da Merge pull request #1087 from frappe/develop
chore: merge 'develop' into 'main'
2024-10-31 12:15:12 +05:30
Jannat Patel 4737551918 Merge pull request #1075 from frappe/develop
chore: merge 'develop' into 'main'
2024-10-23 13:00:37 +05:30
Jannat Patel c2cb79f700 Merge pull request #1067 from frappe/develop
chore: merge 'develop' into 'main'
2024-10-17 12:33:35 +05:30
Jannat Patel d7c05984be Merge pull request #1048 from frappe/develop
chore: merge 'develop' into 'main'
2024-10-09 22:11:23 +05:30
Jannat Patel 55429e2f03 Merge pull request #1036 from frappe/develop
chore: merge 'develop' into 'main'
2024-10-02 12:30:46 +05:30
Jannat Patel 25ffe8b0e4 Merge pull request #1029 from frappe/develop
chore: merge 'develop' into 'main'
2024-09-25 11:47:14 +05:30
Jannat Patel 303a9d1110 Merge pull request #1020 from frappe/develop
chore: merge 'develop' into 'main'
2024-09-18 10:21:11 +05:30
Jannat Patel de8c907c51 Merge pull request #1013 from frappe/develop
chore: merge 'develop' into 'main'
2024-09-11 11:09:55 +05:30
Jannat Patel 0fd1cabd60 Merge pull request #1003 from frappe/develop
chore: merge 'develop' into 'main'
2024-09-04 10:36:05 +05:30
Jannat Patel 8dd480735c Merge pull request #996 from frappe/develop
chore: merge 'develop' into 'main'
2024-08-28 11:24:59 +05:30
Jannat Patel 676f1a1f0e Merge pull request #984 from frappe/develop
chore: merge 'develop' into 'main'
2024-08-21 10:48:23 +05:30
Jannat Patel ce75422126 Merge pull request #966 from frappe/develop
chore: merge 'develop' into 'main'
2024-08-14 11:24:10 +05:30
Jannat Patel 3a097d6b15 Merge pull request #956 from frappe/develop
chore: Merge develop into main
2024-08-06 11:27:00 +05:30
Jannat Patel 9de1bf1020 Merge pull request #954 from frappe/develop
chore: Merge develop into main
2024-08-05 14:47:45 +05:30
Jannat Patel 93e5cf1c25 Merge pull request #952 from frappe/develop
chore: Merge develop to main
2024-08-05 12:22:05 +05:30
Jannat Patel 6e2376570b Merge pull request #949 from frappe/develop
chore: Merge develop to main
2024-08-01 17:16:22 +05:30
Jannat Patel b20c4bf197 Merge pull request #948 from frappe/develop
chore: Merge develop to main
2024-07-31 16:33:43 +05:30
Jannat Patel 6ae1d92033 Merge pull request #925 from frappe/develop
chore: merge `develop` into `main`
2024-07-11 09:11:50 +05:30
381 changed files with 117994 additions and 37914 deletions
+124
View File
@@ -0,0 +1,124 @@
{
"env": {
"browser": true,
"node": true,
"es2022": true
},
"parserOptions": {
"sourceType": "module"
},
"extends": "eslint:recommended",
"rules": {
"indent": "off",
"brace-style": "off",
"no-mixed-spaces-and-tabs": "off",
"no-useless-escape": "off",
"space-unary-ops": ["error", { "words": true }],
"linebreak-style": "off",
"quotes": ["off"],
"semi": "off",
"camelcase": "off",
"no-unused-vars": "off",
"no-console": ["warn"],
"no-extra-boolean-cast": ["off"],
"no-control-regex": ["off"],
},
"root": true,
"globals": {
"frappe": true,
"Vue": true,
"SetVueGlobals": true,
"__": true,
"repl": true,
"Class": true,
"locals": true,
"cint": true,
"cstr": true,
"cur_frm": true,
"cur_dialog": true,
"cur_page": true,
"cur_list": true,
"cur_tree": true,
"msg_dialog": true,
"is_null": true,
"in_list": true,
"has_common": true,
"posthog": true,
"has_words": true,
"validate_email": true,
"open_web_template_values_editor": true,
"validate_name": true,
"validate_phone": true,
"validate_url": true,
"get_number_format": true,
"format_number": true,
"format_currency": true,
"comment_when": true,
"open_url_post": true,
"toTitle": true,
"lstrip": true,
"rstrip": true,
"strip": true,
"strip_html": true,
"replace_all": true,
"flt": true,
"precision": true,
"CREATE": true,
"AMEND": true,
"CANCEL": true,
"copy_dict": true,
"get_number_format_info": true,
"strip_number_groups": true,
"print_table": true,
"Layout": true,
"web_form_settings": true,
"$c": true,
"$a": true,
"$i": true,
"$bg": true,
"$y": true,
"$c_obj": true,
"refresh_many": true,
"refresh_field": true,
"toggle_field": true,
"get_field_obj": true,
"get_query_params": true,
"unhide_field": true,
"hide_field": true,
"set_field_options": true,
"getCookie": true,
"getCookies": true,
"get_url_arg": true,
"md5": true,
"$": true,
"jQuery": true,
"moment": true,
"hljs": true,
"Awesomplete": true,
"Sortable": true,
"Showdown": true,
"Taggle": true,
"Gantt": true,
"Slick": true,
"Webcam": true,
"PhotoSwipe": true,
"PhotoSwipeUI_Default": true,
"io": true,
"JsBarcode": true,
"L": true,
"Chart": true,
"DataTable": true,
"Cypress": true,
"cy": true,
"it": true,
"describe": true,
"expect": true,
"context": true,
"before": true,
"beforeEach": true,
"after": true,
"qz": true,
"localforage": true,
"extend_cscript": true
}
}
+1 -1
View File
@@ -5,7 +5,7 @@ echo "Setting Up System Dependencies..."
sudo apt update
sudo apt remove mysql-server mysql-client
sudo apt-get install libcups2-dev redis-server mariadb-client
sudo apt-get install libcups2-dev redis-server mariadb-client libmariadb-dev
install_wkhtmltopdf() {
wget -q https://github.com/wkhtmltopdf/packaging/releases/download/0.12.6-1/wkhtmltox_0.12.6-1.focal_amd64.deb
+1 -1
View File
@@ -1,7 +1,7 @@
name: Create weekly release
on:
schedule:
- cron: '30 4 15 * *'
- cron: '30 3 * * 3'
workflow_dispatch:
jobs:
+26 -17
View File
@@ -1,10 +1,10 @@
exclude: 'node_modules|.git'
default_stages: [commit]
default_stages: [pre-commit]
fail_fast: false
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.3.0
rev: v5.0.0
hooks:
- id: trailing-whitespace
files: "lms.*"
@@ -16,17 +16,16 @@ repos:
- id: check-toml
- id: debug-statements
- repo: https://github.com/asottile/pyupgrade
rev: v2.34.0
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.8.1
hooks:
- id: pyupgrade
args: ['--py310-plus']
- repo: https://github.com/adityahase/black
rev: 9cb0a69f4d0030cdf687eddf314468b39ed54119
hooks:
- id: black
additional_dependencies: ['click==8.0.4']
- id: ruff
name: "Run ruff import sorter"
args: ["--select=I", "--fix"]
- id: ruff
name: "Run ruff linter"
- id: ruff-format
name: "Run ruff formatter"
- repo: https://github.com/pre-commit/mirrors-prettier
rev: v2.7.1
@@ -44,12 +43,22 @@ repos:
lms/public/js/lib/.*
)$
- repo: https://github.com/PyCQA/flake8
rev: 5.0.4
- repo: https://github.com/pre-commit/mirrors-eslint
rev: v8.44.0
hooks:
- id: flake8
additional_dependencies: ['flake8-bugbear',]
args: ['--config', '.github/helper/flake8.conf']
- id: eslint
types_or: [javascript]
args: ['--quiet']
exclude: |
(?x)^(
lms/public/dist/.*|
cypress/.*|
.*node_modules.*|
.*boilerplate.*|
lms/www/website_script.js|
lms/templates/includes/.*|
lms/public/js/lib/.*
)$
ci:
autoupdate_schedule: weekly
+4
View File
@@ -118,6 +118,10 @@ Replace the following parameters with your values:
The script will set up a production-ready instance of Frappe Learning with all the necessary configurations in about 5 minutes.
**Note:** To avoid a `404 Page Not Found` error:
- If hosting on a **public server**, make sure your DNS **A record** points to your server's IP.
- If hosting **locally**, map your domain to `127.0.0.1` in your `/etc/hosts` file:
## Development Setup
### Docker
+172
View File
@@ -0,0 +1,172 @@
describe("Batch Creation", () => {
it("creates a new batch", () => {
cy.login();
cy.wait(500);
cy.visit("/lms/batches");
cy.closeOnboardingModal();
// Open Settings
cy.get("span").contains("Learning").click();
cy.get("span").contains("Settings").click();
// Add a new member
cy.get('[id^="headlessui-dialog-panel-v-"]')
.find("span")
.contains(/^Members$/)
.click();
cy.get('[id^="headlessui-dialog-panel-v-"]')
.find("button")
.contains("New")
.click();
const dateNow = Date.now();
const randomEmail = `testuser_${dateNow}@example.com`;
const randomName = `Test User ${dateNow}`;
cy.get("input[placeholder='jane@doe.com']").type(randomEmail);
cy.get("input[placeholder='Jane']").type(randomName);
cy.get("button").contains("Add").click();
// Add evaluator
cy.get('[id^="headlessui-dialog-panel-v-"]')
.find("span")
.contains(/^Evaluators$/)
.click();
cy.get('[id^="headlessui-dialog-panel-v-"]')
.find("button")
.contains("New")
.click();
const randomEvaluator = `evaluator${dateNow}@example.com`;
cy.get("input[placeholder='jane@doe.com']").type(randomEvaluator);
cy.get("button").contains("Add").click();
cy.get("div").contains(randomEvaluator).should("be.visible").click();
cy.visit("/lms/batches");
cy.closeOnboardingModal();
// Create a batch
cy.get("button").contains("Create").click();
cy.wait(500);
cy.url().should("include", "/batches/new/edit");
cy.get("label").contains("Title").type("Test Batch");
cy.get("label").contains("Start Date").type("2030-10-01");
cy.get("label").contains("End Date").type("2030-10-31");
cy.get("label").contains("Start Time").type("10:00");
cy.get("label").contains("End Time").type("11:00");
cy.get("label").contains("Timezone").type("IST");
cy.get("label").contains("Seat Count").type("10");
cy.get("label").contains("Published").click();
cy.get("label")
.contains("Short Description")
.type("Test Batch Short Description to test the UI");
cy.get("div[contenteditable=true").invoke(
"text",
"Test Batch Description. I need a very big description to test the UI. This is a very big description. It contains more than once sentence. Its meant to be this long as this is a UI test. Its unbearably long and I'm not sure why I'm typing this much. I'm just going to keep typing until I feel like its long enough. I think its long enough now. I'm going to stop typing now."
);
/* Instructor */
cy.get("label")
.contains("Instructors")
.parent()
.within(() => {
cy.get("input").click().type("evaluator");
cy.get("input")
.invoke("attr", "aria-controls")
.as("instructor_list_id");
});
cy.get("@instructor_list_id").then((instructor_list_id) => {
cy.get(`[id^=${instructor_list_id}`)
.should("be.visible")
.within(() => {
cy.get("[id^=headlessui-combobox-option-").first().click();
});
});
cy.button("Save").click();
cy.wait(1000);
let batchName;
cy.url().then((url) => {
console.log(url);
batchName = url.split("/").pop();
cy.wrap(batchName).as("batchName");
});
cy.wait(500);
// View Batch
cy.wait(1000);
cy.visit("/lms/batches");
cy.closeOnboardingModal();
cy.url().should("include", "/lms/batches");
cy.get('[id^="headlessui-radiogroup-v-"]')
.find("span")
.contains("Upcoming")
.should("be.visible")
.click();
cy.get("@batchName").then((batchName) => {
cy.get(`a[href='/lms/batches/details/${batchName}'`).within(() => {
cy.get("div").contains("Test Batch").should("be.visible");
cy.get("div")
.contains("Test Batch Short Description to test the UI")
.should("be.visible");
cy.get("span")
.contains("01 Oct 2030 - 31 Oct 2030")
.should("be.visible");
cy.get("span")
.contains("10:00 AM - 11:00 AM")
.should("be.visible");
cy.get("span").contains("IST").should("be.visible");
cy.get("a").contains("Evaluator").should("be.visible");
cy.get("div")
.contains("10")
.should("be.visible")
.get("span")
.contains("Seats Left")
.should("be.visible");
});
cy.get(`a[href='/lms/batches/details/${batchName}'`).click();
});
cy.get("div").contains("Test Batch").should("be.visible");
cy.get("div")
.contains("Test Batch Short Description to test the UI")
.should("be.visible");
cy.get("a").contains("Evaluator").should("be.visible");
cy.get("span:visible")
.contains("01 Oct 2030 - 31 Oct 2030")
.should("be.visible");
cy.get("span:visible")
.contains("10:00 AM - 11:00 AM")
.should("be.visible");
cy.get("span:visible").contains("IST").should("be.visible");
cy.contains("div:visible", "10 Seats Left").should("be.visible");
cy.get("p")
.contains(
"Test Batch Description. I need a very big description to test the UI. This is a very big description. It contains more than once sentence. Its meant to be this long as this is a UI test. Its unbearably long and I'm not sure why I'm typing this much. I'm just going to keep typing until I feel like its long enough. I think its long enough now. I'm going to stop typing now."
)
.should("be.visible");
cy.get("button:visible").contains("Manage Batch").click();
/* Add student to batch */
cy.get("button").contains("Add").click();
cy.get('div[id^="headlessui-dialog-panel-v-"]')
.first()
.find("button")
.eq(1)
.click();
cy.get("input[id^='headlessui-combobox-input-v-']").type(randomEmail);
cy.get("div").contains(randomEmail).click();
cy.get("button").contains("Submit").click();
// Verify Seat Count
cy.get("span").contains("Details").click();
cy.contains("div:visible", "9 Seats Left").should("be.visible");
});
});
+11 -6
View File
@@ -1,12 +1,15 @@
describe("Course Creation", () => {
it("creates a new course", () => {
cy.login();
cy.wait(1000);
cy.wait(500);
cy.visit("/lms/courses");
// Close onboarding modal
cy.closeOnboardingModal();
// Create a course
cy.get("button").contains("New").click();
cy.wait(1000);
cy.get("button").contains("Create").click();
cy.wait(500);
cy.url().should("include", "/courses/new/edit");
cy.get("label").contains("Title").type("Test Course");
@@ -95,15 +98,16 @@ describe("Course Creation", () => {
// View Course
cy.wait(1000);
cy.visit("/lms");
cy.wait(500);
cy.visit("/lms/courses");
cy.closeOnboardingModal();
cy.url().should("include", "/lms/courses");
cy.get(".grid a:first").within(() => {
cy.get("div").contains("Test Course");
cy.get("div").contains(
"Test Course Short Introduction to test the UI"
);
cy.get(".course-image")
cy.get(".bg-cover")
.invoke("css", "background-image")
.should("include", "/files/profile");
});
@@ -136,6 +140,7 @@ describe("Course Creation", () => {
);
// Add Discussion
cy.get("span").contains("Community").click();
cy.button("New Question").click();
cy.wait(500);
cy.get("[id^=headlessui-dialog-panel-").within(() => {
+16
View File
@@ -25,6 +25,7 @@
// Cypress.Commands.overwrite('visit', (originalFn, url, options) => { ... })
import "cypress-file-upload";
import "cypress-real-events";
Cypress.Commands.add("login", (email, password) => {
if (!email) {
@@ -68,3 +69,18 @@ Cypress.Commands.add("paste", { prevSubject: true }, (subject, text) => {
element.dispatchEvent(event);
});
});
Cypress.Commands.add("closeOnboardingModal", () => {
cy.wait(500);
cy.get("body").then(($body) => {
// Check if any element with class including 'z-50' exists
if ($body.find('[class*="z-50"]').length > 0) {
cy.get('[class*="z-50"]')
.find('button:has(svg[class*="feather-x"])')
.realClick();
cy.wait(1000);
} else {
cy.log("Onboarding modal not found, skipping close.");
}
});
});
Submodule
+1
Submodule frappe-ui added at 333dce1a4d
+1
View File
@@ -2,4 +2,5 @@ node_modules
.DS_Store
dist
dist-ssr
dev-dist
*.local
+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 {
}
+29 -8
View File
@@ -19,6 +19,10 @@ declare module 'vue' {
AssignmentForm: typeof import('./src/components/Modals/AssignmentForm.vue')['default']
AudioBlock: typeof import('./src/components/AudioBlock.vue')['default']
Autocomplete: typeof import('./src/components/Controls/Autocomplete.vue')['default']
BadgeAssignmentForm: typeof import('./src/components/Settings/BadgeAssignmentForm.vue')['default']
BadgeAssignments: typeof import('./src/components/Settings/BadgeAssignments.vue')['default']
BadgeForm: typeof import('./src/components/Settings/BadgeForm.vue')['default']
Badges: typeof import('./src/components/Settings/Badges.vue')['default']
BatchCard: typeof import('./src/components/BatchCard.vue')['default']
BatchCourseModal: typeof import('./src/components/Modals/BatchCourseModal.vue')['default']
BatchCourses: typeof import('./src/components/BatchCourses.vue')['default']
@@ -27,17 +31,21 @@ declare module 'vue' {
BatchOverlay: typeof import('./src/components/BatchOverlay.vue')['default']
BatchStudentProgress: typeof import('./src/components/Modals/BatchStudentProgress.vue')['default']
BatchStudents: typeof import('./src/components/BatchStudents.vue')['default']
BrandSettings: typeof import('./src/components/BrandSettings.vue')['default']
BrandSettings: typeof import('./src/components/Settings/BrandSettings.vue')['default']
BulkCertificates: typeof import('./src/components/Modals/BulkCertificates.vue')['default']
Categories: typeof import('./src/components/Categories.vue')['default']
Categories: typeof import('./src/components/Settings/Categories.vue')['default']
CertificationLinks: typeof import('./src/components/CertificationLinks.vue')['default']
ChapterModal: typeof import('./src/components/Modals/ChapterModal.vue')['default']
ChildTable: typeof import('./src/components/Controls/ChildTable.vue')['default']
Code: typeof import('./src/components/Controls/Code.vue')['default']
CodeEditor: typeof import('./src/components/Controls/CodeEditor.vue')['default']
CollapseSidebar: typeof import('./src/components/Icons/CollapseSidebar.vue')['default']
ColorSwatches: typeof import('./src/components/Controls/ColorSwatches.vue')['default']
CourseCard: typeof import('./src/components/CourseCard.vue')['default']
CourseCardOverlay: typeof import('./src/components/CourseCardOverlay.vue')['default']
CourseInstructors: typeof import('./src/components/CourseInstructors.vue')['default']
CourseOutline: typeof import('./src/components/CourseOutline.vue')['default']
CourseProgressSummary: typeof import('./src/components/Modals/CourseProgressSummary.vue')['default']
CourseReviews: typeof import('./src/components/CourseReviews.vue')['default']
CreateOutline: typeof import('./src/components/CreateOutline.vue')['default']
DateRange: typeof import('./src/components/Common/DateRange.vue')['default']
@@ -47,14 +55,19 @@ declare module 'vue' {
Discussions: typeof import('./src/components/Discussions.vue')['default']
EditCoverImage: typeof import('./src/components/Modals/EditCoverImage.vue')['default']
EditProfile: typeof import('./src/components/Modals/EditProfile.vue')['default']
EmailTemplateModal: typeof import('./src/components/Modals/EmailTemplateModal.vue')['default']
EmailTemplates: typeof import('./src/components/Settings/EmailTemplates.vue')['default']
EmptyState: typeof import('./src/components/EmptyState.vue')['default']
EvaluationModal: typeof import('./src/components/Modals/EvaluationModal.vue')['default']
Evaluators: typeof import('./src/components/Evaluators.vue')['default']
Evaluators: typeof import('./src/components/Settings/Evaluators.vue')['default']
Event: typeof import('./src/components/Modals/Event.vue')['default']
ExplanationVideos: typeof import('./src/components/Modals/ExplanationVideos.vue')['default']
FeedbackModal: typeof import('./src/components/Modals/FeedbackModal.vue')['default']
FrappeCloudIcon: typeof import('./src/components/Icons/FrappeCloudIcon.vue')['default']
IconPicker: typeof import('./src/components/Controls/IconPicker.vue')['default']
IndicatorIcon: typeof import('./src/components/Icons/IndicatorIcon.vue')['default']
InlineLessonMenu: typeof import('./src/components/Notes/InlineLessonMenu.vue')['default']
InstallPrompt: typeof import('./src/components/InstallPrompt.vue')['default']
InviteIcon: typeof import('./src/components/Icons/InviteIcon.vue')['default']
JobApplicationModal: typeof import('./src/components/Modals/JobApplicationModal.vue')['default']
JobCard: typeof import('./src/components/JobCard.vue')['default']
@@ -62,37 +75,45 @@ declare module 'vue' {
LessonHelp: typeof import('./src/components/LessonHelp.vue')['default']
Link: typeof import('./src/components/Controls/Link.vue')['default']
LiveClass: typeof import('./src/components/LiveClass.vue')['default']
LiveClassAttendance: typeof import('./src/components/Modals/LiveClassAttendance.vue')['default']
LiveClassModal: typeof import('./src/components/Modals/LiveClassModal.vue')['default']
LMSLogo: typeof import('./src/components/Icons/LMSLogo.vue')['default']
Members: typeof import('./src/components/Members.vue')['default']
Members: typeof import('./src/components/Settings/Members.vue')['default']
MobileLayout: typeof import('./src/components/MobileLayout.vue')['default']
MultiSelect: typeof import('./src/components/Controls/MultiSelect.vue')['default']
NoPermission: typeof import('./src/components/NoPermission.vue')['default']
NoSidebarLayout: typeof import('./src/components/NoSidebarLayout.vue')['default']
Notes: typeof import('./src/components/Notes/Notes.vue')['default']
NotPermitted: typeof import('./src/components/NotPermitted.vue')['default']
PageModal: typeof import('./src/components/Modals/PageModal.vue')['default']
PaymentSettings: typeof import('./src/components/PaymentSettings.vue')['default']
PaymentSettings: typeof import('./src/components/Settings/PaymentSettings.vue')['default']
Play: typeof import('./src/components/Icons/Play.vue')['default']
ProgressBar: typeof import('./src/components/ProgressBar.vue')['default']
Question: typeof import('./src/components/Modals/Question.vue')['default']
Quiz: typeof import('./src/components/Quiz.vue')['default']
QuizBlock: typeof import('./src/components/QuizBlock.vue')['default']
QuizInVideo: typeof import('./src/components/Modals/QuizInVideo.vue')['default']
Rating: typeof import('./src/components/Controls/Rating.vue')['default']
RelatedCourses: typeof import('./src/components/RelatedCourses.vue')['default']
ReviewModal: typeof import('./src/components/Modals/ReviewModal.vue')['default']
RouterLink: typeof import('vue-router')['RouterLink']
RouterView: typeof import('vue-router')['RouterView']
SettingDetails: typeof import('./src/components/SettingDetails.vue')['default']
SettingFields: typeof import('./src/components/SettingFields.vue')['default']
Settings: typeof import('./src/components/Modals/Settings.vue')['default']
SettingDetails: typeof import('./src/components/Settings/SettingDetails.vue')['default']
SettingFields: typeof import('./src/components/Settings/SettingFields.vue')['default']
Settings: typeof import('./src/components/Settings/Settings.vue')['default']
SidebarLink: typeof import('./src/components/SidebarLink.vue')['default']
StudentHeatmap: typeof import('./src/components/StudentHeatmap.vue')['default']
StudentModal: typeof import('./src/components/Modals/StudentModal.vue')['default']
Tags: typeof import('./src/components/Tags.vue')['default']
UnsplashImageBrowser: typeof import('./src/components/UnsplashImageBrowser.vue')['default']
UpcomingEvaluations: typeof import('./src/components/UpcomingEvaluations.vue')['default']
Uploader: typeof import('./src/components/Controls/Uploader.vue')['default']
UploadPlugin: typeof import('./src/components/UploadPlugin.vue')['default']
UserAvatar: typeof import('./src/components/UserAvatar.vue')['default']
UserDropdown: typeof import('./src/components/UserDropdown.vue')['default']
VideoBlock: typeof import('./src/components/VideoBlock.vue')['default']
VideoStatistics: typeof import('./src/components/Modals/VideoStatistics.vue')['default']
ZoomAccountModal: typeof import('./src/components/Modals/ZoomAccountModal.vue')['default']
ZoomSettings: typeof import('./src/components/Settings/ZoomSettings.vue')['default']
}
}
+196
View File
@@ -3,6 +3,202 @@
<head>
<meta charset="UTF-8" />
<link rel="icon" href="{{ favicon }}" />
<link rel="apple-touch-icon" href="public/manifest/apple-icon-180.png" />
<meta name="mobile-web-app-capable" content="yes" />
<meta name="theme-color" content="#FFFFFF" media="(prefers-color-scheme: light)" />
<meta name="theme-color" content="#0F0F0F" media="(prefers-color-scheme: dark)" />
<meta name="apple-mobile-web-app-status-bar-style" content="default" />
<meta name="msapplication-navbutton-color" content="#ffffff" />
<link
rel="apple-touch-startup-image"
href="public/manifest/apple-splash-2048-2732.jpg"
media="(device-width: 1024px) and (device-height: 1366px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)"
/>
<link
rel="apple-touch-startup-image"
href="public/manifest/apple-splash-2732-2048.jpg"
media="(device-width: 1024px) and (device-height: 1366px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)"
/>
<link
rel="apple-touch-startup-image"
href="public/manifest/apple-splash-1668-2388.jpg"
media="(device-width: 834px) and (device-height: 1194px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)"
/>
<link
rel="apple-touch-startup-image"
href="public/manifest/apple-splash-2388-1668.jpg"
media="(device-width: 834px) and (device-height: 1194px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)"
/>
<link
rel="apple-touch-startup-image"
href="public/manifest/apple-splash-1536-2048.jpg"
media="(device-width: 768px) and (device-height: 1024px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)"
/>
<link
rel="apple-touch-startup-image"
href="public/manifest/apple-splash-2048-1536.jpg"
media="(device-width: 768px) and (device-height: 1024px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)"
/>
<link
rel="apple-touch-startup-image"
href="public/manifest/apple-splash-1640-2360.jpg"
media="(device-width: 820px) and (device-height: 1180px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)"
/>
<link
rel="apple-touch-startup-image"
href="public/manifest/apple-splash-2360-1640.jpg"
media="(device-width: 820px) and (device-height: 1180px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)"
/>
<link
rel="apple-touch-startup-image"
href="public/manifest/apple-splash-1668-2224.jpg"
media="(device-width: 834px) and (device-height: 1112px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)"
/>
<link
rel="apple-touch-startup-image"
href="public/manifest/apple-splash-2224-1668.jpg"
media="(device-width: 834px) and (device-height: 1112px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)"
/>
<link
rel="apple-touch-startup-image"
href="public/manifest/apple-splash-1620-2160.jpg"
media="(device-width: 810px) and (device-height: 1080px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)"
/>
<link
rel="apple-touch-startup-image"
href="public/manifest/apple-splash-2160-1620.jpg"
media="(device-width: 810px) and (device-height: 1080px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)"
/>
<link
rel="apple-touch-startup-image"
href="public/manifest/apple-splash-1488-2266.jpg"
media="(device-width: 744px) and (device-height: 1133px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)"
/>
<link
rel="apple-touch-startup-image"
href="public/manifest/apple-splash-2266-1488.jpg"
media="(device-width: 744px) and (device-height: 1133px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)"
/>
<link
rel="apple-touch-startup-image"
href="public/manifest/apple-splash-1320-2868.jpg"
media="(device-width: 440px) and (device-height: 956px) and (-webkit-device-pixel-ratio: 3) and (orientation: portrait)"
/>
<link
rel="apple-touch-startup-image"
href="public/manifest/apple-splash-2868-1320.jpg"
media="(device-width: 440px) and (device-height: 956px) and (-webkit-device-pixel-ratio: 3) and (orientation: landscape)"
/>
<link
rel="apple-touch-startup-image"
href="public/manifest/apple-splash-1206-2622.jpg"
media="(device-width: 402px) and (device-height: 874px) and (-webkit-device-pixel-ratio: 3) and (orientation: portrait)"
/>
<link
rel="apple-touch-startup-image"
href="public/manifest/apple-splash-2622-1206.jpg"
media="(device-width: 402px) and (device-height: 874px) and (-webkit-device-pixel-ratio: 3) and (orientation: landscape)"
/>
<link
rel="apple-touch-startup-image"
href="public/manifest/apple-splash-1290-2796.jpg"
media="(device-width: 430px) and (device-height: 932px) and (-webkit-device-pixel-ratio: 3) and (orientation: portrait)"
/>
<link
rel="apple-touch-startup-image"
href="public/manifest/apple-splash-2796-1290.jpg"
media="(device-width: 430px) and (device-height: 932px) and (-webkit-device-pixel-ratio: 3) and (orientation: landscape)"
/>
<link
rel="apple-touch-startup-image"
href="public/manifest/apple-splash-1179-2556.jpg"
media="(device-width: 393px) and (device-height: 852px) and (-webkit-device-pixel-ratio: 3) and (orientation: portrait)"
/>
<link
rel="apple-touch-startup-image"
href="public/manifest/apple-splash-2556-1179.jpg"
media="(device-width: 393px) and (device-height: 852px) and (-webkit-device-pixel-ratio: 3) and (orientation: landscape)"
/>
<link
rel="apple-touch-startup-image"
href="public/manifest/apple-splash-1170-2532.jpg"
media="(device-width: 390px) and (device-height: 844px) and (-webkit-device-pixel-ratio: 3) and (orientation: portrait)"
/>
<link
rel="apple-touch-startup-image"
href="public/manifest/apple-splash-2532-1170.jpg"
media="(device-width: 390px) and (device-height: 844px) and (-webkit-device-pixel-ratio: 3) and (orientation: landscape)"
/>
<link
rel="apple-touch-startup-image"
href="public/manifest/apple-splash-1284-2778.jpg"
media="(device-width: 428px) and (device-height: 926px) and (-webkit-device-pixel-ratio: 3) and (orientation: portrait)"
/>
<link
rel="apple-touch-startup-image"
href="public/manifest/apple-splash-2778-1284.jpg"
media="(device-width: 428px) and (device-height: 926px) and (-webkit-device-pixel-ratio: 3) and (orientation: landscape)"
/>
<link
rel="apple-touch-startup-image"
href="public/manifest/apple-splash-1125-2436.jpg"
media="(device-width: 375px) and (device-height: 812px) and (-webkit-device-pixel-ratio: 3) and (orientation: portrait)"
/>
<link
rel="apple-touch-startup-image"
href="public/manifest/apple-splash-2436-1125.jpg"
media="(device-width: 375px) and (device-height: 812px) and (-webkit-device-pixel-ratio: 3) and (orientation: landscape)"
/>
<link
rel="apple-touch-startup-image"
href="public/manifest/apple-splash-1242-2688.jpg"
media="(device-width: 414px) and (device-height: 896px) and (-webkit-device-pixel-ratio: 3) and (orientation: portrait)"
/>
<link
rel="apple-touch-startup-image"
href="public/manifest/apple-splash-2688-1242.jpg"
media="(device-width: 414px) and (device-height: 896px) and (-webkit-device-pixel-ratio: 3) and (orientation: landscape)"
/>
<link
rel="apple-touch-startup-image"
href="public/manifest/apple-splash-828-1792.jpg"
media="(device-width: 414px) and (device-height: 896px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)"
/>
<link
rel="apple-touch-startup-image"
href="public/manifest/apple-splash-1792-828.jpg"
media="(device-width: 414px) and (device-height: 896px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)"
/>
<link
rel="apple-touch-startup-image"
href="public/manifest/apple-splash-1242-2208.jpg"
media="(device-width: 414px) and (device-height: 736px) and (-webkit-device-pixel-ratio: 3) and (orientation: portrait)"
/>
<link
rel="apple-touch-startup-image"
href="public/manifest/apple-splash-2208-1242.jpg"
media="(device-width: 414px) and (device-height: 736px) and (-webkit-device-pixel-ratio: 3) and (orientation: landscape)"
/>
<link
rel="apple-touch-startup-image"
href="public/manifest/apple-splash-750-1334.jpg"
media="(device-width: 375px) and (device-height: 667px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)"
/>
<link
rel="apple-touch-startup-image"
href="public/manifest/apple-splash-1334-750.jpg"
media="(device-width: 375px) and (device-height: 667px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)"
/>
<link
rel="apple-touch-startup-image"
href="public/manifest/apple-splash-640-1136.jpg"
media="(device-width: 320px) and (device-height: 568px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)"
/>
<link
rel="apple-touch-startup-image"
href="public/manifest/apple-splash-1136-640.jpg"
media="(device-width: 320px) and (device-height: 568px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)"
/>
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>{{ title }}</title>
<meta name="title" content="{{ meta.title }}" />
+8 -2
View File
@@ -10,6 +10,10 @@
"copy-html-entry": "cp ../lms/public/frontend/index.html ../lms/www/lms.html"
},
"dependencies": {
"@codemirror/lang-html": "^6.4.9",
"@codemirror/lang-javascript": "^6.2.4",
"@codemirror/lang-json": "^6.0.1",
"@codemirror/lang-python": "^6.2.1",
"@editorjs/checklist": "^1.6.0",
"@editorjs/code": "^2.9.0",
"@editorjs/editorjs": "^2.29.0",
@@ -24,10 +28,10 @@
"ace-builds": "^1.36.2",
"apexcharts": "^4.3.0",
"chart.js": "^4.4.1",
"codemirror-editor-vue3": "^2.8.0",
"codemirror": "^6.0.1",
"dayjs": "^1.11.6",
"feather-icons": "^4.28.0",
"frappe-ui": "^0.1.143",
"frappe-ui": "0.1.173",
"highlight.js": "^11.11.1",
"lucide-vue-next": "^0.383.0",
"markdown-it": "^14.0.0",
@@ -35,9 +39,11 @@
"plyr": "^3.7.8",
"socket.io-client": "^4.7.2",
"tailwindcss": "3.4.15",
"thememirror": "^2.0.1",
"typescript": "^5.7.2",
"vue": "^3.4.23",
"vue-chartjs": "^5.3.0",
"vue-codemirror": "^6.1.1",
"vue-draggable-next": "^2.2.1",
"vue-router": "^4.0.12",
"vue3-apexcharts": "^1.8.0",
Binary file not shown.
Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 43 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 49 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 45 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 65 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 43 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 45 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

+19 -16
View File
@@ -1,28 +1,31 @@
<template>
<FrappeUIProvider>
<Layout>
<router-view />
<div class="text-base">
<router-view />
</div>
</Layout>
<InstallPrompt v-if="isMobile" />
<Dialogs />
</FrappeUIProvider>
</template>
<script setup>
import { FrappeUIProvider } from 'frappe-ui'
import { Dialogs } from '@/utils/dialogs'
import { computed, onMounted, onUnmounted, ref } from 'vue'
import { computed, onUnmounted, ref, watch } from 'vue'
import { useScreenSize } from './utils/composables'
import { usersStore } from '@/stores/user'
import { useRouter } from 'vue-router'
import { posthogSettings } from '@/telemetry'
import DesktopLayout from './components/DesktopLayout.vue'
import MobileLayout from './components/MobileLayout.vue'
import NoSidebarLayout from './components/NoSidebarLayout.vue'
import { stopSession } from '@/telemetry'
import { init as initTelemetry } from '@/telemetry'
import { usersStore } from '@/stores/user'
import { useRouter } from 'vue-router'
import InstallPrompt from './components/InstallPrompt.vue'
const screenSize = useScreenSize()
let { userResource } = usersStore()
const { isMobile } = useScreenSize()
const router = useRouter()
const noSidebar = ref(false)
const { userResource } = usersStore()
router.beforeEach((to, from, next) => {
if (to.query.fromLesson || to.path === '/persona') {
@@ -37,19 +40,19 @@ const Layout = computed(() => {
if (noSidebar.value) {
return NoSidebarLayout
}
if (screenSize.width < 640) {
if (isMobile.value) {
return MobileLayout
} else {
return DesktopLayout
}
})
onMounted(async () => {
if (userResource.data) await initTelemetry()
return DesktopLayout
})
onUnmounted(() => {
noSidebar.value = false
stopSession()
})
watch(userResource, () => {
if (userResource.data) {
posthogSettings.reload()
}
})
</script>
+72 -38
View File
@@ -181,14 +181,22 @@
import UserDropdown from '@/components/UserDropdown.vue'
import CollapseSidebar from '@/components/Icons/CollapseSidebar.vue'
import SidebarLink from '@/components/SidebarLink.vue'
import { useStorage } from '@vueuse/core'
import { ref, onMounted, inject, watch, reactive, markRaw, h } from 'vue'
import { getSidebarLinks } from '../utils'
import {
ref,
onMounted,
inject,
watch,
reactive,
markRaw,
h,
onUnmounted,
} from 'vue'
import { getSidebarLinks } from '@/utils'
import { usersStore } from '@/stores/user'
import { sessionStore } from '@/stores/session'
import { useSidebar } from '@/stores/sidebar'
import { useSettings } from '@/stores/settings'
import { Button, createResource, Tooltip } from 'frappe-ui'
import { Button, call, createResource, Tooltip } from 'frappe-ui'
import PageModal from '@/components/Modals/PageModal.vue'
import { capture } from '@/telemetry'
import LMSLogo from '@/components/Icons/LMSLogo.vue'
@@ -206,6 +214,7 @@ import {
Users,
BookText,
Zap,
Check,
} from 'lucide-vue-next'
import {
TrialBanner,
@@ -217,7 +226,7 @@ import {
IntermediateStepModal,
} from 'frappe-ui/frappe'
const { user, sidebarSettings } = sessionStore()
const { user } = sessionStore()
const { userResource } = usersStore()
let sidebarStore = useSidebar()
const socket = inject('$socket')
@@ -228,6 +237,7 @@ const isModerator = ref(false)
const isInstructor = ref(false)
const pageToEdit = ref(null)
const settingsStore = useSettings()
const { sidebarSettings } = settingsStore
const showOnboarding = ref(false)
const showIntermediateModal = ref(false)
const currentStep = ref({})
@@ -244,6 +254,7 @@ const iconProps = {
onMounted(() => {
addNotifications()
setSidebarLinks()
setUpOnboarding()
socket.on('publish_lms_notifications', (data) => {
unreadNotifications.reload()
})
@@ -304,7 +315,7 @@ const addNotifications = () => {
const addQuizzes = () => {
if (isInstructor.value || isModerator.value) {
sidebarLinks.value.push({
sidebarLinks.value.splice(4, 0, {
label: 'Quizzes',
icon: 'CircleHelp',
to: 'Quizzes',
@@ -320,7 +331,7 @@ const addQuizzes = () => {
const addAssignments = () => {
if (isInstructor.value || isModerator.value) {
sidebarLinks.value.push({
sidebarLinks.value.splice(5, 0, {
label: 'Assignments',
icon: 'Pencil',
to: 'Assignments',
@@ -334,37 +345,53 @@ const addAssignments = () => {
}
}
const addPrograms = () => {
let activeFor = ['Programs', 'ProgramForm']
let index = 1
let canAddProgram = false
if (
!isInstructor.value &&
!isModerator.value &&
settingsStore.learningPaths.data
) {
sidebarLinks.value = sidebarLinks.value.filter(
(link) => link.label !== 'Courses'
)
activeFor.push('CourseDetail')
activeFor.push('Lesson')
index = 0
canAddProgram = true
} else if (isInstructor.value || isModerator.value) {
canAddProgram = true
}
if (canAddProgram) {
sidebarLinks.value.splice(index, 0, {
label: 'Programs',
icon: 'Route',
to: 'Programs',
activeFor: activeFor,
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 = async () => {
let canAddProgram = await checkIfCanAddProgram()
if (!canAddProgram) return
let activeFor = ['Programs', 'ProgramDetail']
let index = 2
sidebarLinks.value.splice(index, 0, {
label: 'Programs',
icon: 'Route',
to: 'Programs',
activeFor: activeFor,
})
}
const checkIfCanAddProgram = async () => {
if (isModerator.value || isInstructor.value) {
return true
}
const programs = await call('lms.lms.utils.get_programs')
return programs.enrolled.length > 0 || programs.published.length > 0
}
const addHome = () => {
sidebarLinks.value.unshift({
label: 'Home',
icon: 'Home',
to: 'Home',
activeFor: ['Home'],
})
}
const openPageModal = (link) => {
showPageModal.value = true
pageToEdit.value = link
@@ -388,10 +415,6 @@ const deletePage = (link) => {
)
}
const getSidebarFromStorage = () => {
return useStorage('sidebar_is_collapsed', false)
}
const toggleSidebar = () => {
sidebarStore.isSidebarCollapsed = !sidebarStore.isSidebarCollapsed
localStorage.setItem(
@@ -438,6 +461,7 @@ const steps = reactive([
title: __('Add your first chapter'),
icon: markRaw(h(FolderTree, iconProps)),
completed: false,
dependsOn: 'create_first_course',
onClick: async () => {
minimize.value = true
let course = await getFirstCourse()
@@ -453,6 +477,7 @@ const steps = reactive([
title: __('Add your first lesson'),
icon: markRaw(h(FileText, iconProps)),
completed: false,
dependsOn: 'create_first_chapter',
onClick: async () => {
minimize.value = true
let course = await getFirstCourse()
@@ -471,6 +496,7 @@ const steps = reactive([
title: __('Create your first quiz'),
icon: markRaw(h(CircleHelp, iconProps)),
completed: false,
dependsOn: 'create_first_course',
onClick: () => {
minimize.value = true
router.push({ name: 'Quizzes' })
@@ -502,6 +528,7 @@ const steps = reactive([
title: __('Add students to your batch'),
icon: markRaw(h(UserPlus, iconProps)),
completed: false,
dependsOn: 'create_first_batch',
onClick: async () => {
minimize.value = true
let batch = await getFirstBatch()
@@ -522,6 +549,7 @@ const steps = reactive([
title: __('Add courses to your batch'),
icon: markRaw(h(BookText, iconProps)),
completed: false,
dependsOn: 'create_first_batch',
onClick: async () => {
minimize.value = true
let batch = await getFirstBatch()
@@ -615,7 +643,9 @@ watch(userResource, () => {
if (userResource.data) {
isModerator.value = userResource.data.is_moderator
isInstructor.value = userResource.data.is_instructor
addHome()
addPrograms()
addProgrammingExercises()
addQuizzes()
addAssignments()
setUpOnboarding()
@@ -625,4 +655,8 @@ watch(userResource, () => {
const redirectToWebsite = () => {
window.open('https://frappe.io/learning', '_blank')
}
onUnmounted(() => {
socket.off('publish_lms_notifications')
})
</script>
+16 -14
View File
@@ -2,17 +2,24 @@
<Dialog
v-model="show"
:options="{
title:
type == 'quiz'
? __('Add a quiz to your lesson')
: __('Add an assignment to your lesson'),
size: 'xl',
actions: [
{
label: __('Save'),
variant: 'solid',
onClick: () => {
addAssessment()
},
},
],
}"
>
<template #body>
<div class="p-5 space-y-4">
<div v-if="type == 'quiz'" class="text-lg font-semibold">
{{ __('Add a quiz to your lesson') }}
</div>
<div v-else class="text-lg font-semibold">
{{ __('Add an assignment to your lesson') }}
</div>
<template #body-content>
<div class="">
<div>
<Link
v-if="type == 'quiz'"
@@ -29,17 +36,12 @@
:onCreate="(value, close) => redirectToForm()"
/>
</div>
<div class="flex justify-end space-x-2">
<Button variant="solid" @click="addAssessment()">
{{ __('Save') }}
</Button>
</div>
</div>
</template>
</Dialog>
</template>
<script setup>
import { Dialog, Button } from 'frappe-ui'
import { Dialog } from 'frappe-ui'
import { onMounted, ref, nextTick } from 'vue'
import Link from '@/components/Controls/Link.vue'
+30 -2
View File
@@ -40,7 +40,7 @@
<template #default="{ column, item }">
<ListRowItem :item="row[column.key]" :align="column.align">
<div v-if="column.key == 'assessment_type'">
{{ row[column.key] == 'LMS Quiz' ? 'Quiz' : 'Assignment' }}
{{ getAssessmentTypeLabel(row[column.key]) }}
</div>
<div v-else-if="column.key == 'title'">
{{ row[column.key] }}
@@ -172,6 +172,24 @@ const getRowRoute = (row) => {
},
}
}
} else if (row.assessment_type == 'LMS Programming Exercise') {
if (row.submission) {
return {
name: 'ProgrammingExerciseSubmission',
params: {
exerciseID: row.assessment_name,
submissionID: row.submission.name,
},
}
} else {
return {
name: 'ProgrammingExerciseSubmission',
params: {
exerciseID: row.assessment_name,
submissionID: 'new',
},
}
}
} else {
return {
name: 'QuizPage',
@@ -213,7 +231,7 @@ const getAssessmentColumns = () => {
}
const getStatusTheme = (status) => {
if (status === 'Pass') {
if (status === 'Pass' || status === 'Passed') {
return 'green'
} else if (status === 'Not Graded') {
return 'orange'
@@ -221,4 +239,14 @@ const getStatusTheme = (status) => {
return 'red'
}
}
const getAssessmentTypeLabel = (type) => {
if (type == 'LMS Assignment') {
return __('Assignment')
} else if (type == 'LMS Quiz') {
return __('Quiz')
} else if (type == 'LMS Programming Exercise') {
return __('Programming Exercise')
}
}
</script>
+3 -4
View File
@@ -1,6 +1,6 @@
<template>
<div
class="flex flex-col border hover:border-outline-gray-4 rounded-md p-4 h-full"
class="flex flex-col border hover:border-outline-gray-3 rounded-md p-4 h-full"
style="min-height: 150px"
>
<div class="text-lg leading-5 font-semibold mb-2 text-ink-gray-9">
@@ -70,9 +70,8 @@
</div>
</template>
<script setup>
import { Badge } from 'frappe-ui'
import { formatTime } from '../utils'
import { Clock, BookOpen, Globe } from 'lucide-vue-next'
import { formatTime } from '@/utils'
import { Clock, Globe } from 'lucide-vue-next'
import DateRange from '@/components/Common/DateRange.vue'
import CourseInstructors from '@/components/CourseInstructors.vue'
import UserAvatar from '@/components/UserAvatar.vue'
-1
View File
@@ -106,7 +106,6 @@ const courses = createResource({
params: {
batch: props.batch,
},
cache: ['batchCourses', props.batchName],
auto: true,
})
+1 -2
View File
@@ -6,13 +6,12 @@
:courses="batch.data.courses"
/>
<Assessments :batch="batch.data.name" />
<StudentHeatmap />
<!-- <StudentHeatmap /> -->
</div>
</template>
<script setup>
import UpcomingEvaluations from '@/components/UpcomingEvaluations.vue'
import Assessments from '@/components/Assessments.vue'
import StudentHeatmap from '@/components/StudentHeatmap.vue'
const props = defineProps({
batch: {
+50 -122
View File
@@ -1,44 +1,49 @@
<template>
<div v-if="user.data?.is_student">
<div
v-if="feedbackList.data?.length"
class="bg-surface-blue-2 text-blue-700 p-2 rounded-md mb-5"
>
{{ __('Thank you for providing your feedback!') }}
</div>
<div v-else class="flex justify-between items-center mb-5">
<div class="text-lg font-semibold">
{{ __('Help Us Improve') }}
<div>
<div class="leading-5 mb-4">
<div v-if="readOnly">
{{ __('Thank you for providing your feedback.') }}
<span
@click="showFeedbackForm = !showFeedbackForm"
class="underline cursor-pointer"
>{{ __('Click here') }}</span
>
{{ __('to view your feedback.') }}
</div>
<div v-else>
{{ __('Help us improve by providing your feedback.') }}
</div>
</div>
<Button @click="submitFeedback()">
{{ __('Submit') }}
</Button>
</div>
<div class="space-y-8">
<div class="flex items-center justify-between">
<Rating
v-for="key in ratingKeys"
v-model="feedback[key]"
:label="__(convertToTitleCase(key))"
<div class="space-y-4" :class="showFeedbackForm ? 'block' : 'hidden'">
<div class="space-y-4">
<Rating
v-for="key in ratingKeys"
v-model="feedback[key]"
:label="__(convertToTitleCase(key))"
:readonly="readOnly"
/>
</div>
<FormControl
v-model="feedback.feedback"
type="textarea"
:label="__('Feedback')"
:rows="9"
:readonly="readOnly"
/>
<Button v-if="!readOnly" @click="submitFeedback">
{{ __('Submit Feedback') }}
</Button>
</div>
<FormControl
v-model="feedback.feedback"
type="textarea"
:label="__('Feedback')"
:rows="7"
:readonly="readOnly"
/>
</div>
</div>
<div v-else-if="feedbackList.data?.length">
<div class="text-lg font-semibold mb-5">
{{ __('Average of Feedback Received') }}
<div class="leading-5 text-sm mb-2 mt-5">
{{ __('Average Feedback Received') }}
</div>
<div class="flex items-center justify-between mb-10">
<div class="space-y-4">
<Rating
v-for="key in ratingKeys"
v-model="average[key]"
@@ -47,81 +52,32 @@
/>
</div>
<div class="text-lg font-semibold mb-5">
{{ __('All Feedback') }}
</div>
<ListView
:columns="feedbackColumns"
:rows="feedbackList.data"
row-key="name"
:options="{
showTooltip: false,
rowHeight: 'h-16',
selectable: false,
}"
>
<ListHeader
class="mb-2 grid items-center space-x-4 rounded bg-surface-gray-2 p-2"
></ListHeader>
<ListRows>
<ListRow
:row="row"
v-for="row in feedbackList.data"
class="group cursor-pointer feedback-list"
>
<template #default="{ column, item }">
<ListRowItem
:item="row[column.key]"
:align="column.align"
class="text-sm"
>
<template #prefix>
<div v-if="column.key == 'member_name'">
<Avatar
class="flex"
:image="row['member_image']"
:label="item"
size="sm"
/>
</div>
</template>
<div v-if="ratingKeys.includes(column.key)">
<Rating v-model="row[column.key]" :readonly="true" />
</div>
<div v-else class="leading-5">
{{ row[column.key] }}
</div>
</ListRowItem>
</template>
</ListRow>
</ListRows>
</ListView>
<Button variant="outline" class="mt-5" @click="showAllFeedback = true">
{{ __('View all feedback') }}
</Button>
</div>
<div v-else class="text-sm italic text-center text-ink-gray-7 mt-5">
<div v-else class="text-ink-gray-7 mt-5 leading-5">
{{ __('No feedback received yet.') }}
</div>
<FeedbackModal
v-if="feedbackList.data?.length"
v-model="showAllFeedback"
:feedbackList="feedbackList.data"
/>
</template>
<script setup>
import { computed, inject, onMounted, reactive, ref, watch } from 'vue'
import { inject, onMounted, reactive, ref, watch } from 'vue'
import { convertToTitleCase } from '@/utils'
import {
Avatar,
Button,
createListResource,
FormControl,
ListView,
ListHeader,
ListRows,
ListRow,
ListRowItem,
Rating,
} from 'frappe-ui'
import { Button, createListResource, FormControl, Rating } from 'frappe-ui'
import FeedbackModal from '@/components/Modals/FeedbackModal.vue'
const user = inject('$user')
const ratingKeys = ['content', 'instructors', 'value']
const readOnly = ref(false)
const average = reactive({})
const feedback = reactive({})
const showFeedbackForm = ref(true)
const showAllFeedback = ref(false)
const props = defineProps({
batch: {
@@ -167,6 +123,7 @@ watch(
if (feedbackList.data.length) {
let data = feedbackList.data
readOnly.value = true
showFeedbackForm.value = false
ratingKeys.forEach((key) => {
average[key] = 0
@@ -201,40 +158,11 @@ const submitFeedback = () => {
{
onSuccess: () => {
feedbackList.reload()
showFeedbackForm.value = false
},
}
)
}
const feedbackColumns = computed(() => {
return [
{
label: 'Member',
key: 'member_name',
width: '10rem',
},
{
label: 'Feedback',
key: 'feedback',
width: '15rem',
},
{
label: 'Content',
key: 'content',
width: '9rem',
},
{
label: 'Instructors',
key: 'instructors',
width: '9rem',
},
{
label: 'Value',
key: 'value',
width: '9rem',
},
]
})
</script>
<style>
.feedback-list > button > div {
+34 -4
View File
@@ -56,7 +56,7 @@
</div>
<div v-if="!readOnlyMode">
<router-link
v-if="isModerator || isStudent"
v-if="canAccessBatch"
:to="{
name: 'Batch',
params: {
@@ -65,8 +65,12 @@
}"
>
<Button variant="solid" class="w-full mt-4">
<template #prefix>
<LogIn v-if="isStudent" class="size-4 stroke-1.5" />
<Settings v-else class="size-4 stroke-1.5" />
</template>
<span>
{{ isModerator ? __('Manage Batch') : __('Visit Batch') }}
{{ isStudent ? __('Visit Batch') : __('Manage Batch') }}
</span>
</Button>
</router-link>
@@ -85,6 +89,9 @@
"
>
<Button v-if="!isStudent" class="w-full mt-4" variant="solid">
<template #prefix>
<CreditCard class="size-4 stroke-1.5" />
</template>
<span>
{{ __('Register Now') }}
</span>
@@ -100,6 +107,9 @@
"
@click="enrollInBatch()"
>
<template #prefix>
<GraduationCap class="size-4 stroke-1.5" />
</template>
{{ __('Enroll Now') }}
</Button>
<router-link
@@ -112,6 +122,9 @@
}"
>
<Button class="w-full mt-2">
<template #prefix>
<Pencil class="size-4 stroke-1.5" />
</template>
<span>
{{ __('Edit') }}
</span>
@@ -122,8 +135,17 @@
</template>
<script setup>
import { inject, computed } from 'vue'
import { Badge, Button, createResource, toast } from 'frappe-ui'
import { BookOpen, Clock, Globe } from 'lucide-vue-next'
import { Button, createResource, toast } from 'frappe-ui'
import {
BookOpen,
Clock,
CreditCard,
Globe,
GraduationCap,
LogIn,
Pencil,
Settings,
} from 'lucide-vue-next'
import { formatNumberIntoCurrency, formatTime } from '@/utils'
import DateRange from '@/components/Common/DateRange.vue'
import { useRouter } from 'vue-router'
@@ -182,4 +204,12 @@ const isStudent = computed(() => {
const isModerator = computed(() => {
return user.data?.is_moderator
})
const isEvaluator = computed(() => {
return user.data?.is_evaluator
})
const canAccessBatch = computed(() => {
return isModerator.value || isStudent.value || isEvaluator.value
})
</script>
+89 -184
View File
@@ -5,104 +5,60 @@
{{ __('Statistics') }}
</div>
</div>
<div class="grid grid-cols-4 gap-5 mb-8">
<div
class="flex items-center border py-2 px-3 rounded-md text-ink-gray-7"
>
<div class="p-2 rounded-md bg-surface-gray-2 mr-3">
<User class="w-5 h-5 stroke-1.5" />
</div>
<div class="flex items-center space-x-2">
<span class="font-semibold">
{{ students.data?.length }}
</span>
<span class="">
{{ __('Students') }}
</span>
</div>
</div>
<div
class="flex items-center border py-2 px-3 rounded-md text-ink-gray-7"
>
<div class="p-2 rounded-md bg-surface-gray-2 mr-3">
<GraduationCap class="w-5 h-5 stroke-1.5" />
</div>
<div class="flex items-center space-x-2">
<span class="font-semibold">
{{ certificationCount.data }}
</span>
<span class="">
{{ __('Certified') }}
</span>
</div>
</div>
<div
class="flex items-center border py-2 px-3 rounded-md text-ink-gray-7"
>
<div class="p-2 rounded-md bg-surface-gray-2 mr-3">
<BookOpen class="w-5 h-5 stroke-1.5" />
</div>
<div class="flex items-center space-x-2">
<span class="font-semibold">
{{ batch.data.courses?.length }}
</span>
<span>
{{ __('Courses') }}
</span>
</div>
</div>
<div
class="flex items-center border py-2 px-3 rounded-md text-ink-gray-7"
>
<div class="p-2 rounded-md bg-surface-gray-2 mr-3">
<ShieldCheck class="w-5 h-5 stroke-1.5" />
</div>
<div class="flex items-center space-x-2">
<span class="font-semibold">
{{ assessmentCount }}
</span>
<span>
{{ __('Assessments') }}
</span>
</div>
</div>
</div>
<div v-if="showProgressChart" class="mb-8">
<div class="text-ink-gray-7 font-medium">
{{ __('Progress') }}
</div>
<ApexChart
:options="chartOptions"
:series="chartData"
type="bar"
:height="chartData[0].data.length * 30 + 100"
<div class="grid grid-cols-2 md:grid-cols-4 gap-5 mb-8">
<NumberChart
class="border rounded-md"
:config="{ title: __('Students'), value: students.data?.length || 0 }"
/>
<NumberChart
class="border rounded-md"
:config="{
title: __('Certified'),
value: certificationCount.data || 0,
}"
/>
<NumberChart
class="border rounded-md"
:config="{
title: __('Courses'),
value: batch.data.courses?.length || 0,
}"
/>
<NumberChart
class="border rounded-md"
:config="{ title: __('Assessments'), value: assessmentCount || 0 }"
/>
<div
class="flex items-center justify-center text-sm text-ink-gray-7 space-x-4"
>
<div class="flex items-center space-x-2">
<div
class="w-3 h-3 rounded-sm"
:style="{ 'background-color': theme.colors.green[600] }"
></div>
<div>
{{ __('Courses') }}
</div>
</div>
<div class="flex items-center space-x-2">
<div
class="w-3 h-3 rounded-sm"
:style="{ 'background-color': theme.colors.blue[600] }"
></div>
<div>
{{ __('Assessments') }}
</div>
</div>
</div>
</div>
<AxisChart
v-if="showProgressChart"
:config="{
data: chartData,
title: __('Batch Summary'),
subtitle: __('Progress of students in courses and assessments'),
xAxis: {
key: 'task',
title: 'Tasks',
type: 'category',
},
yAxis: {
title: __('Number of Students'),
echartOptions: {
minInterval: 1,
},
},
swapXY: true,
series: [
{
name: 'value',
type: 'bar',
},
],
}"
/>
</div>
<div>
@@ -214,6 +170,7 @@
<script setup>
import {
Avatar,
AxisChart,
Button,
createResource,
FeatherIcon,
@@ -224,6 +181,7 @@ import {
ListRows,
ListView,
ListRowItem,
NumberChart,
toast,
} from 'frappe-ui'
import {
@@ -245,7 +203,6 @@ const showStudentModal = ref(false)
const showStudentProgressModal = ref(false)
const selectedStudent = ref(null)
const chartData = ref(null)
const chartOptions = ref(null)
const showProgressChart = ref(false)
const assessmentCount = ref(0)
const readOnlyMode = window.read_only_mode
@@ -333,96 +290,49 @@ const removeStudents = (selections, unselectAll) => {
}
const getChartData = () => {
let categories = {}
let tasks = []
let data = []
if (!students.data?.length) return []
Object.keys(students.data[0].courses).forEach((course) => {
categories[course] = {
value: 0,
type: 'course',
label: course,
}
students.data.forEach((row) => {
tasks = countAssessments(row, tasks)
tasks = countCourses(row, tasks)
})
Object.keys(students.data?.[0].assessments).forEach((assessment) => {
categories[assessment] = {
value: 0,
type: 'assessment',
label: assessment,
}
})
students.data.forEach((student) => {
Object.keys(student.courses).forEach((course) => {
if (student.courses[course] === 100) {
categories[course].value += 1
}
})
Object.keys(student.assessments).forEach((assessment) => {
if (student.assessments[assessment].result === 'Pass') {
categories[assessment].value += 1
}
tasks.forEach((task) => {
data.push({
task: task.label,
value: task.value,
})
})
chartOptions.value = getChartOptions(categories)
return [
{
name: __('Completed by Students'),
data: Object.values(categories).map((item) => item.value),
},
]
return data
}
const getChartOptions = (categories) => {
const courseColor = theme.colors.green[700]
const assessmentColor = theme.colors.blue[700]
const maxY =
students.data?.length % 5
? students.data?.length + (5 - (students.data?.length % 5))
: students.data?.length
const countAssessments = (row, tasks) => {
Object.keys(row.assessments).forEach((assessment) => {
if (row.assessments[assessment].result === 'Pass') {
tasks.filter((task) => task.label === assessment).length
? tasks.filter((task) => task.label === assessment)[0].value++
: tasks.push({
value: 1,
label: assessment,
})
}
})
return tasks
}
return {
chart: {
type: 'bar',
toolbar: {
show: false,
},
},
plotOptions: {
bar: {
distributed: true,
borderRadius: 3,
borderRadiusApplication: 'end',
horizontal: true,
barHeight: '40%',
},
},
colors: Object.values(categories).map((item) =>
item.type === 'course' ? courseColor : assessmentColor
),
xaxis: {
categories: Object.values(categories).map((item) => item.label),
labels: {
style: {
fontSize: '10px',
},
rotate: 0,
formatter: function (value) {
return value.length > 30 ? `${value.substring(0, 30)}...` : value
},
},
},
yaxis: {
max: maxY,
min: 0,
stepSize: 10,
tickAmount: maxY / 5,
/* reversed: true */
},
}
const countCourses = (row, tasks) => {
Object.keys(row.courses).forEach((course) => {
if (row.courses[course] === 100) {
tasks.filter((task) => task.label === course).length
? tasks.filter((task) => task.label === course)[0].value++
: tasks.push({
value: 1,
label: course,
})
}
})
return tasks
}
watch(students, () => {
@@ -442,8 +352,3 @@ const certificationCount = createResource({
auto: true,
})
</script>
<style>
.apexcharts-legend {
display: none !important;
}
</style>
-130
View File
@@ -1,130 +0,0 @@
<template>
<div class="flex flex-col min-h-0">
<div class="flex items-center justify-between">
<div class="text-xl font-semibold mb-5 text-ink-gray-9">
{{ label }}
</div>
<Button @click="() => showCategoryForm()">
<template #prefix>
<Plus v-if="!showForm" class="h-3 w-3 stroke-1.5" />
<X v-else class="h-3 w-3 stroke-1.5" />
</template>
{{ showForm ? __('Close') : __('New') }}
</Button>
</div>
<div
v-if="showForm"
class="flex items-center justify-between my-4 space-x-2"
>
<FormControl
ref="categoryInput"
v-model="category"
:placeholder="__('Category Name')"
class="flex-1"
/>
<Button @click="addCategory()" variant="subtle">
{{ __('Add') }}
</Button>
</div>
<div class="overflow-y-scroll">
<div class="text-base space-y-2">
<FormControl
:value="cat.category"
type="text"
v-for="cat in categories.data"
@change.stop="(e) => update(cat.name, e.target.value)"
/>
</div>
</div>
</div>
</template>
<script setup>
import {
Button,
FormControl,
createListResource,
createResource,
debounce,
} from 'frappe-ui'
import { Plus, X } from 'lucide-vue-next'
import { ref } from 'vue'
const showForm = ref(false)
const category = ref(null)
const categoryInput = ref(null)
const props = defineProps({
label: {
type: String,
required: true,
},
description: {
type: String,
default: '',
},
})
const categories = createListResource({
doctype: 'LMS Category',
fields: ['name', 'category'],
auto: true,
})
const newCategory = createResource({
url: 'frappe.client.insert',
makeParams(values) {
return {
doc: {
doctype: 'LMS Category',
category: category.value,
},
}
},
})
const addCategory = () => {
newCategory.submit(
{},
{
onSuccess(data) {
categories.reload()
category.value = null
},
}
)
}
const showCategoryForm = () => {
showForm.value = !showForm.value
setTimeout(() => {
categoryInput.value.$el.querySelector('input').focus()
}, 0)
}
const updateCategory = createResource({
url: 'frappe.client.rename_doc',
makeParams(values) {
return {
doctype: 'LMS Category',
old_name: values.name,
new_name: values.category,
}
},
})
const update = (name, value) => {
updateCategory.submit(
{
name: name,
category: value,
},
{
onSuccess() {
categories.reload()
},
}
)
}
</script>
+126 -109
View File
@@ -1,127 +1,140 @@
<template>
<Combobox v-model="selectedValue" nullable v-slot="{ open: isComboboxOpen }">
<Popover class="w-full" v-model:show="showOptions">
<template #target="{ open: openPopover, togglePopover }">
<slot name="target" v-bind="{ open: openPopover, togglePopover }">
<div class="w-full">
<button
class="flex w-full items-center justify-between focus:outline-none"
:class="inputClasses"
@click="() => togglePopover()"
>
<div class="flex items-center">
<slot name="prefix" />
<span
class="overflow-hidden text-ellipsis whitespace-nowrap text-base leading-5"
v-if="selectedValue"
>
{{ displayValue(selectedValue) }}
</span>
<span class="text-base leading-5 text-ink-gray-4" v-else>
{{ placeholder || '' }}
</span>
</div>
<ChevronDown class="h-4 w-4 stroke-1.5" />
</button>
</div>
</slot>
</template>
<template #body="{ isOpen }">
<div v-show="isOpen">
<div class="mt-1 rounded-lg bg-surface-white py-1 text-base border-2">
<div class="relative px-1.5 pt-0.5">
<ComboboxInput
ref="search"
class="form-input w-full"
type="text"
@change="
(e) => {
query = e.target.value
}
"
:value="query"
autocomplete="off"
placeholder="Search"
/>
<div>
<div v-if="label" class="text-xs text-ink-gray-5 mb-1">
{{ __(label) }}
<span class="text-ink-red-3" v-if="attrs.required">*</span>
</div>
<Combobox
v-model="selectedValue"
nullable
v-slot="{ open: isComboboxOpen }"
>
<Popover class="w-full" v-model:show="showOptions">
<template #target="{ open: openPopover, togglePopover }">
<slot name="target" v-bind="{ open: openPopover, togglePopover }">
<div class="w-full">
<button
class="absolute right-1.5 inline-flex h-7 w-7 items-center justify-center"
@click="selectedValue = null"
class="flex w-full items-center justify-between focus:outline-none"
:class="inputClasses"
@click="() => togglePopover()"
:disabled="attrs.readonly"
>
<X class="h-4 w-4 stroke-1.5 text-ink-gray-7" />
<div class="flex items-center">
<slot name="prefix" />
<span
class="overflow-hidden text-ellipsis whitespace-nowrap text-base leading-5"
v-if="selectedValue"
>
{{ displayValue(selectedValue) }}
</span>
<span class="text-base leading-5 text-ink-gray-4" v-else>
{{ placeholder || '' }}
</span>
</div>
<ChevronDown class="h-4 w-4 stroke-1.5" />
</button>
</div>
<ComboboxOptions
class="my-1 max-h-[12rem] overflow-y-auto px-1.5"
static
</slot>
</template>
<template #body="{ isOpen }">
<div v-show="isOpen">
<div
class="mt-1 rounded-lg bg-surface-white py-1 text-base border-2"
>
<div
class="mt-1.5"
v-for="group in groups"
:key="group.key"
v-show="group.items.length > 0"
<div class="relative px-1.5 pt-0.5">
<ComboboxInput
ref="search"
class="form-input w-full"
type="text"
@change="
(e) => {
query = e.target.value
}
"
:value="query"
autocomplete="off"
placeholder="Search"
/>
<button
class="absolute right-1.5 inline-flex h-7 w-7 items-center justify-center"
@click="selectedValue = null"
>
<X class="h-4 w-4 stroke-1.5 text-ink-gray-7" />
</button>
</div>
<ComboboxOptions
class="my-1 max-h-[12rem] overflow-y-auto px-1.5"
static
>
<div
v-if="group.group && !group.hideLabel"
class="px-2.5 py-1.5 text-sm font-medium text-ink-gray-4"
class="mt-1.5"
v-for="group in groups"
:key="group.key"
v-show="group.items.length > 0"
>
{{ group.group }}
</div>
<ComboboxOption
as="template"
v-for="option in group.items"
:key="option.value"
:value="option"
v-slot="{ active, selected }"
>
<li
:class="[
'flex items-center rounded px-2.5 py-2 text-base',
{ 'bg-surface-gray-2': active },
]"
<div
v-if="group.group && !group.hideLabel"
class="px-2.5 py-1.5 text-sm font-medium text-ink-gray-4"
>
<slot
name="item-prefix"
v-bind="{ active, selected, option }"
/>
<slot
name="item-label"
v-bind="{ active, selected, option }"
{{ group.group }}
</div>
<ComboboxOption
as="template"
v-for="option in group.items"
:key="option.value"
:value="option"
v-slot="{ active, selected }"
>
<li
:class="[
'flex items-center rounded px-2.5 py-2 text-base',
{ 'bg-surface-gray-2': active },
]"
>
<div class="flex flex-col space-y-1 text-ink-gray-8">
<div>
{{ option.label }}
<slot
name="item-prefix"
v-bind="{ active, selected, option }"
/>
<slot
name="item-label"
v-bind="{ active, selected, option }"
>
<div class="flex flex-col space-y-1 text-ink-gray-8">
<div>
{{ option.label }}
</div>
<div
v-if="
option.description &&
option.description != option.label
"
class="text-xs text-ink-gray-7"
v-html="option.description"
></div>
</div>
<div
v-if="
option.description &&
option.description != option.label
"
class="text-xs text-ink-gray-7"
v-html="option.description"
></div>
</div>
</slot>
</li>
</ComboboxOption>
</slot>
</li>
</ComboboxOption>
</div>
<li
v-if="groups.length == 0"
class="mt-1.5 rounded-md px-2.5 py-1.5 text-base text-ink-gray-5"
>
No results found
</li>
</ComboboxOptions>
<div v-if="slots.footer" class="border-t p-1.5 pb-0.5">
<slot
name="footer"
v-bind="{ value: search?.el._value, close }"
></slot>
</div>
<li
v-if="groups.length == 0"
class="mt-1.5 rounded-md px-2.5 py-1.5 text-base text-ink-gray-5"
>
No results found
</li>
</ComboboxOptions>
<div v-if="slots.footer" class="border-t p-1.5 pb-0.5">
<slot
name="footer"
v-bind="{ value: search?.el._value, close }"
></slot>
</div>
</div>
</div>
</template>
</Popover>
</Combobox>
</template>
</Popover>
</Combobox>
</div>
</template>
<script setup>
@@ -148,6 +161,10 @@ const props = defineProps({
type: String,
default: 'md',
},
label: {
type: String,
default: '',
},
variant: {
type: String,
default: 'subtle',
@@ -0,0 +1,149 @@
<template>
<div>
<div class="text-xs text-ink-gray-5 mb-2">
{{ label }}
</div>
<div class="overflow-x-auto border rounded-md">
<div
class="grid items-center space-x-4 p-2 border-b"
:style="{ gridTemplateColumns: getGridTemplateColumns() }"
>
<div
v-for="(column, index) in columns"
:key="index"
class="text-sm text-ink-gray-5"
>
{{ column }}
</div>
<div></div>
</div>
<div
v-for="(row, rowIndex) in rows"
:key="rowIndex"
class="grid items-center space-x-4 p-2"
:style="{ gridTemplateColumns: getGridTemplateColumns() }"
>
<template v-for="key in Object.keys(row)" :key="key">
<input
v-if="showKey(key)"
v-model="row[key]"
class="py-1.5 px-2 border-none focus:ring-0 focus:border focus:border-gray-300 focus:bg-surface-gray-2 rounded-sm text-sm focus:outline-none"
/>
</template>
<div class="relative" ref="menuRef">
<Button
variant="ghost"
@click="(event: MouseEvent) => toggleMenu(rowIndex, event)"
>
<template #icon>
<Ellipsis
class="size-4 text-ink-gray-7 stroke-1.5 cursor-pointer"
/>
</template>
</Button>
<div
v-if="menuOpenIndex === rowIndex"
class="absolute right-[30px] top-5 mt-1 w-32 bg-surface-white border border-outline-gray-1 rounded-md shadow-sm"
>
<button
@click="deleteRow(rowIndex)"
class="flex items-center space-x-2 w-full text-left px-3 py-2 text-sm text-ink-red-3"
>
<Trash2 class="size-4 stroke-1.5" />
<span>
{{ __('Delete') }}
</span>
</button>
</div>
</div>
</div>
</div>
<div class="mt-2">
<Button @click="addRow">
<template #prefix>
<Plus class="size-4 text-ink-gray-7" />
</template>
{{ __('Add Row') }}
</Button>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, watch } from 'vue'
import { Button } from 'frappe-ui'
import { Ellipsis, Plus, Trash2 } from 'lucide-vue-next'
import { onClickOutside } from '@vueuse/core'
const rows = defineModel<Cell[][]>()
const menuRef = ref(null)
const menuOpenIndex = ref<number | null>(null)
const menuTopPosition = ref<string>('')
const emit = defineEmits<{
(e: 'update:modelValue', value: Cell[][]): void
}>()
type Cell = {
value: string
editable?: boolean
}
const props = withDefaults(
defineProps<{
modelValue?: Cell[][]
columns?: string[]
label?: string
}>(),
{
columns: [],
}
)
const columns = ref(props.columns)
watch(rows, () => {
if (rows.value?.length < 1) {
addRow()
}
})
const addRow = () => {
if (!rows.value) {
rows.value = []
}
let newRow: { [key: string]: string } = {}
columns.value.forEach((column: any) => {
newRow[column.toLowerCase().split(' ').join('_')] = ''
})
rows.value.push(newRow)
emit('update:modelValue', rows.value)
}
const deleteRow = (index: number) => {
rows.value.splice(index, 1)
emit('update:modelValue', rows.value)
}
const getGridTemplateColumns = () => {
return [...Array(columns.value.length).fill('1fr'), '0.25fr'].join(' ')
}
const toggleMenu = (index: number, event: MouseEvent) => {
menuOpenIndex.value = menuOpenIndex.value === index ? null : index
menuTopPosition.value = `${event.clientY + 10}px`
}
onClickOutside(menuRef, () => {
menuOpenIndex.value = null
})
const showKey = (key: string) => {
let columnsLower = columns.value.map((col) =>
col.toLowerCase().split(' ').join('_')
)
return columnsLower.includes(key)
}
</script>
+162
View File
@@ -0,0 +1,162 @@
<template>
<div class="flex w-full flex-col gap-1.5">
<div v-if="label" class="text-xs text-ink-gray-5">
{{ __(label) }}
</div>
<codemirror
v-model="code"
:extensions="extensions"
:tab-size="2"
:autofocus="autofocus"
:indent-with-tab="true"
:style="{ height: height, maxHeight: maxHeight }"
:disabled="readonly"
@blur="emitEditorValue"
:class="{
'border border-outline-gray-1': showBorder,
}"
/>
<Button
v-if="showSaveButton"
@click="emit('save', code)"
class="mt-3 w-full text-base"
>
{{ __('Save') }}
</Button>
</div>
</template>
<script setup lang="ts">
import { onMounted, ref, computed, watch } from 'vue'
import { Button } from 'frappe-ui'
import { Codemirror } from 'vue-codemirror'
import { autocompletion, closeBrackets } from '@codemirror/autocomplete'
import { LanguageSupport } from '@codemirror/language'
import { EditorView } from '@codemirror/view'
import { tomorrow } from 'thememirror'
const props = withDefaults(
defineProps<{
language: 'json' | 'javascript' | 'html' | 'css' | 'python'
modelValue: string | object | Array<string | object> | null
height?: string
maxHeight?: string
autofocus?: boolean
showSaveButton?: boolean
showLineNumbers?: boolean
completions?: Function | null
label?: string
showBorder?: boolean
required?: boolean
readonly?: boolean
}>(),
{
language: 'javascript',
modelValue: null,
height: 'auto',
maxHeight: '250px',
showLineNumbers: true,
completions: null,
}
)
const emit = defineEmits(['update:modelValue', 'save'])
const code = ref<string>('')
watch(
() => props.modelValue,
(newVal) => {
code.value =
typeof newVal === 'string' ? newVal : JSON.stringify(newVal, null, 2)
},
{ immediate: true }
)
watch(code, (val) => {
emit('update:modelValue', val)
})
const errorMessage = ref('')
const emitEditorValue = () => {
try {
errorMessage.value = ''
let value = code.value || ''
if (!props.showSaveButton && !props.readonly) {
emit('update:modelValue', value)
}
} catch (e) {
console.error('Error while parsing JSON for editor', e)
errorMessage.value = `Invalid object/JSON: ${e.message}`
}
}
const languageExtension = ref<LanguageSupport>()
const autocompleteExtension = ref()
async function setLanguageExtension() {
const importMap = {
json: () => import('@codemirror/lang-json'),
javascript: () => import('@codemirror/lang-javascript'),
html: () => import('@codemirror/lang-html'),
css: () => import('@codemirror/lang-css'),
python: () => import('@codemirror/lang-python'),
}
const languageImport = importMap[props.language]
if (!languageImport) return
const module = await languageImport()
languageExtension.value = (module as any)[props.language]()
if (props.completions) {
const languageData = (module as any)[`${props.language}Language`]
autocompleteExtension.value = languageData.data.of({
autocomplete: props.completions,
})
}
}
onMounted(async () => {
await setLanguageExtension()
})
watch(
() => props.language,
async () => {
await setLanguageExtension()
},
{ immediate: true }
)
const extensions = computed(() => {
const baseExtensions = [
closeBrackets(),
tomorrow,
EditorView.theme({
'&': {
fontFamily: 'monospace',
fontSize: '12px',
},
'.cm-gutters': {
display: props.showLineNumbers ? 'flex' : 'none',
},
}),
]
if (languageExtension.value) {
baseExtensions.push(languageExtension.value)
}
if (autocompleteExtension.value) {
baseExtensions.push(autocompleteExtension.value)
}
const autocompletionOptions = {
activateOnTyping: true,
maxRenderedOptions: 10,
closeOnBlur: false,
icons: false,
optionClass: () => 'flex h-7 !px-2 items-center rounded !text-gray-600',
}
baseExtensions.push(autocompletion(autocompletionOptions))
return baseExtensions
})
</script>
@@ -5,7 +5,7 @@
height: height,
}"
>
<span class="text-xs text-ink-gray-7" v-if="label">
<span class="text-xs text-ink-gray-7 mb-1" v-if="label">
{{ label }}
</span>
<div
@@ -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>
@@ -12,6 +12,7 @@
:variant="attrs.variant"
:placeholder="attrs.placeholder"
:filterable="false"
:readonly="attrs.readonly"
>
<template #target="{ open, togglePopover }">
<slot name="target" v-bind="{ open, togglePopover }" />
@@ -55,9 +55,10 @@
</div>
</li>
</ComboboxOption>
<div class="h-10"></div>
<div
v-if="attrs.onCreate"
class="absolute bottom-2 left-1 w-[98%] pt-2 bg-white border-t"
class="absolute bottom-2 left-1 w-[99%] pt-2 bg-white border-t"
>
<Button
variant="ghost"
@@ -0,0 +1,76 @@
<template>
<div class="mb-4">
<div v-if="label" class="text-xs text-ink-gray-5 mb-2">
{{ __(label) }}
<span class="text-ink-red-3">*</span>
</div>
<FileUploader
v-if="!modelValue"
:fileTypes="['image/*']"
:validateFile="validateFile"
@success="(file: File) => saveImage(file)"
>
<template v-slot="{ file, progress, uploading, openFileSelector }">
<div class="flex items-center">
<div class="border rounded-md w-fit py-7 px-20">
<Image class="size-5 stroke-1 text-ink-gray-7" />
</div>
<div class="ml-4">
<Button @click="openFileSelector">
{{ __('Upload') }}
</Button>
<div class="mt-1 text-ink-gray-5 text-sm leading-5">
{{ __(description) }}
</div>
</div>
</div>
</template>
</FileUploader>
<div v-else class="mb-4">
<div class="flex items-center">
<img :src="modelValue" class="border rounded-md w-44 h-auto" />
<div class="ml-4">
<Button @click="removeImage()">
{{ __('Remove') }}
</Button>
<div
v-if="description"
class="mt-2 text-ink-gray-5 text-sm leading-5"
>
{{ __(description) }}
</div>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { validateFile } from '@/utils'
import { Button, FileUploader } from 'frappe-ui'
import { Image } from 'lucide-vue-next'
const emit = defineEmits<{
(e: 'update:modelValue', value: string): void
}>()
const props = withDefaults(
defineProps<{
modelValue: string
label?: string
description?: string
}>(),
{
modelValue: '',
label: '',
description: '',
}
)
const saveImage = (file: any) => {
emit('update:modelValue', file.file_url)
}
const removeImage = () => {
emit('update:modelValue', '')
}
</script>
+73 -67
View File
@@ -1,41 +1,57 @@
<template>
<div
v-if="course.title"
class="flex flex-col h-full rounded-md border-2 overflow-auto"
class="flex flex-col h-full rounded-md overflow-auto text-ink-gray-9"
style="min-height: 350px"
>
<div
class="course-image"
:class="{ 'default-image': !course.image }"
:style="{ backgroundImage: 'url(\'' + encodeURI(course.image) + '\')' }"
class="w-[100%] h-[168px] bg-cover bg-center bg-no-repeat"
:style="
course.image
? { backgroundImage: `url('${encodeURI(course.image)}')` }
: {
backgroundImage: getGradientColor(),
backgroundBlendMode: 'screen',
}
"
>
<div class="flex items-center flex-wrap relative top-4 px-2 w-fit">
<Badge
<!-- <div class="flex items-center flex-wrap relative top-4 px-2 w-fit">
<div
v-if="course.featured"
variant="subtle"
theme="green"
size="md"
class="mb-1 mr-1"
class="flex items-center space-x-1 text-xs text-ink-amber-3 bg-surface-white border border-outline-amber-1 px-2 py-0.5 rounded-md mr-1 mb-1"
>
{{ __('Featured') }}
</Badge>
<Star class="size-3 stroke-2" />
<span>
{{ __('Featured') }}
</span>
</div>
<div
v-if="course.tags"
v-for="tag in course.tags?.split(', ')"
class="text-xs bg-white text-gray-800 px-2 py-0.5 rounded-md mb-1 mr-1"
class="text-xs border bg-surface-white text-ink-gray-9 px-2 py-0.5 rounded-md mb-1 mr-1"
>
{{ tag }}
</div>
</div>
<div v-if="!course.image" class="image-placeholder">
{{ course.title[0] }}
</div> -->
<div
v-if="!course.image"
class="flex items-center justify-center text-white flex-1 font-extrabold my-auto px-5 text-center leading-6 h-full"
:class="
course.title.length > 32
? 'text-lg'
: course.title.length > 20
? 'text-xl'
: 'text-2xl'
"
>
{{ course.title }}
</div>
</div>
<div class="flex flex-col flex-auto p-4">
<div class="flex flex-col flex-auto p-4 border-x-2 border-b-2 rounded-b-md">
<div class="flex items-center justify-between mb-2">
<div v-if="course.lessons">
<Tooltip :text="__('Lessons')">
<span class="flex items-center text-ink-gray-7">
<span class="flex items-center">
<BookOpen class="h-4 w-4 stroke-1.5 mr-1" />
{{ course.lessons }}
</span>
@@ -44,8 +60,8 @@
<div v-if="course.enrollments">
<Tooltip :text="__('Enrolled Students')">
<span class="flex items-center text-ink-gray-7">
<Users class="h-4 w-4 stroke-1. mr-1" />
<span class="flex items-center">
<Users class="h-4 w-4 stroke-1.5 mr-1" />
{{ course.enrollments }}
</span>
</Tooltip>
@@ -53,29 +69,27 @@
<div v-if="course.rating">
<Tooltip :text="__('Average Rating')">
<span class="flex items-center text-ink-gray-7">
<span class="flex items-center">
<Star class="h-4 w-4 stroke-1.5 mr-1" />
{{ course.rating }}
</span>
</Tooltip>
</div>
<div v-if="course.status != 'Approved'">
<Badge
variant="subtle"
:theme="course.status === 'Under Review' ? 'orange' : 'blue'"
size="sm"
>
{{ course.status }}
</Badge>
</div>
<Tooltip v-if="course.featured" :text="__('Featured')">
<Award class="size-4 stroke-2 text-ink-amber-3" />
</Tooltip>
</div>
<div class="text-xl font-semibold leading-6 text-ink-gray-9">
<div
v-if="course.image"
class="font-semibold leading-6"
:class="course.title.length > 32 ? 'text-lg' : 'text-xl'"
>
{{ course.title }}
</div>
<div class="short-introduction text-ink-gray-7 text-sm">
<div class="short-introduction text-sm">
{{ course.short_introduction }}
</div>
@@ -84,11 +98,8 @@
:progress="course.membership.progress"
/>
<div
v-if="user && course.membership"
class="text-sm text-ink-gray-7 mt-2 mb-4"
>
{{ Math.ceil(course.membership.progress) }}% completed
<div v-if="user && course.membership" class="text-sm mt-2 mb-4">
{{ Math.ceil(course.membership.progress) }}% {{ __('completed') }}
</div>
<div class="flex items-center justify-between mt-auto">
@@ -108,21 +119,23 @@
<div v-if="course.paid_course" class="font-semibold">
{{ course.price }}
</div>
<div
<Tooltip
v-if="course.paid_certificate || course.enable_certification"
class="text-xs text-ink-blue-3 bg-surface-blue-1 py-0.5 px-1 rounded-md"
:text="__('Get Certified')"
>
{{ __('Certification') }}
</div>
<GraduationCap class="size-5 stroke-1.5 text-ink-gray-7" />
</Tooltip>
</div>
</div>
</div>
</template>
<script setup>
import { BookOpen, Users, Star } from 'lucide-vue-next'
import { Award, BookOpen, GraduationCap, Star, Users } from 'lucide-vue-next'
import UserAvatar from '@/components/UserAvatar.vue'
import { sessionStore } from '@/stores/session'
import { Badge, Tooltip } from 'frappe-ui'
import { Tooltip } from 'frappe-ui'
import { theme } from '@/utils/theme'
import CourseInstructors from '@/components/CourseInstructors.vue'
import ProgressBar from '@/components/ProgressBar.vue'
@@ -134,16 +147,24 @@ const props = defineProps({
default: null,
},
})
const getGradientColor = () => {
let color = props.course.card_gradient?.toLowerCase() || 'blue'
let colorMap = theme.backgroundColor[color]
return `linear-gradient(to top right, black, ${colorMap[400]})`
/* return `bg-gradient-to-br from-${color}-100 via-${color}-200 to-${color}-400` */
/* return `linear-gradient(to bottom right, ${colorMap[100]}, ${colorMap[400]})` */
/* return `radial-gradient(ellipse at 80% 20%, black 20%, ${colorMap[500]} 100%)` */
/* return `radial-gradient(ellipse at 30% 70%, black 50%, ${colorMap[500]} 100%)` */
/* return `radial-gradient(ellipse at 80% 20%, ${colorMap[100]} 0%, ${colorMap[300]} 50%, ${colorMap[500]} 100%)` */
/* return `conic-gradient(from 180deg at 50% 50%, ${colorMap[100]} 0%, ${colorMap[200]} 50%, ${colorMap[400]} 100%)` */
/* return `linear-gradient(135deg, ${colorMap[100]}, ${colorMap[300]}), linear-gradient(120deg, rgba(255,255,255,0.4) 0%, transparent 60%) ` */
/* return `radial-gradient(circle at 20% 30%, ${colorMap[100]} 0%, transparent 40%),
radial-gradient(circle at 80% 40%, ${colorMap[200]} 0%, transparent 50%),
linear-gradient(135deg, ${colorMap[300]} 0%, ${colorMap[400]} 100%);` */
}
</script>
<style>
.course-image {
height: 168px;
width: 100%;
background-size: cover;
background-position: center;
background-repeat: no-repeat;
}
.course-card-pills {
background: #ffffff;
margin-left: 0;
@@ -157,14 +178,6 @@ const props = defineProps({
width: fit-content;
}
.default-image {
display: flex;
flex-direction: column;
align-items: center;
background-color: theme('colors.green.100');
color: theme('colors.green.600');
}
.avatar-group {
display: inline-flex;
align-items: center;
@@ -173,14 +186,7 @@ const props = defineProps({
.avatar-group .avatar {
transition: margin 0.1s ease-in-out;
}
.image-placeholder {
display: flex;
align-items: center;
flex: 1;
font-size: 5rem;
color: theme('colors.gray.700');
font-weight: 600;
}
.avatar-group.overlap .avatar + .avatar {
margin-left: calc(-8px);
}
+60 -14
View File
@@ -1,5 +1,5 @@
<template>
<div class="border-2 rounded-md min-w-80">
<div class="border-2 rounded-md min-w-80 max-w-sm">
<iframe
v-if="course.data.video_link"
:src="video_link"
@@ -26,6 +26,9 @@
}"
>
<Button variant="solid" size="md" class="w-full">
<template #prefix>
<BookText class="size-4 stroke-1.5" />
</template>
<span>
{{ __('Continue Learning') }}
</span>
@@ -44,6 +47,9 @@
}"
>
<Button variant="solid" size="md" class="w-full">
<template #prefix>
<CreditCard class="size-4 stroke-1.5" />
</template>
<span>
{{ __('Buy this course') }}
</span>
@@ -57,12 +63,15 @@
{{ __('Contact the Administrator to enroll for this course.') }}
</Badge>
<Button
v-else
v-else-if="!user.data?.is_moderator && !is_instructor()"
@click="enrollStudent()"
variant="solid"
class="w-full"
size="md"
>
<template #prefix>
<BookText class="size-4 stroke-1.5" />
</template>
<span>
{{ __('Start Learning') }}
</span>
@@ -74,8 +83,22 @@
class="w-full mt-2"
size="md"
>
<template #prefix>
<GraduationCap class="size-4 stroke-1.5" />
</template>
{{ __('Get Certificate') }}
</Button>
<Button
v-if="user.data?.is_moderator || is_instructor()"
class="w-full mt-2"
size="md"
@click="showProgressSummary"
>
<template #prefix>
<TrendingUp class="size-4 stroke-1.5" />
{{ __('Progress Summary') }}
</template>
</Button>
<router-link
v-if="user?.data?.is_moderator || is_instructor()"
:to="{
@@ -86,6 +109,9 @@
}"
>
<Button variant="subtle" class="w-full mt-2" size="md">
<template #prefix>
<Pencil class="size-4 stroke-1.5" />
</template>
<span>
{{ __('Edit') }}
</span>
@@ -116,7 +142,7 @@
v-if="parseInt(course.data.rating) > 0"
class="flex items-center text-ink-gray-9"
>
<Star class="h-4 w-4 stroke-1.5 fill-orange-500 text-gray-50" />
<Star class="size-4 stroke-1.5 fill-yellow-500 text-transparent" />
<span class="ml-2">
{{ course.data.rating }} {{ __('Rating') }}
</span>
@@ -142,18 +168,34 @@
</div>
</div>
</div>
<CourseProgressSummary
v-model="showProgressModal"
:courseName="course.data.name"
:enrollments="course.data.enrollments"
/>
</template>
<script setup>
import { BookOpen, Users, Star, GraduationCap } from 'lucide-vue-next'
import { computed, inject } from 'vue'
import { Badge, Button, createResource, toast } from 'frappe-ui'
import {
BookOpen,
BookText,
CreditCard,
GraduationCap,
Pencil,
Star,
TrendingUp,
Users,
} from 'lucide-vue-next'
import { computed, inject, ref } from 'vue'
import { Badge, Button, call, createResource, toast } from 'frappe-ui'
import { formatAmount } from '@/utils/'
import { capture } from '@/telemetry'
import { useRouter } from 'vue-router'
import CertificationLinks from '@/components/CertificationLinks.vue'
import CourseProgressSummary from '@/components/Modals/CourseProgressSummary.vue'
const router = useRouter()
const user = inject('$user')
const showProgressModal = ref(false)
const readOnlyMode = window.read_only_mode
const props = defineProps({
@@ -175,15 +217,11 @@ function enrollStudent() {
toast.success(__('You need to login first to enroll for this course'))
setTimeout(() => {
window.location.href = `/login?redirect-to=${window.location.pathname}`
}, 1000)
}, 500)
} else {
const enrollStudentResource = createResource({
url: 'lms.lms.doctype.lms_enrollment.lms_enrollment.create_membership',
call('lms.lms.doctype.lms_enrollment.lms_enrollment.create_membership', {
course: props.course.data.name,
})
enrollStudentResource
.submit({
course: props.course.data.name,
})
.then(() => {
capture('enrolled_in_course', {
course: props.course.data.name,
@@ -198,7 +236,11 @@ function enrollStudent() {
lessonNumber: 1,
},
})
}, 2000)
}, 1000)
})
.catch((err) => {
toast.warning(__(err.messages?.[0] || err))
console.error(err)
})
}
}
@@ -246,4 +288,8 @@ const fetchCertificate = () => {
member: user.data?.name,
})
}
const showProgressSummary = () => {
showProgressModal.value = true
}
</script>
@@ -1,5 +1,5 @@
<template>
<div class="text-ink-gray-7">
<div class="">
<span v-if="instructors?.length == 1">
<router-link
:to="{
@@ -19,7 +19,7 @@
>
{{ instructors[0].first_name }}
</router-link>
and
{{ __('and') }}
<router-link
:to="{
name: 'Profile',
@@ -38,7 +38,7 @@
>
{{ instructors[0].first_name }}
</router-link>
and {{ instructors?.length - 1 }} others
{{ __('and') }} {{ instructors?.length - 1 }} {{ __('others') }}
</span>
</div>
</template>
+166 -108
View File
@@ -23,119 +23,135 @@
'border-2 rounded-md py-2 px-2': showOutline && outline.data?.length,
}"
>
<Disclosure
v-slot="{ open }"
v-for="(chapter, index) in outline.data"
:key="chapter.name"
:defaultOpen="openChapterDetail(chapter.idx)"
<Draggable
:list="outline.data"
:disabled="!allowEdit"
item-key="name"
group="chapters"
@end="updateChapterOrder"
>
<DisclosureButton ref="" class="flex items-center w-full p-2 group">
<ChevronRight
:class="{
'rotate-90 transform duration-200': open,
'duration-200': !open,
hidden: chapter.is_scorm_package,
open: index == 1,
}"
class="h-4 w-4 text-ink-gray-9 stroke-1"
/>
<div
class="text-base text-left text-ink-gray-9 font-medium leading-5 ml-2"
@click="redirectToChapter(chapter)"
>
{{ chapter.title }}
</div>
<div class="flex ml-auto space-x-4">
<Tooltip :text="__('Edit Chapter')" placement="bottom">
<FilePenLine
v-if="allowEdit"
@click.prevent="openChapterModal(chapter)"
class="h-4 w-4 text-ink-gray-9 invisible group-hover:visible"
/>
</Tooltip>
<Tooltip :text="__('Delete Chapter')" placement="bottom">
<Trash2
v-if="allowEdit"
@click.prevent="trashChapter(chapter.name)"
class="h-4 w-4 text-ink-red-3 invisible group-hover:visible"
/>
</Tooltip>
</div>
</DisclosureButton>
<DisclosurePanel v-if="!chapter.is_scorm_package">
<Draggable
v-if="!chapter.is_scorm_package"
:list="chapter.lessons"
:disabled="!allowEdit"
item-key="name"
group="items"
@end="updateOutline"
:data-chapter="chapter.name"
>
<template #item="{ element: lesson }">
<div
class="outline-lesson pl-8 py-2 pr-4 text-ink-gray-9"
:class="
isActiveLesson(lesson.number) ? 'bg-surface-gray-3' : ''
"
<template #item="{ element: chapter, index }">
<div class="chapter-item">
<Disclosure
v-slot="{ open }"
:key="chapter.name"
:defaultOpen="openChapterDetail(chapter.idx)"
>
<DisclosureButton
ref=""
class="flex items-center w-full p-2 group"
>
<router-link
:to="{
name: allowEdit ? 'LessonForm' : 'Lesson',
params: {
courseName: courseName,
chapterNumber: lesson.number.split('.')[0],
lessonNumber: lesson.number.split('.')[1],
},
<ChevronRight
:class="{
'rotate-90 transform duration-200': open,
'duration-200': !open,
hidden: chapter.is_scorm_package,
open: index == 1,
}"
class="h-4 w-4 text-ink-gray-9 stroke-1"
/>
<div
class="text-base text-left text-ink-gray-9 font-medium leading-5 ml-2"
@click="redirectToChapter(chapter)"
>
<div class="flex items-center text-sm leading-5 group">
<MonitorPlay
v-if="lesson.icon === 'icon-youtube'"
class="h-4 w-4 stroke-1 mr-2"
{{ chapter.title }}
</div>
<div class="flex ml-auto space-x-4">
<Tooltip :text="__('Edit Chapter')" placement="bottom">
<FilePenLine
v-if="allowEdit"
@click.prevent="openChapterModal(chapter)"
class="h-4 w-4 text-ink-gray-9 invisible group-hover:visible"
/>
<HelpCircle
v-else-if="lesson.icon === 'icon-quiz'"
class="h-4 w-4 stroke-1 mr-2"
/>
<FileText
v-else-if="lesson.icon === 'icon-list'"
class="h-4 w-4 text-ink-gray-9 stroke-1 mr-2"
/>
{{ lesson.title }}
</Tooltip>
<Tooltip :text="__('Delete Chapter')" placement="bottom">
<Trash2
v-if="allowEdit"
@click.prevent="trashLesson(lesson.name, chapter.name)"
class="h-4 w-4 text-ink-red-3 ml-auto invisible group-hover:visible"
@click.prevent="trashChapter(chapter.name)"
class="h-4 w-4 text-ink-red-3 invisible group-hover:visible"
/>
<Check
v-if="lesson.is_complete"
class="h-4 w-4 text-green-700 ml-2"
/>
</div>
</router-link>
</div>
</template>
</Draggable>
<div v-if="allowEdit" class="flex mt-2 mb-4 pl-8">
<router-link
v-if="!chapter.is_scorm_package"
:to="{
name: 'LessonForm',
params: {
courseName: courseName,
chapterNumber: chapter.idx,
lessonNumber: chapter.lessons.length + 1,
},
}"
>
<Button>
{{ __('Add Lesson') }}
</Button>
</router-link>
</Tooltip>
</div>
</DisclosureButton>
<DisclosurePanel v-if="!chapter.is_scorm_package">
<Draggable
v-if="!chapter.is_scorm_package"
:list="chapter.lessons"
:disabled="!allowEdit"
item-key="name"
group="items"
@end="updateOutline"
:data-chapter="chapter.name"
>
<template #item="{ element: lesson }">
<div
class="outline-lesson pl-8 py-2 pr-4 text-ink-gray-9"
:class="
isActiveLesson(lesson.number) ? 'bg-surface-gray-3' : ''
"
>
<router-link
:to="{
name: allowEdit ? 'LessonForm' : 'Lesson',
params: {
courseName: courseName,
chapterNumber: lesson.number.split('.')[0],
lessonNumber: lesson.number.split('.')[1],
},
}"
>
<div class="flex items-center text-sm leading-5 group">
<MonitorPlay
v-if="lesson.icon === 'icon-youtube'"
class="h-4 w-4 stroke-1 mr-2"
/>
<HelpCircle
v-else-if="lesson.icon === 'icon-quiz'"
class="h-4 w-4 stroke-1 mr-2"
/>
<FileText
v-else-if="lesson.icon === 'icon-list'"
class="h-4 w-4 text-ink-gray-9 stroke-1 mr-2"
/>
{{ lesson.title }}
<Trash2
v-if="allowEdit"
@click.prevent="
trashLesson(lesson.name, chapter.name)
"
class="h-4 w-4 text-ink-red-3 ml-auto invisible group-hover:visible"
/>
<Check
v-if="lesson.is_complete"
class="h-4 w-4 text-green-700 ml-2"
/>
</div>
</router-link>
</div>
</template>
</Draggable>
<div v-if="allowEdit" class="flex mt-2 mb-4 pl-8">
<router-link
v-if="!chapter.is_scorm_package"
:to="{
name: 'LessonForm',
params: {
courseName: courseName,
chapterNumber: chapter.idx,
lessonNumber: chapter.lessons.length + 1,
},
}"
>
<Button>
{{ __('Add Lesson') }}
</Button>
</router-link>
</div>
</DisclosurePanel>
</Disclosure>
</div>
</DisclosurePanel>
</Disclosure>
</template>
</Draggable>
</div>
</div>
<ChapterModal
@@ -148,7 +164,7 @@
</template>
<script setup>
import { Button, createResource, Tooltip, toast } from 'frappe-ui'
import { getCurrentInstance, inject, ref } from 'vue'
import { getCurrentInstance, inject, ref, watch } from 'vue'
import Draggable from 'vuedraggable'
import { Disclosure, DisclosureButton, DisclosurePanel } from '@headlessui/vue'
import {
@@ -192,18 +208,38 @@ const props = defineProps({
type: Boolean,
default: false,
},
lessonProgress: {
type: Number,
default: 0,
},
})
const outline = createResource({
url: 'lms.lms.utils.get_course_outline',
cache: ['course_outline', props.courseName],
params: {
course: props.courseName,
progress: props.getProgress,
makeParams() {
return {
course: props.courseName,
progress: props.getProgress,
}
},
auto: true,
})
watch(
() => props.courseName,
() => {
outline.reload()
}
)
watch(
() => props.lessonProgress,
() => {
outline.reload()
}
)
const deleteLesson = createResource({
url: 'lms.lms.api.delete_lesson',
makeParams(values) {
@@ -233,6 +269,20 @@ const updateLessonIndex = createResource({
},
})
const updateChapterIndex = createResource({
url: 'lms.lms.api.update_chapter_index',
makeParams(values) {
return {
chapter: values.chapter,
course: values.course,
idx: values.idx,
}
},
onSuccess() {
toast.success(__('Chapter moved successfully'))
},
})
const trashLesson = (lessonName, chapterName) => {
$dialog({
title: __('Delete this lesson?'),
@@ -278,6 +328,14 @@ const updateOutline = (e) => {
})
}
const updateChapterOrder = (e) => {
updateChapterIndex.submit({
chapter: e.item.__draggable_context.element.name,
course: props.courseName,
idx: e.newIndex,
})
}
const deleteChapter = createResource({
url: 'lms.lms.api.delete_chapter',
makeParams(values) {
+16 -7
View File
@@ -35,14 +35,14 @@
<span class="text-ink-gray-7">
{{ review.creation }}
</span>
<div class="flex mt-2">
<div class="flex mt-2 space-x-1">
<Star
v-for="index in 5"
class="h-5 w-5 text-ink-gray-1 rounded-sm mr-2"
class="size-4 text-transparent rounded-sm"
:class="
index <= Math.ceil(review.rating)
? 'fill-orange-500'
: 'fill-gray-600'
? 'fill-yellow-500'
: 'fill-gray-300'
"
/>
</div>
@@ -64,7 +64,7 @@
<script setup>
import { Star } from 'lucide-vue-next'
import { createResource, Button } from 'frappe-ui'
import { computed, ref, inject } from 'vue'
import { watch, ref, inject } from 'vue'
import UserAvatar from '@/components/UserAvatar.vue'
import ReviewModal from '@/components/Modals/ReviewModal.vue'
@@ -101,12 +101,21 @@ const hasReviewed = createResource({
const reviews = createResource({
url: 'lms.lms.utils.get_reviews',
cache: ['course_reviews', props.courseName],
params: {
course: props.courseName,
makeParams() {
return {
course: props.courseName,
}
},
auto: true,
})
watch(
() => props.courseName,
() => {
reviews.reload()
}
)
const showReviewModal = ref(false)
function openReviewModal() {
+10 -4
View File
@@ -32,13 +32,13 @@
"
:options="[
{
label: 'Edit',
label: __('Edit'),
onClick() {
reply.editable = true
},
},
{
label: 'Delete',
label: __('Delete'),
onClick() {
deleteReply(reply)
},
@@ -94,10 +94,10 @@
</template>
<script setup>
import { createResource, TextEditor, Button, Dropdown, toast } from 'frappe-ui'
import { timeAgo } from '../utils'
import { timeAgo } from '@/utils'
import UserAvatar from '@/components/UserAvatar.vue'
import { ChevronLeft, MoreHorizontal } from 'lucide-vue-next'
import { ref, inject, onMounted } from 'vue'
import { ref, inject, onMounted, onUnmounted } from 'vue'
const showTopics = defineModel('showTopics')
const newReply = ref('')
@@ -251,4 +251,10 @@ const deleteReply = (reply) => {
}
)
}
onUnmounted(() => {
socket.off('publish_message')
socket.off('update_message')
socket.off('delete_message')
})
</script>
+12 -5
View File
@@ -5,6 +5,9 @@
class="float-right"
@click="openTopicModal()"
>
<template #prefix>
<Plus class="size-4" />
</template>
{{ __('New {0}').format(singularize(title)) }}
</Button>
<div class="text-xl font-semibold text-ink-gray-9">
@@ -49,7 +52,7 @@
class="flex flex-col items-center justify-center border-2 border-dashed mt-5 py-8 rounded-md"
>
<MessageSquareText class="w-7 h-7 text-ink-gray-4 stroke-1.5 mr-2" />
<div class="">
<div class="mt-2">
<div v-if="emptyStateTitle" class="font-medium mb-2">
{{ __(emptyStateTitle) }}
</div>
@@ -69,11 +72,11 @@
<script setup>
import { createResource, Button } from 'frappe-ui'
import UserAvatar from '@/components/UserAvatar.vue'
import { singularize, timeAgo } from '../utils'
import { ref, onMounted, inject } from 'vue'
import { singularize, timeAgo } from '@/utils'
import { ref, onMounted, inject, onUnmounted } from 'vue'
import DiscussionReplies from '@/components/DiscussionReplies.vue'
import DiscussionModal from '@/components/Modals/DiscussionModal.vue'
import { MessageSquareText } from 'lucide-vue-next'
import { MessageSquareText, Plus } from 'lucide-vue-next'
import { getScrollContainer } from '@/utils/scrollContainer'
const showTopics = ref(true)
@@ -102,7 +105,7 @@ const props = defineProps({
},
emptyStateText: {
type: String,
default: 'Start a discussion',
default: 'Start a Discussion',
},
singleThread: {
type: Boolean,
@@ -153,4 +156,8 @@ const showReplies = (topic) => {
const openTopicModal = () => {
showTopicModal.value = true
}
onUnmounted(() => {
socket.off('new_discussion_topic')
})
</script>
+1 -1
View File
@@ -5,7 +5,7 @@
{{ __('No {0}').format(type?.toLowerCase()) }}
</div>
<div
class="leading-5 text-base w-2/5 text-base text-center text-ink-gray-7"
class="leading-5 text-base w-full md:w-2/5 text-base text-center text-ink-gray-7"
>
{{
__(
-130
View File
@@ -1,130 +0,0 @@
<template>
<div>
<div class="flex items-center justify-between mb-4">
<div>
<div class="text-xl font-semibold mb-1 text-ink-gray-9">
{{ __(label) }}
</div>
<!-- <div class="text-xs text-ink-gray-5">
{{ __(description) }}
</div> -->
</div>
<div class="flex item-center space-x-2">
<FormControl
v-model="search"
:placeholder="__('Search')"
type="text"
:debounce="300"
/>
<Button @click="() => (showForm = !showForm)">
<template #prefix>
<Plus v-if="!showForm" class="size-4 stroke-1.5" />
<X v-else class="size-4 stroke-1.5" />
</template>
{{ showForm ? __('Close') : __('New') }}
</Button>
</div>
</div>
<!-- Form to add new member -->
<div v-if="showForm" class="flex items-center space-x-2 my-4">
<FormControl
v-model="email"
:placeholder="__('Email')"
type="email"
class="w-full"
/>
<Button @click="addEvaluator()" variant="subtle">
{{ __('Add') }}
</Button>
</div>
<div class="divide-y">
<div
v-for="evaluator in evaluators.data"
@click="openProfile(evaluator.username)"
class="cursor-pointer"
>
<div class="flex items-center justify-between py-3">
<div class="flex items-center space-x-3">
<Avatar
:image="evaluator.user_image"
:label="evaluator.full_name"
size="lg"
/>
<div>
<div class="text-base font-semibold text-ink-gray-9">
{{ evaluator.full_name }}
</div>
<div class="text-xs text-ink-gray-5">
{{ evaluator.evaluator }}
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { createResource, Button, FormControl, call, Avatar } from 'frappe-ui'
import { ref, watch } from 'vue'
import { Plus, X } from 'lucide-vue-next'
import { useRouter } from 'vue-router'
const show = defineModel('show')
const search = ref('')
const showForm = ref(false)
const email = ref('')
const router = useRouter()
const props = defineProps({
label: {
type: String,
required: true,
},
description: {
type: String,
default: '',
},
show: {
type: Boolean,
},
})
const evaluators = createResource({
url: 'frappe.client.get_list',
makeParams: () => {
return {
doctype: 'Course Evaluator',
fields: ['evaluator', 'full_name', 'user_image', 'username'],
filters: search.value ? { evaluator: ['like', `%${search.value}%`] } : {},
}
},
auto: true,
})
const addEvaluator = () => {
call('lms.lms.api.add_an_evaluator', {
email: email.value,
}).then((data) => {
showForm.value = false
email.value = ''
evaluators.reload()
})
}
watch(search, () => {
evaluators.reload()
})
const openProfile = (username) => {
show.value = false
router.push({
name: 'Profile',
params: {
username: username,
},
})
}
</script>
+97
View File
@@ -0,0 +1,97 @@
<template>
<Dialog v-model="showDialog">
<template #body-title>
<h2 class="text-lg font-bold">{{ __('Install Frappe Learning') }}</h2>
</template>
<template #body-content>
<p>
{{
__(
'Get the app on your device for easy access & a better experience!'
)
}}
</p>
</template>
<template #actions>
<Button variant="solid" class="w-full py-5" @click="install">
<template #prefix><FeatherIcon name="download" class="w-4" /></template>
{{ __('Install') }}
</Button>
</template>
</Dialog>
<Popover :show="iosInstallMessage" placement="bottom">
<template #body>
<div
class="mx-2 mt-[calc(100vh-15rem)] flex flex-col gap-3 rounded bg-blue-100 py-5 drop-shadow-xl"
>
<div
class="mb-1 flex flex-row items-center justify-between px-3 text-center"
>
<span class="text-base font-bold text-gray-900">
{{ __('Install Frappe Learning') }}
</span>
<span class="inline-flex items-baseline">
<FeatherIcon
name="x"
class="ml-auto h-4 w-4 text-gray-700"
@click="iosInstallMessage = false"
/>
</span>
</div>
<div class="px-3 text-xs text-gray-800">
<span class="flex flex-col gap-2">
<span>
{{
__(
'Get the app on your iPhone for easy access & a better experience'
)
}}
</span>
<span class="inline-flex items-start whitespace-nowrap">
<span>{{ __('Tap') }}&nbsp;</span>
<FeatherIcon name="share" class="h-4 w-4 text-blue-600" />
<span>&nbsp;{{ __("and then 'Add to Home Screen'") }}</span>
</span>
</span>
</div>
</div>
</template>
</Popover>
</template>
<script setup>
import { ref } from 'vue'
import { Button, Dialog, FeatherIcon, Popover } from 'frappe-ui'
const deferredPrompt = ref(null)
const showDialog = ref(false)
const iosInstallMessage = ref(false)
const isIos = () => {
const userAgent = window.navigator.userAgent.toLowerCase()
return /iphone|ipad|ipod/.test(userAgent)
}
const isInStandaloneMode = () =>
'standalone' in window.navigator && window.navigator.standalone
if (isIos() && !isInStandaloneMode()) iosInstallMessage.value = true
window.addEventListener('beforeinstallprompt', (e) => {
e.preventDefault()
deferredPrompt.value = e
if (isIos() && !isInStandaloneMode()) iosInstallMessage.value = true
else showDialog.value = true
})
window.addEventListener('appinstalled', () => {
showDialog.value = false
deferredPrompt.value = null
})
const install = () => {
deferredPrompt.value.prompt()
showDialog.value = false
}
</script>
+1 -1
View File
@@ -1,6 +1,6 @@
<template>
<div
class="flex flex-col border rounded-md p-3 h-full hover:border-outline-gray-4"
class="flex flex-col border rounded-md p-3 h-full hover:border-outline-gray-3"
>
<div class="flex space-x-4 mb-4">
<div class="flex flex-col space-y-2 flex-1">
+25 -50
View File
@@ -15,60 +15,18 @@
</div>
</div>
<div class="space-y-2">
<div class="space-y-2" v-for="(item, key) in contentMap" :key="key">
<div
class="flex items-center text-sm font-medium space-x-2 cursor-pointer"
@click="openHelpDialog('quiz')"
@click="openHelpDialog(key)"
>
<span>
{{ __('How to add a Quiz?') }}
{{ __(item.title) }}
</span>
<Info class="w-3 h-3 text-ink-gray-7" />
</div>
<div class="text-xs text-ink-gray-5 mb-1 leading-5">
{{
__(
'Click on the add icon in the editor and select Quiz from the menu. It opens up a dialog, where you can either select a quiz from the list or create a new quiz. When you select the Create New option it redirects you to the quiz creation page.'
)
}}
</div>
</div>
<div class="space-y-2">
<div
class="flex text-sm font-medium space-x-2 cursor-pointer"
@click="openHelpDialog('upload')"
>
<span class="leading-5">
{{ __(contentMap['upload']) }}
</span>
<Info class="w-3 h-3 text-ink-gray-7" />
</div>
<div class="text-xs text-ink-gray-5 mb-1 leading-5">
{{
__(
'To upload Image, Video, Audio or PDF from your system, click on the add icon and select upload from the menu. Then choose the file you want to add to the lesson and it gets added to your lesson.'
)
}}
</div>
</div>
<div class="space-y-2">
<div
class="flex items-center text-sm font-medium space-x-2 cursor-pointer"
@click="openHelpDialog('youtube')"
>
<span>
{{ __(contentMap['youtube']) }}
</span>
<Info class="w-3 h-3 text-ink-gray-7" />
</div>
<div class="text-xs text-ink-gray-5 mb-1 leading-5">
{{
__(
'Copy the URL of the video from YouTube and paste it in the editor.'
)
}}
{{ __(item.description) }}
</div>
</div>
</div>
@@ -83,14 +41,31 @@ const showExplanation = ref(false)
const type = ref(null)
const title = ref(null)
const contentMap = {
quiz: 'How to add a Quiz?',
upload: 'How to upload content from your system?',
youtube: 'How to add a YouTube Video?',
quiz: {
title: 'How to add a Quiz?',
description:
'Click on the add icon in the editor and select Quiz from the menu. It opens up a dialog, where you can either select a quiz from the list or create a new quiz. When you select the Create New option it redirects you to the quiz creation page.',
},
upload: {
title: 'How to upload content from your system?',
description:
'To upload Image, Video, Audio or PDF from your system, click on the add icon and select upload from the menu. Then choose the file you want to add to the lesson and it gets added to your lesson.',
},
youtube: {
title: 'How to add a YouTube Video?',
description:
'Copy the URL of the video from YouTube and paste it in the editor.',
},
remove: {
title: 'How to remove an embed?',
description:
'To remove an embed like YouTube or Vimeo, put your cursor on the line below the embed, then drag your mouse cursor upwards to select the embed. Once the embed is selected press BackSpace.',
},
}
const openHelpDialog = (contentType) => {
type.value = contentType
title.value = contentMap[contentType]
title.value = contentMap[contentType].title
showExplanation.value = true
}
</script>
+95 -19
View File
@@ -1,5 +1,15 @@
<template>
<div class="flex items-center justify-between mb-5">
<div
v-if="hasPermission() && !props.zoomAccount"
class="flex items-center space-x-2 mb-5 bg-surface-amber-1 py-1 px-2 rounded-md text-ink-amber-3"
>
<AlertCircle class="size-4 stroke-1.5" />
<span>
{{ __('Please add a zoom account to the batch to create live classes.') }}
</span>
</div>
<div class="flex items-center justify-between">
<div class="text-lg font-semibold text-ink-gray-9">
{{ __('Live Class') }}
</div>
@@ -12,10 +22,21 @@
</span>
</Button>
</div>
<div v-if="liveClasses.data?.length" class="grid grid-cols-2 gap-5">
<div
v-if="liveClasses.data?.length"
class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-5 mt-5"
>
<div
v-for="cls in liveClasses.data"
class="flex flex-col border rounded-md h-full text-ink-gray-7 p-3"
class="flex flex-col border rounded-md h-full text-ink-gray-7 hover:border-outline-gray-3 p-3"
:class="{
'cursor-pointer': hasPermission() && cls.attendees > 0,
}"
@click="
() => {
openAttendanceModal(cls)
}
"
>
<div class="font-semibold text-ink-gray-9 text-lg mb-1">
{{ cls.title }}
@@ -23,7 +44,7 @@
<div class="short-introduction">
{{ cls.description }}
</div>
<div class="space-y-3">
<div class="mt-auto space-y-3">
<div class="flex items-center space-x-2">
<Calendar class="w-4 h-4 stroke-1.5" />
<span>
@@ -33,18 +54,20 @@
<div class="flex items-center space-x-2">
<Clock class="w-4 h-4 stroke-1.5" />
<span>
{{ formatTime(cls.time) }}
{{ formatTime(cls.time) }} -
{{ dayjs(getClassEnd(cls)).format('HH:mm A') }}
</span>
</div>
<div
v-if="cls.date >= dayjs().format('YYYY-MM-DD')"
v-if="canAccessClass(cls)"
class="flex items-center space-x-2 text-ink-gray-9 mt-auto"
>
<a
v-if="user.data?.is_moderator || user.data?.is_evaluator"
:href="cls.start_url"
target="_blank"
class="w-1/2 cursor-pointer inline-flex items-center justify-center gap-2 transition-colors focus:outline-none text-ink-gray-8 bg-surface-gray-2 hover:bg-surface-gray-3 active:bg-surface-gray-4 focus-visible:ring focus-visible:ring-outline-gray-3 h-7 text-base px-2 rounded"
class="cursor-pointer inline-flex items-center justify-center gap-2 transition-colors focus:outline-none text-ink-gray-8 bg-surface-gray-2 hover:bg-surface-gray-3 active:bg-surface-gray-4 focus-visible:ring focus-visible:ring-outline-gray-3 h-7 text-base px-2 rounded"
:class="cls.join_url ? 'w-full' : 'w-1/2'"
>
<Monitor class="h-4 w-4 stroke-1.5" />
{{ __('Start') }}
@@ -58,42 +81,63 @@
{{ __('Join') }}
</a>
</div>
<div v-else class="flex items-center space-x-2 text-yellow-700">
<Info class="w-4 h-4 stroke-1.5" />
<span>
{{ __('This class has ended') }}
</span>
</div>
<Tooltip
v-else-if="hasClassEnded(cls)"
:text="__('This class has ended')"
placement="right"
>
<div class="flex items-center space-x-2 text-ink-amber-3 w-fit">
<Info class="w-4 h-4 stroke-1.5" />
<span>
{{ __('Ended') }}
</span>
</div>
</Tooltip>
</div>
</div>
</div>
<div v-else class="text-sm italic text-ink-gray-5">
<div v-else class="text-sm italic text-ink-gray-5 mt-2">
{{ __('No live classes scheduled') }}
</div>
<LiveClassModal
:batch="props.batch"
:zoomAccount="props.zoomAccount"
v-model="showLiveClassModal"
v-model:reloadLiveClasses="liveClasses"
/>
<LiveClassAttendance v-model="showAttendance" :live_class="attendanceFor" />
</template>
<script setup>
import { createListResource, Button } from 'frappe-ui'
import { Plus, Clock, Calendar, Video, Monitor, Info } from 'lucide-vue-next'
import { inject } from 'vue'
import LiveClassModal from '@/components/Modals/LiveClassModal.vue'
import { ref } from 'vue'
import { createListResource, Button, Tooltip } from 'frappe-ui'
import {
Plus,
Clock,
Calendar,
Video,
Monitor,
Info,
AlertCircle,
} from 'lucide-vue-next'
import { inject, ref } from 'vue'
import { formatTime } from '@/utils/'
import LiveClassModal from '@/components/Modals/LiveClassModal.vue'
import LiveClassAttendance from '@/components/Modals/LiveClassAttendance.vue'
const user = inject('$user')
const showLiveClassModal = ref(false)
const dayjs = inject('$dayjs')
const readOnlyMode = window.read_only_mode
const showAttendance = ref(false)
const attendanceFor = ref(null)
const props = defineProps({
batch: {
type: String,
required: true,
},
zoomAccount: String,
})
const liveClasses = createListResource({
@@ -106,6 +150,8 @@ const liveClasses = createListResource({
'description',
'time',
'date',
'duration',
'attendees',
'start_url',
'join_url',
'owner',
@@ -120,8 +166,38 @@ const openLiveClassModal = () => {
const canCreateClass = () => {
if (readOnlyMode) return false
if (!props.zoomAccount) return false
return hasPermission()
}
const hasPermission = () => {
return user.data?.is_moderator || user.data?.is_evaluator
}
const canAccessClass = (cls) => {
if (cls.date < dayjs().format('YYYY-MM-DD')) return false
if (cls.date > dayjs().format('YYYY-MM-DD')) return false
if (hasClassEnded(cls)) return false
return true
}
const getClassEnd = (cls) => {
const classStart = new Date(`${cls.date}T${cls.time}`)
return new Date(classStart.getTime() + cls.duration * 60000)
}
const hasClassEnded = (cls) => {
const classEnd = getClassEnd(cls)
const now = new Date()
return now > classEnd
}
const openAttendanceModal = (cls) => {
if (!hasPermission()) return
if (cls.attendees <= 0) return
showAttendance.value = true
attendanceFor.value = cls
}
</script>
<style>
.short-introduction {
+121 -63
View File
@@ -1,98 +1,118 @@
<template>
<div class="flex h-full flex-col">
<div class="flex h-full flex-col relative">
<div class="h-full pb-10" id="scrollContainer">
<slot />
</div>
<div
v-if="sidebarSettings.data"
class="fixed flex items-center justify-around border-t border-outline-gray-2 bottom-0 z-10 w-full bg-surface-white standalone:pb-4"
:style="{
gridTemplateColumns: `repeat(${
sidebarLinks.length + 1
}, minmax(0, 1fr))`,
}"
>
<button
v-for="tab in sidebarLinks"
:key="tab.label"
:class="isVisible(tab) ? 'block' : 'hidden'"
class="flex flex-col items-center justify-center py-3 transition active:scale-95"
@click="handleClick(tab)"
<div class="relative z-20">
<!-- Dropdown menu -->
<div
class="fixed bottom-16 right-2 w-[80%] rounded-md bg-surface-white text-base p-5 space-y-4 shadow-md"
v-if="showMenu"
ref="menu"
>
<component
:is="icons[tab.icon]"
class="h-6 w-6 stroke-1.5"
:class="[isActive(tab) ? 'text-ink-gray-9' : 'text-ink-gray-5']"
/>
</button>
<Popover
trigger="hover"
popoverClass="bottom-28 mx-2"
placement="top-start"
<div
v-for="link in otherLinks"
:key="link.label"
class="flex items-center space-x-2 cursor-pointer"
@click="handleClick(link)"
>
<component
:is="icons[link.icon]"
class="h-4 w-4 stroke-1.5 text-ink-gray-5"
/>
<div>{{ link.label }}</div>
</div>
</div>
<!-- Fixed menu -->
<div
v-if="sidebarSettings.data"
class="fixed bottom-0 left-0 w-full flex items-center justify-around border-t border-outline-gray-2 bg-surface-white standalone:pb-4 z-10"
>
<template #target>
<button
v-for="tab in sidebarLinks"
:key="tab.label"
:class="isVisible(tab) ? 'block' : 'hidden'"
class="flex flex-col items-center justify-center py-3 transition active:scale-95"
@click="handleClick(tab)"
>
<component
:is="icons[tab.icon]"
class="h-6 w-6 stroke-1.5"
:class="[isActive(tab) ? 'text-ink-gray-9' : 'text-ink-gray-5']"
/>
</button>
<button @click="toggleMenu">
<component
:is="icons['List']"
class="h-6 w-6 stroke-1.5 text-ink-gray-5"
/>
</template>
<template #body-main>
<div class="text-base p-5 space-y-4">
<div
v-for="link in otherLinks"
:key="link.label"
class="flex items-center space-x-2"
@click="handleClick(link)"
>
<component
:is="icons[link.icon]"
class="h-4 w-4 stroke-1.5 text-ink-gray-5"
/>
<div>
{{ link.label }}
</div>
</div>
</div>
</template>
</Popover>
</button>
</div>
</div>
</div>
</template>
<script setup>
import { getSidebarLinks } from '../utils'
import { getSidebarLinks } from '@/utils'
import { useRouter } from 'vue-router'
import { call } from 'frappe-ui'
import { watch, ref, onMounted } from 'vue'
import { sessionStore } from '@/stores/session'
import { useSettings } from '@/stores/settings'
import { usersStore } from '@/stores/user'
import { Popover } from 'frappe-ui'
import * as icons from 'lucide-vue-next'
const { logout, user, sidebarSettings } = sessionStore()
const { logout, user } = sessionStore()
let { isLoggedIn } = sessionStore()
const { sidebarSettings } = useSettings()
const router = useRouter()
let { userResource } = usersStore()
const sidebarLinks = ref(getSidebarLinks())
const otherLinks = ref([])
const showMenu = ref(false)
const menu = ref(null)
const isModerator = ref(false)
const isInstructor = ref(false)
onMounted(() => {
sidebarSettings.reload(
{},
{
onSuccess(data) {
Object.keys(data).forEach((key) => {
if (!parseInt(data[key])) {
sidebarLinks.value = sidebarLinks.value.filter(
(link) => link.label.toLowerCase().split(' ').join('_') !== key
)
}
})
filterLinksToShow(data)
addOtherLinks()
},
}
)
})
const handleOutsideClick = (e) => {
if (menu.value && !menu.value.contains(e.target)) {
showMenu.value = false
}
}
watch(showMenu, (val) => {
if (val) {
setTimeout(() => {
document.addEventListener('click', handleOutsideClick)
}, 0)
} else {
document.removeEventListener('click', handleOutsideClick)
}
})
const filterLinksToShow = (data) => {
Object.keys(data).forEach((key) => {
if (!parseInt(data[key])) {
sidebarLinks.value = sidebarLinks.value.filter(
(link) => link.label.toLowerCase().split(' ').join('_') !== key
)
}
})
}
const addOtherLinks = () => {
if (user) {
otherLinks.value.push({
@@ -117,11 +137,15 @@ const addOtherLinks = () => {
}
watch(userResource, () => {
if (
userResource.data &&
(userResource.data.is_moderator || userResource.data.is_instructor)
) {
addQuizzes()
if (userResource.data) {
isModerator.value = userResource.data.is_moderator
isInstructor.value = userResource.data.is_instructor
addPrograms()
if (isModerator.value || isInstructor.value) {
addProgrammingExercises()
addQuizzes()
addAssignments()
}
}
})
@@ -133,6 +157,36 @@ const addQuizzes = () => {
})
}
const addAssignments = () => {
otherLinks.value.push({
label: 'Assignments',
icon: 'Pencil',
to: 'Assignments',
})
}
const addPrograms = async () => {
let canAddProgram = await checkIfCanAddProgram()
if (!canAddProgram) return
let activeFor = ['Programs', 'ProgramDetail']
let index = 1
sidebarLinks.value.splice(index, 0, {
label: 'Programs',
icon: 'Route',
to: 'Programs',
activeFor: activeFor,
})
}
const checkIfCanAddProgram = async () => {
if (isModerator.value || isInstructor.value) {
return true
}
const programs = await call('lms.lms.utils.get_programs')
return programs.enrolled.length > 0 || programs.published.length > 0
}
let isActive = (tab) => {
return tab.activeFor?.includes(router.currentRoute.value.name)
}
@@ -158,4 +212,8 @@ const isVisible = (tab) => {
else if (tab.label == 'Log out') return isLoggedIn
else return true
}
const toggleMenu = () => {
showMenu.value = !showMenu.value
}
</script>
@@ -25,6 +25,7 @@
<div class="">
<div class="mb-1.5 text-sm text-ink-gray-5">
{{ __('Reply To') }}
<span class="text-ink-red-3">*</span>
</div>
<Input type="text" v-model="announcement.replyTo" />
</div>
@@ -70,8 +71,8 @@ const announcementResource = createResource({
url: 'frappe.core.doctype.communication.email.make',
makeParams(values) {
return {
recipients: props.students.join(', '),
cc: announcement.replyTo,
recipients: announcement.replyTo,
bcc: props.students.join(', '),
subject: announcement.subject,
content: announcement.announcement,
doctype: 'LMS Batch',
@@ -95,6 +96,9 @@ const makeAnnouncement = (close) => {
if (!announcement.announcement) {
return __('Announcement is required')
}
if (!announcement.replyTo) {
return __('Reply To is required')
}
},
onSuccess() {
close()
@@ -99,6 +99,7 @@ const assessmentTypes = computed(() => {
return [
{ label: 'Quiz', value: 'LMS Quiz' },
{ label: 'Assignment', value: 'LMS Assignment' },
{ label: 'Programming Exercise', value: 'LMS Programming Exercise' },
]
})
</script>
@@ -6,7 +6,7 @@
}"
>
<template #body>
<div class="p-5 text-base max-h-[75vh] overflow-y-auto">
<div class="p-5 text-base">
<div class="text-lg text-ink-gray-9 font-semibold mb-5">
{{
assignmentID === 'new'
@@ -14,7 +14,7 @@
: __('Edit Assignment')
}}
</div>
<div class="space-y-4">
<div class="space-y-4 max-h-[75vh] overflow-y-auto">
<FormControl
v-model="assignment.title"
:label="__('Title')"
@@ -0,0 +1,230 @@
<template>
<Dialog
v-model="show"
:options="{
title: __('Course Progress Summary'),
size: '5xl',
}"
>
<template #body-content>
<div
class="flex flex-col-reverse md:flex-row justify-between md:space-x-10 text-base mt-10"
>
<div class="w-full">
<div class="flex items-center justify-between space-x-5 mb-4">
<FormControl
v-model="searchFilter"
:placeholder="__('Search by Member')"
type="text"
class="w-full"
/>
</div>
<div class="max-h-[70vh] overflow-y-auto">
<ListView
v-if="progressList.loading || progressList.data?.length"
:columns="progressColumns"
:rows="progressList.data"
rowKey="name"
:options="{
selectable: false,
showTooltip: false,
}"
>
<ListHeader
class="mb-2 grid items-center space-x-4 rounded bg-surface-gray-2 p-2"
>
<ListHeaderItem
:item="item"
v-for="item in progressColumns"
:key="item.key"
>
<template #prefix="{ item }">
<FeatherIcon
:name="item.icon?.toString()"
class="h-4 w-4"
/>
</template>
</ListHeaderItem>
</ListHeader>
<ListRows v-for="row in progressList.data">
<router-link
:to="{
name: 'Profile',
params: { username: row.member_username },
}"
>
<ListRow :row="row">
<template #default="{ column, item }">
<ListRowItem
:item="row[column.key]"
:align="column.align"
>
<template #prefix>
<div v-if="column.key == 'member_name'">
<Avatar
class="flex items-center"
:image="row['member_image']"
:label="item"
size="sm"
/>
</div>
</template>
<div>
{{ row[column.key].toString() }}
</div>
</ListRowItem>
</template>
</ListRow>
</router-link>
</ListRows>
</ListView>
<div
v-if="progressList.data && progressList.hasNextPage"
class="flex justify-center my-5"
>
<Button @click="progressList.next()">
{{ __('Load More') }}
</Button>
</div>
</div>
</div>
<div class="mb-4 self-start w-full space-y-5">
<div
class="flex flex-col md:flex-row items-center space-y-2 md:space-y-0 md:space-x-4"
>
<NumberChart
class="border rounded-md w-full"
:config="{
title: __('Enrollments'),
value: memberCount || 0,
}"
/>
<NumberChart
class="border rounded-md w-full"
:config="{
title: __('Average Progress %'),
value: chartDetails.data?.average_progress || 0,
}"
/>
</div>
<DonutChart
:config="{
data: chartDetails.data?.progress_distribution || [],
title: __('Progress Distribution'),
categoryColumn: 'category',
valueColumn: 'count',
colors: [
theme.colors.red['400'],
theme.colors.amber['400'],
theme.colors.pink['400'],
theme.colors.blue['400'],
theme.colors.green['400'],
],
}"
/>
</div>
</div>
</template>
</Dialog>
</template>
<script setup lang="ts">
import {
Avatar,
Button,
createListResource,
createResource,
Dialog,
DonutChart,
FeatherIcon,
FormControl,
ListView,
ListHeader,
ListHeaderItem,
ListRows,
ListRow,
ListRowItem,
NumberChart,
} from 'frappe-ui'
import { computed, ref, watch } from 'vue'
import { theme } from '@/utils/theme'
const show = defineModel<boolean>({ default: false })
const searchFilter = ref<string | null>(null)
type Filters = {
course: string | undefined
member_name?: string[]
}
const props = defineProps<{
courseName?: string
enrollments?: number
}>()
const memberCount = ref<number>(props.enrollments || 0)
const chartDetails = createResource({
url: 'lms.lms.api.get_course_progress_distribution',
params: {
course: props.courseName,
},
auto: true,
})
const progressList = createListResource({
doctype: 'LMS Enrollment',
filters: {
course: props.courseName,
},
fields: [
'name',
'member',
'member_name',
'member_image',
'member_username',
'progress',
],
pageLength: 50,
auto: true,
})
watch([searchFilter], () => {
let filterApplied = false
let filters: Filters = {
course: props.courseName,
}
if (searchFilter.value) {
filters.member_name = ['like', `%${searchFilter.value}%`]
filterApplied = true
}
progressList.update({
filters: filters,
})
progressList.reload(
{},
{
onSuccess(data: any[]) {
memberCount.value = filterApplied ? data.length : props.enrollments || 0
},
}
)
})
const progressColumns = computed(() => {
return [
{
label: __('Member'),
key: 'member_name',
width: '60%',
icon: 'user',
},
{
label: __('Progress'),
key: 'progress',
align: 'right',
icon: 'trending-up',
},
]
})
</script>
@@ -97,7 +97,7 @@ import {
} from 'frappe-ui'
import { reactive, watch } from 'vue'
import { FileText, X } from 'lucide-vue-next'
import { getFileSize, escapeHTML } from '@/utils'
import { getFileSize } from '@/utils'
const reloadProfile = defineModel('reloadProfile')
@@ -132,7 +132,6 @@ const imageResource = createResource({
const updateProfile = createResource({
url: 'frappe.client.set_value',
makeParams(values) {
profile.bio = escapeHTML(profile.bio)
return {
doctype: 'User',
name: props.profile.data.name,
@@ -0,0 +1,192 @@
<template>
<Dialog
v-model="show"
:options="{
title:
templateID == 'new'
? __('New Email Template')
: __('Edit Email Template'),
size: 'lg',
actions: [
{
label: __('Save'),
variant: 'solid',
onClick: ({ close }) => {
saveTemplate(close)
},
},
],
}"
>
<template #body-content>
<div class="space-y-4">
<FormControl
:label="__('Name')"
v-model="template.name"
type="text"
:required="true"
:placeholder="__('Batch Enrollment Confirmation')"
/>
<FormControl
:label="__('Subject')"
v-model="template.subject"
type="text"
:required="true"
:placeholder="__('Your enrollment in {{ batch_name }} is confirmed')"
/>
<FormControl
:label="__('Use HTML')"
v-model="template.use_html"
type="checkbox"
/>
<FormControl
v-if="template.use_html"
:label="__('Content')"
v-model="template.response_html"
type="textarea"
:required="true"
:rows="10"
:placeholder="
__(
'<p>Dear {{ member_name }},</p>\n\n<p>You have been enrolled in our upcoming batch {{ batch_name }}.</p>\n\n<p>Thanks,</p>\n<p>Frappe Learning</p>'
)
"
/>
<div v-else>
<div class="text-xs text-ink-gray-5 mb-2">
{{ __('Content') }}
<span class="text-ink-red-3">*</span>
</div>
<TextEditor
:content="template.response"
@change="(val) => (template.response = val)"
:editable="true"
:fixedMenu="true"
:placeholder="
__(
'Dear {{ member_name }},\n\nYou have been enrolled in our upcoming batch {{ batch_name }}.\n\nThanks,\nFrappe Learning'
)
"
editorClass="prose-sm max-w-none border-b border-x bg-surface-gray-2 rounded-b-md py-1 px-2 min-h-[7rem] max-h-[18rem] overflow-y-auto"
/>
</div>
</div>
</template>
</Dialog>
</template>
<script setup lang="ts">
import { call, Dialog, FormControl, TextEditor, toast } from 'frappe-ui'
import { reactive, watch } from 'vue'
import { cleanError } from '@/utils'
const props = defineProps({
templateID: {
type: String,
default: 'new',
},
})
const show = defineModel()
const emailTemplates = defineModel('emailTemplates')
const template = reactive({
name: '',
subject: '',
use_html: false,
response: '',
response_html: '',
})
const saveTemplate = (close) => {
if (props.templateID == 'new') {
createNewTemplate(close)
} else {
updateTemplate(close)
}
}
const createNewTemplate = (close) => {
emailTemplates.value.insert.submit(
{
__newname: template.name,
...template,
},
{
onSuccess() {
emailTemplates.value.reload()
refreshForm(close)
toast.success(__('Email Template created successfully'))
},
onError(err) {
refreshForm(close)
toast.error(
cleanError(err.messages[0]) || __('Error creating email template')
)
},
}
)
}
const updateTemplate = async (close) => {
if (props.templateID != template.name) {
await renameDoc()
}
setValue(close)
}
const setValue = (close) => {
emailTemplates.value.setValue.submit(
{
...template,
name: template.name,
},
{
onSuccess() {
emailTemplates.value.reload()
refreshForm(close)
toast.success(__('Email Template updated successfully'))
},
onError(err) {
refreshForm(close)
toast.error(
cleanError(err.messages[0]) || __('Error updating email template')
)
},
}
)
}
const renameDoc = async () => {
await call('frappe.client.rename_doc', {
doctype: 'Email Template',
old_name: props.templateID,
new_name: template.name,
})
}
watch(
() => props.templateID,
(val) => {
if (val !== 'new') {
emailTemplates.value?.data.forEach((row) => {
if (row.name === val) {
template.name = row.name
template.subject = row.subject
template.use_html = row.use_html
template.response = row.response
template.response_html = row.response_html
}
})
}
},
{ flush: 'post' }
)
const refreshForm = (close) => {
close()
template.name = ''
template.subject = ''
template.use_html = false
template.response = ''
template.response_html = ''
}
</script>
@@ -66,7 +66,7 @@
</Dialog>
</template>
<script setup>
import { Dialog, createResource, Select, FormControl } from 'frappe-ui'
import { Dialog, createResource, Select, FormControl, toast } from 'frappe-ui'
import { reactive, watch, inject } from 'vue'
import { formatTime } from '@/utils/'
@@ -90,7 +90,7 @@ const props = defineProps({
},
})
let evaluation = reactive({
const evaluation = reactive({
course: '',
date: '',
start_time: '',
@@ -139,22 +139,13 @@ function submitEvaluation(close) {
close()
},
onError(err) {
let message = err.messages?.[0] || err
let unavailabilityMessage
if (typeof message === 'string') {
unavailabilityMessage = message?.includes('unavailable')
} else {
unavailabilityMessage = false
}
toast.warning(__('Evaluator is unavailable'))
toast.warning(__(err.messages?.[0] || err))
},
})
}
const getCourses = () => {
let courses = []
const courses = []
for (const course of props.courses) {
if (course.evaluator) {
courses.push({
@@ -164,7 +155,7 @@ const getCourses = () => {
}
}
if (courses.length == 1) {
if (courses.length === 1) {
evaluation.course = courses[0].value
}
+11 -2
View File
@@ -76,8 +76,8 @@
</Button>
</div>
</div>
<Tabs :tabs="tabs" v-model="tabIndex" class="border-l w-1/2">
<template #default="{ tab }">
<Tabs :tabs="tabs" as="div" v-model="tabIndex" class="border-l w-1/2">
<template #tab-panel="{ tab }">
<div
v-if="tab.label == 'Evaluation'"
class="flex flex-col space-y-4 p-5"
@@ -255,6 +255,9 @@ const saveEvaluation = () => {
}
toast.success(__('Evaluation saved successfully'))
},
onError(err) {
toast.warning(__(err.messages?.[0] || err))
},
}
)
}
@@ -277,6 +280,9 @@ const certificateResource = createResource({
onSuccess(data) {
certificate.name = data
},
onError(err) {
toast.warning(__(err.messages?.[0] || err))
},
})
const certificateDetails = createResource({
@@ -310,6 +316,9 @@ const saveCertificate = () => {
onSuccess: () => {
toast.success(__('Certificate saved successfully'))
},
onError(err) {
toast.error(__(err.messages?.[0] || err))
},
}
)
}
@@ -35,5 +35,6 @@ const file = computed(() => {
if (props.type == 'youtube') return '/assets/lms/frontend/Youtube.mp4'
if (props.type == 'quiz') return '/assets/lms/frontend/Quiz.mp4'
if (props.type == 'upload') return '/assets/lms/frontend/Upload.mp4'
if (props.type == 'remove') return '/assets/lms/frontend/Remove.mp4'
})
</script>
@@ -0,0 +1,115 @@
<template>
<Dialog
v-model="show"
:options="{
size: '4xl',
}"
>
<template #body>
<div class="p-5 min-h-[300px]">
<div class="text-lg font-semibold mb-4">
{{ __('Training Feedback') }}
</div>
<ListView
:columns="feedbackColumns"
:rows="feedbackList"
row-key="name"
:options="{
showTooltip: false,
rowHeight: 'h-16',
selectable: false,
}"
>
<ListHeader
class="mb-2 grid items-center space-x-4 rounded bg-surface-gray-2 p-2"
></ListHeader>
<ListRows>
<ListRow
:row="row"
v-for="row in feedbackList"
class="group feedback-list"
>
<template #default="{ column, item }">
<ListRowItem
:item="row[column.key]"
:align="column.align"
class="text-sm"
>
<template #prefix>
<div v-if="column.key == 'member_name'">
<Avatar
class="flex"
:image="row['member_image']"
:label="item"
size="sm"
/>
</div>
</template>
<div v-if="ratingKeys.includes(column.key)">
<Rating v-model="row[column.key]" :readonly="true" />
</div>
<div v-else class="leading-5">
{{ row[column.key] }}
</div>
</ListRowItem>
</template>
</ListRow>
</ListRows>
</ListView>
</div>
</template>
</Dialog>
</template>
<script setup lang="ts">
import {
Dialog,
ListView,
Avatar,
ListHeader,
ListRows,
ListRow,
ListRowItem,
Rating,
} from 'frappe-ui'
import { reactive, computed } from 'vue'
const show = defineModel()
const ratingKeys = ['content', 'instructors', 'value']
const props = defineProps({
feedbackList: {
type: Array,
required: true,
},
})
const feedbackColumns = computed(() => {
return [
{
label: 'Member',
key: 'member_name',
width: '10rem',
},
{
label: 'Feedback',
key: 'feedback',
width: '15rem',
},
{
label: 'Content',
key: 'content',
width: '9rem',
},
{
label: 'Instructors',
key: 'instructors',
width: '9rem',
},
{
label: 'Value',
key: 'value',
width: '9rem',
},
]
})
</script>
@@ -0,0 +1,106 @@
<template>
<Dialog
v-model="show"
:options="{
title: __('Attendance for Class - {0}').format(live_class?.title),
size: '4xl',
}"
>
<template #body-content>
<div
class="grid grid-cols-2 gap-12 text-sm font-semibold text-ink-gray-5 pb-2"
>
<div>
{{ __('Member') }}
</div>
<div class="grid grid-cols-3 gap-20">
<div>
{{ __('Joined at') }}
</div>
<div class="text-center">
{{ __('Left at') }}
</div>
<div>
{{ __('Attended for') }}
</div>
</div>
</div>
<div class="divide-y text-base">
<div
v-for="participant in participants.data"
@click="redirectToProfile(participant.member_username)"
class="grid grid-cols-2 items-center w-full text-base w-fit py-2"
>
<div class="flex items-center space-x-2">
<Avatar
:image="participant.member_image"
:label="participant.member_name"
size="xl"
/>
<div class="space-y-1">
<div class="font-medium">
{{ participant.member_name }}
</div>
<div>
{{ participant.member }}
</div>
</div>
</div>
<div class="grid grid-cols-3 gap-20 text-right">
<div>
{{ dayjs(participant.joined_at).format('HH:mm a') }}
</div>
<div>
{{ dayjs(participant.left_at).format('HH:mm a') }}
</div>
<div>{{ participant.duration }} {{ __('minutes') }}</div>
</div>
</div>
</div>
</template>
</Dialog>
</template>
<script setup lang="ts">
import { Avatar, createListResource, Dialog, Tooltip } from 'frappe-ui'
import { useRouter } from 'vue-router'
import { inject } from 'vue'
const show = defineModel()
const router = useRouter()
const dayjs = inject('$dayjs')
interface LiveClass {
name: String
title: String
}
const props = defineProps<{
live_class: LiveClass | null
}>()
const participants = createListResource({
doctype: 'LMS Live Class Participant',
filter: {
live_class: props.live_class?.name,
},
fields: [
'name',
'member',
'member_name',
'member_image',
'member_username',
'joined_at',
'left_at',
'duration',
],
auto: true,
})
const redirectToProfile = (username: string) => {
router.push({
name: 'Profile',
params: { username },
})
}
</script>

Some files were not shown because too many files have changed in this diff Show More